Загрузка данных
import {
DeepPartial,
HistogramData,
LineData,
LineWidth,
PriceScaleOptions,
SeriesDataItemTypeMap,
SeriesPartialOptionsMap,
SeriesType,
Time,
WhitespaceData,
} from 'lightweight-charts';
import { IndicatorDataFormatter, indicatorLabelById } from '@src/core/Indicators';
import { ChartSeriesType } from '@src/types/chart';
export enum IndicatorsIds {
Volume = 'vol',
SMA = 'sma',
EMA = 'ema',
MACD = 'macd',
}
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 const indicatorSeriesLabelById: Partial<Record<IndicatorsIds, Record<string, string>>> = {
[IndicatorsIds.MACD]: {
oscillatorFastMa: 'Fast',
oscillatorSlowMa: 'Slow',
macdLine: 'MACD',
signalLine: 'Signal',
histogram: 'Histogram',
},
};
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;
}
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 { DOMObject } from '@src/core/DOMObject';
import { ChartTypeOptions, IndicatorsIds } 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()));
this.indicatorsMap$.next(indicatorsMap);
}
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 indicatorsMapExt = this.indicatorsMap$.value;
const entity = indicatorsMapExt.get(id);
if (!entity) {
return;
}
this.removeEntity(entity);
entity.destroy();
indicatorsMapExt.delete(id);
this.indicatorsMap$.next(indicatorsMapExt);
};
public entities(): Observable<Indicator[]> {
return this.entities$.asObservable();
}
}
import { BehaviorSubject } from 'rxjs';
enum DOMObjectType {
Drawing = 'Drawing',
Indicator = 'Indicator',
}
export interface IDOMObject {
id: string;
hidden: BehaviorSubject<boolean>;
zIndex: number;
type: DOMObjectType;
name: string;
delete(): void;
hide(): void;
show(): void;
lastUpdated(): void;
moveUp(): void;
moveDown(): void;
setZIndex(next: number): void;
}
export interface DOMObjectParams {
id: string;
zIndex: number;
onDelete: (id: string) => void;
moveUp: (id: string) => void;
moveDown: (id: string) => void;
}
export class DOMObject implements IDOMObject {
public readonly id: string;
public zIndex: number;
public hidden = new BehaviorSubject(false);
public moveUp: () => void;
public moveDown: () => void;
protected onDelete: (id: string) => void;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
name: string;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
type: DOMObjectType;
constructor({ id, zIndex, onDelete, moveUp, moveDown }: DOMObjectParams) {
this.id = id;
this.zIndex = zIndex;
this.onDelete = onDelete;
this.moveUp = () => moveUp(this.id);
this.moveDown = () => moveDown(this.id);
}
delete(): void {
this.onDelete(this.id);
}
hide(): void {
this.hidden.next(true);
}
show(): void {
this.hidden.next(false);
}
lastUpdated(): void {}
setZIndex(next: number): void {
this.zIndex = next;
}
}
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 { indicatorsMap } from '@src/core/Indicators';
import { SeriesFactory, SeriesStrategies } from '@src/modules/series-strategies/SeriesFactory';
import { ChartTypeOptions, IndicatorConfig, IndicatorSettings, IndicatorsIds } 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: id as string, zIndex, onDelete, moveUp, moveDown });
this.lwcChart = lwcChart;
this.dataSource = dataSource;
this.mainSymbol$ = mainSymbol$;
this.indicatorType = type;
if (type) {
this.config = ensureDefined(indicatorsMap[type]);
} else {
this.config = ensureDefined(config);
}
this.settings = this.getDefaultSettings();
this.associatedPane = this.config.newPane ? paneManager.addPane() : paneManager.getMainPane();
this.createSeries();
this.associatedPane.setIndicator(id as IndicatorsIds, this);
}
public subscribeDataChange(handler: () => void): Subscription {
this.dataChangeHandlers.add(handler);
return new Subscription(() => {
this.dataChangeHandlers.delete(handler);
});
}
public getId(): string {
return this.id;
}
public getType(): IndicatorsIds {
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 as IndicatorsIds);
}
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;
}
}