Загрузка данных


import { MoexChart, Timeframes } from 'moex-chart';

// eslint-disable-next-line import/no-unresolved -- MOEX_CHART
import { CompareManager } from 'moex-chart/dist/types/core/CompareManager';
import { useEffect, useRef, useState } from 'react';

import { useChangeProperties, useSelectProperties } from '@modules/widgetProperties';
import { ChartIndicativeData, ChartProps } from '@widgets/Chart/types';

import { MOEX_CHART_CONFIG } from '../constants';
import { dataSourceProvider, DataSourceProvider } from '../dataSourceProvide';

import type { IMoexChart } from 'moex-chart';

type TUseMoexChartProps = {
  symbol: string;
  indicativeData: ChartIndicativeData | undefined;
};

export const useMoexChart = ({ symbol, indicativeData }: TUseMoexChartProps) => {
  const moexChartState = useSelectProperties((wProps: Partial<ChartProps>) => wProps.moexChartState);

  const { updateProperties } = useChangeProperties<ChartProps>();

  const [isCompareOpen, setIsCompareOpen] = useState(false);

  const containerRef = useRef<HTMLDivElement | null>(null);
  const chartRef = useRef<MoexChart | null>(null);
  const compareManagerRef = useRef<null | CompareManager>(null);

  const timeframeRef = useRef<Timeframes | undefined>(moexChartState?.tf);
  const savedDataRef = useRef<string | undefined>(moexChartState?.savedData);
  const updateTimeframeRef = useRef<((tf: Timeframes) => void) | null>(null);

  useEffect(() => {
    timeframeRef.current = moexChartState?.tf;
    savedDataRef.current = moexChartState?.savedData;
  }, [moexChartState]);

  updateTimeframeRef.current = (tf: Timeframes) => {
    if (timeframeRef.current === tf) {
      return;
    }

    timeframeRef.current = tf;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        tf,
      };
    });
  };

  const saveSnapshot = () => {
    const snapshot = chartRef.current?.getSnapshot();

    if (!snapshot) {
      return;
    }

    const savedData = JSON.stringify(snapshot);
    savedDataRef.current = savedData;

    updateProperties((state) => {
      state.moexChartState = {
        ...state.moexChartState,
        tf: timeframeRef.current || Timeframes['1m'],
        savedData,
      };
    });
  };

  const applySnapshot = () => {
    if (!savedDataRef.current || !chartRef.current) {
      return;
    }

    const savedSnapshot = JSON.parse(JSON.stringify(savedDataRef.current)) as IMoexChart['snapshot'];
    const timeframe = timeframeRef.current || Timeframes['1m'];

    chartRef.current.setSnapshot({
      ...savedSnapshot,
      charts: savedSnapshot.charts.map((chartSnapshot) => ({
        ...chartSnapshot,
        symbol,
        timeframe,
      })),
    });

    compareManagerRef.current = chartRef.current.getCompareManager();
  };

  useEffect(() => {
    const container = containerRef.current;

    if (!container) {
      return undefined;
    }

    const timeframe = timeframeRef.current || Timeframes['1m'];
    const savedSnapshot = savedDataRef.current
      ? (JSON.parse(savedDataRef.current) as IMoexChart['snapshot'])
      : MOEX_CHART_CONFIG.snapshot;

    const chart = new MoexChart({
      ...MOEX_CHART_CONFIG,
      container,
      snapshot: {
        ...savedSnapshot,
        charts: savedSnapshot.charts.map((chartSnapshot) => ({
          ...chartSnapshot,
          symbol,
          timeframe,
        })),
      },
      chartCollectionPreset: {
        ...MOEX_CHART_CONFIG.chartCollectionPreset,
        openCompareModal: () => setIsCompareOpen(true),
        getDataSource: DataSourceProvider.getDataSource(indicativeData, (tf) => {
          updateTimeframeRef.current?.(tf);
        }),
        startRealtime: (getSymbols, getTimeframe, update) =>
          dataSourceProvider.startRealtime({
            getSymbols,
            getTimeframe,
            update,
          }),
      },
    });

    chartRef.current = chart;
    compareManagerRef.current = chart.getCompareManager();

    return () => {
      chartRef.current = null;
      compareManagerRef.current = null;
      chart.destroy();
    };
  }, [symbol, indicativeData]);

  return {
    containerRef,
    isCompareOpen,
    compareManagerRef,
    setIsCompareOpen,
    saveSnapshot,
    applySnapshot,
    hasSavedSnapshot: Boolean(moexChartState?.savedData),
  };
};



import { DateFormat, IndicatorsIds, Timeframes } from 'moex-chart';

// eslint-disable-next-line import/no-unresolved -- Ожидаем экспорт из moex-chart
import { ChartCollectionPreset } from 'moex-chart/dist/types/core/MoexChart';

// eslint-disable-next-line import/no-unresolved -- Ожидаем экспорт из moex-chart
import { MoexChartSnapshot } from 'moex-chart/dist/types/types/snapshot';

import type { IMoexChart } from 'moex-chart';

type ChartCollectionPresetConfig = Omit<ChartCollectionPreset, 'getDataSource' | 'startRealtime'>;
type ChartSnapshotItemConfig = Omit<MoexChartSnapshot['charts'][number], 'symbol'>;
type MoexChartSnapshotConfig = Omit<MoexChartSnapshot, 'charts'> & {
  charts: ChartSnapshotItemConfig[];
};

export type MoexChartConfig = Omit<IMoexChart, 'container' | 'chartCollectionPreset' | 'snapshot'> & {
  snapshot: MoexChartSnapshotConfig;
  chartCollectionPreset: ChartCollectionPresetConfig;
};

const MOEX_CHART_CONFIG: MoexChartConfig = {
  snapshot: {
    charts: [
      {
        timeframe: Timeframes['10s'],
        chartSeriesType: 'Candlestick',
        panes: [
          {
            isMain: true,
            id: 0,
            indicators: [
              {
                indicatorType: IndicatorsIds.Volume,
              },
            ],
            drawings: [],
          },
        ],
      },
    ],
  },

  chartCollectionPreset: {
    undoRedoEnabled: true,
    showMenuButton: true,
    showBottomPanel: true,
    showControlBar: true,
    showFullscreenButton: true,
    showSettingsButton: true,
    showCompareButton: true,
    tooltipConfig: {
      showTooltip: false,
      time: { visible: true, label: 'Время' },
      close: { visible: true, label: 'Закр.' },
      change: { visible: true, label: 'Изм.' },
      volume: { visible: true, label: 'Объем' },
      open: { visible: true, label: 'Откр.' },
      high: { visible: true, label: 'Макс.' },
      low: { visible: true, label: 'Мин.' },
    },

    supportedTimeframes: [
      Timeframes['1m'],
      Timeframes['5m'],
      Timeframes['10m'],
      Timeframes['15m'],
      Timeframes['30m'],
      Timeframes['45m'],
      Timeframes['1h'],
      Timeframes['4h'],
      Timeframes['1d'],
      Timeframes['1w'],
      Timeframes['1М'],
    ],
    supportedChartSeriesTypes: ['Candlestick', 'Line', 'Bar'],
    theme: 'tr',
    ohlc: {
      show: true,
      precision: 4,
    },
    mode: 'dark',
  },

  lwcInheritedChartOptions: {
    timeVisible: true,
    secondsVisible: false,
    timeFormat: '24h',
    dateFormat: DateFormat.DD_MM_YYYY_HH_mm_ss,
  },
};

export { MOEX_CHART_CONFIG };



// eslint-disable-next-line import/no-unresolved -- MOEX_CHART
import { CompareManager } from 'moex-chart/dist/types/core/CompareManager';
import React, { MutableRefObject, useEffect, useState } from 'react';

import { InstrumentSearch } from '@components/InstrumentSearch';

/**
 * @deprecated
 *
 * TODO: удалить это, когда moex-chart добавит экспорт у себя.
 */
enum CompareMode {
  Percentage = 'PCT',
  NewScale = 'SCALE',
  NewPane = 'PANE',
}

export const CompareModal = ({
  compareManager,
  widgetId,
  isOpen,
  setOpen,
}: {
  onClose: () => void;
  widgetId: number;
  compareManager: MutableRefObject<CompareManager | null>;

  isOpen: boolean;
  setOpen: (isOpen: boolean) => void;
}) => {
  const [isNewScaleDisabled, setIsNewScaleDisabled] = useState(false);

  useEffect(() => {
    const manager = compareManager.current;

    if (!isOpen || !manager) {
      setIsNewScaleDisabled(false);
      return;
    }

    setIsNewScaleDisabled(manager.isNewScaleDisabled());

    const subscription = manager.isNewScaleDisabledObservable().subscribe(setIsNewScaleDisabled);

    return () => subscription.unsubscribe();
  }, [compareManager, isOpen]);

  const handlePercent = (symbol: string) => {
    compareManager?.current?.setSymbolMode('Line', symbol, CompareMode.Percentage);
  };

  const handleNewScale = (symbol: string) => {
    compareManager?.current?.setSymbolMode('Line', symbol, CompareMode.NewScale);
  };

  const handleNewPanel = (symbol: string) => {
    compareManager?.current?.setSymbolMode('Line', symbol, CompareMode.NewPane);
  };

  return (
    <InstrumentSearch
      setOpen={setOpen}
      isOpen={isOpen}
      variant="single"
      widgetId={widgetId}
      withNRD={false}
      // Временный костыль, пока не завезем свой поиск интструментов
      addInstruments={() => {
        // nothing
      }}
      isNewScaleDisabled={isNewScaleDisabled}
      customActionsFooterHandlers={{
        handlePercent,
        handleNewScale,
        handleNewPanel,
      }}
    />
  );
};

import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';

import { moexChartTimeConverter } from '@utils/chartToReqTimeConverter';

import { requestBars, requestRealtimeBars } from '../../requestBars';
import { ChartIndicativeData } from '../../types';

import type { Candle, Timeframes } from 'moex-chart';

dayjs.extend(duration);

function normalizeSymbol(symbolRaw?: string): string {
  return String(symbolRaw ?? '')
    .trim()
    .toUpperCase();
}

class DataSourceProvider {
  static instance: DataSourceProvider | null = null;

  private realtimeTimer: ReturnType<typeof setInterval> | null = null;

  static getInstance(): DataSourceProvider {
    if (!this.instance) {
      this.instance = new DataSourceProvider();
    }
    return this.instance;
  }

  static getDataSource =
    (indicativeData?: ChartIndicativeData | undefined, cb?: (tf: Timeframes) => void) =>
    async (timeframe: Timeframes, symbol: string, until?: Candle) => {
      cb?.(timeframe);

      const interval = moexChartTimeConverter(timeframe);

      const date = until?.time || Math.round(Date.now() / 1000);

      const data = await requestBars({
        currencyPair: symbol.replaceAll(':', '.'),
        interval,
        periodParams: {
          firstDataRequest: true,
          to: date,
          from: Date.now(),
          countBack: 2000,
        },
        ticker: symbol,
        indicativeData,
      });

      return data.length === 0 ? null : data;
    };

  public startRealtime({
    getSymbols,
    getTimeframe,
    update,
    periodMs = 60_000,
    indicativeData,
  }: {
    getSymbols: () => string[];
    getTimeframe: () => Timeframes;
    update: (symbol: string, candle: Candle) => void;
    periodMs?: number;
    indicativeData?: ChartIndicativeData | undefined;
  }) {
    if (this.realtimeTimer) {
      clearInterval(this.realtimeTimer);
    }

    this.realtimeTimer = setInterval(() => {
      const tf = getTimeframe();
      const symbols = getSymbols();

      symbols.forEach(async (value) => {
        const symbol = normalizeSymbol(value);
        const interval = moexChartTimeConverter(tf);
        const currencyPair = symbol.replaceAll(':', '.');

        const data = await requestRealtimeBars({
          currencyPair,
          interval,
          ticker: symbol,
          indicativeData,
        });

        if (data) {
          update(symbol, data);
        }
      });
    }, periodMs);

    return () => {
      if (this.realtimeTimer) {
        clearInterval(this.realtimeTimer);
      }

      this.realtimeTimer = null;
    };
  }
}

export { DataSourceProvider };
export const dataSourceProvider = DataSourceProvider.getInstance();


Тесты, которые есть:
import { renderHook } from '@testing-library/react';

import { useAppSelect } from '@hooks/useAppSelector';
import { useContracts } from '@modules/contracts';
import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';

import { useChartPublicContext } from '../hooks/useChartPublicContext';

// Mock the dependencies
jest.mock('@hooks/useAppSelector');
jest.mock('@modules/contracts');
jest.mock('@utils/filterByUniqIssKey');

jest.mock('@api/index', () => ({
  updateMenuLocked: jest.fn(),
  widgetsController: {
    delete: jest.fn(),
  },
  widgetPropertiesController: {
    update: jest.fn(),
  },
  workspaceController: {
    update: jest.fn(),
  },
}));
jest.mock('@api/controllers/workspace', () => ({
  workspaceController: {
    update: jest.fn(),
  },
}));

describe('useChartPublicContext', () => {
  const mockUseAppSelect = useAppSelect as jest.Mock;
  const mockUseContracts = useContracts as jest.Mock;
  const mockFilterByUniqIssKey = filterByUniqIssKey as jest.Mock;

  const mockContracts = [
    {
      issKey: 'MOEX:TEST1',
      instrName: 'Test Instrument 1',
      symbol: 'TEST1',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST2',
      instrName: 'Test Instrument 2',
      symbol: 'TEST2',
      // ... other contract properties
    },
    {
      issKey: 'MOEX:TEST3',
      instrName: 'Test Instrument 3',
      symbol: 'TEST3',
      // ... other contract properties
    },
  ];

  const mockWidget = {
    id: 1,
    name: 'Test Widget',
    type: 'graphic',
    master: 123,
    externalProperties: [{ key: 'instrument', value: 'MOEX:TEST1' }],
    // ... other widget properties
  };

  const mockPublicContext = {
    instrument: 'MOEX:TEST1',
  };

  beforeEach(() => {
    jest.clearAllMocks();

    // Mock the useAppSelect to return our test data
    mockUseAppSelect.mockImplementation((selector) => {
      if (selector.toString().includes('publicContext')) {
        return mockPublicContext;
      }
      if (selector.toString().includes('widgets')) {
        return mockWidget;
      }
      return {};
    });

    // Mock useContracts to return our test contracts
    mockUseContracts.mockReturnValue({
      contracts: mockContracts,
    });

    // Mock filterByUniqIssKey to return the same contracts
    mockFilterByUniqIssKey.mockImplementation((contracts, keys) => contracts);
  });

  it('should return the correct issKey when a matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBe('MOEX:TEST1');
  });

  it('should return undefined when no matching instrument is found', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:NONEXISTENT');

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should return undefined when getMasterInstrumentFromPublicContext returns null', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should return undefined when getMasterInstrumentFromPublicContext returns undefined', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(undefined);

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });

  it('should set current instrument to DEFAULT_SYMBOL when no field value and widget has master', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue(null);

    // Act
    renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(mockSetCurrInstrument).toHaveBeenCalledWith(DEFAULT_SYMBOL);
  });

  it('should set current instrument to the found issKey when a matching instrument exists', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST2');

    // Act
    renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(mockSetCurrInstrument).toHaveBeenCalledWith('MOEX:TEST2');
  });

  it('should handle case when widget is not found in widgets array', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Mock useAppSelect to return null for widget (widget not found)
    mockUseAppSelect.mockImplementation((selector) => {
      if (selector.toString().includes('publicContext')) {
        return mockPublicContext;
      }
      if (selector.toString().includes('widgets')) {
        return null; // Widget not found
      }
      return {};
    });

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 999, // Non-existent widget ID
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBe('MOEX:TEST1');
  });

  it('should handle case when contracts array is empty', () => {
    // Arrange
    const mockSetCurrInstrument = jest.fn();
    const mockGetMasterInstrumentFromPublicContext = jest.fn().mockReturnValue('MOEX:TEST1');

    // Mock useContracts to return empty contracts
    mockUseContracts.mockReturnValue({
      contracts: [],
    });

    // Act
    const { result } = renderHook(() =>
      useChartPublicContext({
        widgetId: 1,
        setCurrInstrument: mockSetCurrInstrument,
        getMasterInstrumentFromPublicContext: mockGetMasterInstrumentFromPublicContext,
      }),
    );

    // Assert
    expect(result.current.issKey).toBeUndefined();
  });
});


import { useEffect, useMemo } from 'react';

import { useAppSelect } from '@hooks/useAppSelector';

import { Contract, useContracts } from '@modules/contracts';
import { filterByUniqIssKey } from '@utils/filterByUniqIssKey';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { Widget } from 'types/Widgets';

interface UseChartPublicContextArg {
  setCurrInstrument: (instrumentId: string) => void;
  widgetId: Widget['id'];
  getMasterInstrumentFromPublicContext: () => string | number | null | undefined;
}

interface UseChartPublicContextReturn {
  issKey: Contract['issKey'] | undefined;
}

export function useChartPublicContext({
  widgetId,
  setCurrInstrument,
  getMasterInstrumentFromPublicContext,
}: UseChartPublicContextArg): UseChartPublicContextReturn {
  const publicContext = useAppSelect((state) => state.publicContext.publicContext);
  const widget = useAppSelect((state) => state.widgets.widgets.find(({ id }) => id === widgetId));

  const { contracts } = useContracts();

  const instruments = useMemo(() => [...(contracts && filterByUniqIssKey(contracts, ['issKey']))], [contracts]);

  const issKey = useMemo(() => {
    const fieldValue = getMasterInstrumentFromPublicContext();
    const instrKey = instruments.find((item) => item.issKey === fieldValue)?.issKey;
    return instrKey;
    // eslint-disable-next-line react-hooks/exhaustive-deps -- Посмотреть этот момент.
  }, [publicContext, instruments]);

  useEffect(() => {
    const fieldValue = getMasterInstrumentFromPublicContext();
    if (!fieldValue && widget?.master) {
      setCurrInstrument(DEFAULT_SYMBOL);
      return;
    }
    const instrKey = instruments.find((item) => item.issKey === fieldValue)?.issKey;
    if (instrKey) {
      setCurrInstrument(instrKey);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- Посмотреть этот момент.
  }, [publicContext, instruments, widget?.externalProperties, setCurrInstrument]);

  return {
    issKey: issKey ?? undefined,
  };
}

import { indicativeQuotesController } from '@api/controllers/indicativeQuotesController';
import api from '@api/index';
import { candleToBar } from '@utils/candleToBar';

import { requestBars, requestRealtimeBars } from '../requestBars';
import { isIndicativeTicker } from '../utils/isIndicativeTicker';

// Mock the dependencies
jest.mock('../utils/isIndicativeTicker');
jest.mock('@utils/candleToBar');

// Mock the api module
jest.mock('@api/index', () => ({
  __esModule: true,
  default: {
    getBars: jest.fn(),
  },
}));

// Mock the indicativeQuotesController module
jest.mock('@api/controllers/indicativeQuotesController', () => ({
  indicativeQuotesController: {
    getCandles: jest.fn(),
  },
}));

describe('requestBars', () => {
  const mockPeriodParams = {
    countBack: 100,
    from: 1640195200,
    to: 1640995200, // 2022-01-01T00:00:00Z
    firstDataRequest: true,
  };

  const mockCandle = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    ticker: 'TEST',
    volume: 1000,
    end: '2022-01-01T10:00:00Z',
    interval: '1',
  };

  const mockBar = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    volume: 1000,
    time: 1640995200000,
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should handle indicative instrument correctly when indicativeData matches ticker', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockIndicativeData = {
      key: 'test-ticker',
      secId: 'string',
      instrumentName: 'string',
      settlement: 'string',
      firmName: 'string',
    };

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'test-ticker',
      indicativeData: mockIndicativeData,
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: mockPeriodParams.countBack,
      key: 'USD/RUB',
      date: '2022-01-01T00:00:00',
      interval: '1',
    });
    expect(result).toEqual([mockBar]);
  });

  it('should handle indicative instrument correctly when ticker contains indicative board', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: mockPeriodParams.countBack,
      key: 'USD/RUB',
      date: '2022-01-01T00:00:00',
      interval: '1',
    });
    expect(result).toEqual([mockBar]);
  });

  it('should handle non-indicative instrument correctly', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: [mockCandle],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST',
    });

    // Assert
    expect(api.getBars).toHaveBeenCalledWith({
      currencyPair: 'USD/RUB',
      date: '2022-01-01%2000:00:00',
      interval: '1',
      count: mockPeriodParams.countBack,
      ticker: 'TEST',
    });
    expect(result).toEqual([mockBar]);
  });

  it('should call onHistoryCallback with data when indicative instrument succeeds', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    const mockHistoryCallback = jest.fn();

    // Act
    await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([mockBar], { noData: false });
  });

  it('should call onHistoryCallback with empty array and noData flag when indicative instrument fails', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    (
      indicativeQuotesController.getCandles as unknown as { mockRejectedValue: (mockResponse: object) => void }
    ).mockRejectedValue(new Error('API Error'));

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });

  it('should call onHistoryCallback with empty array and noData flag when non-indicative instrument fails', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    (api.getBars as unknown as { mockRejectedValue: (mockResponse: object) => void }).mockRejectedValue(
      new Error('API Error'),
    );

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });

  it('should handle multiple candles for indicative instrument', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockCandles = [
      { ...mockCandle, end: '2022-01-01T10:00:00Z' },
      { ...mockCandle, end: '2022-01-01T11:00:00Z' },
      { ...mockCandle, end: '2022-01-01T12:00:00Z' },
    ];

    const mockResponse = {
      data: {
        indicativeCandles: mockCandles,
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: mockPeriodParams.countBack,
      key: 'USD/RUB',
      date: '2022-01-01T00:00:00',
      interval: '1',
    });
    expect(candleToBar).toHaveBeenCalledTimes(3);
    expect(result).toEqual([mockBar, mockBar, mockBar]);
  });

  it('should handle multiple candles for non-indicative instrument', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockCandles = [
      { ...mockCandle, end: '2022-01-01T10:00:00Z' },
      { ...mockCandle, end: '2022-01-01T11:00:00Z' },
      { ...mockCandle, end: '2022-01-01T12:00:00Z' },
    ];

    const mockResponse = {
      data: mockCandles,
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      ticker: 'TEST',
    });

    // Assert
    expect(api.getBars).toHaveBeenCalledWith({
      currencyPair: 'USD/RUB',
      date: '2022-01-01%2000:00:00',
      interval: '1',
      count: mockPeriodParams.countBack,
      ticker: 'TEST',
    });
    expect(candleToBar).toHaveBeenCalledTimes(3);
    expect(result).toEqual([mockBar, mockBar, mockBar]);
  });

  it('should handle empty indicativeCandles array', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    const mockResponse = {
      data: {
        indicativeCandles: [],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });

  it('should handle empty bars array for non-indicative instrument', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    const mockResponse = {
      data: [],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    const mockHistoryCallback = jest.fn();

    // Act
    const result = await requestBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      periodParams: mockPeriodParams,
      onHistoryCallback: mockHistoryCallback,
      ticker: 'TEST',
    });

    // Assert
    expect(mockHistoryCallback).toHaveBeenCalledWith([], { noData: true });
    expect(result).toEqual([]);
  });
});

describe('requestRealtimeBars', () => {
  const mockCandle = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    ticker: 'TEST',
    volume: 1000,
    end: '2022-01-01T10:00:00Z',
    interval: '1',
  };

  const mockBar = {
    open: 100,
    close: 110,
    high: 120,
    low: 90,
    volume: 1000,
    time: 1640995200000,
  };

  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should handle indicative instrument correctly', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(indicativeQuotesController.getCandles).toHaveBeenCalledWith({
      count: 1,
      key: 'USD/RUB',
      date: expect.any(String), // We can't predict the exact date string
      interval: '1',
    });
    expect(candleToBar).toHaveBeenCalledWith(mockCandle);
    expect(result).toEqual(mockBar);
  });

  it('should handle non-indicative instrument correctly', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: [mockCandle],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST',
    });

    // Assert
    expect(api.getBars).toHaveBeenCalledWith({
      currencyPair: 'USD/RUB',
      date: expect.any(String), // We can't predict the exact date string
      interval: '1',
      count: 1,
      ticker: 'TEST',
    });
    expect(candleToBar).toHaveBeenCalledWith(mockCandle);
    expect(result).toEqual(mockBar);
  });

  it('should handle empty indicativeCandles array in realtime request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    const mockResponse = {
      data: {
        indicativeCandles: [],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST.indicative_spot',
    });

    // Assert
    expect(result).toBeUndefined();
  });

  it('should handle empty bars array in realtime request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    const mockResponse = {
      data: [],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    // Act
    const result = await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST',
    });

    // Assert
    expect(result).toBeUndefined();
  });

  it('should call onRealtimeCallback with bar when indicative instrument succeeds', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: {
        indicativeCandles: [mockCandle],
      },
    };

    (
      indicativeQuotesController.getCandles as unknown as { mockResolvedValue: (mockResponse: object) => void }
    ).mockResolvedValue(mockResponse);

    const mockRealtimeCallback = jest.fn();

    // Act
    await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST.indicative_spot',
      onRealtimeCallback: mockRealtimeCallback,
    });

    // Assert
    expect(mockRealtimeCallback).toHaveBeenCalledWith(mockBar);
  });

  it('should call onRealtimeCallback with bar when non-indicative instrument succeeds', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);
    (candleToBar as jest.Mock).mockReturnValue(mockBar);

    const mockResponse = {
      data: [mockCandle],
    };

    (api.getBars as unknown as { mockResolvedValue: (mockResponse: object) => void }).mockResolvedValue(mockResponse);

    const mockRealtimeCallback = jest.fn();

    // Act
    await requestRealtimeBars({
      currencyPair: 'USD/RUB',
      interval: '1',
      ticker: 'TEST',
      onRealtimeCallback: mockRealtimeCallback,
    });

    // Assert
    expect(mockRealtimeCallback).toHaveBeenCalledWith(mockBar);
  });

  it('should handle error in indicative instrument request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(true);

    (
      indicativeQuotesController.getCandles as unknown as { mockRejectedValue: (mockResponse: object) => void }
    ).mockRejectedValue(new Error('API Error'));

    const mockRealtimeCallback = jest.fn();

    // Act & Assert
    await expect(
      requestRealtimeBars({
        currencyPair: 'USD/RUB',
        interval: '1',
        ticker: 'TEST.indicative_spot',
        onRealtimeCallback: mockRealtimeCallback,
      }),
    ).resolves.toBeUndefined();
  });

  it('should handle error in non-indicative instrument request', async () => {
    // Arrange
    (isIndicativeTicker as jest.Mock).mockReturnValue(false);

    (api.getBars as unknown as { mockRejectedValue: (mockResponse: object) => void }).mockRejectedValue(
      new Error('API Error'),
    );

    const mockRealtimeCallback = jest.fn();

    // Act & Assert
    await expect(
      requestRealtimeBars({
        currencyPair: 'USD/RUB',
        interval: '1',
        ticker: 'TEST',
        onRealtimeCallback: mockRealtimeCallback,
      }),
    ).resolves.toBeUndefined();
  });
});