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


import type {
  BarData,
  CustomData,
  HistogramData,
  LineData,
  SeriesDataItemTypeMap,
  SingleValueData,
  Time,
} from 'lightweight-charts';

import type { Ohlc } from '@core/Legend';
import { BaseSeries, BaseSeriesParams, calcCandleChange } from '@core/Series/BaseSeries';

import type { ISeries } from '@src/modules/series-strategies';
import { t } from '@src/translations';
import type { Candle, LineCandle } from '@src/types';
import { formatCompactNumber } from '@src/utils';
import { isSingleValueData } from '@src/utils/isSingleValueData';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';

type SingleValueSeriesType = 'Line' | 'Histogram' | 'Baseline';

type SourceData = Partial<Candle> & Partial<LineCandle>;

type LegendData = BarData | LineData | HistogramData | CustomData | null;

type LegendValues = Partial<
  Record<
    keyof Ohlc,
    {
      value: number | string | Time;
      color: string;
      name: string;
    }
  >
>;

export abstract class SingleValueSeriesStrategy<TSeries extends SingleValueSeriesType>
  extends BaseSeries<TSeries>
  implements ISeries<TSeries>
{
  constructor(params: BaseSeriesParams<TSeries>) {
    super(params);

    this.subscribeDataSource(params.dataSource);
  }

  public validateData(data: SourceData[]): boolean {
    return Array.isArray(data) && data.every((point) => typeof point.time === 'number');
  }

  protected dataSourceSubscription = (dataToSet: Candle[]): void => {
    if (!this.validateData(dataToSet)) {
      console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);

      return;
    }

    this.setData(this.formatData(dataToSet));
  };

  protected dataSourceRealtimeSubscription = (dataToSet: Candle): void => {
    if (!this.validateData([dataToSet])) {
      console.error(`LightweightAPI: Invalid data format for ${this.getTypeName()} chart`);

      return;
    }

    const [formattedData] = this.formatData([dataToSet]);

    if (!formattedData) {
      return;
    }

    this.update(formattedData);
  };

  protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>[TSeries][] {
    return inputData.map((point) => this.formatPoint(point));
  }

  protected formatLegendValues(currentBar: LegendData, prevBar: LegendData): LegendValues {
    if (!isSingleValueData(currentBar)) {
      return {};
    }

    const change = calcCandleChange(prevBar, currentBar);

    if (!change) {
      return {};
    }

    const color = this.getLegendColor(currentBar);

    return {
      value: {
        value: formatCompactNumber(currentBar.value) ?? '',
        name: '',
        color,
      },
      absoluteChange: {
        value: formatCompactNumber(change.absoluteChange ?? 0),
        name: t('Change'),
        color,
      },
      percentageChange: {
        value:
          change.percentageChange !== undefined
            ? `${formatCompactNumber(change.percentageChange)}%`
            : '',
        name: t('Change'),
        color,
      },
      time: {
        value: change.time,
        name: t('Time'),
        color,
      },
    };
  }

  protected getLegendColor(currentBar: SingleValueData<Time>): string {
    if ('color' in currentBar && typeof currentBar.color === 'string') {
      return removeAlphaFromHex(currentBar.color);
    }

    const options = this.options();

    if ('color' in options && typeof options.color === 'string') {
      return options.color;
    }

    return '';
  }

  private formatPoint(point: SourceData): SeriesDataItemTypeMap<Time>[TSeries] {
    const time = point.time as Time;
    const value = typeof point.close === 'number' ? point.close : point.value;

    if (typeof value !== 'number') {
      return { time } as SeriesDataItemTypeMap<Time>[TSeries];
    }

    return {
      time,
      value,
      customValues: point as unknown as Record<string, unknown>,
    } as SeriesDataItemTypeMap<Time>[TSeries];
  }

  public abstract getTypeName(): string;
}