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