Загрузка данных
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);
});