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


import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';

import { indicativeQuotesController } from '@api/controllers/indicativeQuotesController';
import api from '@api/index';

import { candleToBar } from '@utils/candleToBar';

import { ChartIndicativeData } from './types';
import { isIndicativeTicker } from './utils/isIndicativeTicker';
import { transformKeyToLowerCase } from './utils/transformKeyToLowerCase';

import type { Candle } from 'moex-chart';

dayjs.extend(utc);

export interface PeriodParams {
  from: number;
  to: number;
  countBack: number;
  firstDataRequest: boolean;
}

export interface HistoryMetadata {
  noData: boolean;
}

export type HistoryCallback = (candles: Candle[], metadata: HistoryMetadata) => void;

export type SubscribeBarsCallback = (candle: Candle) => void;

interface RequestBarsArgs {
  currencyPair: string;
  interval: string;
  periodParams: PeriodParams;
  onHistoryCallback?: HistoryCallback;
  ticker?: string;
  indicativeData?: ChartIndicativeData;
}

interface RequestRealtimeBarsArgs {
  currencyPair: string;
  interval: string;
  ticker?: string;
  indicativeData?: ChartIndicativeData;
  onRealtimeCallback?: SubscribeBarsCallback;
}

export type CustomBarsResolver = (args: {
  ticker?: string;
  currencyPair: string;
  periodParams: PeriodParams;
  interval: string;
}) => Promise<Candle[]>;

const barsResolvers = new Map<string, CustomBarsResolver>();

export const registerBarsResolver = (boardKey: string, resolver: CustomBarsResolver) => {
  barsResolvers.set(boardKey, resolver);
};

const getResolverKey = (ticker?: string) => {
  const [source, board] = ticker?.split(/[:.]/) ?? [];

  if (!source || !board) {
    return '';
  }

  return `${source}:${board}`;
};

const getBarsResolver = (ticker?: string) => barsResolvers.get(getResolverKey(ticker));

export async function requestBars({
  currencyPair,
  interval,
  periodParams,
  onHistoryCallback,
  ticker,
  indicativeData,
}: RequestBarsArgs): Promise<Candle[]> {
  const customResolver = getBarsResolver(ticker);

  if (customResolver) {
    try {
      const customBars = await customResolver({
        ticker,
        currencyPair,
        periodParams,
        interval,
      });

      const olderBars = customBars.filter((bar) => bar.time < periodParams.to);

      onHistoryCallback?.(olderBars, {
        noData: olderBars.length === 0,
      });

      return olderBars;
    } catch {
      onHistoryCallback?.([], {
        noData: true,
      });

      return [];
    }
  }

  const date = new Date(periodParams.to * 1000);
  const year = date.getUTCFullYear();
  const month = `0${date.getUTCMonth() + 1}`.slice(-2);
  const day = `0${date.getUTCDate()}`.slice(-2);
  const hours = `0${date.getUTCHours()}`.slice(-2);
  const minutes = `0${date.getUTCMinutes()}`.slice(-2);
  const seconds = `0${date.getUTCSeconds()}`.slice(-2);

  const hasIndicativeBoardInTicker = isIndicativeTicker(ticker);

  // если при инициализации графика были данные indicativeData и текущий инструмент совпадает
  // то отправляем запрос на индикатив
  // иначе на инструменты
  const isIndicativeInstrument =
    (indicativeData && indicativeData.key === ticker) || hasIndicativeBoardInTicker;

  if (isIndicativeInstrument) {
    const dateStr = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;

    const lowerCaseKey = transformKeyToLowerCase(currencyPair);

    if (!lowerCaseKey) {
      onHistoryCallback?.([], {
        noData: true,
      });

      return [];
    }

    try {
      const { data } = await indicativeQuotesController.getCandles({
        count: periodParams.countBack,
        key: lowerCaseKey,
        date: dateStr,
        interval,
      });

      const candles = data.indicativeCandles.reverse().map(candleToBar);

      onHistoryCallback?.(candles, {
        noData: candles.length === 0,
      });

      return candles;
    } catch {
      onHistoryCallback?.([], {
        noData: true,
      });

      return [];
    }
  }

  const dateStr = `${year}-${month}-${day}%20${hours}:${minutes}:${seconds}`;

  try {
    const { data } = await api.getBars({
      currencyPair,
      date: dateStr,
      interval,
      count: periodParams.countBack,
      ticker,
    });

    const bars = data.reverse().map(candleToBar);

    onHistoryCallback?.(bars, {
      noData: bars.length === 0,
    });

    return bars;
  } catch {
    onHistoryCallback?.([], {
      noData: true,
    });

    return [];
  }
}

export async function requestRealtimeBars({
  currencyPair,
  interval,
  ticker,
  onRealtimeCallback,
  indicativeData,
}: RequestRealtimeBarsArgs): Promise<Candle | undefined> {
  if (getBarsResolver(ticker)) {
    return;
  }

  const hasIndicativeBoardInTicker = isIndicativeTicker(ticker);

  // если при инициализации графика были данные indicativeData и текущий инструмент совпадает
  // то отправляем запрос на индикатив
  // иначе на инструменты
  const isIndicativeInstrument =
    (indicativeData && indicativeData.key === ticker) || hasIndicativeBoardInTicker;

  if (isIndicativeInstrument) {
    const lowerCaseKey = transformKeyToLowerCase(currencyPair);

    if (!lowerCaseKey) {
      return undefined;
    }

    try {
      const { data } = await indicativeQuotesController.getCandles({
        count: 1,
        key: lowerCaseKey,
        date: dayjs().utc().add(1, 'minute').format('YYYY-MM-DDTHH:mm:ss'),
        interval,
      });

      if (data.indicativeCandles.length === 0) {
        return undefined;
      }

      const sortedData = [...data.indicativeCandles].sort(
        (first, second) => new Date(first.end).valueOf() - new Date(second.end).valueOf(),
      );

      const firstCandle = sortedData[0];

      if (!firstCandle) {
        return undefined;
      }

      const bar = candleToBar(firstCandle);

      onRealtimeCallback?.(bar);

      return bar;
    } catch (error) {
      console.error('error from requestRealTimeBars indicativeQuotesController: ', error);

      return undefined;
    }
  }

  try {
    const { data } = await api.getBars({
      currencyPair,
      date: dayjs().utc().add(1, 'minute').format('YYYY-MM-DD%20HH:mm:ss'),
      interval,
      count: 1,
      ticker,
    });

    if (data.length === 0) {
      return undefined;
    }

    const sortedData = [...data].sort(
      (first, second) => new Date(first.end).valueOf() - new Date(second.end).valueOf(),
    );

    const firstCandle = sortedData[0];

    if (!firstCandle) {
      return undefined;
    }

    const bar = candleToBar(firstCandle);

    onRealtimeCallback?.(bar);

    return bar;
  } catch (error) {
    console.error('error from requestRealTimeBars: ', error);

    return undefined;
  }
}