Загрузка данных


// По хорошему - класс должен быть синглтоном, чтобы кормить MoexChart одинаковой датой,
// и не плодить несколько подключений на одни символа
class DataSourceProvider {
  private prevRealtimeDataArr: Candle[] = [];

  private prevRealtimeData: Candle | undefined;

  private realtimeShouldBeConvoluted = false;

  private realtimeTimer: ReturnType<typeof setInterval> | null = null;

  private isRealtimeRequestPending = false;

  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.isRealtimeRequestPending = false;

    this.realtimeTimer = setInterval(async () => {
      if (this.isRealtimeRequestPending) {
        return;
      }

      const symbols = Array.from(
        new Set(
          getSymbols()
            .map(getRequestSymbol)
            .filter((symbol): symbol is string => Boolean(symbol)),
        ),
      );

      if (symbols.length === 0) {
        return;
      }

      this.isRealtimeRequestPending = true;

      try {
        const timeframe = getTimeframe();

        await Promise.all(
          symbols.map(async (symbol) => {
            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);
              });
            }
          }),
        );
      } finally {
        this.isRealtimeRequestPending = false;
      }
    }, periodMs);

    return () => {
      if (this.realtimeTimer) {
        clearInterval(this.realtimeTimer);
      }

      this.realtimeTimer = null;
      this.isRealtimeRequestPending = false;
    };
  }

  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 };