Загрузка данных
import {
DataChangedHandler,
IChartApi,
ISeriesApi,
LineSeries,
LogicalRange,
Time,
WhitespaceData,
} from 'lightweight-charts';
import { Observable, Subscription } from 'rxjs';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
interface FutureTimeScaleExtenderParams {
chart: IChartApi;
mainSeries$: Observable<SeriesStrategies | null>;
timeframe$: Observable<unknown>;
getTimeframe: () => unknown;
}
type TimeStepUnit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month';
interface TimeStep {
unit: TimeStepUnit;
amount: number;
key: string;
}
const MIN_FUTURE_BARS_COUNT = 200;
const FUTURE_BARS_BUFFER = 200;
const MAX_FUTURE_BARS_COUNT = 5000;
export class FutureTimeScaleExtender {
private readonly chart: IChartApi;
private readonly getTimeframe: () => unknown;
private readonly subscriptions = new Subscription();
private readonly whitespaceSeries: ISeriesApi<'Line'>;
private mainSeries: SeriesStrategies | null = null;
private visibleLogicalRange: LogicalRange | null = null;
private lastDataKey = '';
constructor({ chart, mainSeries$, timeframe$, getTimeframe }: FutureTimeScaleExtenderParams) {
this.chart = chart;
this.getTimeframe = getTimeframe;
this.whitespaceSeries = this.chart.addSeries(LineSeries, {
color: 'rgba(0, 0, 0, 0)',
lineVisible: false,
lastValueVisible: false,
priceLineVisible: false,
crosshairMarkerVisible: false,
priceScaleId: '',
});
this.visibleLogicalRange = this.chart.timeScale().getVisibleLogicalRange();
this.chart.timeScale().subscribeVisibleLogicalRangeChange(this.handleVisibleLogicalRangeChange);
this.subscriptions.add(
mainSeries$.subscribe((series) => {
this.setMainSeries(series);
}),
);
this.subscriptions.add(
timeframe$.subscribe(() => {
this.lastDataKey = '';
this.update();
}),
);
}
public destroy(): void {
this.mainSeries?.unsubscribeDataChanged(this.handleDataChanged);
this.chart.timeScale().unsubscribeVisibleLogicalRangeChange(this.handleVisibleLogicalRangeChange);
this.subscriptions.unsubscribe();
try {
this.chart.removeSeries(this.whitespaceSeries);
} catch {
// Series can already be removed together with chart.
}
}
private setMainSeries(series: SeriesStrategies | null): void {
if (this.mainSeries === series) {
return;
}
this.mainSeries?.unsubscribeDataChanged(this.handleDataChanged);
this.mainSeries = series;
this.lastDataKey = '';
this.mainSeries?.subscribeDataChanged(this.handleDataChanged);
this.update();
}
private handleDataChanged: DataChangedHandler = (): void => {
this.update();
};
private handleVisibleLogicalRangeChange = (range: LogicalRange | null): void => {
this.visibleLogicalRange = range;
this.update();
};
private update(): void {
if (!this.mainSeries) {
this.setFutureData([]);
return;
}
const data = this.mainSeries.data();
if (!data.length) {
this.setFutureData([]);
return;
}
const lastBar = data[data.length - 1];
if (!lastBar || typeof lastBar.time !== 'number') {
this.setFutureData([]);
return;
}
const futureBarsCount = this.getFutureBarsCount(data.length);
if (futureBarsCount <= 0) {
this.setFutureData([]);
return;
}
const step = getTimeStep(this.getTimeframe());
const dataKey = `${lastBar.time}:${step.key}:${futureBarsCount}`;
if (this.lastDataKey === dataKey) {
return;
}
this.lastDataKey = dataKey;
this.setFutureData(createFutureWhitespace(lastBar.time, step, futureBarsCount));
}
private getFutureBarsCount(realBarsCount: number): number {
const range = this.visibleLogicalRange;
if (!range) {
return 0;
}
const lastRealLogicalIndex = realBarsCount - 1;
const visibleFutureBars = Math.ceil(range.to - lastRealLogicalIndex);
if (visibleFutureBars <= 0) {
return 0;
}
return clampNumber(
Math.max(visibleFutureBars + FUTURE_BARS_BUFFER, MIN_FUTURE_BARS_COUNT),
0,
MAX_FUTURE_BARS_COUNT,
);
}
private setFutureData(data: WhitespaceData<Time>[]): void {
if (!data.length) {
this.lastDataKey = '';
}
this.whitespaceSeries.setData(data);
}
}
function createFutureWhitespace(lastTime: number, step: TimeStep, count: number): WhitespaceData<Time>[] {
const result: WhitespaceData<Time>[] = [];
let currentTime = lastTime;
for (let index = 0; index < count; index += 1) {
currentTime = addTime(currentTime, step);
result.push({
time: currentTime as Time,
});
}
return result;
}
function addTime(time: number, step: TimeStep): number {
const date = new Date(time * 1000);
switch (step.unit) {
case 'second':
date.setUTCSeconds(date.getUTCSeconds() + step.amount);
break;
case 'minute':
date.setUTCMinutes(date.getUTCMinutes() + step.amount);
break;
case 'hour':
date.setUTCHours(date.getUTCHours() + step.amount);
break;
case 'day':
date.setUTCDate(date.getUTCDate() + step.amount);
break;
case 'week':
date.setUTCDate(date.getUTCDate() + step.amount * 7);
break;
case 'month':
date.setUTCMonth(date.getUTCMonth() + step.amount);
break;
default:
date.setUTCMinutes(date.getUTCMinutes() + 1);
break;
}
return Math.floor(date.getTime() / 1000);
}
function getTimeStep(value: unknown): TimeStep {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
return createStep('minute', 1, 'default');
}
const normalizedValue = rawValue.toLowerCase();
const platformFormat = normalizedValue.match(/^(m|h|d|w|mn)(\d+)$/);
if (platformFormat) {
const [, unit, amountRaw] = platformFormat;
const amount = Number(amountRaw);
if (Number.isFinite(amount) && amount > 0) {
if (unit === 'm') {
return createStep('minute', amount, rawValue);
}
if (unit === 'h') {
return createStep('hour', amount, rawValue);
}
if (unit === 'd') {
return createStep('day', amount, rawValue);
}
if (unit === 'w') {
return createStep('week', amount, rawValue);
}
if (unit === 'mn') {
return createStep('month', amount, rawValue);
}
}
}
const suffixFormat = rawValue.match(/^(\d+)(s|m|h|d|w|M)$/);
if (suffixFormat) {
const [, amountRaw, unit] = suffixFormat;
const amount = Number(amountRaw);
if (Number.isFinite(amount) && amount > 0) {
if (unit === 's') {
return createStep('second', amount, rawValue);
}
if (unit === 'm') {
return createStep('minute', amount, rawValue);
}
if (unit === 'h') {
return createStep('hour', amount, rawValue);
}
if (unit === 'd') {
return createStep('day', amount, rawValue);
}
if (unit === 'w') {
return createStep('week', amount, rawValue);
}
if (unit === 'M') {
return createStep('month', amount, rawValue);
}
}
}
if (/^\d+$/.test(rawValue)) {
return createStep('minute', Number(rawValue), rawValue);
}
return createStep('minute', 1, rawValue);
}
function createStep(unit: TimeStepUnit, amount: number, key: string): TimeStep {
return {
unit,
amount,
key,
};
}
function clampNumber(value: number, min: number, max: number): number {
return Math.max(min, Math.min(value, max));
}