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


import { MouseEventParams, Point, Time } from 'lightweight-charts';

import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';

import { ChartMouseEvents } from '@core/ChartMouseEvents';

import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { indicatorLabelById } from '@src/core/Indicators';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { CompareMode, IndicatorLabel, IndicatorsIds, OHLCConfig } from '@src/types';

export interface LegendParams {
  config: OHLCConfig;
  eventManager: EventManager;
  indicators: BehaviorSubject<Map<IndicatorsIds, Indicator>>;
  subscribeChartEvent: ChartMouseEvents['subscribe'];
  mainSeries: BehaviorSubject<SeriesStrategies | null> | null;
  paneId: number;
}

export interface Ohlc {
  time: Time;
  open?: number;
  high?: number;
  low?: number;
  close?: number;
  value?: number;
  absoluteChange?: number;
  percentageChange?: number;
}

export interface CompareLegendItem {
  symbol: string;
  mode: CompareMode;
  value: string;
  color: string;
}

export interface LegendVM {
  vm: LegendModel;
}

export type LegendModel = {
  name: IndicatorLabel | string;
  isIndicator: boolean;
  values: Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>;
  remove?: () => void;
}[];

export const ohlcValuesToShowForMainSerie = [
  // 'time',
  'open',
  'high',
  'low',
  'close',
  'value',
  // 'absoluteChange',
  'percentageChange',
];

export const ohlcValuesToShowForIndicators = [
  // 'time',
  'open',
  // 'high',
  // 'low',
  // 'close',
  'value',
  // 'absoluteChange',
  // 'percentageChange'
];

export class Legend {
  private eventManager: EventManager;
  private indicators: Map<IndicatorsIds, Indicator> = new Map();
  private config: OHLCConfig;

  private mainSeries!: SeriesStrategies;
  private isChartHovered = false;
  private model$ = new BehaviorSubject<LegendModel>([]);
  private tooltipVisability = new BehaviorSubject<boolean>(false);
  private tooltipPos = new BehaviorSubject<null | Point>(null);
  private paneId: number;

  private subscriptions = new Subscription();

  constructor({ config, eventManager, indicators, subscribeChartEvent, mainSeries, paneId }: LegendParams) {
    this.config = config;
    this.eventManager = eventManager;
    this.paneId = paneId;

    if (!mainSeries) {
      this.subscriptions.add(
        indicators.subscribe((value: Map<IndicatorsIds, Indicator>) => {
          this.indicators = value;
          this.handleIndicatorSeriesDataChange();
        }),
      );
    } else {
      this.subscriptions.add(
        combineLatest([mainSeries, indicators]).subscribe(([mainSerie, inds]) => {
          if (!mainSerie) {
            return;
          }
          this.mainSeries = mainSerie;
          this.indicators = inds;
          this.handleMainSeriesDataChange();
        }),
      );
    }
    this.subscriptions.add(subscribeChartEvent('crosshairMove', this.handleCrosshairMove));
  }

  public subscribeCursorPosition(cb: (point: Point | null) => void) {
    this.subscriptions.add(this.tooltipPos.subscribe(cb));
  }

  public subscribeCursorVisability(cb: (isVisible: boolean) => void) {
    this.subscriptions.add(this.tooltipVisability.subscribe(cb));
  }

  private handleIndicatorSeriesDataChange = () => {
    for (const [_, indicator] of this.indicators) {
      this.subscriptions.add(indicator?.subscribeDataChange(this.updateWithLastCandle));
    }
  };

  private handleMainSeriesDataChange = () => {
    const handler = () => {
      this.updateWithLastCandle();
    };
    this.mainSeries?.subscribeDataChanged(handler);
    this.subscriptions.add(() => this.mainSeries?.unsubscribeDataChanged(handler));
  };

  private updateWithLastCandle = () => {
    if (this.isChartHovered) return;

    const model: LegendModel = [];

    if (this.mainSeries) {
      const series = new Map();

      const serieData = this.mainSeries.getLegendData();

      Object.entries(serieData).forEach(([key, sd]) => {
        series.set(`${key}`, {
          ...sd,
        });
      });

      model.push({
        name: this.mainSeries.options().title,
        values: series as Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>,
        isIndicator: false,
      });
    }

    if (!this.indicators) {
      return;
    }

    for (const [indicatorName, indicator] of this.indicators) {
      const indicatorSeries = new Map();

      for (const [serieName, serie] of indicator.getSeriesMap()) {
        if (!serie.isVisible()) {
          continue;
        }

        const serieData = serie.getLegendData();

        const value = serieData.value ?? serieData.close;

        indicatorSeries.set(serieName, { ...value });
      }

      model.push({
        name: indicatorLabelById[indicatorName],
        values: indicatorSeries as Partial<
          Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
        >,
        isIndicator: true,
        remove: () => indicator.delete(),
      });
    }

    this.model$.next(model);
  };

  private handleCrosshairMove = (param: MouseEventParams) => {
    // todo: есть одинаковый код с updateWithLastCandle
    if (param.point === undefined || !param.time || param.point.x < 0 || param.point.y < 0) {
      this.tooltipVisability.next(false);

      this.isChartHovered = false;
      this.updateWithLastCandle();
      return;
    }

    if (this.paneId === param.paneIndex) {
      this.tooltipVisability.next(true);
    } else {
      this.tooltipVisability.next(false);
    }

    this.isChartHovered = true;

    const model: LegendModel = [];

    if (this.mainSeries) {
      const series = new Map();
      const serieData = this.mainSeries.getLegendData(param);

      Object.entries(serieData).forEach(([key, sd]) => {
        series.set(`${key}`, {
          ...sd,
        });
      });

      model.push({
        name: this.mainSeries.options().title,
        values: series as Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>,
        isIndicator: false,
      });
    }

    if (!this.indicators) {
      return;
    }

    for (const [indicatorName, indicator] of this.indicators) {
      const indicatorSeries = new Map();

      for (const [serieName, serie] of indicator.getSeriesMap()) {
        if (!serie.isVisible()) {
          continue;
        }

        const serieData = serie.getLegendData(param);

        const value = serieData.value ?? serieData.close;

        indicatorSeries.set(serieName, value);
      }

      model.push({
        name: indicatorLabelById[indicatorName],
        values: indicatorSeries as Partial<
          Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
        >,
        isIndicator: true,
        remove: () => indicator.delete(),
      });
    }

    this.model$.next(model);
    this.tooltipPos.next(param.point);
  };

  public getConfig(): OHLCConfig {
    return this.config;
  }

  public getLegendViewModel(): Observable<LegendModel> {
    return this.model$;
  }

  public destroy = () => {
    this.subscriptions.unsubscribe();

    this.model$.complete();
    this.tooltipVisability.complete();
    this.tooltipPos.complete();
  };
}




import { MouseEventParams, Point, Time } from 'lightweight-charts';

import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';

import { ChartMouseEvents } from '@core/ChartMouseEvents';

import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { indicatorLabelById } from '@src/core/Indicators';
import { SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { CompareMode, IndicatorLabel, IndicatorsIds, OHLCConfig } from '@src/types';

export interface LegendParams {
  config: OHLCConfig;
  eventManager: EventManager;
  indicators: BehaviorSubject<Map<IndicatorsIds, Indicator>>;
  subscribeChartEvent: ChartMouseEvents['subscribe'];
  mainSeries: BehaviorSubject<SeriesStrategies | null> | null;
  paneId: number;
}

export interface Ohlc {
  time: Time;
  open?: number;
  high?: number;
  low?: number;
  close?: number;
  value?: number;
  absoluteChange?: number;
  percentageChange?: number;
}

export interface CompareLegendItem {
  symbol: string;
  mode: CompareMode;
  value: string;
  color: string;
}

export interface LegendVM {
  vm: LegendModel;
}

export type LegendModel = {
  name: IndicatorLabel | string;
  isIndicator: boolean;
  values: Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>;
  remove?: () => void;
}[];

export const ohlcValuesToShowForMainSerie = [
  // 'time',
  'open',
  'high',
  'low',
  'close',
  'value',
  // 'absoluteChange',
  'percentageChange',
];

export const ohlcValuesToShowForIndicators = [
  // 'time',
  'open',
  // 'high',
  // 'low',
  // 'close',
  'value',
  // 'absoluteChange',
  // 'percentageChange'
];

export class Legend {
  private eventManager: EventManager;
  private indicators: Map<IndicatorsIds, Indicator> = new Map();
  private config: OHLCConfig;

  private mainSeries!: SeriesStrategies;
  private isChartHovered = false;
  private model$ = new BehaviorSubject<LegendModel>([]);
  private tooltipVisability = new BehaviorSubject<boolean>(false);
  private tooltipPos = new BehaviorSubject<null | Point>(null);
  private paneId: number;

  private subscriptions = new Subscription();

  constructor({ config, eventManager, indicators, subscribeChartEvent, mainSeries, paneId }: LegendParams) {
    this.config = config;
    this.eventManager = eventManager;
    this.paneId = paneId;

    if (!mainSeries) {
      this.subscriptions.add(
        indicators.subscribe((value: Map<IndicatorsIds, Indicator>) => {
          this.indicators = value;
          this.handleIndicatorSeriesDataChange();
        }),
      );
    } else {
      this.subscriptions.add(
        combineLatest([mainSeries, indicators]).subscribe(([mainSerie, inds]) => {
          if (!mainSerie) {
            return;
          }
          this.mainSeries = mainSerie;
          this.indicators = inds;
          this.handleMainSeriesDataChange();
        }),
      );
    }
    this.subscriptions.add(subscribeChartEvent('crosshairMove', this.handleCrosshairMove));
  }

  public subscribeCursorPosition(cb: (point: Point | null) => void) {
    this.subscriptions.add(this.tooltipPos.subscribe(cb));
  }

  public subscribeCursorVisability(cb: (isVisible: boolean) => void) {
    this.subscriptions.add(this.tooltipVisability.subscribe(cb));
  }

  private handleIndicatorSeriesDataChange = () => {
    for (const [_, indicator] of this.indicators) {
      this.subscriptions.add(indicator?.subscribeDataChange(this.updateWithLastCandle));
    }
  };

  private handleMainSeriesDataChange = () => {
    const handler = () => {
      this.updateWithLastCandle();
    };
    this.mainSeries?.subscribeDataChanged(handler);
    this.subscriptions.add(() => this.mainSeries?.unsubscribeDataChanged(handler));
  };

  private updateWithLastCandle = () => {
    if (this.isChartHovered) return;

    const model: LegendModel = [];

    if (this.mainSeries) {
      const series = new Map();

      const serieData = this.mainSeries.getLegendData();

      Object.entries(serieData).forEach(([key, sd]) => {
        series.set(`${key}`, {
          ...sd,
        });
      });

      model.push({
        name: this.mainSeries.options().title,
        values: series as Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>,
        isIndicator: false,
      });
    }

    if (!this.indicators) {
      return;
    }

    for (const [indicatorName, indicator] of this.indicators) {
      const indicatorSeries = new Map();

      for (const [serieName, serie] of indicator.getSeriesMap()) {
        if (!serie.isVisible()) {
          continue;
        }

        const serieData = serie.getLegendData();

        const value = serieData.value ?? serieData.close;

        indicatorSeries.set(serieName, { ...value });
      }

      model.push({
        name: indicatorLabelById[indicatorName],
        values: indicatorSeries as Partial<
          Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
        >,
        isIndicator: true,
        remove: () => indicator.delete(),
      });
    }

    this.model$.next(model);
  };

  private handleCrosshairMove = (param: MouseEventParams) => {
    // todo: есть одинаковый код с updateWithLastCandle
    if (param.point === undefined || !param.time || param.point.x < 0 || param.point.y < 0) {
      this.tooltipVisability.next(false);

      this.isChartHovered = false;
      this.updateWithLastCandle();
      return;
    }

    if (this.paneId === param.paneIndex) {
      this.tooltipVisability.next(true);
    } else {
      this.tooltipVisability.next(false);
    }

    this.isChartHovered = true;

    const model: LegendModel = [];

    if (this.mainSeries) {
      const series = new Map();
      const serieData = this.mainSeries.getLegendData(param);

      Object.entries(serieData).forEach(([key, sd]) => {
        series.set(`${key}`, {
          ...sd,
        });
      });

      model.push({
        name: this.mainSeries.options().title,
        values: series as Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>>,
        isIndicator: false,
      });
    }

    if (!this.indicators) {
      return;
    }

    for (const [indicatorName, indicator] of this.indicators) {
      const indicatorSeries = new Map();

      for (const [serieName, serie] of indicator.getSeriesMap()) {
        if (!serie.isVisible()) {
          continue;
        }

        const serieData = serie.getLegendData(param);

        const value = serieData.value ?? serieData.close;

        indicatorSeries.set(serieName, value);
      }

      model.push({
        name: indicatorLabelById[indicatorName],
        values: indicatorSeries as Partial<
          Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>
        >,
        isIndicator: true,
        remove: () => indicator.delete(),
      });
    }

    this.model$.next(model);
    this.tooltipPos.next(param.point);
  };

  public getConfig(): OHLCConfig {
    return this.config;
  }

  public getLegendViewModel(): Observable<LegendModel> {
    return this.model$;
  }

  public destroy = () => {
    this.subscriptions.unsubscribe();

    this.model$.complete();
    this.tooltipVisability.complete();
    this.tooltipPos.complete();
  };
}