Загрузка данных
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 { getThemeStore } from '@src/theme';
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: {
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: 'oscillatorSlowMa',
dataFormatter: (params) => macdOscillatorSlowMa(params as IndicatorDataFormatter<'Line'>),
seriesOptions: {
priceScaleId: 'macd_oscillator_ma',
visible: false,
lastValueVisible: false,
color: getThemeStore().colors.chartPriceLineText,
},
},
{
name: 'Line', // todo: change with enum
id: 'oscillatorFastMa',
dataFormatter: (params) => macdOscillatorFastMa(params as IndicatorDataFormatter<'Line'>),
seriesOptions: {
priceScaleId: 'macd_oscillator_ma',
visible: false,
lastValueVisible: false,
color: getThemeStore().colors.chartPriceLineText,
},
},
{
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,
color: getThemeStore().colors.indicatorLineSma,
},
},
{
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,
color: getThemeStore().colors.chartCandleWickUp,
},
},
{
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 { 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;
}