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


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>;
    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 { 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 { 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 { Direction, 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',
          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: 'Длина', 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: {},
        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: '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',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdLine(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'signalLine',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdSignal(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
      },
      {
        name: 'Histogram', // todo: change with enum
        id: 'histogram',
        priceScaleOptions: {
          autoScale: true,
        },
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
        dataFormatter: (params) => macdHist(params as IndicatorDataFormatter<'Histogram'>),
      },
    ],
    settings: [
      {
        type: 'select',
        key: 'source',
        label: 'Данные',
        defaultValue: 'close',
        options: [
          { label: 'Цена открытия', value: 'open' },
          { label: 'Максимум', value: 'high' },
          { label: 'Минимум', value: 'low' },
          { label: 'Цена закрытия', value: 'close' },
        ],
      },
      { type: 'number', key: 'fastLength', label: 'Длина Fast', defaultValue: 12, min: 1, max: 500 },
      { type: 'number', key: 'slowLength', label: 'Длина Slow', defaultValue: 26, min: 1, max: 500 },
      { type: 'number', key: 'signalLength', label: 'Signal length', defaultValue: 9, min: 1, max: 500 },
      {
        type: 'select',
        key: 'oscillatorMaType',
        label: 'Oscillator MA type',
        defaultValue: 'ema',
        options: [
          { label: 'EMA', value: 'ema' },
          { label: 'SMA', value: 'sma' },
        ],
      },
      {
        type: 'select',
        key: 'signalMaType',
        label: 'Signal MA type',
        defaultValue: 'ema',
        options: [
          { label: 'EMA', value: 'ema' },
          { label: 'SMA', value: 'sma' },
        ],
      },
    ],
  },
};
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 { Direction, 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',
          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: 'Длина', 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: {},
        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: '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',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdLine(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
      },
      {
        name: 'Line', // todo: change with enum
        id: 'signalLine',
        priceScaleOptions: {
          mode: PriceScaleMode.Normal,
        },
        dataFormatter: (params) => macdSignal(params as IndicatorDataFormatter<'Line'>),
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
      },
      {
        name: 'Histogram', // todo: change with enum
        id: 'histogram',
        priceScaleOptions: {
          autoScale: true,
        },
        seriesOptions: {
          priceScaleId: Direction.Right,
          lastValueVisible: false,
        },
        dataFormatter: (params) => macdHist(params as IndicatorDataFormatter<'Histogram'>),
      },
    ],
    settings: [
      {
        type: 'select',
        key: 'source',
        label: 'Данные',
        defaultValue: 'close',
        options: [
          { label: 'Цена открытия', value: 'open' },
          { label: 'Максимум', value: 'high' },
          { label: 'Минимум', value: 'low' },
          { label: 'Цена закрытия', value: 'close' },
        ],
      },
      { type: 'number', key: 'fastLength', label: 'Длина Fast', defaultValue: 12, min: 1, max: 500 },
      { type: 'number', key: 'slowLength', label: 'Длина Slow', defaultValue: 26, min: 1, max: 500 },
      { type: 'number', key: 'signalLength', label: 'Signal length', defaultValue: 9, min: 1, max: 500 },
      {
        type: 'select',
        key: 'oscillatorMaType',
        label: 'Oscillator MA type',
        defaultValue: 'ema',
        options: [
          { label: 'EMA', value: 'ema' },
          { label: 'SMA', value: 'sma' },
        ],
      },
      {
        type: 'select',
        key: 'signalMaType',
        label: 'Signal MA type',
        defaultValue: 'ema',
        options: [
          { label: 'EMA', value: 'ema' },
          { label: 'SMA', value: 'sma' },
        ],
      },
    ],
  },
};


import cloneDeep from 'lodash-es/cloneDeep';

import { INDICATOR_COLOR_PALETTE } from '@src/theme';
import { IndicatorConfig, IndicatorSerie, MASource, SerieOptionsWithColor } from '@src/types';
import { normalizeColor } from '@src/utils/normalizeColor';

export function getMASource(value: unknown): MASource {
  return value === 'open' || value === 'high' || value === 'low' || value === 'close' ? value : 'close';
}

export function applyNextIndicatorColors(config: IndicatorConfig, usedColors: readonly string[]): IndicatorConfig {
  const nextConfig = cloneDeep(config);
  const reservedColors = new Set(usedColors.map(normalizeColor));
  const colorizableSeriesCount = nextConfig.series.filter(shouldUsePaletteColor).length;
  const startIndex = getNextFreePaletteIndex(reservedColors);

  let colorizableSeriesIndex = 0;

  nextConfig.series = nextConfig.series.map((serie) => {
    if (!shouldUsePaletteColor(serie)) {
      return serie;
    }

    const color =
      colorizableSeriesCount > 1
        ? getGroupedPaletteColor(reservedColors, startIndex, colorizableSeriesIndex, colorizableSeriesCount)
        : getNextPaletteColor(reservedColors);

    colorizableSeriesIndex += 1;
    reservedColors.add(normalizeColor(color));

    return {
      ...serie,
      seriesOptions: {
        ...serie.seriesOptions,
        color,
      },
    };
  });

  return nextConfig;
}

export function getIndicatorColors(config: IndicatorConfig): string[] {
  return config.series.reduce<string[]>((colors, serie) => {
    if (!shouldUsePaletteColor(serie)) {
      return colors;
    }

    const color = getSerieColor(serie);

    if (color) {
      colors.push(color);
    }

    return colors;
  }, []);
}

function getNextPaletteColor(usedColors: Set<string>): string {
  return getPaletteColorFromIndex(usedColors, 0);
}

function getGroupedPaletteColor(
  usedColors: Set<string>,
  startIndex: number,
  seriesIndex: number,
  seriesCount: number,
): string {
  const step = Math.max(1, Math.floor(INDICATOR_COLOR_PALETTE.length / seriesCount));

  return getPaletteColorFromIndex(usedColors, startIndex + seriesIndex * step);
}

function getPaletteColorFromIndex(usedColors: Set<string>, startIndex: number): string {
  for (let offset = 0; offset < INDICATOR_COLOR_PALETTE.length; offset += 1) {
    const color = INDICATOR_COLOR_PALETTE[(startIndex + offset) % INDICATOR_COLOR_PALETTE.length];

    if (!usedColors.has(normalizeColor(color))) {
      return color;
    }
  }

  return createFallbackColor(usedColors.size);
}

function getNextFreePaletteIndex(usedColors: Set<string>): number {
  const index = INDICATOR_COLOR_PALETTE.findIndex((color) => !usedColors.has(normalizeColor(color)));

  return index === -1 ? 0 : index;
}

function createFallbackColor(index: number): string {
  const hue = Math.round((index * 137.508) % 360);

  return `hsl(${hue}deg 72% 56%)`;
}

function shouldUsePaletteColor(serie: IndicatorSerie): boolean {
  const options = serie.seriesOptions as SerieOptionsWithColor | undefined;

  return serie.name === 'Line' && options?.visible !== false;
}

function getSerieColor(serie: IndicatorSerie): string | null {
  const options = serie.seriesOptions as SerieOptionsWithColor | undefined;
  const color = options?.color;

  return typeof color === 'string' && color.trim() ? color : null;
}


import { THEME_KEY, THEME_MODE } from './constants';
import { hexWithAplha } from './helpers';
import { ThemePalette } from './types';

export const THEME_PALETTE: ThemePalette = {
  [THEME_KEY.MXT]: {
    [THEME_MODE.LIGHT]: {
      neutral0: '#ffffff',
      neutral1Inv: '#fbfcfe',
      neutral2: '#f8f9fa',
      neutral2Inv: 'f8f9fa',
      neutral4: '#e7e8e8',
      neutral5: '#dfe0e4',
      neutral7: '#cececf',
      neutral12: '#202020',
      neutral12Inv: '#202020',
      neutral13: '#000000',
      neutral13Inv: hexWithAplha('#000000', 0.8),
      blue1: '#4865ef',
      red1: '#d71d38',
      orange1: '#ef623a',
      blue2: hexWithAplha('#4865ef', 0.2),
      red2: hexWithAplha('#d71d38', 0.2),
      red4: hexWithAplha('#d71d38', 0.4),
      red5: hexWithAplha('#d71d38', 0.2),
      green1: '#24a249',
      green4: hexWithAplha('#24a249', 0.4),
      green5: hexWithAplha('#24a249', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#26c4d9', 0.8),
      teal3: hexWithAplha('#24a249', 0.1),
      pink2: hexWithAplha('#e05287', 0.8),
      gray5: '#dfe0e4',
      gray10: '#818182',
      gray11: '#646465',
      gray12: '#202020',
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
  },
  [THEME_KEY.MB]: {
    [THEME_MODE.LIGHT]: {
      neutral0: '#ffffff',
      neutral1Inv: '#fcfcfd',
      neutral2: '#f9f9fb',
      neutral2Inv: '#f9f9fb',
      neutral4: '#e7E8EC',
      neutral5: '#E0E1E6',
      neutral7: '#cdced7',
      neutral12: '#1E1F24',
      neutral12Inv: '#1e1f24',
      neutral13: '#000000',
      neutral13Inv: hexWithAplha('#000000', 0.8),
      blue1: '#4c69e7',
      red1: '#d71d38',
      orange1: '#e7613f',
      blue2: hexWithAplha('#4c69e7', 0.2),
      red2: hexWithAplha('#d71d38', 0.2),
      red4: hexWithAplha('#d71d38', 0.4),
      red5: hexWithAplha('#d71d38', 0.2),
      green1: '#3cae0b',
      green4: hexWithAplha('#3cae0b', 0.4),
      green5: hexWithAplha('#3cae0b', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#26c4d9', 0.8),
      teal3: hexWithAplha('#3cae0b', 0.1),
      pink2: hexWithAplha('#e05287', 0.8),
      gray5: '#e0e1e6',
      gray10: '#80828d',
      gray11: '#62636c',
      gray12: '#1e1f24',
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
    [THEME_MODE.DARK]: {
      neutral0: '#000000',
      neutral2: '#19191b',
      neutral3: hexWithAplha('#222325', 0.8),
      neutral4: '#292A2E',
      neutral5: '#303136',
      neutral7: '#46484f',
      neutral12: '#EEEEF0',
      neutral13: '#FFFFFF',
      blue1: '#4c69e7',
      red1: '#d71d38',
      orange1: '#e7613f',
      blue2: hexWithAplha('#4c69e7', 0.4),
      red2: hexWithAplha('#d71d38', 0.2),
      red4: hexWithAplha('#d71d38', 0.4),
      red5: hexWithAplha('#d71d38', 0.2),
      green1: '#3cae0b',
      green4: hexWithAplha('#3cae0b', 0.4),
      green5: hexWithAplha('#3cae0b', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#26c4d9', 0.8),
      teal3: hexWithAplha('#3cae0b', 0.2),
      pink2: hexWithAplha('#e05287', 0.8),
      gray5: '#303136',
      gray10: '#797b86',
      gray11: '#b2b3bd',
      gray12: '#eeeef0',
      gray12Inv: '#303136',
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
  },
  [THEME_KEY.TR]: {
    [THEME_MODE.DARK]: {
      neutral0: '#111112',
      neutral2: hexWithAplha('#3d3d5c', 0.52),
      neutral3: '#21212c',
      neutral4: hexWithAplha('#5041ff', 0.28),
      neutral5: hexWithAplha('#5041ff', 0.52),
      neutral7: hexWithAplha('#3d3d5c', 0.52),
      neutral12: hexWithAplha('#eeeef0', 0.52),
      neutral13: hexWithAplha('#eeeef0', 0.84),
      blue1: '#5041ff',
      red1: hexWithAplha('#ff0000', 0.52),
      blue2: hexWithAplha('#5041ff', 0.4),
      red2: hexWithAplha('#ff0000', 0.2),
      red4: hexWithAplha('#ff0000', 0.2),
      red5: hexWithAplha('#ff0000', 0.2),
      orange1: '#ff5a33',
      green1: hexWithAplha('#27ef00', 0.52),
      green4: hexWithAplha('#27ef00', 0.2),
      green5: hexWithAplha('#27ef00', 0.2),
      magenta1: '#aa01ff',
      magenta2: hexWithAplha('#aa01ff', 0.2),
      teal2: hexWithAplha('#4db2b2', 0.8),
      teal3: hexWithAplha('#27ef00', 0.2),
      pink2: hexWithAplha('#b24ca1', 0.8),
      gray5: hexWithAplha('#3d3d5c', 0.28),
      gray10: hexWithAplha('#ffffff', 0.68),
      gray11: hexWithAplha('#ffffff', 0.52),
      gray12: hexWithAplha('#ffffff', 0.84),
      gray12Inv: hexWithAplha('#3d3d5c', 0.68),
      contrast1: '#ffffff',
      contrast2: hexWithAplha('#ffffff', 0.84),
    },
  },
} as const;

export const INDICATOR_COLOR_PALETTE = [
  '#56A8FF',
  '#4A90FF',
  '#4F79FF',
  '#6A63FF',
  '#8B5FFF',
  '#AF5DF8',
  '#D45FEE',
  '#F062DB',
  '#FF67BB',
  '#FF6F8F',
  '#FF8A4A',
  '#F5A43C',
  '#E7BE44',
  '#D5D44E',
  '#B8DB58',
  '#96D663',
  '#74CF73',
  '#5FCB97',
  '#67D4BF',
  '#79D8E2',
] as const;