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


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

import { SerieData } from '@core/Series/BaseSeries';

import { getMASource } from '@src/utils';

import { LineCandle } from '../../types';

import { IndicatorDataFormatter } from './index';

export function emaIndicator(
  { mainSeriesData, selfData, candle, settings }: IndicatorDataFormatter<'Line'>,
  defaultLength = 25,
): SeriesDataItemTypeMap<Time>['Line'][] {
  const length = typeof settings?.length === 'number' ? settings.length : defaultLength;
  const offset = typeof settings?.offset === 'number' ? settings.offset : 0;
  const source = getMASource(settings?.source);

  const sourceData: LineCandle[] = mainSeriesData.map((point) => {
    const values = point.customValues as unknown as Record<string, unknown>;
    if (!values) {
      return {
        time: point.time as Time,
      } as unknown as LineCandle;
    }

    const sourceValue = values[source];
    const fallbackValue = values.close ?? values.value;

    return {
      time: Number(values.time),
      value: typeof sourceValue === 'number' ? sourceValue : typeof fallbackValue === 'number' ? fallbackValue : 0,
    };
  });

  if (!candle || offset !== 0) {
    const calculatedSeries = calculateEMASeriesData(sourceData, length);

    if (offset === 0) {
      return calculatedSeries;
    }

    const shiftedSeries: SeriesDataItemTypeMap<Time>['Line'][] = [];

    for (let index = 0; index < calculatedSeries.length; index += 1) {
      const targetIndex = index + offset;

      if (targetIndex < 0 || targetIndex >= sourceData.length) {
        continue;
      }

      const point = calculatedSeries[index];
      const targetTime = sourceData[targetIndex].time as Time;

      if ('value' in point && typeof point.value === 'number') {
        shiftedSeries.push({
          time: targetTime,
          value: point.value,
        });
      } else {
        shiftedSeries.push({
          time: targetTime,
        });
      }
    }

    return shiftedSeries;
  }

  if (!selfData) {
    return [{ time: candle.time as Time }];
  }

  return [calculatePreciseEMASeriesData(sourceData, selfData, candle, length)];
}

export function calculatePreciseEMASeriesData(
  candleData: LineCandle[],
  currentIndicatorData: LineCandle[],
  candle: SerieData,
  maLength: number,
): SeriesDataItemTypeMap<Time>['Line'] {
  const candleIndexToCalculate = candleData.findIndex((c) => c.time === candle.time);

  if (candleIndexToCalculate === -1) {
    console.error('[Indicators]: нет подходящей свечи в массиве');
    return {
      time: candle.time as Time,
      value: 0,
    };
  }

  const prevCandleIndex = currentIndicatorData.findIndex((c) => c.time === candle.time);
  const prevCandle = currentIndicatorData[prevCandleIndex - 1];

  const smoothing = 2;
  const k = smoothing / (maLength + 1);

  const prevEma = prevCandle?.value ?? candleData[candleIndexToCalculate].value;
  const ema = k * candleData[candleIndexToCalculate].value + prevEma * (1 - k);

  return {
    time: candle.time as Time,
    value: ema,
  };
}

export function calculateEMASeriesData(
  candleData: LineCandle[],
  maLength: number,
): SeriesDataItemTypeMap<Time>['Line'][] {
  // todo: change signature to {time & value}
  const maData: LineData<Time>[] = [];

  const smoothing = 2;
  const k = smoothing / (maLength + 1);

  for (let i = 0; i < candleData.length; i++) {
    if (i < maLength - 1 || !candleData[i].value) {
      // Provide whitespace data points until the MA can be calculated
      maData.push({ time: candleData[i].time as Time } as LineData<Time>);
    } else if (i === maLength - 1) {
      let sum = 0;
      for (let j = 0; j < maLength; j++) {
        sum += candleData[i - j].value;
      }
      const maValue = sum / maLength;
      maData.push({
        time: candleData[i].time as Time,
        value: maValue,
      });
    } else {
      const prevEma = maData[maData.length - 1]?.value ?? 0;
      const ema = k * candleData[i].value + prevEma * (1 - k);

      maData.push({
        time: candleData[i].time as Time,
        value: ema,
      });
    }
  }

  return maData;
}



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

import { calculateEMASeriesData, calculatePreciseEMASeriesData, emaIndicator } from '@core/Indicators/ema';
import { ChartTypeToCandleData, IndicatorDataFormatter } from '@core/Indicators/index';
import { calculateMASeriesData, calculatePreciseMASeriesData, smaIndicator } from '@core/Indicators/sma';
import { getThemeStore } from '@src/theme';
import { getMASource } from '@src/utils';

type MACDMaType = 'ema' | 'sma';

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

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

  if (!macd.length) return [];

  const signalLength = typeof settings?.signalLength === 'number' ? settings.signalLength : 9;
  const signalMaType = getMACDMaType(settings?.signalMaType);

  if (!candle) {
    return signalMaType === 'sma'
      ? calculateMASeriesData(macd, signalLength)
      : calculateEMASeriesData(macd, signalLength);
  }

  if (signalMaType === 'sma') {
    return [calculatePreciseMASeriesData(macd, candle, signalLength)];
  }

  if (!selfData) {
    return [{ time: candle.time as Time, value: 0 }];
  }

  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('macdLine')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];
  const signal = (indicatorReference.getSeriesMap().get('signalLine')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];

  if (!macd.length || !signal.length) return [];

  const { colors } = getThemeStore();

  if (!candle) {
    const signalByTime = new Map<number, number>();

    signal.forEach((point) => {
      if (typeof point.value === 'number') {
        signalByTime.set(Number(point.time), point.value);
      }
    });

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

    macd.forEach((point) => {
      if (typeof point.value !== 'number') {
        return;
      }

      const signalValue = signalByTime.get(Number(point.time));

      if (signalValue === undefined) {
        return;
      }

      const value = point.value - signalValue;
      const prevValue = result[result.length - 1]?.value ?? 0;

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

    return result;
  }

  const macdPoint = macd.find((point) => Number(point.time) === Number(candle.time) && typeof point.value === 'number');
  const signalPoint = signal.find(
    (point) => Number(point.time) === Number(candle.time) && typeof point.value === 'number',
  );

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

  const value = macdPoint.value - signalPoint.value;
  const prevPoint =
    selfData[selfData.length - 1]?.time === candle.time ? selfData[selfData.length - 2] : selfData[selfData.length - 1];
  const prevValue = prevPoint?.value ?? 0;

  return [
    {
      value,
      time: candle.time as Time,
      color:
        value > 0
          ? value > prevValue
            ? colors.chartCandleUp
            : colors.chartCandleWickUp
          : value < prevValue
            ? colors.chartCandleDown
            : colors.chartCandleWickDown,
    },
  ];
}

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

  const slowMa = (indicatorReference.getSeriesMap().get('oscillatorSlowMa')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];
  const fastMa = (indicatorReference.getSeriesMap().get('oscillatorFastMa')?.data() ??
    []) as unknown as ChartTypeToCandleData['Line'][];

  if (!slowMa.length || !fastMa.length) return [];

  if (!candle) {
    const fastByTime = new Map<number, number>();

    fastMa.forEach((point) => {
      if (typeof point.value === 'number') {
        fastByTime.set(Number(point.time), point.value);
      }
    });

    const result: LineData[] = [];

    slowMa.forEach((point) => {
      if (typeof point.value !== 'number') {
        return;
      }

      const fastValue = fastByTime.get(Number(point.time));

      if (fastValue === undefined) {
        return;
      }

      result.push({
        value: fastValue - point.value,
        time: point.time as Time,
      });
    });

    return result;
  }

  const slowPoint = slowMa.find(
    (point) => Number(point.time) === Number(candle.time) && typeof point.value === 'number',
  );
  const fastPoint = fastMa.find(
    (point) => Number(point.time) === Number(candle.time) && typeof point.value === 'number',
  );

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

  return [
    {
      value: fastPoint.value - slowPoint.value,
      time: candle.time as Time,
    },
  ];
}

export function macdOscillatorFastMa(params: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
  const source = getMASource(params.settings?.source);
  const fastLength = typeof params.settings?.fastLength === 'number' ? params.settings.fastLength : 12;
  const oscillatorMaType = getMACDMaType(params.settings?.oscillatorMaType);

  const nextParams = {
    ...params,
    settings: {
      length: fastLength,
      source,
      offset: 0,
    },
  } as IndicatorDataFormatter<'Line'>;

  return oscillatorMaType === 'sma' ? smaIndicator(nextParams) : emaIndicator(nextParams);
}

export function macdOscillatorSlowMa(params: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
  const source = getMASource(params.settings?.source);
  const slowLength = typeof params.settings?.slowLength === 'number' ? params.settings.slowLength : 26;
  const oscillatorMaType = getMACDMaType(params.settings?.oscillatorMaType);

  const nextParams = {
    ...params,
    settings: {
      length: slowLength,
      source,
      offset: 0,
    },
  } as IndicatorDataFormatter<'Line'>;

  return oscillatorMaType === 'sma' ? smaIndicator(nextParams) : emaIndicator(nextParams);
}

function getMACDMaType(value: unknown): MACDMaType {
  return value === 'sma' ? 'sma' : 'ema';
}



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

import { IndicatorDataFormatter } from '@core/Indicators/index';
import { SerieData } from '@core/Series/BaseSeries';
import { LineCandle } from '@src/types';
import { getMASource } from '@src/utils';

export function smaIndicator(
  { mainSeriesData, candle, settings }: IndicatorDataFormatter<'Line'>,
  defaultLength = 10,
): SeriesDataItemTypeMap<Time>['Line'][] {
  const length = typeof settings?.length === 'number' ? settings.length : defaultLength;
  const offset = typeof settings?.offset === 'number' ? settings.offset : 0;
  const source = getMASource(settings?.source);

  const sourceData: LineCandle[] = mainSeriesData.map((point) => {
    const values = point.customValues as unknown as Record<string, unknown>;
    const sourceValue = values[source];
    const fallbackValue = values.close ?? values.value;

    return {
      time: Number(values.time),
      value: typeof sourceValue === 'number' ? sourceValue : typeof fallbackValue === 'number' ? fallbackValue : 0,
    };
  });

  if (!candle || offset !== 0) {
    const calculatedSeries = calculateMASeriesData(sourceData, length);

    if (offset === 0) {
      return calculatedSeries;
    }

    const shiftedSeries: SeriesDataItemTypeMap<Time>['Line'][] = [];

    for (let index = 0; index < calculatedSeries.length; index += 1) {
      const targetIndex = index + offset;

      if (targetIndex < 0 || targetIndex >= sourceData.length) {
        continue;
      }

      const point = calculatedSeries[index];
      const targetTime = sourceData[targetIndex].time as Time;

      if ('value' in point && typeof point.value === 'number') {
        shiftedSeries.push({
          time: targetTime,
          value: point.value,
        });
      } else {
        shiftedSeries.push({
          time: targetTime,
        });
      }
    }

    return shiftedSeries;
  }

  return [calculatePreciseMASeriesData(sourceData, candle, length)];
}

export function calculatePreciseMASeriesData(
  candleData: LineCandle[],
  candle: SerieData,
  maLength: number,
): SeriesDataItemTypeMap<Time>['Line'] {
  const candleIndexToCalculate = candleData.findIndex((c) => c.time === candle.time);

  if (candleIndexToCalculate === -1) {
    console.error('[Indicators]: нет подходящей свечи в массиве');
    return {
      time: candle.time as Time,
      value: 0,
    };
  }

  let sum;
  for (let i = candleIndexToCalculate; i > candleIndexToCalculate - maLength; i--) {
    if (i < 0) break;

    sum = sum ?? 0;

    sum += candleData[i].value;
  }

  return {
    time: candle.time as Time,
    value: sum && sum / maLength,
  };
}

export function calculateMASeriesData(
  candleData: LineCandle[],
  maLength: number,
): SeriesDataItemTypeMap<Time>['Line'][] {
  const maData: SeriesDataItemTypeMap<Time>['Line'][] = [];

  for (let i = 0; i < candleData.length; i++) {
    if (i < maLength) {
      // Provide whitespace data points until the MA can be calculated
      maData.push({ time: candleData[i].time as Time });
    } else {
      // Calculate the moving average, slow but simple way
      let sum = 0;
      for (let j = 0; j < maLength; j++) {
        sum += candleData[i - j].value;
      }
      const maValue = sum / maLength;
      maData.push({
        time: candleData[i].time as Time,
        value: maValue,
      });
    }
  }

  return maData;
}



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

import { IndicatorDataFormatter } from '@core/Indicators/index';
import { getThemeStore } from '@src/theme';

export function volume({
  mainSeriesData,
  candle,
}: IndicatorDataFormatter<'Histogram'>): SeriesDataItemTypeMap<Time>['Histogram'][] {
  const { colors } = getThemeStore();
  if (!candle) {
    return mainSeriesData.map((d) => {
      const cv = d.customValues;

      if (!cv) {
        return {
          time: d.time as Time,
        };
      }

      return {
        time: d.time as Time,
        value: cv.volume,
        color: cv.close && cv.open && cv.close >= cv.open ? colors.chartCandleWickUp : colors.chartCandleWickDown,
      };
    });
  }

  const cv = candle.customValues;

  return [
    {
      time: candle.time as Time,
      value: cv.volume,
      color: cv.close && cv.open && cv.close >= cv.open ? colors.chartCandleWickUp : colors.chartCandleWickDown,
    },
  ];
}


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

import { Indicator } from '@core/Indicator';
import { emaIndicator } from '@core/Indicators/ema';
import { macdHist, macdLine, macdOscillatorFastMa, macdOscillatorSlowMa, 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 { IndicatorsIds } from '@src/constants';
import { INDICATOR_COLOR_PALETTE_MIDDLE_INDEX } from '@src/theme';
import { t } from '@src/translations';
import { IndicatorConfig, LineCandle, SettingsValues } 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?: SettingsValues;
}

export const indicatorsMap = (): Partial<Record<IndicatorsIds, IndicatorConfig>> => ({
  [IndicatorsIds.Volume]: {
    series: [
      {
        name: 'Histogram',
        id: 'volume',
        priceScaleOptions: {
          scaleMargins: { top: 0.7, bottom: 0 },
        },
        seriesOptions: {
          priceScaleId: 'vol',
          lastValueVisible: false,
          priceLineVisible: false,
          priceFormat: {
            type: 'volume',
          },
        },
        dataFormatter: (params) => volume(params as IndicatorDataFormatter<'Histogram'>),
      },
    ],
  },
  [IndicatorsIds.SMA]: {
    series: [
      {
        name: 'Line', // todo: change with enum
        id: 'sma',
        seriesOptions: {},
        dataFormatter: (params) => smaIndicator(params as IndicatorDataFormatter<'Line'>),
      },
    ],
    settings: [
      { type: 'number', key: 'length', label: t('Length'), defaultValue: 10, min: 1, max: 500 },
      {
        type: 'select',
        key: 'source',
        label: t('Data'),
        defaultValue: 'close',
        options: [
          { label: t('Open price'), value: 'open' },
          { label: t('Max'), value: 'high' },
          { label: t('Min'), value: 'low' },
          { label: t('Close price'), value: 'close' },
        ],
      },
      { type: 'number', key: 'offset', label: t('Offset'), defaultValue: 0, min: -100, max: 100 },
    ],
  },
  [IndicatorsIds.EMA]: {
    paletteStartIndex: INDICATOR_COLOR_PALETTE_MIDDLE_INDEX,
    series: [
      {
        name: 'Line', // todo: change with enum
        id: 'ema',
        seriesOptions: {},
        dataFormatter: (params) => {
          return emaIndicator(params as IndicatorDataFormatter<'Line'>);
        },
      },
    ],
    settings: [
      { type: 'number', key: 'length', label: t('Length'), defaultValue: 10, min: 1, max: 500 },
      {
        type: 'select',
        key: 'source',
        label: t('Data'),
        defaultValue: 'close',
        options: [
          { label: t('Open price'), value: 'open' },
          { label: t('Max'), value: 'high' },
          { label: t('Min'), value: 'low' },
          { label: t('Close price'), value: 'close' },
        ],
      },
      { type: 'number', key: 'offset', label: t('Offset'), defaultValue: 0, min: -100, max: 100 },
    ],
  },
  [IndicatorsIds.MACD]: {
    newPane: true,
    series: [
      {
        name: 'Line', // todo: change with enum
        id: 'oscillatorSlowMa',
        dataFormatter: (params) => macdOscillatorSlowMa(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: 'macd_oscillator_ma',
          visible: false,
          lastValueVisible: false,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'oscillatorFastMa',
        dataFormatter: (params) => macdOscillatorFastMa(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: 'macd_oscillator_ma',
          visible: false,
          lastValueVisible: false,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'macdLine',
        actLikeMainSerie: true,
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdLine(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          lastValueVisible: false,
        },
      },
      {
        name: 'Line', // todo: change with enum
        actLikeMainSerie: true,
        id: 'signalLine',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdSignal(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          lastValueVisible: false,
        },
      },
      {
        name: 'Histogram', // todo: change with enum
        actLikeMainSerie: true,
        id: 'histogram',
        priceScaleOptions: {
          autoScale: true,
        },
        seriesOptions: {
          lastValueVisible: false,
        },
        dataFormatter: (params) => macdHist(params as IndicatorDataFormatter<'Histogram'>),
      },
    ],
    settings: [
      {
        type: 'select',
        key: 'source',
        label: t('Data'),
        defaultValue: 'close',
        options: [
          { label: t('Open price'), value: 'open' },
          { label: t('Max'), value: 'high' },
          { label: t('Min'), value: 'low' },
          { label: t('Close price'), value: 'close' },
        ],
      },
      { type: 'number', key: 'fastLength', label: t('Fast length'), defaultValue: 12, min: 1, max: 500 },
      { type: 'number', key: 'slowLength', label: t('Slow length'), defaultValue: 26, min: 1, max: 500 },
      { type: 'number', key: 'signalLength', label: t('Signal length'), defaultValue: 9, min: 1, max: 500 },
      {
        type: 'select',
        key: 'oscillatorMaType',
        label: t('Oscillator MA type'),
        defaultValue: 'ema',
        options: [
          { label: 'EMA', value: 'ema' },
          { label: 'SMA', value: 'sma' },
        ],
      },
      {
        type: 'select',
        key: 'signalMaType',
        label: t('Signal MA type'),
        defaultValue: 'ema',
        options: [
          { label: 'EMA', value: 'ema' },
          { label: 'SMA', value: 'sma' },
        ],
      },
    ],
  },
});