Загрузка данных
import { IChartApi, IPaneApi, Time } from 'lightweight-charts';
import { BehaviorSubject, Subscription } from 'rxjs';
import { ChartTooltip } from '@components/ChartTooltip';
import { LegendComponent } from '@components/Legend';
import { ChartMouseEvents } from '@core/ChartMouseEvents';
import { ContainerManager } from '@core/ContainerManager';
import { DataSource } from '@core/DataSource';
import { DOMModel } from '@core/DOMModel';
import { DrawingsManager } from '@core/DrawingsManager';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { Legend } from '@core/Legend';
import { ReactRenderer } from '@core/ReactRenderer';
import { TooltipService } from '@core/Tooltip';
import { UIRenderer } from '@core/UIRenderer';
import { IndicatorSettingsModal } from '@src/components/IndicatorSettingsModal';
import { DrawingsNames, indicatorLabelById } from '@src/constants';
import { ModalRenderer } from '@src/core/ModalRenderer';
import { SeriesFactory, SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { OHLCConfig, TooltipConfig } from '@src/types';
import { ensureDefined } from '@src/utils';
export interface PaneParams {
id: number;
lwcChart: IChartApi;
eventManager: EventManager;
DOM: DOMModel;
isMainPane: boolean;
ohlcConfig: OHLCConfig;
dataSource: DataSource | null; // todo: deal with dataSource. На каких то пейнах он нужен, на каких то нет
basedOn?: Pane; // Pane на котором находится главная серия, или серия, по которой строятся серии на текущем пейне
subscribeChartEvent: ChartMouseEvents['subscribe'];
tooltipConfig: TooltipConfig;
onDelete: () => void;
chartContainer: HTMLElement;
modalRenderer: ModalRenderer;
}
// todo: Pane, ему должна принадлежать mainSerie, а также IndicatorManager и drawingsManager, mouseEvents. Также перекинуть соответствующие/необходимые свойства из чарта, и из чарта удалить
// todo: Учитывать, что есть линейка, которая рисуется одна для всех пейнов
// todo: в CompareManage, при создании нового пейна для сравнения - инициализируем новый dataSource, принадлежащий только конкретному пейну. Убираем возможность добавлять индикаторы на такие пейны
// todo: на каждый символ свой DataSource (учитывать что есть MainPane и "главный" DataSource, который инициализиурется во время старта moexChart)
// todo: сделать два разных представления для compare, в зависимости от отображения на главном пейне или на второстепенном
export class Pane {
private readonly id: number;
private isMain: boolean;
private mainSeries: BehaviorSubject<SeriesStrategies | null> = new BehaviorSubject<SeriesStrategies | null>(null); // Main Series. Exists in a single copy
private legend!: Legend;
private tooltip: TooltipService | undefined;
private indicatorsMap: BehaviorSubject<Map<string, Indicator>> = new BehaviorSubject<Map<string, Indicator>>(
new Map(),
);
private lwcPane: IPaneApi<Time>;
private lwcChart: IChartApi;
private eventManager: EventManager;
private drawingsManager: DrawingsManager;
private legendContainer!: HTMLElement;
private paneOverlayContainer!: HTMLElement;
private legendRenderer!: UIRenderer;
private tooltipRenderer: UIRenderer | undefined;
private modalRenderer: ModalRenderer;
private mainSerieSub!: Subscription;
private subscribeChartEvent: ChartMouseEvents['subscribe'];
private onDelete: () => void;
private subscriptions = new Subscription();
constructor({
lwcChart,
eventManager,
dataSource,
DOM,
isMainPane,
ohlcConfig,
id,
basedOn,
subscribeChartEvent,
tooltipConfig,
onDelete,
chartContainer,
modalRenderer,
}: PaneParams) {
this.onDelete = onDelete;
this.eventManager = eventManager;
this.lwcChart = lwcChart;
this.modalRenderer = modalRenderer;
this.subscribeChartEvent = subscribeChartEvent;
this.isMain = isMainPane ?? false;
this.id = id;
this.initializeLegend({ ohlcConfig });
if (isMainPane) {
this.lwcPane = this.lwcChart.panes()[this.id];
} else {
this.lwcPane = this.lwcChart.addPane(true);
}
this.tooltip = new TooltipService({
config: tooltipConfig,
legend: this.legend,
paneOverlayContainer: this.paneOverlayContainer,
});
this.tooltipRenderer = new ReactRenderer(this.paneOverlayContainer);
this.tooltipRenderer.renderComponent(
<ChartTooltip
formatObs={this.eventManager.getChartOptionsModel()}
timeframeObs={this.eventManager.getTimeframeObs()}
viewModel={this.tooltip.getTooltipViewModel()}
// ohlcConfig={this.legend.getConfig()}
ohlcConfig={ohlcConfig}
tooltipConfig={this.tooltip.getConfig()}
/>,
);
if (dataSource) {
this.initializeMainSerie({ lwcChart, dataSource });
} else if (basedOn) {
this.mainSeries = basedOn?.getMainSerie();
} else {
console.error('[Pane]: There is no any mainSerie for new pane');
}
this.drawingsManager = new DrawingsManager({
// todo: менеджер дровингов должен быть один на чарт, не на пейн
eventManager,
DOM,
mainSeries$: this.mainSeries.asObservable(),
lwcChart,
container: chartContainer,
});
this.subscriptions.add(
// todo: переедет в пейн
this.drawingsManager.entities().subscribe((drawings) => {
const hasRuler = drawings.some((drawing) => drawing.id.startsWith(DrawingsNames.ruler));
this.legendContainer.style.display = hasRuler ? 'none' : '';
}),
);
}
public getMainSerie = () => {
return this.mainSeries;
};
public getId = () => {
return this.id;
};
public setIndicator(indicatorId: string, indicator: Indicator): void {
const map = this.indicatorsMap.value;
map.set(indicatorId, indicator);
this.indicatorsMap.next(map);
}
public removeIndicator(indicatorId: string): void {
const map = this.indicatorsMap.value;
map.delete(indicatorId);
this.indicatorsMap.next(map);
if (map.size === 0 && !this.isMain) {
this.destroy();
}
}
public getDrawingManager(): DrawingsManager {
return this.drawingsManager;
}
private initializeLegend({ ohlcConfig }: { ohlcConfig: OHLCConfig }) {
const { legendContainer, paneOverlayContainer } = ContainerManager.createPaneContainers();
this.legendContainer = legendContainer;
this.paneOverlayContainer = paneOverlayContainer;
this.legendRenderer = new ReactRenderer(legendContainer);
requestAnimationFrame(() => {
setTimeout(() => {
const lwcPaneElement = this.lwcPane.getHTMLElement();
if (!lwcPaneElement) return;
lwcPaneElement.style.position = 'relative';
lwcPaneElement.appendChild(legendContainer);
lwcPaneElement.appendChild(paneOverlayContainer);
}, 0);
});
// todo: переписать код ниже под логику пейнов
// /*
// Внутри lightweight-chart DOM построен как таблица из 3 td
// [0] left priceScale, [1] center chart, [2] right priceScale
// Кладём легенду в td[1] и тогда легенда сама будет адаптироваться при изменении ширины шкал
// */
// requestAnimationFrame(() => {
// const root = chartAreaContainer.querySelector('.tv-lightweight-charts');
// console.log(root)
// const table = root?.querySelector('table');
// console.log(table)
//
// const htmlCollectionOfPanes = table?.getElementsByTagName('td')
// console.log(htmlCollectionOfPanes)
//
// const centerId = htmlCollectionOfPanes?.[1];
// console.log(centerId)
//
// if (centerId && legendContainer && legendContainer.parentElement !== centerId) {
// centerId.appendChild(legendContainer);
// }
// });
// /*
// Внутри lightweight-chart DOM построен как таблица из 3 td
// [0] left priceScale, [1] center chart, [2] right priceScale
// Кладём легенду в td[1] и тогда легенда сама будет адаптироваться при изменении ширины шкал
// */
// requestAnimationFrame(() => {
// const root = chartAreaContainer.querySelector('.tv-lightweight-charts');
// const table = root?.querySelector('table');
// const centerId = table?.getElementsByTagName('td')?.[1];
//
// if (centerId && legendContainer && legendContainer.parentElement !== centerId) {
// centerId.appendChild(legendContainer);
// }
// });
this.legend = new Legend({
config: ohlcConfig,
indicators: this.indicatorsMap,
eventManager: this.eventManager,
subscribeChartEvent: this.subscribeChartEvent,
mainSeries: this.isMain ? this.mainSeries : null,
paneId: this.id,
openIndicatorSettings: (indicatorId, indicator) => {
let settings = indicator.getSettings();
this.modalRenderer.renderComponent(
<IndicatorSettingsModal
fields={indicator.getSettingsConfig()}
values={settings}
onChange={(nextSettings) => {
settings = nextSettings;
}}
/>,
{
size: 'sm',
title: indicatorLabelById[indicatorId],
onSave: () => indicator.updateSettings(settings),
},
);
},
// todo: throw isMainPane
});
this.legendRenderer.renderComponent(
<LegendComponent
ohlcConfig={this.legend.getConfig()}
viewModel={this.legend.getLegendViewModel()}
/>,
);
}
private initializeMainSerie({ lwcChart, dataSource }: { lwcChart: IChartApi; dataSource: DataSource }) {
this.mainSerieSub = this.eventManager.subscribeSeriesSelected((nextSeries) => {
this.mainSeries.value?.destroy();
const next = ensureDefined(SeriesFactory.create(nextSeries))({
lwcChart,
dataSource,
mainSymbol$: this.eventManager.getSymbol(),
mainSerie$: this.mainSeries,
});
this.mainSeries.next(next);
});
}
public destroy() {
this.subscriptions.unsubscribe();
this.tooltip?.destroy();
this.legend?.destroy();
this.legendRenderer.destroy();
this.tooltipRenderer?.destroy();
this.indicatorsMap.complete();
this.mainSerieSub?.unsubscribe();
try {
this.lwcChart.removePane(this.id);
} catch (e) {
console.log(e);
}
this.onDelete();
}
}
import { useEffect, useState } from 'react';
import { NumberField, SelectField } from '@src/components/FormFields';
import { IndicatorSetting, IndicatorSettings } from '@src/types';
import styles from './index.module.scss';
interface IndicatorSettingsModalProps {
fields: IndicatorSetting[];
values: IndicatorSettings;
onChange: (settings: IndicatorSettings) => void;
}
export const IndicatorSettingsModal = ({ fields, values, onChange }: IndicatorSettingsModalProps) => {
const [form, setForm] = useState<IndicatorSettings>(values);
useEffect(() => {
setForm(values);
}, [values]);
const changeValue = (key: string, value: string | number) => {
setForm((current) => {
const next = {
...current,
[key]: value,
};
onChange(next);
return next;
});
};
return (
<div className={styles.form}>
{fields.map((field) => {
const value = form[field.key] ?? field.defaultValue;
if (field.type === 'select') {
return (
<SelectField
key={field.key}
label={field.label}
value={String(value)}
options={field.options}
onValueChange={(nextValue) => {
changeValue(field.key, nextValue);
}}
/>
);
}
return (
<NumberField
key={field.key}
label={field.label}
value={typeof value === 'number' ? value : field.defaultValue}
min={field.min}
max={field.max}
onValueChange={(nextValue) => {
changeValue(field.key, nextValue);
}}
/>
);
})}
</div>
);
};
import { IChartApi } from 'lightweight-charts';
import { BehaviorSubject, map, Observable } from 'rxjs';
import { DataSource } from '@core/DataSource';
import { DOMModel } from '@core/DOMModel';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { PaneManager } from '@core/PaneManager';
import { IndicatorsIds } from '@src/constants';
import { DOMObject } from '@src/core/DOMObject';
import { ChartTypeOptions } from '@src/types';
interface SeriesParams {
eventManager: EventManager;
dataSource: DataSource;
lwcChart: IChartApi;
paneManager: PaneManager;
DOM: DOMModel;
initialIndicators?: IndicatorsIds[];
chartOptions?: ChartTypeOptions;
}
export class IndicatorManager {
private eventManager: EventManager;
private lwcChart: IChartApi;
private chartOptions?: ChartTypeOptions;
private entities$: BehaviorSubject<Indicator[]> = new BehaviorSubject<Indicator[]>([]);
private indicatorsMap$: BehaviorSubject<Map<string, Indicator>> = new BehaviorSubject(new Map()); // todo: заменить IndicatorsIds ключ на уникальный id индикатора
private DOM: DOMModel;
private dataSource: DataSource;
private paneManager: PaneManager;
constructor({ eventManager, dataSource, lwcChart, DOM, chartOptions, initialIndicators, paneManager }: SeriesParams) {
this.eventManager = eventManager;
this.lwcChart = lwcChart;
this.chartOptions = chartOptions;
this.DOM = DOM;
this.dataSource = dataSource;
this.paneManager = paneManager;
this.indicatorsMap$ = new BehaviorSubject<Map<string, Indicator>>(new Map());
initialIndicators?.forEach((ind) => {
this.addIndicator(ind);
});
}
public addEntity<T extends Indicator>(
factory: (zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) => T,
): T {
return this.DOM.setEntity(factory);
}
public addIndicator(indicatorType: IndicatorsIds): void {
const indicatorsMap = new Map(this.indicatorsMap$.value);
const id = `${indicatorType}-${crypto.randomUUID()}`;
const indicatorToSet = this.addEntity<Indicator>(
(zIndex: number, moveUp: (id: string) => void, moveDown: (id: string) => void) =>
new Indicator({
id,
type: indicatorType,
lwcChart: this.lwcChart,
mainSymbol$: this.eventManager.getSymbol(),
dataSource: this.dataSource,
chartOptions: this.chartOptions,
zIndex,
onDelete: this.deleteIndicator,
moveUp,
moveDown,
paneManager: this.paneManager,
}),
);
indicatorsMap.set(id, indicatorToSet);
this.indicatorsMap$.next(indicatorsMap);
this.entities$.next(Array.from(indicatorsMap.values()));
}
public getIndicators() {
return this.indicatorsMap$.pipe(map((indicators) => Array.from(indicators.keys())));
}
public removeEntity(entity: DOMObject): void {
this.DOM.removeEntity(entity);
}
public deleteIndicator = (id: string) => {
const indicatorsMap = new Map(this.indicatorsMap$.value);
const entity = indicatorsMap.get(id);
if (!entity) {
return;
}
this.removeEntity(entity);
entity.destroy();
indicatorsMap.delete(id);
this.indicatorsMap$.next(indicatorsMap);
this.entities$.next(Array.from(indicatorsMap.values()));
};
public entities(): Observable<Indicator[]> {
return this.entities$.asObservable();
}
}
import { IChartApi } from 'lightweight-charts';
import { Observable, Subscription } from 'rxjs';
import { DataSource } from '@core/DataSource';
import { DOMObject, DOMObjectParams } from '@core/DOMObject';
import { Pane } from '@core/Pane';
import { PaneManager } from '@core/PaneManager';
import { indicatorLabelById, indicatorSeriesLabelById, IndicatorsIds } from '@src/constants';
import { indicatorsMap } from '@src/core/Indicators';
import { SeriesFactory, SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ChartTypeOptions, IndicatorConfig, IndicatorSettings } from '@src/types';
import { ensureDefined } from '@src/utils';
type IIndicator = DOMObject;
export interface IndicatorParams extends DOMObjectParams {
type?: IndicatorsIds;
mainSymbol$: Observable<string>;
lwcChart: IChartApi;
dataSource: DataSource;
paneManager: PaneManager;
chartOptions?: ChartTypeOptions;
config?: IndicatorConfig;
}
export class Indicator extends DOMObject implements IIndicator {
private indicatorType?: IndicatorsIds;
private series: SeriesStrategies[] = [];
private seriesMap: Map<string, SeriesStrategies> = new Map();
private lwcChart: IChartApi;
private dataSource: DataSource;
private mainSymbol$: Observable<string>;
private associatedPane: Pane;
private config: IndicatorConfig;
private settings: IndicatorSettings = {};
private dataChangeHandlers = new Set<() => void>();
private seriesSubscriptions = new Subscription();
constructor({
id,
type,
lwcChart,
dataSource,
zIndex,
onDelete,
moveUp,
moveDown,
mainSymbol$,
paneManager,
config,
}: IndicatorParams) {
super({ id, name: config?.label ?? id, zIndex, onDelete, moveUp, moveDown });
this.lwcChart = lwcChart;
this.dataSource = dataSource;
this.mainSymbol$ = mainSymbol$;
this.indicatorType = type;
this.config = ensureDefined(type ? indicatorsMap[type] : config);
this.name = this.getLabel();
this.settings = this.getDefaultSettings();
this.associatedPane = this.config.newPane ? paneManager.addPane() : paneManager.getMainPane();
this.createSeries();
this.associatedPane.setIndicator(this.id, this);
}
public subscribeDataChange(handler: () => void): Subscription {
this.dataChangeHandlers.add(handler);
return new Subscription(() => {
this.dataChangeHandlers.delete(handler);
});
}
public getLabel(): string {
if (this.config.label) {
return this.config.label;
}
if (this.indicatorType) {
return indicatorLabelById[this.indicatorType];
}
return this.id;
}
public getSeriesLabel(serieName: string): string | undefined {
if (this.config.seriesLabels?.[serieName]) {
return this.config.seriesLabels[serieName];
}
if (this.indicatorType) {
return indicatorSeriesLabelById[this.indicatorType]?.[serieName];
}
return undefined;
}
public getId(): string {
return this.id;
}
public getType(): IndicatorsIds | undefined {
return this.indicatorType;
}
public getPane(): Pane {
return this.associatedPane;
}
public getSeriesMap(): Map<string, SeriesStrategies> {
return this.seriesMap;
}
public getSettings(): IndicatorSettings {
return { ...this.settings };
}
public getSettingsConfig() {
return this.config.settings ?? [];
}
public hasSettings(): boolean {
return Boolean(this.config.settings?.length);
}
public updateSettings(settings: IndicatorSettings): void {
this.settings = settings;
// todo: обновлять данные серий без удаления и повторного создания
this.destroySeries();
this.createSeries();
this.notifyDataChanged();
}
public show() {
this.series.forEach((s) => {
s.show();
});
super.show();
}
public hide() {
this.series.forEach((s) => {
s.hide();
});
super.hide();
}
// destroy и delete принципиально отличаются!
// delete вызовет destroy в конце концов. По сути - это destroy с сайд-эффектом в eventManager
public delete() {
super.delete();
}
// destroy и delete принципиально отличаются!
public destroy() {
this.destroySeries();
this.dataChangeHandlers.clear();
this.associatedPane.removeIndicator(this.id);
}
private createSeries(): void {
this.config.series.forEach(({ name, id: serieId, dataFormatter, seriesOptions, priceScaleOptions }) => {
const serie = SeriesFactory.create(name!)({
lwcChart: this.lwcChart,
dataSource: this.dataSource,
customFormatter: dataFormatter
? (params) =>
dataFormatter({
...params,
settings: this.settings,
indicatorReference: this,
})
: undefined,
seriesOptions,
priceScaleOptions,
mainSymbol$: this.mainSymbol$,
mainSerie$: this.associatedPane.getMainSerie(),
showSymbolLabel: false,
paneIndex: this.associatedPane.getId(),
indicatorReference: this,
});
const handleDataChanged = () => {
this.notifyDataChanged();
};
serie.subscribeDataChanged(handleDataChanged);
this.seriesSubscriptions.add(() => {
serie.unsubscribeDataChanged(handleDataChanged);
});
this.seriesMap.set(serieId, serie);
this.series.push(serie);
});
}
private destroySeries(): void {
this.seriesSubscriptions.unsubscribe();
this.seriesSubscriptions = new Subscription();
this.series.forEach((s) => {
s.destroy();
});
this.series = [];
this.seriesMap.clear();
}
private notifyDataChanged(): void {
this.dataChangeHandlers.forEach((handler) => {
handler();
});
}
private getDefaultSettings(): IndicatorSettings {
const settings: IndicatorSettings = {};
this.config.settings?.forEach((field) => {
settings[field.key] = field.defaultValue;
});
return settings;
}
}
import {
DeepPartial,
HistogramData,
LineData,
LineWidth,
PriceScaleOptions,
SeriesDataItemTypeMap,
SeriesPartialOptionsMap,
SeriesType,
Time,
WhitespaceData,
} from 'lightweight-charts';
import { indicatorLabelById, IndicatorsIds } from '@src/constants';
import { IndicatorDataFormatter } from '@src/core/Indicators';
import { ChartSeriesType } from '@src/types/chart';
export type IndicatorData = HistogramData<Time> | LineData<Time> | WhitespaceData<Time>;
export type IndicatorType = 'SMA' | 'EMA' | 'RSI' | 'OHLC' | 'VOL';
export type MASource = 'open' | 'high' | 'low' | 'close';
export type IndicatorSetting = NumberIndicatorSetting | SelectIndicatorSetting;
export type IndicatorSettings = Record<string, string | number>;
export type IndicatorLabel = (typeof indicatorLabelById)[IndicatorsIds];
export interface IndicatorStateConfig {
type: IndicatorType;
id?: IndicatorsIds;
period?: number;
color?: string;
lineWidth?: LineWidth;
visible?: boolean;
params?: Record<string, any>;
pane?: string;
}
interface BaseIndicatorSetting {
key: string;
label: string;
}
interface NumberIndicatorSetting extends BaseIndicatorSetting {
type: 'number';
defaultValue: number;
min?: number;
max?: number;
}
interface SelectIndicatorSetting extends BaseIndicatorSetting {
type: 'select';
defaultValue: string;
options: { label: string; value: string | number }[];
}
export interface IndicatorSerie {
id: string;
name: ChartSeriesType;
seriesOptions?: SeriesPartialOptionsMap[ChartSeriesType];
priceScaleOptions?: DeepPartial<PriceScaleOptions>;
priceScaleId?: string;
dataFormatter?<T extends SeriesType>(params: IndicatorDataFormatter<T>): SeriesDataItemTypeMap<Time>[T][];
}
export interface IndicatorConfig {
series: IndicatorSerie[];
settings?: IndicatorSetting[];
newPane?: boolean;
label?: string;
seriesLabels?: Record<string, string>;
}
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, IndicatorSettings, 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 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' },
],
},
],
},
};