Загрузка данных
import { HistogramData, LineData, SeriesDataItemTypeMap, Time } from 'lightweight-charts';
import { calculateEMASeriesData, calculatePreciseEMASeriesData } from '@core/Indicators/ema';
import { ChartTypeToCandleData, IndicatorDataFormatter } from '@core/Indicators/index';
import { getThemeStore } from '@src/theme';
export function macdSignal({
selfData,
candle,
indicatorReference,
}: IndicatorDataFormatter<'Line'>): SeriesDataItemTypeMap<Time>['Line'][] {
if (!indicatorReference) return [];
const macd = (indicatorReference.getSeriesMap().get('macd')?.data() ??
[]) as unknown as ChartTypeToCandleData['Line'][];
if (!macd) return [];
const signalLength = 9;
if (!candle) {
return calculateEMASeriesData(macd, signalLength);
}
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('macd')?.data() ??
[]) as unknown as ChartTypeToCandleData['Line'][];
const macdSign = (indicatorReference.getSeriesMap().get('macd_signal')?.data() ??
[]) as unknown as ChartTypeToCandleData['Line'][];
if (!macd || !macdSign) return [];
const { colors } = getThemeStore();
if (!candle) {
let shift = 0;
const res: HistogramData<Time>[] = [];
macdSign.forEach((point, i) => {
while (macd[shift + i].time !== point.time) {
shift++;
}
const value = macd[shift + i].value - macdSign[i].value;
const prevValue = (res && res[res.length - 1]?.value) ?? 0;
res.push({
value,
time: point.time as Time,
color:
value > 0
? value > prevValue
? colors.chartCandleUp
: colors.chartCandleWickUp
: value < prevValue
? colors.chartCandleDown
: colors.chartCandleWickDown,
});
});
return res;
}
const short = macdSign.findIndex((c) => c.time === candle.time);
const long = macd.findIndex((c) => c.time === candle.time);
if (short === -1 || long === -1) {
console.error('[Indicators]: ошибка при расчете индикатора macd');
return [
{
value: 0,
time: candle.time as Time,
},
];
}
const value = macd[long].value - macdSign[short].value;
const prevValueIndex = macd.findIndex((c) => c.time === candle.time);
const prevValue = (selfData && selfData[prevValueIndex - 1]?.value) ?? 0;
const res = {
value,
time: candle.time as Time,
color:
value > 0
? value > prevValue
? colors.chartCandleUp
: colors.chartCandleWickUp
: value < prevValue
? colors.chartCandleDown
: colors.chartCandleWickDown,
};
return [res];
}
export function macdLine({ candle, indicatorReference }: IndicatorDataFormatter<'Line'>): LineData[] {
if (!indicatorReference) return [];
const longEma = (indicatorReference.getSeriesMap().get('longEma')?.data() as LineData[]) ?? null;
const shortEma = (indicatorReference.getSeriesMap().get('shortEma')?.data() as LineData[]) ?? null;
if (!longEma || !shortEma) return [];
if (!candle) {
let shift = 0;
const res: LineData[] = [];
longEma.forEach((point, i) => {
while (shortEma[shift + i].time !== point.time) {
shift++;
}
const value = shortEma[shift + i].value - longEma[i].value; // todo: use signalLength
res.push({
value,
time: point.time as Time,
});
});
return res;
}
const short = shortEma.findIndex((c) => c.time === candle.time);
const long = longEma.findIndex((c) => c.time === candle.time);
if (short === -1 || long === -1) {
console.error('[Indicators]: ошибка при расчете индикатора macd');
return [
{
value: 0,
time: candle.time as Time,
},
];
}
const value = shortEma[short].value - longEma[long].value;
const res = {
value,
time: candle.time as Time,
};
return [res];
}
import { PriceScaleMode, SeriesType } from 'lightweight-charts';
import { Indicator } from '@core/Indicator';
import { emaIndicator } from '@core/Indicators/ema';
import { macdHist, macdLine, 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 { getThemeStore } from '@src/theme';
import { Direction, IndicatorConfig, IndicatorSettings, IndicatorsIds, LineCandle } 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?: IndicatorSettings;
}
export const indicatorLabelById = {
[IndicatorsIds.Volume]: 'Объём',
[IndicatorsIds.SMA]: 'SMA',
[IndicatorsIds.EMA]: 'EMA',
[IndicatorsIds.MACD]: 'MACD',
} as const satisfies Record<IndicatorsIds, string>;
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: 'longEma',
dataFormatter: (params) => {
return emaIndicator(params as IndicatorDataFormatter<'Line'>, 26);
},
seriesOptions: {
priceScaleId: 'macd_emas',
visible: false,
lastValueVisible: false,
color: getThemeStore().colors.chartPriceLineText,
},
},
{
name: 'Line', // todo: change with enum
id: 'shortEma',
dataFormatter: (params) => {
return emaIndicator(params as IndicatorDataFormatter<'Line'>, 12);
},
seriesOptions: {
priceScaleId: 'macd_emas',
visible: false,
lastValueVisible: false,
color: getThemeStore().colors.chartPriceLineText,
},
},
{
name: 'Line', // todo: change with enum
id: 'macd',
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: 'macd_signal',
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: 'hist',
priceScaleOptions: {
autoScale: true,
},
seriesOptions: {
priceScaleId: Direction.Right,
lastValueVisible: false,
},
dataFormatter: (params) => macdHist(params as IndicatorDataFormatter<'Histogram'>),
},
],
},
};