Загрузка данных
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { getStartTime, parseTimeframe } from 'moex-chart';
import {
getISSOddTimeframePart,
moexChartTimeConverter,
moexChartToIssTimeframe,
} from '@utils/chartToReqTimeConverter';
import { DEFAULT_SYMBOL } from '@widgets/Chart/const';
import { requestBars, requestRealtimeBars } from '../../requestBars';
import { ChartIndicativeData } from '../../types';
import type { ManipulateType } from 'dayjs';
import type { Candle, Timeframes } from 'moex-chart';
dayjs.extend(duration);
function getRequestSymbol(symbolRaw?: string): string | undefined {
const symbol = String(symbolRaw ?? '').trim();
if (!symbol || symbol === DEFAULT_SYMBOL) {
return undefined;
}
return symbol;
}
const collectCandleGroup = ({
data,
startIndex,
iterationCounter,
startTime,
}: {
data: Candle[];
startIndex: number;
iterationCounter: number;
startTime: number;
}): {
aggregatedCandle: Candle | undefined;
consumedCount: number;
} => {
let aggregatedCandle: Candle | undefined;
let offset = 0;
let consumedCount = 0;
while (offset < iterationCounter) {
const currentCandle = data[startIndex + offset];
if (!currentCandle) {
break;
}
if (!aggregatedCandle) {
aggregatedCandle = currentCandle;
} else {
aggregatedCandle = {
...aggregatedCandle,
high: Math.max(currentCandle.high, aggregatedCandle.high),
low: Math.min(currentCandle.low, aggregatedCandle.low),
close: currentCandle.close,
volume: (aggregatedCandle.volume ?? 0) + (currentCandle.volume ?? 0),
};
}
consumedCount += 1;
if (startIndex + offset + 1 === data.length || currentCandle.time > startTime * 1000) {
break;
}
offset += 1;
}
return {
aggregatedCandle,
consumedCount,
};
};
const proceedConvolution = ({
data,
moexCandle,
requestedTimeframe,
dayjsUnit,
iterationCounter,
}: {
data: Candle[];
moexCandle: number;
requestedTimeframe: Timeframes;
dayjsUnit: ManipulateType;
iterationCounter: number;
}): Candle[] => {
const result: Candle[] = [];
let index = 0;
while (index < data.length - 1) {
const firstCandle = data[index];
if (!firstCandle) {
break;
}
const startTime = getStartTime(
requestedTimeframe,
dayjs(firstCandle.time).add(moexCandle, dayjsUnit).unix() * 1000,
);
const { aggregatedCandle, consumedCount } = collectCandleGroup({
data,
startIndex: index,
iterationCounter,
startTime,
});
if (!aggregatedCandle || consumedCount === 0) {
break;
}
result.push(aggregatedCandle);
index += consumedCount;
}
return result;
};
const timeframeConvolution = (data: Candle[], requestedTimeframe: Timeframes): Candle[] => {
const issTimeframe = moexChartToIssTimeframe(requestedTimeframe);
if (issTimeframe === requestedTimeframe) {
return data;
}
const { candleWidth: moexCandle, dayjsUnit } = parseTimeframe(requestedTimeframe);
const firstCandle = data[0];
if (!firstCandle) {
return [];
}
const dataStartTime = getStartTime(
requestedTimeframe,
dayjs(firstCandle.time).add(moexCandle, dayjsUnit).unix() * 1000,
);
const { value, unit } = getISSOddTimeframePart(issTimeframe);
let startIndex = 0;
while (
startIndex < data.length &&
startIndex < 60 &&
dataStartTime !== dayjs(data[startIndex].time).subtract(value, unit).unix()
) {
startIndex += 1;
}
if (startIndex >= data.length || startIndex >= 60) {
return [];
}
const { candleWidth: issCandle } = parseTimeframe(issTimeframe);
return proceedConvolution({
data: data.slice(startIndex),
moexCandle,
requestedTimeframe,
dayjsUnit,
iterationCounter: moexCandle / issCandle,
});
};
// По хорошему - класс должен быть синглтоном, чтобы кормить MoexChart одинаковой датой,
// и не плодить несколько подключений на одни символа
class DataSourceProvider {
private prevRealtimeDataArr: Candle[] = [];
private prevRealtimeData: Candle | undefined;
private realtimeShouldBeConvoluted = false;
private realtimeTimer: ReturnType<typeof setInterval> | null = null;
public getDataSource =
(indicativeData?: ChartIndicativeData, cb?: (timeframe: Timeframes) => void) =>
async (timeframe: Timeframes, symbolId: string, until?: Candle) => {
cb?.(timeframe);
const symbol = getRequestSymbol(symbolId);
if (!symbol) {
return null;
}
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,
});
if (data.length === 0) {
return null;
}
const issTimeframe = moexChartToIssTimeframe(timeframe);
if (issTimeframe === timeframe) {
this.realtimeShouldBeConvoluted = false;
return data;
}
this.realtimeShouldBeConvoluted = true;
return timeframeConvolution(data, timeframe);
};
public startRealtime({
getSymbols,
getTimeframe,
update,
periodMs = 5000,
indicativeData,
}: {
getSymbols: () => string[];
getTimeframe: () => Timeframes;
update: (symbolId: string, candle: Candle) => void;
periodMs?: number;
indicativeData?: ChartIndicativeData;
}): () => void {
if (this.realtimeTimer) {
clearInterval(this.realtimeTimer);
}
this.realtimeTimer = setInterval(() => {
const timeframe = getTimeframe();
const symbolIds = getSymbols();
Promise.all(
symbolIds.map(async (symbolId) => {
const symbol = getRequestSymbol(symbolId);
if (!symbol) {
return;
}
const data = await requestRealtimeBars({
currencyPair: symbol.replaceAll(':', '.'),
interval: moexChartTimeConverter(timeframe),
ticker: symbol,
indicativeData,
});
if (!data) {
return;
}
if (!this.prevRealtimeData) {
this.prevRealtimeData = data;
this.prevRealtimeDataArr = [data];
update(symbol, data);
return;
}
if (JSON.stringify(data) !== JSON.stringify(this.prevRealtimeData)) {
this.realtimeConvolution(timeframe, data, (candle) => {
update(symbol, candle);
});
}
}),
);
}, periodMs);
return () => {
if (this.realtimeTimer) {
clearInterval(this.realtimeTimer);
}
this.realtimeTimer = null;
};
}
private realtimeConvolution(timeframe: Timeframes, data: Candle, update: (candle: Candle) => void): void {
let candle = data;
if (this.realtimeShouldBeConvoluted && this.prevRealtimeData) {
const { candleWidth: moexCandle, dayjsUnit } = parseTimeframe(timeframe);
const dataStartTimeFromPrevData = getStartTime(
timeframe,
dayjs(this.prevRealtimeData.time).add(moexCandle, dayjsUnit).unix() * 1000,
);
const dataStartTime = getStartTime(timeframe, dayjs(data.time).add(moexCandle, dayjsUnit).unix() * 1000);
const isNextTimeframeStep = dataStartTime !== dataStartTimeFromPrevData;
if (isNextTimeframeStep) {
this.prevRealtimeDataArr = [data];
} else {
const isNewBarInsideTimeframe = this.prevRealtimeData.time !== data.time;
if (isNewBarInsideTimeframe) {
this.prevRealtimeDataArr.push(data);
} else {
this.prevRealtimeDataArr[this.prevRealtimeDataArr.length - 1] = data;
}
const firstCandle = this.prevRealtimeDataArr[0];
const lastCandle = this.prevRealtimeDataArr[this.prevRealtimeDataArr.length - 1];
if (firstCandle && lastCandle) {
candle = {
time: firstCandle.time,
open: firstCandle.open,
high: Math.max(...this.prevRealtimeDataArr.map(({ high }) => high)),
low: Math.min(...this.prevRealtimeDataArr.map(({ low }) => low)),
close: lastCandle.close,
volume: this.prevRealtimeDataArr.reduce((total, currentCandle) => total + (currentCandle.volume ?? 0), 0),
};
}
}
}
this.prevRealtimeData = data;
update(candle);
}
}
export { DataSourceProvider };