goldenChat base source add
This commit is contained in:
@@ -0,0 +1,545 @@
|
||||
import {
|
||||
createChart,
|
||||
CandlestickSeries,
|
||||
BarSeries,
|
||||
LineSeries,
|
||||
AreaSeries,
|
||||
BaselineSeries,
|
||||
HistogramSeries,
|
||||
createSeriesMarkers,
|
||||
type IChartApi,
|
||||
type ISeriesApi,
|
||||
type SeriesType,
|
||||
type MouseEventParams,
|
||||
type Time,
|
||||
type IPriceLine,
|
||||
type ISeriesMarkersPluginApi,
|
||||
ColorType,
|
||||
LineStyle,
|
||||
} from 'lightweight-charts-v5';
|
||||
import type { OHLCVBar, ChartType, Theme, IndicatorConfig } from './types';
|
||||
import { calculateIndicator, getIndicatorDef, type PlotData, type MarkerData } from './indicatorRegistry';
|
||||
import { calcAutoFib } from './calculations';
|
||||
|
||||
interface ThemeTokens {
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
gridColor: string;
|
||||
borderColor: string;
|
||||
crosshairColor: string;
|
||||
upColor: string;
|
||||
downColor: string;
|
||||
}
|
||||
|
||||
const DARK: ThemeTokens = {
|
||||
bgColor: '#131722',
|
||||
textColor: '#D1D4DC',
|
||||
gridColor: '#1e2330',
|
||||
borderColor: '#2A2E39',
|
||||
crosshairColor: '#9598A1',
|
||||
upColor: '#26a69a',
|
||||
downColor: '#ef5350',
|
||||
};
|
||||
const LIGHT: ThemeTokens = {
|
||||
bgColor: '#FFFFFF',
|
||||
textColor: '#131722',
|
||||
gridColor: '#F0F3FA',
|
||||
borderColor: '#E0E3EB',
|
||||
crosshairColor: '#758696',
|
||||
upColor: '#26a69a',
|
||||
downColor: '#ef5350',
|
||||
};
|
||||
|
||||
interface IndicatorEntry {
|
||||
id: string;
|
||||
type: string;
|
||||
seriesList: ISeriesApi<SeriesType>[];
|
||||
paneIndex?: number;
|
||||
alertLines: IPriceLine[];
|
||||
config: IndicatorConfig;
|
||||
}
|
||||
|
||||
interface AlertEntry { price: number; label: string; line: IPriceLine; }
|
||||
interface FibLevel { price: number; label: string; line: IPriceLine; }
|
||||
|
||||
function getTheme(theme: Theme): ThemeTokens {
|
||||
return theme === 'dark' ? DARK : LIGHT;
|
||||
}
|
||||
|
||||
export class ChartManager {
|
||||
private chart: IChartApi;
|
||||
private container: HTMLElement;
|
||||
private mainSeries: ISeriesApi<SeriesType> | null = null;
|
||||
private volumeSeries: ISeriesApi<'Histogram'> | null = null;
|
||||
private indicators = new Map<string, IndicatorEntry>();
|
||||
private alertEntries: AlertEntry[] = [];
|
||||
private fibLines: FibLevel[] = [];
|
||||
private drawingTool = 'cursor';
|
||||
private drawingPoints: { time: Time; price: number }[] = [];
|
||||
private drawingLines: IPriceLine[] = [];
|
||||
private _clickHandler: ((p: MouseEventParams<Time>) => void) | null = null;
|
||||
private rawBars: OHLCVBar[] = [];
|
||||
private currentTheme: Theme = 'dark';
|
||||
private mainMarkersPlugin: ISeriesMarkersPluginApi<Time> | null = null;
|
||||
private patternMarkers: Array<{ id: string; markers: MarkerData[] }> = [];
|
||||
private compareSeriesMap = new Map<string, ISeriesApi<SeriesType>>();
|
||||
|
||||
constructor(container: HTMLElement, theme: Theme) {
|
||||
this.container = container;
|
||||
const t = getTheme(theme);
|
||||
this.chart = createChart(container, {
|
||||
layout: {
|
||||
background: { type: ColorType.Solid, color: t.bgColor },
|
||||
textColor: t.textColor,
|
||||
fontSize: 12,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: t.gridColor },
|
||||
horzLines: { color: t.gridColor },
|
||||
},
|
||||
crosshair: {
|
||||
vertLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' },
|
||||
horzLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' },
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: t.borderColor,
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
rightOffset: 8,
|
||||
},
|
||||
rightPriceScale: { borderColor: t.borderColor },
|
||||
handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: false },
|
||||
handleScale: { axisPressedMouseMove: true, axisDoubleClickReset: true, mouseWheel: true, pinch: true },
|
||||
});
|
||||
}
|
||||
|
||||
/** 내부 IChartApi 직접 접근 (커스텀 시리즈 추가 등) */
|
||||
getChart(): IChartApi { return this.chart; }
|
||||
|
||||
// ─── Data ───────────────────────────────────────────────────────────────
|
||||
setData(bars: OHLCVBar[], chartType: ChartType, theme: Theme): void {
|
||||
this.rawBars = bars;
|
||||
this.currentTheme = theme;
|
||||
const t = getTheme(theme);
|
||||
|
||||
if (this.mainSeries) { try { this.chart.removeSeries(this.mainSeries); } catch { /* ok */ } this.mainSeries = null; }
|
||||
if (this.volumeSeries) { try { this.chart.removeSeries(this.volumeSeries); } catch { /* ok */ } this.volumeSeries = null; }
|
||||
this.mainMarkersPlugin = null;
|
||||
|
||||
this.mainSeries = this._createMainSeries(chartType, t);
|
||||
const mainData = bars.map(b => {
|
||||
if (chartType === 'line' || chartType === 'area' || chartType === 'baseline')
|
||||
return { time: b.time as Time, value: b.close };
|
||||
return { time: b.time as Time, open: b.open, high: b.high, low: b.low, close: b.close };
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this.mainSeries as ISeriesApi<any>).setData(mainData);
|
||||
|
||||
// Volume sub-pane (pane index 1)
|
||||
this.volumeSeries = this.chart.addSeries(HistogramSeries, {
|
||||
priceFormat: { type: 'volume' },
|
||||
priceScaleId: 'volume',
|
||||
}, 1);
|
||||
|
||||
const panes = this.chart.panes();
|
||||
if (panes[1]) panes[1].setHeight(60);
|
||||
|
||||
this.volumeSeries.setData(bars.map(b => ({
|
||||
time: b.time as Time,
|
||||
value: b.volume,
|
||||
color: b.close >= b.open ? `${t.upColor}88` : `${t.downColor}88`,
|
||||
})));
|
||||
|
||||
this._reapplyAllPatternMarkers();
|
||||
this.chart.timeScale().fitContent();
|
||||
}
|
||||
|
||||
private _createMainSeries(chartType: ChartType, t: ThemeTokens): ISeriesApi<SeriesType> {
|
||||
switch (chartType) {
|
||||
case 'candlestick':
|
||||
return this.chart.addSeries(CandlestickSeries, {
|
||||
upColor: t.upColor, downColor: t.downColor,
|
||||
borderUpColor: t.upColor, borderDownColor: t.downColor,
|
||||
wickUpColor: t.upColor, wickDownColor: t.downColor,
|
||||
});
|
||||
case 'bar':
|
||||
return this.chart.addSeries(BarSeries, { upColor: t.upColor, downColor: t.downColor });
|
||||
case 'line':
|
||||
return this.chart.addSeries(LineSeries, { color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: true });
|
||||
case 'area':
|
||||
return this.chart.addSeries(AreaSeries, {
|
||||
lineColor: '#2962FF', topColor: '#2962FF44', bottomColor: '#2962FF00', lineWidth: 2,
|
||||
});
|
||||
case 'baseline':
|
||||
return this.chart.addSeries(BaselineSeries, {
|
||||
baseValue: { type: 'price', price: 0 },
|
||||
topLineColor: '#26a69a', topFillColor1: '#26a69a44', topFillColor2: '#26a69a00',
|
||||
bottomLineColor: '#ef5350', bottomFillColor1: '#ef535000', bottomFillColor2: '#ef535044',
|
||||
});
|
||||
default:
|
||||
return this.chart.addSeries(CandlestickSeries, {
|
||||
upColor: t.upColor, downColor: t.downColor,
|
||||
borderUpColor: t.upColor, borderDownColor: t.downColor,
|
||||
wickUpColor: t.upColor, wickDownColor: t.downColor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Theme ──────────────────────────────────────────────────────────────
|
||||
setTheme(theme: Theme): void {
|
||||
const t = getTheme(theme);
|
||||
this.chart.applyOptions({
|
||||
layout: { background: { type: ColorType.Solid, color: t.bgColor }, textColor: t.textColor },
|
||||
grid: { vertLines: { color: t.gridColor }, horzLines: { color: t.gridColor } },
|
||||
crosshair: { vertLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' }, horzLine: { color: t.crosshairColor, labelBackgroundColor: '#9598A1' } },
|
||||
timeScale: { borderColor: t.borderColor },
|
||||
rightPriceScale: { borderColor: t.borderColor },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Indicators ─────────────────────────────────────────────────────────
|
||||
async addIndicator(config: IndicatorConfig): Promise<void> {
|
||||
if (this.indicators.has(config.id) || this.rawBars.length === 0) return;
|
||||
const def = getIndicatorDef(config.type);
|
||||
const seriesList: ISeriesApi<SeriesType>[] = [];
|
||||
|
||||
try {
|
||||
const result = await calculateIndicator(config.type, this.rawBars, config.params);
|
||||
|
||||
if (def?.returnsMarkers && result.markers && result.markers.length > 0) {
|
||||
this.patternMarkers.push({ id: config.id, markers: result.markers });
|
||||
this._reapplyAllPatternMarkers();
|
||||
this.indicators.set(config.id, { id: config.id, type: config.type, seriesList: [], alertLines: [], config });
|
||||
return;
|
||||
}
|
||||
|
||||
const pane = def?.overlay ? 0 : this._getNextSubPane();
|
||||
const plotDefs = config.plots ?? def?.plots ?? [];
|
||||
|
||||
for (const plotDef of plotDefs) {
|
||||
const plotData = result.plots[plotDef.id] as PlotData | undefined;
|
||||
if (!plotData || plotData.length === 0) continue;
|
||||
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
let series: ISeriesApi<SeriesType>;
|
||||
if (plotDef.type === 'histogram') {
|
||||
series = this.chart.addSeries(HistogramSeries, { color: plotDef.color }, pane);
|
||||
const colorData = filtered.map((p, i, arr) => ({
|
||||
time: p.time as Time,
|
||||
value: p.value,
|
||||
color: p.color ?? (i > 0 && p.value < arr[i - 1].value
|
||||
? '#ef535088'
|
||||
: p.value < 0 ? '#ef535088' : `${plotDef.color}88`),
|
||||
}));
|
||||
series.setData(colorData);
|
||||
} else {
|
||||
series = this.chart.addSeries(LineSeries, {
|
||||
color: plotDef.color,
|
||||
lineWidth: (plotDef.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: true,
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
}
|
||||
|
||||
const hlines = config.hlines ?? def?.hlines ?? [];
|
||||
for (const hl of hlines) {
|
||||
series.createPriceLine({ price: hl.price, color: hl.color, lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false });
|
||||
}
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
this.indicators.set(config.id, { id: config.id, type: config.type, seriesList, paneIndex: pane, alertLines: [], config });
|
||||
} catch (e) {
|
||||
console.error(`[ChartV5 Indicator] ${config.type} error:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
removeIndicator(id: string): void {
|
||||
const entry = this.indicators.get(id);
|
||||
if (!entry) return;
|
||||
for (const s of entry.seriesList) { try { this.chart.removeSeries(s); } catch { /* ok */ } }
|
||||
this.patternMarkers = this.patternMarkers.filter(p => p.id !== id);
|
||||
this._reapplyAllPatternMarkers();
|
||||
this.indicators.delete(id);
|
||||
}
|
||||
|
||||
private _reapplyAllPatternMarkers(): void {
|
||||
if (!this.mainSeries) return;
|
||||
const all = this.patternMarkers.flatMap(({ markers }) =>
|
||||
markers.map(m => ({
|
||||
time: m.time as Time,
|
||||
position: m.position,
|
||||
shape: m.shape,
|
||||
color: m.color,
|
||||
text: m.text,
|
||||
}))
|
||||
);
|
||||
if (!this.mainMarkersPlugin) {
|
||||
this.mainMarkersPlugin = createSeriesMarkers(this.mainSeries, all);
|
||||
} else {
|
||||
this.mainMarkersPlugin.setMarkers(all);
|
||||
}
|
||||
}
|
||||
|
||||
private _getNextSubPane(): number {
|
||||
const used = new Set(Array.from(this.indicators.values()).map(e => e.paneIndex).filter(Boolean));
|
||||
let pane = 2;
|
||||
while (used.has(pane)) pane++;
|
||||
return pane;
|
||||
}
|
||||
|
||||
// ─── 실시간 바 업데이트 ────────────────────────────────────────────────
|
||||
updateBar(bar: OHLCVBar): void {
|
||||
if (!this.mainSeries) return;
|
||||
const t = getTheme(this.currentTheme);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this.mainSeries as ISeriesApi<any>).update({
|
||||
time: bar.time as Time,
|
||||
open: bar.open, high: bar.high, low: bar.low, close: bar.close,
|
||||
});
|
||||
|
||||
this.volumeSeries?.update({
|
||||
time: bar.time as Time,
|
||||
value: bar.volume,
|
||||
color: bar.close >= bar.open ? `${t.upColor}88` : `${t.downColor}88`,
|
||||
});
|
||||
|
||||
if (this.rawBars.length > 0) {
|
||||
const last = this.rawBars[this.rawBars.length - 1];
|
||||
if (last.time === bar.time) {
|
||||
this.rawBars[this.rawBars.length - 1] = bar;
|
||||
} else if (bar.time > last.time) {
|
||||
this.rawBars.push(bar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async appendBar(bar: OHLCVBar): Promise<void> {
|
||||
if (!this.mainSeries) return;
|
||||
const t = getTheme(this.currentTheme);
|
||||
|
||||
const last = this.rawBars[this.rawBars.length - 1];
|
||||
if (last && last.time === bar.time) {
|
||||
this.rawBars[this.rawBars.length - 1] = bar;
|
||||
} else {
|
||||
this.rawBars.push(bar);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(this.mainSeries as ISeriesApi<any>).update({
|
||||
time: bar.time as Time,
|
||||
open: bar.open, high: bar.high, low: bar.low, close: bar.close,
|
||||
});
|
||||
|
||||
this.volumeSeries?.update({
|
||||
time: bar.time as Time,
|
||||
value: bar.volume,
|
||||
color: bar.close >= bar.open ? `${t.upColor}88` : `${t.downColor}88`,
|
||||
});
|
||||
|
||||
for (const [, entry] of Array.from(this.indicators.entries())) {
|
||||
if (!entry.config || entry.seriesList.length === 0) continue;
|
||||
if (entry.config.type) {
|
||||
try {
|
||||
const { plots } = await calculateIndicator(
|
||||
entry.config.type, this.rawBars, entry.config.params ?? {}
|
||||
);
|
||||
const plotValues = Object.values(plots);
|
||||
for (let i = 0; i < entry.seriesList.length && i < plotValues.length; i++) {
|
||||
const plotData = plotValues[i];
|
||||
if (!plotData?.length) continue;
|
||||
const lastPoint = plotData[plotData.length - 1];
|
||||
if (lastPoint) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(entry.seriesList[i] as ISeriesApi<any>).update(lastPoint as any);
|
||||
}
|
||||
}
|
||||
} catch { /* 인디케이터 계산 실패 무시 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async recalculateIndicators(newBars: OHLCVBar[]): Promise<void> {
|
||||
this.rawBars = newBars;
|
||||
|
||||
const configs = Array.from(this.indicators.values()).map(e => e.config);
|
||||
|
||||
for (const entry of Array.from(this.indicators.values())) {
|
||||
for (const s of entry.seriesList) { try { this.chart.removeSeries(s); } catch { /* ok */ } }
|
||||
}
|
||||
this.indicators.clear();
|
||||
this.patternMarkers = [];
|
||||
this._reapplyAllPatternMarkers();
|
||||
|
||||
for (const cfg of configs) {
|
||||
await this.addIndicator(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 커스텀 시리즈 (pre-calculated data) ────────────────────────────────
|
||||
addCustomSeriesGroup(
|
||||
id: string,
|
||||
seriesSpecs: Array<{
|
||||
data: Array<{ time: number; value: number; color?: string }>;
|
||||
seriesType: 'line' | 'histogram';
|
||||
color: string;
|
||||
lineWidth?: number;
|
||||
}>,
|
||||
hlines?: Array<{ price: number; color: string }>,
|
||||
pane?: number
|
||||
): void {
|
||||
if (this.indicators.has(id)) return;
|
||||
const targetPane = pane ?? this._getNextSubPane();
|
||||
const seriesList: ISeriesApi<SeriesType>[] = [];
|
||||
|
||||
for (const spec of seriesSpecs) {
|
||||
if (!spec.data || spec.data.length === 0) continue;
|
||||
const filtered = spec.data.filter(p => p.value !== null && !isNaN(p.value));
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
let series: ISeriesApi<SeriesType>;
|
||||
if (spec.seriesType === 'histogram') {
|
||||
series = this.chart.addSeries(HistogramSeries, { color: spec.color }, targetPane);
|
||||
series.setData(filtered.map(p => ({
|
||||
time: p.time as Time,
|
||||
value: p.value,
|
||||
color: p.color ?? (p.value < 0 ? '#ef535088' : `${spec.color}88`),
|
||||
})));
|
||||
} else {
|
||||
series = this.chart.addSeries(LineSeries, {
|
||||
color: spec.color,
|
||||
lineWidth: (spec.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: true,
|
||||
}, targetPane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
}
|
||||
|
||||
if (hlines) {
|
||||
for (const hl of hlines) {
|
||||
series.createPriceLine({ price: hl.price, color: hl.color, lineWidth: 1, lineStyle: LineStyle.Dashed, axisLabelVisible: false });
|
||||
}
|
||||
}
|
||||
seriesList.push(series);
|
||||
}
|
||||
|
||||
this.indicators.set(id, {
|
||||
id, type: '__custom__', seriesList, paneIndex: targetPane, alertLines: [],
|
||||
config: { id, type: '__custom__', params: {} },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Alerts ──────────────────────────────────────────────────────────────
|
||||
addAlert(price: number, label = ''): void {
|
||||
if (!this.mainSeries) return;
|
||||
const line = this.mainSeries.createPriceLine({
|
||||
price, color: '#FFB300', lineWidth: 1, lineStyle: LineStyle.Dashed,
|
||||
axisLabelVisible: true, title: label || `Alert ${price.toFixed(2)}`,
|
||||
});
|
||||
this.alertEntries.push({ price, label, line });
|
||||
}
|
||||
|
||||
removeAlert(price: number): void {
|
||||
const idx = this.alertEntries.findIndex(a => Math.abs(a.price - price) < 0.0001);
|
||||
if (idx < 0) return;
|
||||
try { this.mainSeries?.removePriceLine(this.alertEntries[idx].line); } catch { /* ok */ }
|
||||
this.alertEntries.splice(idx, 1);
|
||||
}
|
||||
|
||||
getAlerts(): Array<{ price: number; label: string }> {
|
||||
return this.alertEntries.map(({ price, label }) => ({ price, label }));
|
||||
}
|
||||
|
||||
// ─── Auto Fibonacci ──────────────────────────────────────────────────────
|
||||
drawAutoFib(lookback = 50): void {
|
||||
if (!this.mainSeries || this.rawBars.length === 0) return;
|
||||
this._clearFibLines();
|
||||
const { levels } = calcAutoFib(this.rawBars, lookback);
|
||||
const COLORS: Record<string, string> = {
|
||||
'0%': '#EF5350', '23.6%': '#FF9800', '38.2%': '#FFC107',
|
||||
'50%': '#FFFFFF', '61.8%': '#4CAF50', '78.6%': '#2196F3',
|
||||
'100%': '#9C27B0', '127.2%': '#607D8B', '161.8%': '#607D8B',
|
||||
};
|
||||
for (const l of levels) {
|
||||
const line = this.mainSeries.createPriceLine({
|
||||
price: l.price, color: COLORS[l.label] ?? '#607D8B',
|
||||
lineWidth: l.label === '0%' || l.label === '100%' ? 2 : 1,
|
||||
lineStyle: l.label === '50%' ? LineStyle.Dashed : LineStyle.Dotted,
|
||||
axisLabelVisible: true, title: `Fib ${l.label}`,
|
||||
});
|
||||
this.fibLines.push({ price: l.price, label: l.label, line });
|
||||
}
|
||||
}
|
||||
|
||||
clearFib(): void { this._clearFibLines(); }
|
||||
private _clearFibLines(): void {
|
||||
for (const f of this.fibLines) { try { this.mainSeries?.removePriceLine(f.line); } catch { /* ok */ } }
|
||||
this.fibLines = [];
|
||||
}
|
||||
|
||||
// ─── Crosshair ────────────────────────────────────────────────────────────
|
||||
subscribeCrosshair(cb: (p: MouseEventParams<Time>) => void): () => void {
|
||||
this.chart.subscribeCrosshairMove(cb);
|
||||
return () => this.chart.unsubscribeCrosshairMove(cb);
|
||||
}
|
||||
|
||||
// ─── Layout ───────────────────────────────────────────────────────────────
|
||||
fitContent(): void { this.chart.timeScale().fitContent(); }
|
||||
setLogScale(enabled: boolean): void {
|
||||
this.mainSeries?.applyOptions({ priceScale: { mode: enabled ? 1 : 0 } } as Parameters<ISeriesApi<SeriesType>['applyOptions']>[0]);
|
||||
}
|
||||
resize(w: number, h: number): void { this.chart.resize(w, h); }
|
||||
|
||||
// ─── Compare overlay ──────────────────────────────────────────────────────
|
||||
addCompareSeries(symbol: string, bars: OHLCVBar[], color: string): void {
|
||||
if (this.compareSeriesMap.has(symbol)) return;
|
||||
const s = this.chart.addSeries(LineSeries, { color, lineWidth: 2, priceLineVisible: false });
|
||||
const base = bars[0]?.close ?? 1;
|
||||
s.setData(bars.map(b => ({ time: b.time as Time, value: (b.close / base - 1) * 100 })));
|
||||
this.compareSeriesMap.set(symbol, s);
|
||||
}
|
||||
|
||||
removeCompareSeries(symbol: string): void {
|
||||
const s = this.compareSeriesMap.get(symbol);
|
||||
if (s) { try { this.chart.removeSeries(s); } catch { /* ok */ } this.compareSeriesMap.delete(symbol); }
|
||||
}
|
||||
|
||||
// ─── Coordinate conversion ───────────────────────────────────────────────
|
||||
timeToX(time: number): number | null {
|
||||
const coord = this.chart.timeScale().timeToCoordinate(time as Time);
|
||||
return coord === null ? null : coord;
|
||||
}
|
||||
|
||||
priceToY(price: number): number | null {
|
||||
const coord = this.mainSeries?.priceToCoordinate(price);
|
||||
return coord === null || coord === undefined ? null : coord;
|
||||
}
|
||||
|
||||
xToTime(x: number): number | null {
|
||||
const t = this.chart.timeScale().coordinateToTime(x);
|
||||
return t === null ? null : (t as number);
|
||||
}
|
||||
|
||||
yToPrice(y: number): number | null {
|
||||
const p = this.mainSeries?.coordinateToPrice(y);
|
||||
return p === null || p === undefined ? null : p;
|
||||
}
|
||||
|
||||
subscribeViewport(cb: () => void): () => void {
|
||||
this.chart.timeScale().subscribeVisibleTimeRangeChange(cb);
|
||||
this.chart.timeScale().subscribeVisibleLogicalRangeChange(cb);
|
||||
return () => {
|
||||
try { this.chart.timeScale().unsubscribeVisibleTimeRangeChange(cb); } catch { /* ok */ }
|
||||
try { this.chart.timeScale().unsubscribeVisibleLogicalRangeChange(cb); } catch { /* ok */ }
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Destroy ─────────────────────────────────────────────────────────────
|
||||
destroy(): void {
|
||||
if (this._clickHandler) this.chart.unsubscribeClick(this._clickHandler);
|
||||
this.chart.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user