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


import { HistogramData, LineData, SeriesDataItemTypeMap, Time } from 'lightweight-charts';

import { calculateEMASeriesData, calculatePreciseEMASeriesData } from '@core/Indicators/ema';
import { ChartTypeToCandleData, IndicatorDataFormatter } from '@core/Indicators/index';
import { getThemeStore } from '@src/theme';

export function macdSignal({
  selfData,
  candle,
  indicatorReference,
}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
  if (!indicatorReference) return [];

  const macd = (indicatorReference.getSeriesMap().get('macd')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];

  if (!macd) return [];

  const signalLength = 9;

  if (!candle) {
    return calculateEMASeriesData(macd, signalLength);
  }

  return [calculatePreciseEMASeriesData(macd, selfData, candle, signalLength)];
}

export function macdHist({
  selfData,
  candle,
  indicatorReference,
}: IndicatorDataFormatter<'Histogram'>): SeriesDataItemTypeMap<Time>['Histogram'][] {
  if (!indicatorReference) return [];

  const macd = (indicatorReference.getSeriesMap().get('macd')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];
  const macdSign = (indicatorReference.getSeriesMap().get('macd_signal')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];

  if (!macd || !macdSign) return [];

  const { colors } = getThemeStore();

  if (!candle) {
    let shift = 0;

    const res: HistogramData<Time>[] = [];

    macdSign.forEach((point, i) => {
      while (macd[shift + i].time !== point.time) {
        shift++;
      }

      const value = macd[shift + i].value - macdSign[i].value;
      const prevValue = (res && res[res.length - 1]?.value) ?? 0;

      res.push({
        value,
        time: point.time as Time,
        color:
          value > 0
            ? value > prevValue
              ? colors.chartCandleUp
              : colors.chartCandleWickUp
            : value < prevValue
              ? colors.chartCandleDown
              : colors.chartCandleWickDown,
      });
    });

    return res;
  }

  const short = macdSign.findIndex((c) => c.time === candle.time);
  const long = macd.findIndex((c) => c.time === candle.time);

  if (short === -1 || long === -1) {
    console.error('[Indicators]: ошибка при расчете индикатора macd');
    return [
      {
        value: 0,
        time: candle.time as Time,
      },
    ];
  }

  const value = macd[long].value - macdSign[short].value;

  const prevValueIndex = macd.findIndex((c) => c.time === candle.time);

  const prevValue = (selfData && selfData[prevValueIndex - 1]?.value) ?? 0;

  const res = {
    value,
    time: candle.time as Time,
    color:
      value > 0
        ? value > prevValue
          ? colors.chartCandleUp
          : colors.chartCandleWickUp
        : value < prevValue
          ? colors.chartCandleDown
          : colors.chartCandleWickDown,
  };

  return [res];
}

export function macdLine({ candle, indicatorReference }: IndicatorDataFormatter<'Line'>): LineData[] {
  if (!indicatorReference) return [];

  const longEma = (indicatorReference.getSeriesMap().get('longEma')?.data() as LineData[]) ?? null;
  const shortEma = (indicatorReference.getSeriesMap().get('shortEma')?.data() as LineData[]) ?? null;

  if (!longEma || !shortEma) return [];

  if (!candle) {
    let shift = 0;

    const res: LineData[] = [];

    longEma.forEach((point, i) => {
      while (shortEma[shift + i].time !== point.time) {
        shift++;
      }

      const value = shortEma[shift + i].value - longEma[i].value; // todo: use signalLength

      res.push({
        value,
        time: point.time as Time,
      });
    });

    return res;
  }

  const short = shortEma.findIndex((c) => c.time === candle.time);
  const long = longEma.findIndex((c) => c.time === candle.time);

  if (short === -1 || long === -1) {
    console.error('[Indicators]: ошибка при расчете индикатора macd');
    return [
      {
        value: 0,
        time: candle.time as Time,
      },
    ];
  }

  const value = shortEma[short].value - longEma[long].value;

  const res = {
    value,
    time: candle.time as Time,
  };

  return [res];
}



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

import { Indicator } from '@core/Indicator';
import { emaIndicator } from '@core/Indicators/ema';
import { macdHist, macdLine, macdSignal } from '@core/Indicators/macd';
import { smaIndicator } from '@core/Indicators/sma';
import { volume } from '@core/Indicators/volume';
import { SerieData } from '@core/Series/BaseSeries';

import { Candle } from '@lib';
import { getThemeStore } from '@src/theme';
import { Direction, IndicatorConfig, IndicatorSettings, IndicatorsIds, LineCandle } from '@src/types';

export type ChartTypeToCandleData = {
  ['Bar']: Candle;
  ['Candlestick']: Candle;
  ['Area']: LineCandle;
  ['Baseline']: LineCandle;
  ['Line']: LineCandle;
  ['Histogram']: LineCandle;
  ['Custom']: Candle;
};

export interface IndicatorDataFormatter<T extends SeriesType> {
  mainSeriesData: SerieData[];
  selfData: ChartTypeToCandleData[T][];
  candle?: SerieData;
  indicatorReference?: Indicator;
  settings?: IndicatorSettings;
}

export const indicatorLabelById = {
  [IndicatorsIds.Volume]: 'Объём',
  [IndicatorsIds.SMA]: 'SMA',
  [IndicatorsIds.EMA]: 'EMA',
  [IndicatorsIds.MACD]: 'MACD',
} as const satisfies Record<IndicatorsIds, string>;

export const indicatorsMap: Partial<Record<IndicatorsIds, IndicatorConfig>> = {
  [IndicatorsIds.Volume]: {
    series: [
      {
        name: 'Histogram',
        id: 'volume',
        priceScaleOptions: {
          scaleMargins: { top: 0.7, bottom: 0 },
        },
        seriesOptions: {
          priceScaleId: 'vol',
          priceFormat: {
            type: 'volume',
          },
        },
        dataFormatter: (params) => volume(params as IndicatorDataFormatter<'Histogram'>),
      },
    ],
  },
  [IndicatorsIds.SMA]: {
    series: [
      {
        name: 'Line', // todo: change with enum
        id: 'sma',
        seriesOptions: {
          color: getThemeStore().colors.indicatorLineSma,
        },
        dataFormatter: (params) => smaIndicator(params as IndicatorDataFormatter<'Line'>),
      },
    ],
    settings: [
      { type: 'number', key: 'length', label: 'Длина', defaultValue: 10, min: 1, max: 500 },
      {
        type: 'select',
        key: 'source',
        label: 'Данные',
        defaultValue: 'close',
        options: [
          { label: 'Цена открытия', value: 'open' },
          { label: 'Максимум', value: 'high' },
          { label: 'Минимум', value: 'low' },
          { label: 'Цена закрытия', value: 'close' },
        ],
      },
      { type: 'number', key: 'offset', label: 'Отступ', defaultValue: 0, min: -100, max: 100 },
    ],
  },
  [IndicatorsIds.EMA]: {
    series: [
      {
        name: 'Line', // todo: change with enum
        id: 'ema',
        seriesOptions: {
          color: getThemeStore().colors.indicatorLineEma,
        },
        dataFormatter: (params) => {
          return emaIndicator(params as IndicatorDataFormatter<'Line'>);
        },
      },
    ],
    settings: [
      { type: 'number', key: 'length', label: 'Длина', defaultValue: 10, min: 1, max: 500 },
      {
        type: 'select',
        key: 'source',
        label: 'Данные',
        defaultValue: 'close',
        options: [
          { label: 'Цена открытия', value: 'open' },
          { label: 'Максимум', value: 'high' },
          { label: 'Минимум', value: 'low' },
          { label: 'Цена закрытия', value: 'close' },
        ],
      },
      { type: 'number', key: 'offset', label: 'Отступ', defaultValue: 0, min: -100, max: 100 },
    ],
  },
  [IndicatorsIds.MACD]: {
    newPane: true,
    series: [
      {
        name: 'Line', // todo: change with enum
        id: 'longEma',
        dataFormatter: (params) => {
          return emaIndicator(params as IndicatorDataFormatter<'Line'>, 26);
        },
        seriesOptions: {
          priceScaleId: 'macd_emas',
          visible: false,
          lastValueVisible: false,
          color: getThemeStore().colors.chartPriceLineText,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'shortEma',
        dataFormatter: (params) => {
          return emaIndicator(params as IndicatorDataFormatter<'Line'>, 12);
        },
        seriesOptions: {
          priceScaleId: 'macd_emas',
          visible: false,
          lastValueVisible: false,
          color: getThemeStore().colors.chartPriceLineText,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'macd',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdLine(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
          color: getThemeStore().colors.indicatorLineSma,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'macd_signal',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdSignal(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
          color: getThemeStore().colors.chartCandleWickUp,
        },
      },
      {
        name: 'Histogram', // todo: change with enum
        id: 'hist',
        priceScaleOptions: {
          autoScale: true,
        },
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
        dataFormatter: (params) => macdHist(params as IndicatorDataFormatter<'Histogram'>),
      },
    ],
  },
};