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


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

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

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

import { IndicatorDataFormatter } from './index';

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

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 rawSource = settings?.source;
  const source: MASource =
    rawSource === 'open' || rawSource === 'high' || rawSource === 'low' || rawSource === 'close'
      ? rawSource
      : 'close';

  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 = 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, value: 0 }];
  }

  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.length - 2;

  const prevCandle = currentIndicatorData[prevCandleIndex];

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

  const prevEma = prevCandle?.value ?? 0;
  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) {
      // Provide whitespace data points until the MA can be calculated
      maData.push({ time: candleData[i].time as Time, value: 0 });
    } 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 { SeriesDataItemTypeMap, Time } from 'lightweight-charts';

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

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

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 rawSource = settings?.source;
  const source: MASource =
    rawSource === 'open' || rawSource === 'high' || rawSource === 'low' || rawSource === 'close'
      ? rawSource
      : 'close';

  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 = 0;
  for (let i = candleIndexToCalculate; i > candleIndexToCalculate - maLength; i--) {
    if (i < 0) break;

    sum += candleData[i].value;
  }

  return {
    time: candle.time as Time,
    value: 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;
}