수정3
This commit is contained in:
@@ -214,8 +214,8 @@ function detachBbBandFill(entry: IndicatorEntry): void {
|
||||
entry.bbFillPrimitive = undefined;
|
||||
}
|
||||
|
||||
/** OBV·CCI·RSI·TRIX — 본선+신호선 이중 plot (누락 시리즈 보정 대상) */
|
||||
const DUAL_LINE_INDICATOR_TYPES = new Set(['OBV', 'CCI', 'RSI', 'TRIX']);
|
||||
/** CCI·RSI·TRIX — 본선+신호선 이중 plot (누락 시리즈 보정 대상). OBV는 별도 경로. */
|
||||
const DUAL_LINE_INDICATOR_TYPES = new Set(['CCI', 'RSI', 'TRIX']);
|
||||
|
||||
const DUAL_LINE_PLOT_ORDER = ['plot0', 'plot1', 'plot2', 'plot3'];
|
||||
|
||||
@@ -240,11 +240,6 @@ function sortPlotDefsForSeriesAdd(type: string, plotDefs: PlotDef[]): PlotDef[]
|
||||
});
|
||||
}
|
||||
|
||||
/** OBV 신호선 — pane 내 독립 price scale (본선과 값이 가까워도 겹치지 않게) */
|
||||
function obvSignalPriceScaleId(indicatorId: string): string {
|
||||
return `obv-sig-${indicatorId}`;
|
||||
}
|
||||
|
||||
export class ChartManager {
|
||||
private chart: IChartApi;
|
||||
private container: HTMLElement;
|
||||
@@ -779,28 +774,6 @@ export class ChartManager {
|
||||
return expected.some(id => !existing.has(id));
|
||||
}
|
||||
|
||||
/** OBV — 본선(하단)·신호선(상단) 독립 price scale margin */
|
||||
private _configureObvDualLinePriceScales(
|
||||
seriesList: ISeriesApi<SeriesType>[],
|
||||
seriesMeta: IndicatorEntry['seriesMeta'],
|
||||
): void {
|
||||
for (let i = 0; i < seriesList.length; i++) {
|
||||
const meta = seriesMeta[i];
|
||||
if (!meta) continue;
|
||||
try {
|
||||
const ps = seriesList[i].priceScale();
|
||||
if (meta.plotId === 'plot0') {
|
||||
ps.applyOptions({ scaleMargins: { top: 0.52, bottom: 0.05 } });
|
||||
} else if (meta.plotId === 'plot1') {
|
||||
ps.applyOptions({
|
||||
visible: false,
|
||||
scaleMargins: { top: 0.05, bottom: 0.52 },
|
||||
});
|
||||
}
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
|
||||
private _dualLinePlotDefsForRender(
|
||||
config: IndicatorConfig,
|
||||
def: ReturnType<typeof getIndicatorDef>,
|
||||
@@ -810,7 +783,59 @@ export class ChartManager {
|
||||
.filter(p => p.type !== 'histogram' && enriched.plotVisibility?.[p.id] !== false);
|
||||
}
|
||||
|
||||
/** OBV·RSI 등 — plot0→plot1 순서로 시리즈를 한 번에 생성 (append 누락·순서 어긋남 방지) */
|
||||
/** OBV — plot0·plot1 항상 쌍으로 생성 (메인 차트와 동일, DUAL_LINE repair 경로 회피) */
|
||||
private _addObvIndicatorSeries(
|
||||
seriesList: ISeriesApi<SeriesType>[],
|
||||
seriesMeta: IndicatorEntry['seriesMeta'],
|
||||
config: IndicatorConfig,
|
||||
result: { plots: Record<string, PlotData> },
|
||||
pane: number,
|
||||
indicatorHidden: boolean,
|
||||
def: ReturnType<typeof getIndicatorDef>,
|
||||
): void {
|
||||
const enriched = enrichIndicatorConfig(config);
|
||||
const plotById = Object.fromEntries(
|
||||
sortPlotDefsForSeriesAdd('OBV', enriched.plots ?? def?.plots ?? []).map(p => [p.id, p]),
|
||||
);
|
||||
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
|
||||
|
||||
for (const plotId of ['plot0', 'plot1'] as const) {
|
||||
if (enriched.plotVisibility?.[plotId] === false) continue;
|
||||
const plotDef = plotById[plotId] ?? def?.plots?.find(p => p.id === plotId);
|
||||
if (!plotDef || plotDef.type === 'histogram') continue;
|
||||
|
||||
const plotData = result.plots[plotId] as PlotData | undefined;
|
||||
if (!plotData?.length) continue;
|
||||
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
|
||||
if (filtered.length === 0) continue;
|
||||
|
||||
const isPlotVisible = !indicatorHidden;
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(enriched, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotId);
|
||||
const fallbackColor = plotId === 'plot0'
|
||||
? (regPlot?.color ?? '#2196F3')
|
||||
: (regPlot?.color ?? plotDef.color ?? '#2962FF');
|
||||
|
||||
const series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||
lineWidth: (plotId === 'plot0'
|
||||
? Math.max(2, plotDef.lineWidth ?? regPlot?.lineWidth ?? 2)
|
||||
: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1)) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(enriched, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesList.push(series);
|
||||
seriesMeta.push({ plotId, isHistogram: false, color: plotDef.color });
|
||||
}
|
||||
}
|
||||
|
||||
/** RSI·CCI·TRIX — plot0→plot1 순서로 시리즈를 한 번에 생성 */
|
||||
private _addDualLinePlotSeries(
|
||||
seriesList: ISeriesApi<SeriesType>[],
|
||||
seriesMeta: IndicatorEntry['seriesMeta'],
|
||||
@@ -836,33 +861,20 @@ export class ChartManager {
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
||||
const fallbackColor = config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? (regPlot?.color ?? '#2196F3')
|
||||
: (regPlot?.color ?? plotDef.color ?? '#2962FF');
|
||||
const obvSignalScale = config.type === 'OBV' && plotDef.id === 'plot1'
|
||||
? obvSignalPriceScaleId(config.id)
|
||||
: undefined;
|
||||
const series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||
lineWidth: (config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? Math.max(2, plotDef.lineWidth ?? regPlot?.lineWidth ?? 2)
|
||||
: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1)) as 1 | 2 | 3 | 4,
|
||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
||||
lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
...(obvSignalScale ? { priceScaleId: obvSignalScale } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesList.push(series);
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
}
|
||||
|
||||
if (config.type === 'OBV' && seriesList.length > 0) {
|
||||
this._configureObvDualLinePriceScales(seriesList, seriesMeta);
|
||||
}
|
||||
}
|
||||
|
||||
private _appendMissingPlotSeriesToLists(
|
||||
@@ -894,33 +906,20 @@ export class ChartManager {
|
||||
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
|
||||
const showSeriesTitle = pane < 2 && showPriceLabel;
|
||||
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
|
||||
const fallbackColor = config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? (regPlot?.color ?? '#2196F3')
|
||||
: (regPlot?.color ?? plotDef.color ?? '#2962FF');
|
||||
const obvSignalScale = config.type === 'OBV' && plotDef.id === 'plot1'
|
||||
? obvSignalPriceScaleId(config.id)
|
||||
: undefined;
|
||||
const series = this.chart.addSeries(LineSeries, {
|
||||
color: resolvePlotLineColor(plotDef.color, fallbackColor),
|
||||
lineWidth: (config.type === 'OBV' && plotDef.id === 'plot0'
|
||||
? Math.max(2, plotDef.lineWidth ?? regPlot?.lineWidth ?? 2)
|
||||
: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1)) as 1 | 2 | 3 | 4,
|
||||
color: resolvePlotLineColor(plotDef.color, regPlot?.color ?? plotDef.color ?? '#2962FF'),
|
||||
lineWidth: Math.max(1, plotDef.lineWidth ?? regPlot?.lineWidth ?? 1) as 1 | 2 | 3 | 4,
|
||||
lineStyle: plotSeriesLineStyle(plotDef.lineStyle),
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: showPriceLabel,
|
||||
title: showSeriesTitle ? this.seriesTitleForPlot(config, plotDef) : '',
|
||||
visible: isPlotVisible,
|
||||
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
|
||||
...(obvSignalScale ? { priceScaleId: obvSignalScale } : {}),
|
||||
}, pane);
|
||||
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
|
||||
seriesList.push(series);
|
||||
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
|
||||
}
|
||||
|
||||
if (config.type === 'OBV' && seriesList.length > 0) {
|
||||
this._configureObvDualLinePriceScales(seriesList, seriesMeta);
|
||||
}
|
||||
}
|
||||
|
||||
/** plot0·plot1 시리즈 전부 제거 후 DB 설정 스타일로 재생성 */
|
||||
@@ -1036,7 +1035,17 @@ export class ChartManager {
|
||||
|
||||
const plotDefsRaw = config.plots ?? def?.plots ?? [];
|
||||
|
||||
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||
if (config.type === 'OBV') {
|
||||
this._addObvIndicatorSeries(
|
||||
seriesList,
|
||||
seriesMeta,
|
||||
config,
|
||||
result,
|
||||
pane,
|
||||
indicatorHidden,
|
||||
def,
|
||||
);
|
||||
} else if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||
this._addDualLinePlotSeries(
|
||||
seriesList,
|
||||
seriesMeta,
|
||||
@@ -1147,7 +1156,11 @@ export class ChartManager {
|
||||
return;
|
||||
}
|
||||
this.indicators.set(config.id, entry);
|
||||
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||
if (config.type === 'OBV') {
|
||||
if (entry.seriesList.length > 0 && !indicatorHidden) {
|
||||
this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
|
||||
}
|
||||
} else if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
|
||||
await this._ensureDualLinePlotSeriesForEntry(entry);
|
||||
if (entry.seriesList.length > 0 && !indicatorHidden) {
|
||||
this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
|
||||
@@ -1211,7 +1224,44 @@ export class ChartManager {
|
||||
/** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 + 전체 plot setData + 스타일(DB) 반영 */
|
||||
async repairDualLineSeries(): Promise<void> {
|
||||
for (const entry of this.indicators.values()) {
|
||||
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type) || !entry.config) continue;
|
||||
if (!entry.config) continue;
|
||||
|
||||
if (entry.type === 'OBV') {
|
||||
const config = enrichIndicatorConfig(entry.config);
|
||||
const def = getIndicatorDef('OBV');
|
||||
const missingPlot = !entry.seriesMeta.some(m => m.plotId === 'plot0')
|
||||
|| !entry.seriesMeta.some(m => m.plotId === 'plot1');
|
||||
if (missingPlot && def && this.rawBars.length > 0) {
|
||||
const pane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? 2;
|
||||
const hidden = !this._isIndicatorEffectivelyVisible(config);
|
||||
for (const s of entry.seriesList) {
|
||||
try { this.chart.removeSeries(s); } catch { /* ok */ }
|
||||
}
|
||||
entry.seriesList = [];
|
||||
entry.seriesMeta = [];
|
||||
try {
|
||||
const result = await calculateIndicator('OBV', this.rawBars.slice(), config.params ?? {});
|
||||
this._addObvIndicatorSeries(
|
||||
entry.seriesList, entry.seriesMeta, config, result, pane, hidden, def,
|
||||
);
|
||||
if (entry.seriesList.length > 0) {
|
||||
this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
|
||||
}
|
||||
entry.config = config;
|
||||
} catch (e) {
|
||||
console.error('[Indicator] OBV repair error:', e);
|
||||
}
|
||||
} else if (entry.seriesList.length > 0 && this.rawBars.length > 0) {
|
||||
try {
|
||||
const result = await calculateIndicator('OBV', this.rawBars.slice(), config.params ?? {});
|
||||
this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
this.applyIndicatorStyle(config, { skipDualLineRefresh: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue;
|
||||
const config = enrichIndicatorConfig(entry.config);
|
||||
const def = getIndicatorDef(entry.type);
|
||||
if (!def) continue;
|
||||
@@ -3089,9 +3139,7 @@ export class ChartManager {
|
||||
if (!entry.seriesMeta[i]?.isHistogram) {
|
||||
const regPlot = def?.plots?.find(p => p.id === pid);
|
||||
opts['color'] = resolvePlotLineColor(plot.color, regPlot?.color ?? plot.color ?? '#aaa');
|
||||
opts['lineWidth'] = config.type === 'OBV' && pid === 'plot0'
|
||||
? Math.max(2, plot.lineWidth ?? regPlot?.lineWidth ?? 2)
|
||||
: Math.max(1, plot.lineWidth ?? regPlot?.lineWidth ?? 1);
|
||||
opts['lineWidth'] = Math.max(1, plot.lineWidth ?? regPlot?.lineWidth ?? 1);
|
||||
opts['lineStyle'] = plotSeriesLineStyle(plot.lineStyle);
|
||||
let plotAllows = true;
|
||||
if (entry.type === 'IchimokuCloud') {
|
||||
@@ -3109,10 +3157,6 @@ export class ChartManager {
|
||||
series.applyOptions(opts as any);
|
||||
});
|
||||
|
||||
if (config.type === 'OBV' && entry.seriesList.length > 0) {
|
||||
this._configureObvDualLinePriceScales(entry.seriesList, entry.seriesMeta);
|
||||
}
|
||||
|
||||
// ── hlines(과열선/중앙선/침체선) 동기화 ─────────────────────────────────
|
||||
if (entry.seriesList.length > 0) {
|
||||
const firstSeries = entry.seriesList[0];
|
||||
|
||||
Reference in New Issue
Block a user