Загрузка данных
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import { UTCTimestamp } from 'lightweight-charts';
import { BehaviorSubject } from 'rxjs';
import { Candle, getStartTime, parseTimeframe, Timeframes } from '@lib';
// todo: import from @lib, then this commit will be published into moex-chart
import { normalizeSymbol } from '@src/utils';
dayjs.extend(duration);
class DataSourceProvider {
// todo: add cache по `${symbol}${timeframe}`
static instance: DataSourceProvider | null = null;
private currentCandle: BehaviorSubject<Candle | null> = new BehaviorSubject<Candle | null>(null);
private historicalStartDate: dayjs.Dayjs;
private realtimeTimer: ReturnType<typeof setInterval> | null = null;
private realtimeLastBySymbol = new Map<string, Candle>();
constructor() {
this.historicalStartDate = dayjs(Date.now()).subtract(5, 'year');
}
static getInstance(): DataSourceProvider {
if (!this.instance) {
this.instance = new DataSourceProvider();
}
return this.instance;
}
public startRealtime(
getSymbols: () => string[],
getTimeframe: () => Timeframes,
update: (symbol: string, candle: Candle) => void,
periodMs = 500,
): () => void {
if (this.realtimeTimer) window.clearIntervalReliable(this.realtimeTimer);
this.realtimeTimer = window.setIntervalReliable(() => {
const tf = getTimeframe();
const symbols = getSymbols();
for (let i = 0; i < symbols.length; i += 1) {
const symbol = normalizeSymbol(symbols[i]);
if (!symbol) continue;
update(symbol, this.nextRealtimeCandle(tf, symbol, periodMs));
}
}, periodMs);
return () => {
if (this.realtimeTimer) window.clearIntervalReliable(this.realtimeTimer);
this.realtimeTimer = null;
};
}
private nextRealtimeCandle(timeframe: Timeframes, symbol: string, realtimePeriodMs: number): Candle {
const now = Date.now();
const time = getStartTime(timeframe, now) as UTCTimestamp;
console.log(
'[DataSourceProvider:realtime:getStartTime]',
JSON.stringify(
{
timeframe,
symbol,
now,
nowIso: new Date(now).toISOString(),
nowLocal: dayjs(now).format('YYYY-MM-DD HH:mm:ss Z'),
timezoneOffsetMinutes: new Date().getTimezoneOffset(),
result: time,
resultIso: new Date(time * 1000).toISOString(),
resultUtc: dayjs.unix(time).utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
resultLocal: dayjs.unix(time).format('YYYY-MM-DD HH:mm:ss Z'),
},
null,
2,
),
);
const prev = this.realtimeLastBySymbol.get(symbol);
const sameBar = !!prev && prev.time === time;
const basePrice = sameBar ? prev.open : prev ? prev.close : 100 + Math.random() * 20;
const open = sameBar ? prev.open : basePrice;
const close = basePrice + ((Math.random() - 0.5) * 2) / 10;
const high = Math.max(open, close) + Math.random() / 10;
const low = Math.min(open, close) - Math.random() / 10;
const { candleWidth, dayjsUnit } = parseTimeframe(timeframe);
const candleDuration = dayjs.duration(candleWidth, dayjsUnit);
const volume = Math.max(
1,
Math.floor((Math.random() * 1000000 + 100000) / (candleDuration.asMilliseconds() / realtimePeriodMs)),
);
const next: Candle = sameBar
? {
time,
open: prev.open,
high: Math.max(prev.high, high),
low: Math.min(prev.low, low),
close,
volume,
}
: {
time,
open,
high,
low,
close,
volume,
};
this.realtimeLastBySymbol.set(symbol, next);
return next;
}
/**
* Универсальная функция для генерации исторических свечей.
* @param timeframe - Таймфрейм.
* @param symbol - symbol.
* @param untilCandle - (Опционально) Свеча, ДО которой нужно генерировать данные.
*/
public async generateCandles(timeframe: Timeframes, _symbol: string, untilCandle?: Candle): Promise<Candle[] | null> {
const data: Candle[] = [];
const dataLength = 2000;
const { candleWidth, dayjsUnit } = parseTimeframe(timeframe);
const now = Date.now();
const calculatedEndSec = getStartTime(timeframe, now) as number;
const endSec = untilCandle ? untilCandle.time : calculatedEndSec;
const endTime = dayjs.unix(endSec);
console.log(
'[DataSourceProvider:generateCandles:start]',
JSON.stringify(
{
timeframe,
symbol: _symbol,
candleWidth,
dayjsUnit,
untilCandle: untilCandle
? {
time: untilCandle.time,
iso: new Date(untilCandle.time * 1000).toISOString(),
utc: dayjs.unix(untilCandle.time).utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
local: dayjs.unix(untilCandle.time).format('YYYY-MM-DD HH:mm:ss Z'),
}
: null,
now,
nowIso: new Date(now).toISOString(),
nowLocal: dayjs(now).format('YYYY-MM-DD HH:mm:ss Z'),
timezoneOffsetMinutes: new Date().getTimezoneOffset(),
calculatedEndSec,
calculatedEndIso: new Date(calculatedEndSec * 1000).toISOString(),
calculatedEndUtc: dayjs.unix(calculatedEndSec).utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
calculatedEndLocal: dayjs.unix(calculatedEndSec).format('YYYY-MM-DD HH:mm:ss Z'),
endSec,
endIso: new Date(endSec * 1000).toISOString(),
endUtc: endTime.utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
endLocal: endTime.format('YYYY-MM-DD HH:mm:ss Z'),
},
null,
2,
),
);
const historicalStartInput = this.historicalStartDate.unix() * 1000;
const historyStartUnix = getStartTime(timeframe, historicalStartInput);
const historyStartTime = dayjs.unix(historyStartUnix);
console.log(
'[DataSourceProvider:generateCandles:historyStart]',
JSON.stringify(
{
timeframe,
historicalStartInput,
historicalStartInputIso: new Date(historicalStartInput).toISOString(),
historicalStartInputLocal: dayjs(historicalStartInput).format('YYYY-MM-DD HH:mm:ss Z'),
historyStartUnix,
historyStartIso: new Date(historyStartUnix * 1000).toISOString(),
historyStartUtc: historyStartTime.utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
historyStartLocal: historyStartTime.format('YYYY-MM-DD HH:mm:ss Z'),
},
null,
2,
),
);
if (untilCandle && untilCandle.time <= historyStartUnix) {
return null;
}
const startTimeInput = endTime.subtract(candleWidth * dataLength, dayjsUnit).unix() * 1000;
const calculatedStartTime = getStartTime(timeframe, startTimeInput);
let startTime = dayjs(calculatedStartTime * 1000);
console.log(
'[DataSourceProvider:generateCandles:startTime]',
JSON.stringify(
{
timeframe,
startTimeInput,
startTimeInputIso: new Date(startTimeInput).toISOString(),
startTimeInputLocal: dayjs(startTimeInput).format('YYYY-MM-DD HH:mm:ss Z'),
calculatedStartTime,
calculatedStartTimeIso: new Date(calculatedStartTime * 1000).toISOString(),
calculatedStartTimeUtc: dayjs.unix(calculatedStartTime).utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
calculatedStartTimeLocal: dayjs.unix(calculatedStartTime).format('YYYY-MM-DD HH:mm:ss Z'),
},
null,
2,
),
);
if (startTime.isBefore(historyStartTime)) {
startTime = historyStartTime;
}
if (!startTime.isBefore(endTime)) {
return [];
}
let basePrice = untilCandle?.open ?? 100 + Math.random() * 20;
let currentTime = startTime.clone();
const limit = untilCandle ? endTime : endTime.add(candleWidth, dayjsUnit);
console.log(
'[DataSourceProvider:generateCandles:range]',
JSON.stringify(
{
timeframe,
startTimeUnix: startTime.unix(),
startTimeIso: new Date(startTime.unix() * 1000).toISOString(),
startTimeUtc: startTime.utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
startTimeLocal: startTime.format('YYYY-MM-DD HH:mm:ss Z'),
endTimeUnix: endTime.unix(),
endTimeIso: new Date(endTime.unix() * 1000).toISOString(),
endTimeUtc: endTime.utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
endTimeLocal: endTime.format('YYYY-MM-DD HH:mm:ss Z'),
limitUnix: limit.unix(),
limitIso: new Date(limit.unix() * 1000).toISOString(),
limitUtc: limit.utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
limitLocal: limit.format('YYYY-MM-DD HH:mm:ss Z'),
},
null,
2,
),
);
while (currentTime.isBefore(limit)) {
const time = currentTime.unix();
const open = basePrice;
const close = basePrice + (Math.random() - 0.5) * 2;
const high = Math.max(open, close) + Math.random();
const low = Math.min(open, close) - Math.random();
const volume = Math.floor(Math.random() * 1000000) + 100000;
data.push({
time,
open,
high,
low,
close,
volume,
});
basePrice = close;
currentTime = currentTime.add(candleWidth, dayjsUnit);
}
console.log(
'[DataSourceProvider:generateCandles:result]',
JSON.stringify(
{
timeframe,
symbol: _symbol,
length: data.length,
firstCandles: data.slice(0, 3).map((candle) => ({
time: candle.time,
iso: new Date(candle.time * 1000).toISOString(),
utc: dayjs.unix(candle.time).utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
local: dayjs.unix(candle.time).format('YYYY-MM-DD HH:mm:ss Z'),
})),
lastCandles: data.slice(-3).map((candle) => ({
time: candle.time,
iso: new Date(candle.time * 1000).toISOString(),
utc: dayjs.unix(candle.time).utc().format('YYYY-MM-DD HH:mm:ss [UTC]'),
local: dayjs.unix(candle.time).format('YYYY-MM-DD HH:mm:ss Z'),
})),
},
null,
2,
),
);
await delay(300);
if (this.currentCandle.value === null && data.length > 0) {
this.currentCandle.next(data[data.length - 1]);
}
if(!untilCandle){
this.realtimeLastBySymbol.set(_symbol, data[data.length - 1])
}
return data;
}
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
export const dataSourceProvider = DataSourceProvider.getInstance();