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


if (!data) {
  return;
}

if (this.prevRealtimeData && JSON.stringify(data) === JSON.stringify(this.prevRealtimeData)) {
  return;
}

if (!this.realtimeShouldBeConvoluted) {
  this.prevRealtimeData = data;
  update(symbol, data);
  return;
}

this.realtimeConvolution(timeframe, data, (candle) => {
  update(symbol, candle);
});




import { DEFAULT_SYMBOL } from '@widgets/Chart/const';

import type { Timeframes as TimeframesType } from 'moex-chart';

type DataSourceProvideModule = typeof import('@widgets/Chart/components/MoexChart/dataSourceProvide');

const mockRequestBars = jest.fn();
const mockRequestRealtimeBars = jest.fn();
const mockMoexChartTimeConverter = jest.fn();
const mockParseTimeframe = jest.fn();
const mockMoexChartToIssTimeframe = jest.fn();

jest.mock('moex-chart', () => ({
  Timeframes: {
    '1m': '1m',
    '5m': '5m',
    '1h': '1h',
    '2h': '2h',
    '3h': '3h',
    '4h': '4h',
  },
  parseTimeframe: mockParseTimeframe,
}));

jest.mock('@utils/chartToReqTimeConverter', () => ({
  moexChartTimeConverter: mockMoexChartTimeConverter,
  moexChartToIssTimeframe: mockMoexChartToIssTimeframe,
}));

jest.mock('@widgets/Chart/requestBars', () => ({
  requestBars: mockRequestBars,
  requestRealtimeBars: mockRequestRealtimeBars,
}));

const { DataSourceProvider } = jest.requireActual(
  '@widgets/Chart/components/MoexChart/dataSourceProvide',
) as DataSourceProvideModule;

const Timeframes = {
  '1m': '1m' as TimeframesType,
  '5m': '5m' as TimeframesType,
  '1h': '1h' as TimeframesType,
  '2h': '2h' as TimeframesType,
  '3h': '3h' as TimeframesType,
  '4h': '4h' as TimeframesType,
};

const baseTime = Math.floor(Date.parse('2026-05-19T10:00:00Z') / 1000);

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

const createBar = (minute: number, overrides: Partial<typeof mockBar> = {}) => ({
  time: baseTime + minute * 60,
  open: 100 + minute,
  close: 101 + minute,
  high: 102 + minute,
  low: 99 - minute,
  volume: minute + 1,
  ...overrides,
});

const createHourBar = (startTime: number, hour: number) => ({
  time: startTime + hour * 60 * 60,
  open: 100 + hour,
  close: 101 + hour,
  high: 102 + hour,
  low: 99 - hour,
  volume: hour + 1,
});

const flushPromises = async (): Promise<void> => {
  await Promise.resolve();
  await Promise.resolve();
  await Promise.resolve();
};

const runRealtimeTick = async (periodMs = 1000): Promise<void> => {
  jest.advanceTimersByTime(periodMs);
  await flushPromises();
};

const configureConvolution = (
  requestedTimeframe: TimeframesType,
  sourceTimeframe: TimeframesType,
): void => {
  mockMoexChartToIssTimeframe.mockImplementation((timeframe: TimeframesType) =>
    timeframe === requestedTimeframe ? sourceTimeframe : timeframe,
  );
};

describe('DataSourceProvider', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
    jest.setSystemTime(new Date('2026-05-19T10:00:00Z'));

    mockMoexChartTimeConverter.mockReturnValue('1');
    mockMoexChartToIssTimeframe.mockImplementation((timeframe: TimeframesType) => timeframe);
    mockParseTimeframe.mockImplementation((timeframe: TimeframesType) => {
      switch (timeframe) {
        case Timeframes['5m']:
          return {
            candleWidth: 5,
            dayjsUnit: 'minutes',
          };
        case Timeframes['2h']:
          return {
            candleWidth: 2,
            dayjsUnit: 'hours',
          };
        case Timeframes['3h']:
          return {
            candleWidth: 3,
            dayjsUnit: 'hours',
          };
        case Timeframes['4h']:
          return {
            candleWidth: 4,
            dayjsUnit: 'hours',
          };
        case Timeframes['1h']:
          return {
            candleWidth: 1,
            dayjsUnit: 'hours',
          };
        default:
          return {
            candleWidth: 1,
            dayjsUnit: 'minutes',
          };
      }
    });
  });

  afterEach(() => {
    jest.clearAllTimers();
    jest.useRealTimers();
  });

  it('should request chart history data with converted timeframe', async () => {
    const mockTimeframeCallback = jest.fn();

    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource(undefined, mockTimeframeCallback);

    const result = await dataSource(Timeframes['1m'], 'MOEX:SBER');

    const now = Math.round(Date.now() / 1000);

    expect(mockTimeframeCallback).toHaveBeenCalledWith(Timeframes['1m']);
    expect(mockMoexChartTimeConverter).toHaveBeenCalledWith(Timeframes['1m']);
    expect(mockRequestBars).toHaveBeenCalledWith({
      currencyPair: 'MOEX.SBER',
      interval: '1',
      periodParams: {
        firstDataRequest: true,
        to: now,
        from: now,
        countBack: 2000,
      },
      ticker: 'MOEX:SBER',
      indicativeData: undefined,
    });
    expect(result).toEqual([mockBar]);
  });

  it('should normalize symbol before requesting chart history data', async () => {
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    await dataSource(Timeframes['1m'], '  MOEX:SBER  ');

    expect(mockRequestBars).toHaveBeenCalledWith(
      expect.objectContaining({
        currencyPair: 'MOEX.SBER',
        ticker: 'MOEX:SBER',
      }),
    );
  });

  it('should not request chart history data for default symbol', async () => {
    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['1m'], DEFAULT_SYMBOL);

    expect(result).toBeNull();
    expect(mockRequestBars).not.toHaveBeenCalled();
  });

  it('should not request chart history data for default symbol with surrounding spaces', async () => {
    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['1m'], `  ${DEFAULT_SYMBOL}  `);

    expect(result).toBeNull();
    expect(mockRequestBars).not.toHaveBeenCalled();
  });

  it('should not request chart history data for empty symbol', async () => {
    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['1m'], '   ');

    expect(result).toBeNull();
    expect(mockRequestBars).not.toHaveBeenCalled();
  });

  it('should request chart history data with indicative data', async () => {
    const indicativeData = {
      id: 1,
      title: 'Test instrument',
      secId: 'SBER',
      instrumentName: 'SBER',
      settlement: 'TQBR',
      firmName: 'Test firm',
      key: 'SBER_TBQR',
    };

    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource(indicativeData);

    await dataSource(Timeframes['1m'], 'MOEX:SBER');

    expect(mockRequestBars).toHaveBeenCalledWith(
      expect.objectContaining({
        indicativeData,
      }),
    );
  });

  it('should use until time when it is provided', async () => {
    mockMoexChartTimeConverter.mockReturnValue('5');
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const until = {
      time: baseTime - 60,
    } as NonNullable<Parameters<typeof dataSource>[2]>;

    await dataSource(Timeframes['5m'], 'MOEX:GAZP', until);

    expect(mockRequestBars).toHaveBeenCalledWith(
      expect.objectContaining({
        periodParams: expect.objectContaining({
          to: until.time,
        }),
      }),
    );
  });

  it('should return null when history data is empty', async () => {
    mockRequestBars.mockResolvedValue([]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['1m'], 'MOEX:SBER');

    expect(result).toBeNull();
  });

  it('should reuse pending history request for the same symbol, timeframe and until', async () => {
    let resolveRequest: ((value: typeof mockBar[]) => void) | undefined;

    mockRequestBars.mockImplementation(
      () =>
        new Promise<typeof mockBar[]>((resolve) => {
          resolveRequest = resolve;
        }),
    );

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const until = {
      time: baseTime - 60,
    } as NonNullable<Parameters<typeof dataSource>[2]>;

    const firstRequest = dataSource(Timeframes['1m'], 'MOEX:SBER', until);
    const secondRequest = dataSource(Timeframes['1m'], 'MOEX:SBER', until);

    expect(mockRequestBars).toHaveBeenCalledTimes(1);

    resolveRequest?.([mockBar]);

    await expect(firstRequest).resolves.toEqual([mockBar]);
    await expect(secondRequest).resolves.toEqual([mockBar]);
  });

  it('should not repeat completed history request with the same until', async () => {
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const until = {
      time: baseTime - 60,
    } as NonNullable<Parameters<typeof dataSource>[2]>;

    const firstResult = await dataSource(Timeframes['1m'], 'MOEX:SBER', until);
    const secondResult = await dataSource(Timeframes['1m'], 'MOEX:SBER', until);

    expect(firstResult).toEqual([mockBar]);
    expect(secondResult).toBeNull();
    expect(mockRequestBars).toHaveBeenCalledTimes(1);
  });

  it('should request initial history again after previous initial request is completed', async () => {
    mockRequestBars.mockResolvedValue([mockBar]);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    await dataSource(Timeframes['1m'], 'MOEX:SBER');
    await dataSource(Timeframes['1m'], 'MOEX:SBER');

    expect(mockRequestBars).toHaveBeenCalledTimes(2);
  });

  it('should request realtime data and update normalized symbol', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const realtimeBar = {
      ...mockBar,
      volume: 500,
    };

    mockRequestRealtimeBars.mockResolvedValue(realtimeBar);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => [' moex:sber '],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).toHaveBeenCalledWith({
      currencyPair: 'moex.sber',
      interval: '1',
      ticker: 'moex:sber',
      indicativeData: undefined,
    });
    expect(mockUpdate).toHaveBeenCalledWith('moex:sber', realtimeBar);
  });

  it('should not request realtime data for default symbol', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const unsubscribe = provider.startRealtime({
      getSymbols: () => [DEFAULT_SYMBOL],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('should not request realtime data for default symbol with surrounding spaces', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const unsubscribe = provider.startRealtime({
      getSymbols: () => [`  ${DEFAULT_SYMBOL}  `],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('should not request realtime data for empty normalized symbol', async () => {
    const provider = new DataSourceProvider();

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['   '],
      getTimeframe: () => Timeframes['1m'],
      update: jest.fn(),
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
  });

  it('should request realtime data with indicative data', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const indicativeData = {
      id: 1,
      title: 'Test instrument',
      secId: 'SBER',
      instrumentName: 'SBER',
      settlement: 'TQBR',
      firmName: 'Test firm',
      key: 'SBER_TBQR',
    };

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
      indicativeData,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).toHaveBeenCalledWith(
      expect.objectContaining({
        indicativeData,
      }),
    );
  });

  it('should not call update when realtime data is empty', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    mockRequestRealtimeBars.mockResolvedValue(undefined);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockUpdate).not.toHaveBeenCalled();
  });

  it('should not request realtime data when symbols list is empty', async () => {
    const provider = new DataSourceProvider();

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => [],
      getTimeframe: () => Timeframes['1m'],
      update: jest.fn(),
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
  });

  it('should clear realtime timer on unsubscribe', async () => {
    const provider = new DataSourceProvider();

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: jest.fn(),
      periodMs: 1000,
    });

    unsubscribe();

    await runRealtimeTick();

    expect(mockRequestRealtimeBars).not.toHaveBeenCalled();
  });

  it('should replace existing realtime timer when startRealtime is called again', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).toHaveBeenCalledTimes(1);
    expect(mockUpdate).toHaveBeenCalledTimes(1);
  });

  it('should update distinct realtime candles without convolution', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    const firstCandle = {
      ...mockBar,
      close: 110,
    };

    const secondCandle = {
      ...mockBar,
      time: mockBar.time + 60,
      close: 111,
    };

    mockRequestRealtimeBars.mockResolvedValueOnce(firstCandle).mockResolvedValueOnce(secondCandle);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();
    await runRealtimeTick();

    unsubscribe();

    expect(mockUpdate).toHaveBeenNthCalledWith(1, 'MOEX:SBER', firstCandle);
    expect(mockUpdate).toHaveBeenNthCalledWith(2, 'MOEX:SBER', secondCandle);
  });

  it('should not update chart for duplicated realtime candle', async () => {
    const provider = new DataSourceProvider();
    const mockUpdate = jest.fn();

    mockRequestRealtimeBars.mockResolvedValue(mockBar);

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['1m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();
    await runRealtimeTick();

    unsubscribe();

    expect(mockRequestRealtimeBars).toHaveBeenCalledTimes(2);
    expect(mockUpdate).toHaveBeenCalledTimes(1);
    expect(mockUpdate).toHaveBeenCalledWith('MOEX:SBER', mockBar);
  });

  it('should convolve five minute history data from one minute candles', async () => {
    configureConvolution(Timeframes['5m'], Timeframes['1m']);

    const historyData = Array.from({ length: 11 }, (_, minute) => createBar(minute));

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    expect(result).toEqual([
      {
        time: baseTime,
        open: 100,
        close: 105,
        high: 106,
        low: 95,
        volume: 15,
      },
      {
        time: baseTime + 5 * 60,
        open: 105,
        close: 110,
        high: 111,
        low: 90,
        volume: 40,
      },
      {
        time: baseTime + 10 * 60,
        open: 110,
        close: 111,
        high: 112,
        low: 89,
        volume: 11,
      },
    ]);
  });

  it('should convolve incomplete history candle group', async () => {
    configureConvolution(Timeframes['5m'], Timeframes['1m']);

    const historyData = Array.from({ length: 8 }, (_, minute) => createBar(minute));

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    expect(result).toEqual([
      {
        time: baseTime,
        open: 100,
        close: 105,
        high: 106,
        low: 95,
        volume: 15,
      },
      {
        time: baseTime + 5 * 60,
        open: 105,
        close: 108,
        high: 109,
        low: 92,
        volume: 21,
      },
    ]);
  });

  it('should start a new convolution session after a large gap', async () => {
    configureConvolution(Timeframes['5m'], Timeframes['1m']);

    const historyData = [
      createBar(0),
      createBar(1),
      createBar(2),
      createBar(3),
      createBar(4),
      createBar(5),
      createBar(11),
      createBar(12),
    ];

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['5m'], 'MOEX:SBER');

    expect(result).toEqual([
      {
        time: baseTime,
        open: 100,
        close: 105,
        high: 106,
        low: 95,
        volume: 15,
      },
      {
        time: baseTime + 5 * 60,
        open: 105,
        close: 106,
        high: 107,
        low: 94,
        volume: 6,
      },
      {
        time: baseTime + 11 * 60,
        open: 111,
        close: 113,
        high: 114,
        low: 87,
        volume: 25,
      },
    ]);
  });

  it.each([
    [Timeframes['2h'], 5, [0, 2, 4]],
    [Timeframes['3h'], 7, [0, 3, 6]],
  ])(
    'should convolve %s history from one hour candles',
    async (timeframe, candlesCount, expectedHourOffsets) => {
      configureConvolution(timeframe, Timeframes['1h']);
      mockMoexChartTimeConverter.mockReturnValue('60');

      const sessionStart = Math.floor(Date.parse('2026-07-31T03:00:00Z') / 1000);
      const historyData = Array.from({ length: candlesCount }, (_, hour) => createHourBar(sessionStart, hour));

      mockRequestBars.mockResolvedValue(historyData);

      const provider = new DataSourceProvider();
      const dataSource = provider.getDataSource();

      const result = await dataSource(timeframe, 'MOEX:SBER');

      expect(mockRequestBars).toHaveBeenCalledWith(
        expect.objectContaining({
          interval: '60',
        }),
      );
      expect(result?.map(({ time }) => time)).toEqual(
        expectedHourOffsets.map((hour) => sessionStart + hour * 60 * 60),
      );
    },
  );

  it('should align four hour candles to the beginning of the trading session', async () => {
    configureConvolution(Timeframes['4h'], Timeframes['1h']);
    mockMoexChartTimeConverter.mockReturnValue('60');

    const sessionStart = Math.floor(Date.parse('2026-07-31T03:00:00Z') / 1000);
    const historyData = Array.from({ length: 12 }, (_, hour) => createHourBar(sessionStart, hour));

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['4h'], 'MOEX:SBER');

    expect(result?.map(({ time }) => time)).toEqual([
      sessionStart,
      sessionStart + 4 * 60 * 60,
      sessionStart + 8 * 60 * 60,
    ]);
  });

  it('should reset four hour convolution at the next trading session', async () => {
    configureConvolution(Timeframes['4h'], Timeframes['1h']);

    const firstSessionStart = Math.floor(Date.parse('2026-07-30T03:00:00Z') / 1000);
    const secondSessionStart = Math.floor(Date.parse('2026-07-31T03:00:00Z') / 1000);

    const historyData = [
      ...Array.from({ length: 9 }, (_, hour) => createHourBar(firstSessionStart, hour)),
      ...Array.from({ length: 5 }, (_, hour) => createHourBar(secondSessionStart, hour)),
    ];

    mockRequestBars.mockResolvedValue(historyData);

    const provider = new DataSourceProvider();
    const dataSource = provider.getDataSource();

    const result = await dataSource(Timeframes['4h'], 'MOEX:SBER');

    expect(result?.map(({ time }) => time)).toEqual([
      firstSessionStart,
      firstSessionStart + 4 * 60 * 60,
      firstSessionStart + 8 * 60 * 60,
      secondSessionStart,
      secondSessionStart + 4 * 60 * 60,
    ]);
  });

  it('should append, replace and reset realtime candles during convolution', async () => {
    configureConvolution(Timeframes['5m'], Timeframes['1m']);

    mockRequestBars.mockResolvedValue(Array.from({ length: 11 }, (_, minute) => createBar(minute)));

    const provider = new DataSourceProvider();

    await provider.getDataSource()(Timeframes['5m'], 'MOEX:SBER');

    const firstCandle = {
      time: baseTime + 10 * 60,
      open: 100,
      high: 103,
      low: 99,
      close: 102,
      volume: 10,
    };

    const secondCandle = {
      time: baseTime + 11 * 60,
      open: 102,
      high: 106,
      low: 98,
      close: 105,
      volume: 20,
    };

    const updatedSecondCandle = {
      ...secondCandle,
      high: 107,
      low: 97,
      close: 106,
      volume: 25,
    };

    const nextTimeframeCandle = {
      time: baseTime + 15 * 60,
      open: 106,
      high: 108,
      low: 105,
      close: 107,
      volume: 30,
    };

    mockRequestRealtimeBars
      .mockResolvedValueOnce(firstCandle)
      .mockResolvedValueOnce(secondCandle)
      .mockResolvedValueOnce(updatedSecondCandle)
      .mockResolvedValueOnce(nextTimeframeCandle);

    const mockUpdate = jest.fn();

    const unsubscribe = provider.startRealtime({
      getSymbols: () => ['MOEX:SBER'],
      getTimeframe: () => Timeframes['5m'],
      update: mockUpdate,
      periodMs: 1000,
    });

    await runRealtimeTick();
    await runRealtimeTick();
    await runRealtimeTick();
    await runRealtimeTick();

    unsubscribe();

    expect(mockUpdate).toHaveBeenNthCalledWith(1, 'MOEX:SBER', firstCandle);
    expect(mockUpdate).toHaveBeenNthCalledWith(2, 'MOEX:SBER', {
      time: firstCandle.time,
      open: firstCandle.open,
      high: 106,
      low: 98,
      close: 105,
      volume: 30,
    });
    expect(mockUpdate).toHaveBeenNthCalledWith(3, 'MOEX:SBER', {
      time: firstCandle.time,
      open: firstCandle.open,
      high: 107,
      low: 97,
      close: 106,
      volume: 35,
    });
    expect(mockUpdate).toHaveBeenNthCalledWith(4, 'MOEX:SBER', nextTimeframeCandle);
  });
});