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


import { IChartApi, PriceScaleMode, SeriesType } from 'lightweight-charts';
import { BehaviorSubject, distinctUntilChanged, map, Observable } from 'rxjs';

import { DataSource } from '@core/DataSource';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { IndicatorManager } from '@core/IndicatorManager';
import { PaneManager } from '@core/PaneManager';

import { MAIN_PANE_INDEX } from '@src/constants';
import { getThemeStore } from '@src/theme';
import { CompareItem, CompareMode, Direction, IndicatorConfig } from '@src/types';
import { normalizeSymbol } from '@src/utils';

interface CompareEntry {
  key: string;
  symbol: string;
  mode: CompareMode;
  paneIndex: number;
  symbol$: BehaviorSubject<string>;
  entity: Indicator;
}

function makeKey(symbol: string, mode: CompareMode): string {
  return `${symbol}|${mode}`;
}

interface CompareManagerParams {
  chart: IChartApi;
  eventManager: EventManager;
  dataSource: DataSource;
  indicatorManager: IndicatorManager;
  paneManager: PaneManager;
}

const getDefaultCompareIndicatorConfig = (): IndicatorConfig => {
  return {
    newPane: true,
    series: [
      {
        name: 'Line', // todo: change with enum
        id: `compare-${Math.random()}`,
        seriesOptions: {
          visible: true,
          color: getThemeStore().colors.chartLineColor,
        },
      },
    ],
  };
};

export class CompareManager {
  private readonly chart: IChartApi;
  private readonly eventManager: EventManager;
  private readonly dataSource: DataSource;
  private readonly indicatorManager: IndicatorManager;
  private readonly paneManager: PaneManager;

  private readonly entries = new Map<string, CompareEntry>();
  private readonly itemsSubject = new BehaviorSubject<CompareItem[]>([]);
  private readonly entitiesSubject = new BehaviorSubject<Indicator[]>([]);

  constructor({ chart, eventManager, dataSource, indicatorManager, paneManager }: CompareManagerParams) {
    this.chart = chart;
    this.eventManager = eventManager;
    this.dataSource = dataSource;
    this.indicatorManager = indicatorManager;
    this.paneManager = paneManager;

    this.eventManager.timeframe().subscribe(() => {
      this.applyPolicy();
    });
  }

  public itemsObs(): Observable<CompareItem[]> {
    return this.itemsSubject.asObservable();
  }

  public entitiesObs(): Observable<Indicator[]> {
    return this.entitiesSubject.asObservable();
  }

  public snapshot(): CompareItem[] {
    return this.itemsSubject.value;
  }

  public clear(): void {
    const keys = Array.from(this.entries.keys());
    for (let i = 0; i < keys.length; i += 1) {
      this.removeByKey(keys[i]);
    }
    this.applyPolicy();
    this.publish();
  }

  public async setSymbolMode(seriesType: SeriesType, symbolRaw: string, mode: CompareMode): Promise<void> {
    const symbol = normalizeSymbol(symbolRaw);
    if (!symbol) return;

    if (mode === CompareMode.NewScale && this.isNewScaleDisabled()) {
      return;
    }

    const key = makeKey(symbol, mode);
    if (this.entries.has(key)) return;

    const symbol$ = new BehaviorSubject(symbol);

    const entity = this.indicatorManager.addEntity<Indicator>((zIndex, moveUp, moveDown) => {
      const config = getDefaultCompareIndicatorConfig();

      return new Indicator({
        id: key,
        lwcChart: this.chart,
        mainSymbol$: new BehaviorSubject<string>(symbol),
        dataSource: this.dataSource,
        zIndex,
        onDelete: () => this.removeByKey(key),
        moveUp,
        moveDown,
        paneManager: this.paneManager,
        config: {
          ...config,
          series: [
            {
              ...config.series[0],
              seriesOptions: {
                ...config.series[0]?.seriesOptions,
                priceScaleId: mode === CompareMode.NewScale ? Direction.Left : Direction.Right,
              },
            },
          ],
          newPane: mode === CompareMode.NewPane,
        },
      });
    });

    this.entries.set(key, { key, symbol, mode, paneIndex: entity.getPane().getId(), symbol$, entity });

    this.applyPolicy();
    this.publish();

    await this.dataSource.isReady(symbol);
  }

  public removeSymbolMode(symbolRaw: string, mode: CompareMode): void {
    const symbol = normalizeSymbol(symbolRaw);
    if (!symbol) return;

    this.removeByKey(makeKey(symbol, mode));
    this.applyPolicy();
    this.publish();
  }

  public removeSymbol(symbolRaw: string): void {
    const symbol = normalizeSymbol(symbolRaw);
    if (!symbol) return;

    const all = Array.from(this.entries.entries());
    for (let i = 0; i < all.length; i += 1) {
      const [key, entry] = all[i];
      if (entry.symbol === symbol) this.removeByKey(key);
    }

    this.applyPolicy();
    this.publish();
  }

  public isNewScaleDisabled(): boolean {
    return this.itemsSubject.value.length > 0;
  }

  public isNewScaleDisabledObservable(): Observable<boolean> {
    return this.itemsSubject.pipe(
      map((items) => items.length > 0),
      distinctUntilChanged(),
    );
  }

  public getAllEntities() {
    return Array.from(this.entries.values()).map(({ symbol, entity, mode }) => ({
      symbol,
      entity,
      mode,
    }));
  }

  public destroy(): void {
    this.clear();
    this.itemsSubject.complete();
    this.entitiesSubject.complete();
  }

  private removeByKey(key: string): void {
    const entry = this.entries.get(key);
    if (!entry) return;

    this.entries.delete(key);
    this.indicatorManager.removeEntity(entry.entity);
    entry.entity.destroy();
    entry.symbol$.complete();

    this.applyPolicy();
    this.publish();
  }

  private publish(): void {
    const values = Array.from(this.entries.values());

    const items: CompareItem[] = [];
    const entities: Indicator[] = [];

    for (let i = 0; i < values.length; i += 1) {
      items.push({ symbol: values[i].symbol, mode: values[i].mode });
      entities.push(values[i].entity);
    }

    this.itemsSubject.next(items);
    this.entitiesSubject.next(entities);
  }

  private applyPolicy(): void {
    const list = Array.from(this.entries.values());

    let percentEnabled = false;
    for (let i = 0; i < list.length; i += 1) {
      if (list[i].mode === CompareMode.Percentage) {
        percentEnabled = true;
        break;
      }
    }

    let hasMainNewScale = false;
    for (let i = 0; i < list.length; i += 1) {
      // [0 - в индикаторах compare сущности может быть только одна серия] [1 - entry]
      const paneIndex = Array.from(list[i].entity.getSeriesMap())[0][1].getPane().paneIndex();
      if (paneIndex === MAIN_PANE_INDEX && list[i].mode === CompareMode.NewScale) {
        hasMainNewScale = true;
        break;
      }
    }

    const paneSet = new Set<number>();
    for (let i = 0; i < list.length; i += 1) {
      // [0 - в индикаторах compare сущности может быть только одна серия] [1 - entry]
      const paneIndex = Array.from(list[i].entity.getSeriesMap())[0][1].getPane().paneIndex();

      if (paneIndex !== MAIN_PANE_INDEX) paneSet.add(paneIndex);
    }

    this.chart.applyOptions({
      leftPriceScale: { visible: hasMainNewScale, borderVisible: false },
    });

    this.chart.priceScale(Direction.Right, MAIN_PANE_INDEX).applyOptions({
      visible: true,
      mode: percentEnabled ? PriceScaleMode.Percentage : PriceScaleMode.Normal,
    });

    this.chart.priceScale(Direction.Left, MAIN_PANE_INDEX).applyOptions({
      visible: hasMainNewScale,
      mode: PriceScaleMode.Normal,
      borderVisible: false,
    });

    const panes = Array.from(paneSet.values());
    for (let i = 0; i < panes.length; i += 1) {
      const paneIndex = panes[i];

      this.chart.priceScale(Direction.Right, paneIndex).applyOptions({
        visible: true,
        mode: PriceScaleMode.Normal,
      });

      if (hasMainNewScale) {
        this.chart.priceScale(Direction.Left, paneIndex).applyOptions({
          visible: true,
          mode: PriceScaleMode.Normal,
          borderVisible: false,
        });
      }
    }

    for (let i = 0; i < list.length; i += 1) {
      const entry = list[i];

      // [0 - в индикаторах compare сущности может быть только одна серия] [1 - entry]
      const serie = Array.from(entry.entity.getSeriesMap())[0][1];
      const paneIndex = serie.getPane().paneIndex();

      if (paneIndex !== MAIN_PANE_INDEX) {
        serie.applyOptions({ priceScaleId: Direction.Right });
        continue;
      }

      serie.applyOptions({
        priceScaleId: entry.mode === CompareMode.NewScale ? Direction.Left : Direction.Right,
      });
    }
  }
}