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


it('should convolve history data when ISS timeframe differs', async () => {
  // Arrange
  const baseTime = Date.parse('2026-05-19T10:00:00Z');

  mockMoexChartToIssTimeframe.mockReturnValue(Timeframes['1m']);
  mockParseTimeframe.mockImplementation((timeframe: TimeframesType) =>
    timeframe === Timeframes['5m']
      ? {
          candleWidth: 5,
          dayjsUnit: 'minute',
        }
      : {
          candleWidth: 1,
          dayjsUnit: 'minute',
        },
  );
  mockGetStartTime
    .mockImplementationOnce(() => baseTime / 1000)
    .mockImplementation(() => (baseTime + 10 * 60_000) / 1000);

  mockRequestBars.mockResolvedValue([
    {
      time: baseTime,
      open: 100,
      high: 101,
      low: 99,
      close: 100,
      volume: 1,
    },
    {
      time: baseTime + 60_000,
      open: 100,
      high: 103,
      low: 98,
      close: 102,
      volume: 2,
    },
    {
      time: baseTime + 2 * 60_000,
      open: 102,
      high: 104,
      low: 97,
      close: 103,
      volume: 3,
    },
    {
      time: baseTime + 3 * 60_000,
      open: 103,
      high: 105,
      low: 96,
      close: 104,
      volume: 4,
    },
    {
      time: baseTime + 4 * 60_000,
      open: 104,
      high: 106,
      low: 95,
      close: 105,
      volume: 5,
    },
    {
      time: baseTime + 5 * 60_000,
      open: 105,
      high: 107,
      low: 94,
      close: 106,
      volume: 6,
    },
  ]);

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

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

  // Assert
  expect(result).toEqual([
    {
      time: baseTime,
      open: 100,
      high: 106,
      low: 95,
      close: 105,
      volume: 15,
    },
  ]);
});

it('should convolve incomplete history candle group', async () => {
  // Arrange
  const baseTime = Date.parse('2026-05-19T10:00:00Z');

  mockMoexChartToIssTimeframe.mockReturnValue(Timeframes['1m']);
  mockParseTimeframe.mockImplementation((timeframe: TimeframesType) =>
    timeframe === Timeframes['5m']
      ? {
          candleWidth: 5,
          dayjsUnit: 'minute',
        }
      : {
          candleWidth: 1,
          dayjsUnit: 'minute',
        },
  );
  mockGetStartTime
    .mockImplementationOnce(() => baseTime / 1000)
    .mockImplementation(() => (baseTime + 10 * 60_000) / 1000);

  mockRequestBars.mockResolvedValue([
    {
      time: baseTime,
      open: 100,
      high: 101,
      low: 99,
      close: 100,
      volume: 1,
    },
    {
      time: baseTime + 60_000,
      open: 100,
      high: 103,
      low: 98,
      close: 102,
      volume: 2,
    },
    {
      time: baseTime + 2 * 60_000,
      open: 102,
      high: 104,
      low: 97,
      close: 103,
      volume: 3,
    },
  ]);

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

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

  // Assert
  expect(result).toEqual([
    {
      time: baseTime,
      open: 100,
      high: 104,
      low: 97,
      close: 103,
      volume: 6,
    },
  ]);
});

it('should return empty data when convolution start index is not found', async () => {
  // Arrange
  const baseTime = Date.parse('2026-05-19T10:00:00Z');

  mockMoexChartToIssTimeframe.mockReturnValue(Timeframes['1m']);
  mockParseTimeframe.mockImplementation((timeframe: TimeframesType) =>
    timeframe === Timeframes['5m']
      ? {
          candleWidth: 5,
          dayjsUnit: 'minute',
        }
      : {
          candleWidth: 1,
          dayjsUnit: 'minute',
        },
  );
  mockGetStartTime.mockReturnValue(1);

  mockRequestBars.mockResolvedValue([
    {
      time: baseTime,
      open: 100,
      high: 101,
      low: 99,
      close: 100,
      volume: 1,
    },
    {
      time: baseTime + 60_000,
      open: 100,
      high: 102,
      low: 98,
      close: 101,
      volume: 2,
    },
  ]);

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

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

  // Assert
  expect(result).toEqual([]);
});

it('should append, replace and reset realtime candles during convolution', async () => {
  // Arrange
  const baseTime = Date.parse('2026-05-19T10:00:00Z');

  mockMoexChartToIssTimeframe.mockReturnValue(Timeframes['1m']);
  mockParseTimeframe.mockImplementation((timeframe: TimeframesType) =>
    timeframe === Timeframes['5m']
      ? {
          candleWidth: 5,
          dayjsUnit: 'minute',
        }
      : {
          candleWidth: 1,
          dayjsUnit: 'minute',
        },
  );
  mockGetStartTime
    .mockImplementationOnce(() => baseTime / 1000)
    .mockImplementation(() => (baseTime + 10 * 60_000) / 1000);

  mockRequestBars.mockResolvedValue([
    {
      time: baseTime,
      open: 100,
      high: 101,
      low: 99,
      close: 100,
      volume: 1,
    },
    {
      time: baseTime + 60_000,
      open: 100,
      high: 102,
      low: 98,
      close: 101,
      volume: 2,
    },
  ]);

  const provider = new DataSourceProvider();

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

  mockGetStartTime.mockImplementation(
    (_timeframe: TimeframesType, time: number) =>
      (Math.floor(time / (5 * 60_000)) * 5 * 60_000) / 1000,
  );

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

  const secondCandle = {
    time: baseTime + 6 * 60_000,
    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 + 10 * 60_000,
    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,
  });

  // Act
  jest.advanceTimersByTime(1000);
  await flushPromises();

  jest.advanceTimersByTime(1000);
  await flushPromises();

  jest.advanceTimersByTime(1000);
  await flushPromises();

  jest.advanceTimersByTime(1000);
  await flushPromises();

  unsubscribe();

  // Assert
  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);
});

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

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

  mockRequestRealtimeBars.mockResolvedValue(realtimeCandle);

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

  // Act
  jest.advanceTimersByTime(1000);
  await flushPromises();

  jest.advanceTimersByTime(1000);
  await flushPromises();

  unsubscribe();

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

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

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

  // Act
  jest.advanceTimersByTime(1000);
  await flushPromises();

  unsubscribe();

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

it('should replace existing realtime timer when startRealtime is called again', async () => {
  // Arrange
  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,
  });

  // Act
  jest.advanceTimersByTime(1000);
  await flushPromises();

  unsubscribe();

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