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


import { IChartApi } from 'lightweight-charts';
import { Observable, Subscription } from 'rxjs';

import { DataSource } from '@core/DataSource';
import { DOMObject, DOMObjectParams } from '@core/DOMObject';
import { Pane } from '@core/Pane';

import { indicatorLabelById, indicatorSeriesLabelById, IndicatorsIds } from '@src/constants';
import { SeriesFactory, SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ChartTypeOptions, IndicatorConfig, SettingsValues } from '@src/types';
import { DOMObjectSnapshot, IndicatorSnapshot, ISerializable } from '@src/types/snapshot';

type IIndicator = DOMObject;

export interface IndicatorParams extends DOMObjectParams {
  mainSymbol$: Observable<string>;
  lwcChart: IChartApi;
  dataSource: DataSource;
  associatedPane: Pane;
  config: IndicatorConfig;
  type?: IndicatorsIds;
  chartOptions?: ChartTypeOptions;
}

export class Indicator extends DOMObject implements ISerializable<IndicatorSnapshot> {
  private indicatorType?: IndicatorsIds;
  private series: SeriesStrategies[] = [];
  private seriesMap: Map<string, SeriesStrategies> = new Map();
  private lwcChart: IChartApi;
  private dataSource: DataSource;
  private mainSymbol$: Observable<string>;
  private associatedPane: Pane;
  private config: IndicatorConfig;
  private settings: SettingsValues = {};

  private dataChangeHandlers = new Set<() => void>();
  private seriesSubscriptions = new Subscription();

  constructor({
    id,
    type,
    lwcChart,
    dataSource,
    zIndex,
    onDelete,
    moveUp,
    moveDown,
    mainSymbol$,
    associatedPane,
    paneId,
    config,
  }: IndicatorParams) {
    super({ id, name: config?.label ?? id, zIndex, onDelete, moveUp, moveDown, paneId });
    this.lwcChart = lwcChart;
    this.dataSource = dataSource;
    this.mainSymbol$ = mainSymbol$;
    this.indicatorType = type;
    this.config = config;
    this.name = this.getLabel();

    this.settings = this.getDefaultSettings();

    this.associatedPane = associatedPane;

    this.createSeries();

    this.associatedPane.setIndicator(this.id, this);
  }

  public subscribeDataChange(handler: () => void): Subscription {
    this.dataChangeHandlers.add(handler);

    return new Subscription(() => {
      this.dataChangeHandlers.delete(handler);
    });
  }

  public deletable = (): boolean => {
    return this.associatedPane.isMainPane() || this.associatedPane.isLast();
  };

  public getLabel = () => {
    if (this.config.label) {
      return this.config.label;
    }

    if (this.indicatorType) {
      return indicatorLabelById()[this.indicatorType];
    }

    return this.id;
  };

  public getSeriesLabel(serieName: string): string | undefined {
    if (this.config.seriesLabels?.[serieName]) {
      return this.config.seriesLabels[serieName];
    }

    if (this.indicatorType) {
      return indicatorSeriesLabelById[this.indicatorType]?.[serieName];
    }

    return undefined;
  }

  public getId(): string {
    return this.id;
  }

  public getType(): IndicatorsIds | undefined {
    return this.indicatorType;
  }

  public getPane(): Pane {
    return this.associatedPane;
  }

  public getSeriesMap(): Map<string, SeriesStrategies> {
    return this.seriesMap;
  }

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

  public getIndicatorType(): IndicatorSnapshot['indicatorType'] {
    return this.indicatorType;
  }

  public getSettings(): SettingsValues {
    return { ...this.settings };
  }

  public getSettingsConfig() {
    return this.config.settings ?? [];
  }

  public hasSettings(): boolean {
    return Boolean(this.config.settings?.length);
  }

  public updateSettings(settings: SettingsValues): void {
    this.settings = settings;

    // todo: обновлять данные серий без удаления и повторного создания
    this.destroySeries();
    this.createSeries();

    this.notifyDataChanged();
  }

  public show() {
    this.series.forEach((s) => {
      s.show();
    });
    super.show();
  }

  public hide() {
    this.series.forEach((s) => {
      s.hide();
    });
    super.hide();
  }

  public override getSnapshot(): DOMObjectSnapshot & IndicatorSnapshot {
    const domSnap = super.getSnapshot();

    return {
      ...domSnap,
      dataSource: this.dataSource,
      indicatorType: this.indicatorType,
      config: this.config,
    };
  }

  public setSnapshot(snap: IndicatorSnapshot): void {}

  // destroy и delete принципиально отличаются!
  // delete вызовет destroy в конце концов. По сути - это destroy с сайд-эффектом в eventManager
  public delete() {
    super.delete();
  }

  // destroy и delete принципиально отличаются!
  public destroy() {
    this.destroySeries();
    this.dataChangeHandlers.clear();
    this.associatedPane.removeIndicator(this.id);
  }

  private createSeries(): void {
    this.config.series.forEach(({ name, id: serieId, dataFormatter, seriesOptions, priceScaleOptions }) => {
      const serie = SeriesFactory.create(name!)({
        lwcChart: this.lwcChart,
        dataSource: this.dataSource,
        customFormatter: dataFormatter
          ? (params) =>
              dataFormatter({
                ...params,
                settings: this.settings,
                indicatorReference: this,
              })
          : undefined,
        seriesOptions,
        priceScaleOptions,
        mainSymbol$: this.mainSymbol$,
        mainSerie$: this.associatedPane.getMainSerie(),
        showSymbolLabel: false,
        paneIndex: this.associatedPane.getId(),
        indicatorReference: this,
      });

      const handleDataChanged = () => {
        this.notifyDataChanged();
      };

      serie.subscribeDataChanged(handleDataChanged);

      this.seriesSubscriptions.add(() => {
        serie.unsubscribeDataChanged(handleDataChanged);
      });

      this.seriesMap.set(serieId, serie);

      this.series.push(serie);
    });
  }

  private destroySeries(): void {
    this.seriesSubscriptions.unsubscribe();
    this.seriesSubscriptions = new Subscription();

    this.series.forEach((s) => {
      s.destroy();
    });

    this.series = [];
    this.seriesMap.clear();
  }

  private notifyDataChanged(): void {
    this.dataChangeHandlers.forEach((handler) => {
      handler();
    });
  }

  private getDefaultSettings(): SettingsValues {
    const settings: SettingsValues = {};

    this.config.settings?.forEach((field) => {
      settings[field.key] = field.defaultValue;
    });

    return settings;
  }
}



import { IChartApi, IRange, LogicalRange, MouseEventParams, Time } from 'lightweight-charts';

export interface ChartMouseEventsParams {
  lwcChart: IChartApi;
  container: HTMLElement;
}

export interface ChartEventsMap {
  crosshairMove: MouseEventParams;
  visibleTimeRangeChange: IRange<Time> | null;
  visibleLogicalRangeChange: LogicalRange | null;

  wheel: WheelEvent;
  pointerDown: PointerEvent;
  pointerMove: PointerEvent;
  pointerUp: PointerEvent;
  pointerCancel: PointerEvent;
}
export type ChartEvent = keyof ChartEventsMap;
export class ChartMouseEvents {
  private eventCallbacks = new Map<ChartEvent, ((data: any) => void)[]>();
  private lwcChart: IChartApi;
  private container: HTMLElement;

  constructor({ lwcChart, container }: ChartMouseEventsParams) {
    this.lwcChart = lwcChart;
    this.container = container;

    this.setupDomEvents();
    this.setupInternalEvents();
  }

  public unsubscribe<K extends ChartEvent>(event: K, callback: (data: ChartEventsMap[K]) => void): void {
    const callbacks = this.eventCallbacks.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }

  public subscribe<K extends ChartEvent>(event: K, callback: (data: ChartEventsMap[K]) => void): () => void {
    if (!this.eventCallbacks.has(event)) {
      this.eventCallbacks.set(event, []);
    }
    this.eventCallbacks.get(event)!.push(callback);

    // Возвращаем функцию отписки
    return () => this.unsubscribe(event, callback);
  }

  public destroy() {
    this.teardownDomEvents();
    this.eventCallbacks.clear();
  }

  private setupInternalEvents(): void {
    // Подписываемся на события и сохраняем функции отписки
    this.lwcChart.subscribeCrosshairMove((param) => {
      this.emitChartEvent('crosshairMove', param);
    });
    this.lwcChart.timeScale().subscribeVisibleTimeRangeChange((range) => {
      this.emitChartEvent('visibleTimeRangeChange', range);
    });
    this.lwcChart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
      this.emitChartEvent('visibleLogicalRangeChange', logicalRange);
    });
  }

  private onWheel = (e: WheelEvent) => this.emitChartEvent('wheel', e);
  private onPointerDown = (e: PointerEvent) => this.emitChartEvent('pointerDown', e);
  private onPointerMove = (e: PointerEvent) => this.emitChartEvent('pointerMove', e);
  private onPointerUp = (e: PointerEvent) => this.emitChartEvent('pointerUp', e);
  private onPointerCancel = (e: PointerEvent) => this.emitChartEvent('pointerCancel', e);

  private setupDomEvents(): void {
    this.container.addEventListener('wheel', this.onWheel, { passive: true });
    this.container.addEventListener('pointerdown', this.onPointerDown, { passive: true });
    this.container.addEventListener('pointermove', this.onPointerMove, { passive: true });
    window.addEventListener('pointerup', this.onPointerUp, { passive: true });
    window.addEventListener('pointercancel', this.onPointerCancel, { passive: true });
  }

  private teardownDomEvents(): void {
    this.container.removeEventListener('wheel', this.onWheel);
    this.container.removeEventListener('pointerdown', this.onPointerDown);
    this.container.removeEventListener('pointermove', this.onPointerMove);
    window.removeEventListener('pointerup', this.onPointerUp);
    window.removeEventListener('pointercancel', this.onPointerCancel);
  }

  private emitChartEvent<K extends ChartEvent>(event: K, data: ChartEventsMap[K]): void {
    const callbacks = this.eventCallbacks.get(event);
    if (callbacks) {
      callbacks.forEach((callback) => callback(data));
    }
  }
}



import {
  BarData,
  BarSeries,
  CustomData,
  HistogramData,
  LineData,
  SeriesDataItemTypeMap,
  SeriesDefinition,
  SeriesPartialOptionsMap,
  Time,
} from 'lightweight-charts';

import { Ohlc } from '@core/Legend';

import { BaseSeries, BaseSeriesParams, calcCandleChange } from '@src/core/Series/BaseSeries';
import { ISeries } from '@src/modules/series-strategies';
import { getThemeStore } from '@src/theme';
import { t } from '@src/translations';
import { Candle, LineCandle } from '@src/types';
import { ensureDefined, formatCompactNumber, isBarData } from '@src/utils';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';

export class BarSeriesStrategy extends BaseSeries<'Bar'> implements ISeries<'Bar'> {
  constructor(params: BaseSeriesParams<'Bar'>) {
    super(params);
    this.subscribeDataSource(params.dataSource);
  }

  protected seriesDefinition(): SeriesDefinition<'Bar'> {
    return BarSeries;
  }

  getDefaultOptions(): SeriesPartialOptionsMap['Bar'] {
    const { colors } = getThemeStore();

    return {
      upColor: colors.chartCandleUp,
      downColor: colors.chartCandleDown,
    };
  }

  getTypeName(): string {
    return 'Bar';
  }

  public validateData(data: (Partial<Candle> & Partial<LineCandle>)[]): boolean {
    if (!Array.isArray(data)) {
      return false;
    }

    return data.every(
      (point) =>
        typeof point.time === 'number' &&
        typeof point.open === 'number' &&
        typeof point.high === 'number' &&
        typeof point.low === 'number' &&
        typeof point.close === '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]);
    this.update(formattedData[0]);
  };

  protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Bar'][] {
    return inputData.map((point) => ({
      time: point.time as Time,
      open: point.open,
      high: point.high,
      low: point.low,
      close: point.close,
      customValues: point as unknown as Record<string, unknown>,
    }));
  }

  protected formatLegendValues(
    currentBar: null | BarData | LineData | HistogramData | CustomData,
    prevBar: null | BarData | LineData | HistogramData | CustomData,
  ): Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>> {
    if (!currentBar || !isBarData(currentBar)) {
      return {};
    }

    const { colors } = getThemeStore();

    const color = removeAlphaFromHex(
      currentBar.close < currentBar.open ? colors.chartCandleWickDown : colors.chartCandleWickUp,
    );

    const { absoluteChange, percentageChange, time } = ensureDefined(calcCandleChange(prevBar, currentBar));

    return {
      open: {
        value: formatCompactNumber(currentBar.open) ?? '',
        name: t('Open'),
        color,
      },
      high: {
        value: formatCompactNumber(currentBar.high) ?? '',
        name: t('High'),
        color,
      },
      low: {
        value: formatCompactNumber(currentBar.low) ?? '',
        name: t('Low'),
        color,
      },
      close: {
        value: formatCompactNumber(currentBar.close) ?? '',
        name: t('Close'),
        color,
      },
      absoluteChange: {
        value: formatCompactNumber(absoluteChange ?? 0),
        name: t('Change'),
        color,
      },
      percentageChange: {
        value: percentageChange !== undefined ? `${formatCompactNumber(percentageChange)}%` : '',
        name: t('Change'),
        color,
      },
      time: {
        value: time,
        name: t('Time'),
        color,
      },
    };
  }
}

import {
  BarData,
  CandlestickSeries,
  CustomData,
  HistogramData,
  LineData,
  SeriesDataItemTypeMap,
  SeriesDefinition,
  SeriesPartialOptionsMap,
  Time,
} from 'lightweight-charts';

import { Ohlc } from '@core/Legend';

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

import { ISeries } from '@src/modules/series-strategies';

import { getThemeStore } from '@src/theme/store';
import { t } from '@src/translations';
import { Candle, LineCandle } from '@src/types';
import { ensureDefined, formatCompactNumber, isBarData } from '@src/utils';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';

export class CandlestickSeriesStrategy extends BaseSeries<'Candlestick'> implements ISeries<'Candlestick'> {
  constructor(params: BaseSeriesParams<'Candlestick'>) {
    super(params);
    this.subscribeDataSource(params.dataSource);
  }

  protected seriesDefinition(): SeriesDefinition<'Candlestick'> {
    return CandlestickSeries;
  }

  public getDefaultOptions(): SeriesPartialOptionsMap['Candlestick'] {
    const { colors } = getThemeStore();

    return {
      upColor: colors.chartCandleUp,
      downColor: colors.chartCandleDown,
      borderVisible: false,
      wickUpColor: colors.chartCandleWickUp,
      wickDownColor: colors.chartCandleWickDown,
    };
  }

  public validateData(data: (Partial<Candle> & Partial<LineCandle>)[]): boolean {
    // todo: should be private
    if (!Array.isArray(data)) {
      return false;
    }

    return data.every((point) => {
      if (!point) {
        return false;
      }
      // Проверяем обязательные поля
      if (typeof point.time !== 'number' || typeof point.close !== 'number') {
        return false;
      }

      // Если указаны OHLC, проверяем их корректность
      if (point.open !== undefined && point.high !== undefined && point.low !== undefined) {
        return point.high >= Math.max(point.open, point.close) && point.low <= Math.min(point.open, point.close);
      }

      return true;
    });
  }

  public getTypeName(): string {
    return 'Candlestick';
  }

  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]);
    this.update(formattedData[0]);
  };

  protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Candlestick'][] {
    return inputData.map((point) => ({
      time: point.time as Time,
      open: point.open,
      high: point.high,
      low: point.low,
      close: point.close,
      customValues: point as unknown as Record<string, unknown>,
    }));
  }

  protected formatLegendValues(
    currentBar: null | BarData | LineData | HistogramData | CustomData,
    prevBar: null | BarData | LineData | HistogramData | CustomData,
  ): Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>> {
    if (!currentBar || !isBarData(currentBar)) {
      return {};
    }

    const { colors } = getThemeStore();

    const color = removeAlphaFromHex(
      currentBar.close < currentBar.open ? colors.chartCandleWickDown : colors.chartCandleWickUp,
    );

    const { absoluteChange, percentageChange, time } = ensureDefined(calcCandleChange(prevBar, currentBar));

    return {
      open: {
        value: formatCompactNumber(currentBar.open) ?? '',
        name: t('Open'),
        color,
      },
      high: {
        value: formatCompactNumber(currentBar.high) ?? '',
        name: t('High'),
        color,
      },
      low: {
        value: formatCompactNumber(currentBar.low) ?? '',
        name: t('Low'),
        color,
      },
      close: {
        value: formatCompactNumber(currentBar.close) ?? '',
        name: t('Close'),
        color,
      },
      absoluteChange: {
        value: formatCompactNumber(absoluteChange ?? 0),
        name: t('Change'),
        color,
      },
      percentageChange: {
        value: percentageChange !== undefined ? `${formatCompactNumber(percentageChange)}%` : '',
        name: t('Change'),
        color,
      },
      time: {
        value: time,
        name: t('Time'),
        color,
      },
    };
  }
}


import {
  BarData,
  CustomData,
  HistogramData,
  HistogramSeries,
  LineData,
  SeriesDataItemTypeMap,
  SeriesDefinition,
  SeriesPartialOptionsMap,
  Time,
} from 'lightweight-charts';

import { Ohlc } from '@core/Legend';

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

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

export class HistogramSeriesStrategy extends BaseSeries<'Histogram'> implements ISeries<'Histogram'> {
  constructor(params: BaseSeriesParams<'Histogram'>) {
    super(params);
    this.subscribeDataSource(params.dataSource);
  }

  protected seriesDefinition(): SeriesDefinition<'Histogram'> {
    return HistogramSeries;
  }

  protected getDefaultOptions(): SeriesPartialOptionsMap['Histogram'] {
    return {};
  }

  public validateData(data: (Partial<Candle> & Partial<LineCandle>)[]): boolean {
    if (!Array.isArray(data)) {
      return false;
    }

    return data.every(
      (point) => typeof point.time === 'number' && typeof point.volume === 'number' && !Number.isNaN(point.volume),
    );
  }

  public getTypeName(): string {
    return 'Histogram';
  }

  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]);
    this.update(formattedData[0]);
  };

  protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Histogram'][] {
    return inputData.map((point) => ({
      time: point.time as Time,
      value: point.close,
      customValues: point as unknown as Record<string, unknown>,
    }));
  }

  protected formatLegendValues(
    currentBar: null | BarData | LineData | HistogramData | CustomData,
    prevBar: null | BarData | LineData | HistogramData | CustomData,
  ): Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>> {
    if (!currentBar || !isHistogramData(currentBar)) {
      return {};
    }

    const { absoluteChange, percentageChange, time } = ensureDefined(calcCandleChange(prevBar, currentBar));

    const color = currentBar.color ? removeAlphaFromHex(currentBar.color) : this.options().color;

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



import {
  BarData,
  CustomData,
  HistogramData,
  LineData,
  LineSeries,
  LineStyle,
  SeriesDataItemTypeMap,
  SeriesDefinition,
  SeriesPartialOptionsMap,
  Time,
} from 'lightweight-charts';

import { Ohlc } from '@core/Legend';

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

import { ISeries } from '@src/modules/series-strategies';

import { getThemeStore } from '@src/theme/store';
import { t } from '@src/translations';
import { Candle, LineCandle } from '@src/types';
import { ensureDefined, formatCompactNumber, isLineData } from '@src/utils';
import { removeAlphaFromHex } from '@src/utils/removeAlphaFromHex';

export class LineSeriesStrategy extends BaseSeries<'Line'> implements ISeries<'Line'> {
  constructor(params: BaseSeriesParams<'Line'>) {
    super(params);
    this.subscribeDataSource(params.dataSource);
  }

  protected seriesDefinition(): SeriesDefinition<'Line'> {
    return LineSeries;
  }

  protected getDefaultOptions(): SeriesPartialOptionsMap['Line'] {
    const { colors } = getThemeStore();

    return {
      color: colors.chartLineColor,
      lineWidth: 2,
      lineStyle: LineStyle.Solid,
    };
  }

  public validateData(data: (Partial<Candle> & Partial<LineCandle>)[]): boolean {
    if (!Array.isArray(data)) {
      return false;
    }

    return data.every(
      (point) => typeof point.time === 'number' && typeof point.close === 'number' && !Number.isNaN(point.close),
    );
  }

  public getTypeName(): string {
    return 'Line';
  }

  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]);
    this.update(formattedData[0]);
  };

  protected formatMainSerie(inputData: Candle[]): SeriesDataItemTypeMap<Time>['Line'][] {
    return inputData.map((point) => ({
      time: point.time as Time,
      value: point.close, // Для line графика используем close как value
      customValues: point as unknown as Record<string, unknown>,
    }));
  }

  protected formatLegendValues(
    currentBar: null | BarData | LineData | HistogramData | CustomData,
    prevBar: null | BarData | LineData | HistogramData | CustomData,
  ): Partial<Record<keyof Ohlc, { value: number | string | Time; color: string; name: string }>> {
    if (!currentBar || !isLineData(currentBar)) {
      return {};
    }

    const { absoluteChange, percentageChange, time } = ensureDefined(calcCandleChange(prevBar, currentBar));

    const color = currentBar.color ? removeAlphaFromHex(currentBar.color) : this.options().color;

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


import { SeriesType, Time } from 'lightweight-charts';

import { DateFormat } from '@src/utils/formatter';

import { TimeFormat } from './timeScale';
/**
 * Типы графиков, поддерживаемые библиотекой
 */
export type ChartSeriesType = SeriesType;

/**
 * Универсальный формат данных для всех типов графиков
 */
export interface ChartDataPoint {
  time?: Time;
  value?: number;
  open?: number;
  high?: number;
  low?: number;
  close?: number;
  volume?: number;
}

interface CandlestickChartOptions {
  upColor?: string;
  downColor?: string;
  wickUpColor?: string;
  wickDownColor?: string;
  borderVisible?: boolean;
}

interface LineChartOptions {
  lineStyle?: 'solid' | 'dotted' | 'dashed';
}

interface AreaChartOptions {
  topColor?: string;
  bottomColor?: string;
}

interface BaselineChartOptions {
  baseValue?: number;
  topLineColor?: string;
  bottomLineColor?: string;
  topFillColor1?: string;
  topFillColor2?: string;
  bottomFillColor1?: string;
  bottomFillColor2?: string;
}

interface TimescaleOptions {
  timeVisible?: boolean;
  secondsVisible?: boolean;
  timeFormat?: TimeFormat;
  dateFormat?: DateFormat;
}

interface CrosshairOptions {
  crosshairLineXColor?: string;
  crosshairLineYColor?: string;
  crosshairLabelXBgColor?: string;
  crosshairLabelYBgColor?: string;
}

interface HistogramOptions {
  scaleMargins?: {
    top: number;
    bottom: number;
  };
}

/**
 * @deprecated
 * Опции для разных типов графиков
 */
export interface ChartTypeOptions // todo: неверный интерфейс
  extends HistogramOptions,
    CrosshairOptions,
    TimescaleOptions,
    BaselineChartOptions,
    AreaChartOptions,
    LineChartOptions,
    CandlestickChartOptions {
  // Общие опции
  color?: string;
  lineWidth?: number;
}

export interface LineCandle {
  time: number;
  value: number;
  volume?: number;
}

export interface Candle {
  time: number;
  open: number;
  high: number;
  low: number;
  close: number;
  volume?: number; // todo: volume & future metaData to customFields
}

export type Destroyable = {
  destroy(): void;
};

export enum Direction {
  Top = 'top',
  Bottom = 'bottom',
  Left = 'left',
  Right = 'right',
}

// Настройки графика, которые могут изменяться в рантайме
export interface ChartOptionsModel {
  dateFormat: DateFormat;
  timeFormat: TimeFormat;
  showTime: boolean;
}


import {
  DeepPartial,
  HistogramData,
  LineData,
  LineWidth,
  PriceScaleOptions,
  SeriesDataItemTypeMap,
  SeriesPartialOptionsMap,
  SeriesType,
  Time,
  WhitespaceData,
} from 'lightweight-charts';

import { indicatorLabelById, IndicatorsIds } from '@src/constants';
import { IndicatorDataFormatter } from '@src/core/Indicators';
import { ChartSeriesType } from '@src/types/chart';
import { SettingField } from '@src/types/settings';

export type IndicatorData = HistogramData<Time> | LineData<Time> | WhitespaceData<Time>;

export type IndicatorType = 'SMA' | 'EMA' | 'RSI' | 'OHLC' | 'VOL';

export type MASource = 'open' | 'high' | 'low' | 'close';

export type IndicatorLabel = (ReturnType<typeof indicatorLabelById>)[IndicatorsIds];

export interface IndicatorStateConfig {
  type: IndicatorType;
  id?: IndicatorsIds;
  period?: number;
  color?: string;
  lineWidth?: LineWidth;
  visible?: boolean;
  params?: Record<string, any>;
  pane?: string;
}

export interface IndicatorSerie {
  id: string;
  name: ChartSeriesType;
  seriesOptions?: SeriesPartialOptionsMap[ChartSeriesType];
  priceScaleOptions?: DeepPartial<PriceScaleOptions>;
  priceScaleId?: string;
  dataFormatter?<T extends SeriesType>(params: IndicatorDataFormatter<T>): SeriesDataItemTypeMap<Time>[T][];
}

export interface IndicatorConfig {
  series: IndicatorSerie[];
  settings?: SettingField[];
  newPane?: boolean;
  label?: string;
  seriesLabels?: Record<string, string>;
  paletteStartIndex?: number;
}

export interface SerieOptionsWithColor {
  color?: string;
  visible?: boolean;
}


export * from '../theme/types';
export * from './chart';
export * from './compare';
export * from './drawing';
export * from './indicator';
export * from './intervals';
export * from './ohlc';
export * from './settings';
export * from './snapshot';
export * from './state';
export * from './timeframes';
export * from './timeScale';


import { BehaviorSubject } from 'rxjs';

import { DEFAULT_THEME_MODE, THEME_KEY, THEME_MODE } from './constants';
import { getThemeColors } from './theme';
import { ThemeConfig, ThemeKey, ThemeMode } from './types';

export type ThemeStore = ReturnType<typeof createThemeStore>;

function createThemeStore(initial: ThemeConfig) {
  const { theme, mode = DEFAULT_THEME_MODE[theme] } = initial;

  const theme$ = new BehaviorSubject<ThemeKey>(theme);
  const mode$ = new BehaviorSubject<ThemeMode>(mode);

  return {
    theme$,
    mode$,
    get theme() {
      return theme$.value;
    },
    get mode() {
      return mode$.value;
    },
    get colors() {
      return getThemeColors({ theme: theme$.value, mode: mode$.value });
    },
    setTheme: (t: ThemeKey) => theme$.next(t),
    setMode: (m: ThemeMode) => mode$.next(m),
    toggleMode: () => mode$.next(mode$.value === THEME_MODE.LIGHT ? THEME_MODE.DARK : THEME_MODE.LIGHT),
  };
}

let store: ThemeStore | null = null;

export function configureThemeStore(initial: ThemeConfig): ThemeStore {
  store = createThemeStore(initial);

  return store;
}

export function getThemeStore(): ThemeStore {
  if (!store) {
    store = createThemeStore({ theme: THEME_KEY.MXT, mode: THEME_MODE.LIGHT });
  }

  return store!;
}



export * from './constants';
export * from './helpers';
export * from './palette';
export * from './store';
export * from './theme';
export * from './types';