Загрузка данных
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;