Загрузка данных
import { IChartApi, PriceScaleMode, SeriesType } from 'lightweight-charts';
import { BehaviorSubject, distinctUntilChanged, map, Observable } from 'rxjs';
import { DataSource } from '@core/DataSource';
import { EventManager } from '@core/EventManager';
import { Indicator } from '@core/Indicator';
import { IndicatorManager } from '@core/IndicatorManager';
import { PaneManager } from '@core/PaneManager';
import { MAIN_PANE_INDEX } from '@src/constants';
import { getThemeStore } from '@src/theme';
import { CompareItem, CompareMode, Direction, IndicatorConfig } from '@src/types';
import { normalizeSymbol } from '@src/utils';
interface CompareEntry {
key: string;
symbol: string;
mode: CompareMode;
paneIndex: number;
symbol$: BehaviorSubject<string>;
entity: Indicator;
}
function makeKey(symbol: string, mode: CompareMode): string {
return `${symbol}|${mode}`;
}
interface CompareManagerParams {
chart: IChartApi;
eventManager: EventManager;
dataSource: DataSource;
indicatorManager: IndicatorManager;
paneManager: PaneManager;
}
const getDefaultCompareIndicatorConfig = (): IndicatorConfig => {
return {
newPane: true,
series: [
{
name: 'Line', // todo: change with enum
id: `compare-${Math.random()}`,
seriesOptions: {
visible: true,
color: getThemeStore().colors.chartLineColor,
},
},
],
};
};
export class CompareManager {
private readonly chart: IChartApi;
private readonly eventManager: EventManager;
private readonly dataSource: DataSource;
private readonly indicatorManager: IndicatorManager;
private readonly paneManager: PaneManager;
private readonly entries = new Map<string, CompareEntry>();
private readonly itemsSubject = new BehaviorSubject<CompareItem[]>([]);
private readonly entitiesSubject = new BehaviorSubject<Indicator[]>([]);
constructor({ chart, eventManager, dataSource, indicatorManager, paneManager }: CompareManagerParams) {
this.chart = chart;
this.eventManager = eventManager;
this.dataSource = dataSource;
this.indicatorManager = indicatorManager;
this.paneManager = paneManager;
this.eventManager.timeframe().subscribe(() => {
this.applyPolicy();
});
}
public itemsObs(): Observable<CompareItem[]> {
return this.itemsSubject.asObservable();
}
public entitiesObs(): Observable<Indicator[]> {
return this.entitiesSubject.asObservable();
}
public snapshot(): CompareItem[] {
return this.itemsSubject.value;
}
public clear(): void {
const keys = Array.from(this.entries.keys());
for (let i = 0; i < keys.length; i += 1) {
this.removeByKey(keys[i]);
}
this.applyPolicy();
this.publish();
}
public async setSymbolMode(seriesType: SeriesType, symbolRaw: string, mode: CompareMode): Promise<void> {
const symbol = normalizeSymbol(symbolRaw);
if (!symbol) return;
if (mode === CompareMode.NewScale && this.isNewScaleDisabled()) {
return;
}
const key = makeKey(symbol, mode);
if (this.entries.has(key)) return;
const symbol$ = new BehaviorSubject(symbol);
const entity = this.indicatorManager.addEntity<Indicator>((zIndex, moveUp, moveDown) => {
const config = getDefaultCompareIndicatorConfig();
return new Indicator({
id: key,
lwcChart: this.chart,
mainSymbol$: new BehaviorSubject<string>(symbol),
dataSource: this.dataSource,
zIndex,
onDelete: () => this.removeByKey(key),
moveUp,
moveDown,
paneManager: this.paneManager,
config: {
...config,
series: [
{
...config.series[0],
seriesOptions: {
...config.series[0]?.seriesOptions,
priceScaleId: mode === CompareMode.NewScale ? Direction.Left : Direction.Right,
},
},
],
newPane: mode === CompareMode.NewPane,
},
});
});
this.entries.set(key, { key, symbol, mode, paneIndex: entity.getPane().getId(), symbol$, entity });
this.applyPolicy();
this.publish();
await this.dataSource.isReady(symbol);
}
public removeSymbolMode(symbolRaw: string, mode: CompareMode): void {
const symbol = normalizeSymbol(symbolRaw);
if (!symbol) return;
this.removeByKey(makeKey(symbol, mode));
this.applyPolicy();
this.publish();
}
public removeSymbol(symbolRaw: string): void {
const symbol = normalizeSymbol(symbolRaw);
if (!symbol) return;
const all = Array.from(this.entries.entries());
for (let i = 0; i < all.length; i += 1) {
const [key, entry] = all[i];
if (entry.symbol === symbol) this.removeByKey(key);
}
this.applyPolicy();
this.publish();
}
public isNewScaleDisabled(): boolean {
return this.itemsSubject.value.length > 0;
}
public isNewScaleDisabledObservable(): Observable<boolean> {
return this.itemsSubject.pipe(
map((items) => items.length > 0),
distinctUntilChanged(),
);
}
public getAllEntities() {
return Array.from(this.entries.values()).map(({ symbol, entity, mode }) => ({
symbol,
entity,
mode,
}));
}
public destroy(): void {
this.clear();
this.itemsSubject.complete();
this.entitiesSubject.complete();
}
private removeByKey(key: string): void {
const entry = this.entries.get(key);
if (!entry) return;
this.entries.delete(key);
this.indicatorManager.removeEntity(entry.entity);
entry.entity.destroy();
entry.symbol$.complete();
this.applyPolicy();
this.publish();
}
private publish(): void {
const values = Array.from(this.entries.values());
const items: CompareItem[] = [];
const entities: Indicator[] = [];
for (let i = 0; i < values.length; i += 1) {
items.push({ symbol: values[i].symbol, mode: values[i].mode });
entities.push(values[i].entity);
}
this.itemsSubject.next(items);
this.entitiesSubject.next(entities);
}
private applyPolicy(): void {
const list = Array.from(this.entries.values());
let percentEnabled = false;
for (let i = 0; i < list.length; i += 1) {
if (list[i].mode === CompareMode.Percentage) {
percentEnabled = true;
break;
}
}
let hasMainNewScale = false;
for (let i = 0; i < list.length; i += 1) {
// [0 - в индикаторах compare сущности может быть только одна серия] [1 - entry]
const paneIndex = Array.from(list[i].entity.getSeriesMap())[0][1].getPane().paneIndex();
if (paneIndex === MAIN_PANE_INDEX && list[i].mode === CompareMode.NewScale) {
hasMainNewScale = true;
break;
}
}
const paneSet = new Set<number>();
for (let i = 0; i < list.length; i += 1) {
// [0 - в индикаторах compare сущности может быть только одна серия] [1 - entry]
const paneIndex = Array.from(list[i].entity.getSeriesMap())[0][1].getPane().paneIndex();
if (paneIndex !== MAIN_PANE_INDEX) paneSet.add(paneIndex);
}
this.chart.applyOptions({
leftPriceScale: { visible: hasMainNewScale, borderVisible: false },
});
this.chart.priceScale(Direction.Right, MAIN_PANE_INDEX).applyOptions({
visible: true,
mode: percentEnabled ? PriceScaleMode.Percentage : PriceScaleMode.Normal,
});
this.chart.priceScale(Direction.Left, MAIN_PANE_INDEX).applyOptions({
visible: hasMainNewScale,
mode: PriceScaleMode.Normal,
borderVisible: false,
});
const panes = Array.from(paneSet.values());
for (let i = 0; i < panes.length; i += 1) {
const paneIndex = panes[i];
this.chart.priceScale(Direction.Right, paneIndex).applyOptions({
visible: true,
mode: PriceScaleMode.Normal,
});
if (hasMainNewScale) {
this.chart.priceScale(Direction.Left, paneIndex).applyOptions({
visible: true,
mode: PriceScaleMode.Normal,
borderVisible: false,
});
}
}
for (let i = 0; i < list.length; i += 1) {
const entry = list[i];
// [0 - в индикаторах compare сущности может быть только одна серия] [1 - entry]
const serie = Array.from(entry.entity.getSeriesMap())[0][1];
const paneIndex = serie.getPane().paneIndex();
if (paneIndex !== MAIN_PANE_INDEX) {
serie.applyOptions({ priceScaleId: Direction.Right });
continue;
}
serie.applyOptions({
priceScaleId: entry.mode === CompareMode.NewScale ? Direction.Left : Direction.Right,
});
}
}
}