추가수정

This commit is contained in:
Macbook
2026-06-09 22:40:46 +09:00
parent 420050dfb7
commit 47e287c0f6
2 changed files with 141 additions and 62 deletions
+111 -57
View File
@@ -748,18 +748,72 @@ export class ChartManager {
return loadGen !== this._dataGeneration;
}
/** 이중 plot 지표에서 누락된 plotId 목록 (series 개수만으로는 plot0 누락 감지 불가) */
private _dualLineMissingPlotIds(
entry: IndicatorEntry,
/** 이중 plot 지표에서 표시해야 할 plotId 목록 */
private _expectedDualLinePlotIds(
config: IndicatorConfig,
def: ReturnType<typeof getIndicatorDef> | undefined,
): string[] {
const plotDefs = config.plots ?? def?.plots ?? [];
const existing = new Set(entry.seriesMeta.map(m => m.plotId));
return plotDefs
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false)
.map(p => p.id)
.filter(id => !existing.has(id));
.map(p => p.id);
}
/** seriesMeta·seriesList 불일치 또는 plot0 누락 */
private _dualLineSeriesOutOfSync(
entry: IndicatorEntry,
config: IndicatorConfig,
def: ReturnType<typeof getIndicatorDef> | undefined,
): boolean {
const expected = this._expectedDualLinePlotIds(config, def);
if (expected.length === 0) return false;
if (entry.seriesList.length !== entry.seriesMeta.length) return true;
if (entry.seriesMeta.length !== expected.length) return true;
const existing = new Set(entry.seriesMeta.map(m => m.plotId));
return expected.some(id => !existing.has(id));
}
/** OBV·RSI 등 — plot0→plot1 순서로 시리즈를 한 번에 생성 (append 누락·순서 어긋남 방지) */
private _addDualLinePlotSeries(
seriesList: ISeriesApi<SeriesType>[],
seriesMeta: IndicatorEntry['seriesMeta'],
config: IndicatorConfig,
result: { plots: Record<string, PlotData> },
pane: number,
indicatorHidden: boolean,
def: ReturnType<typeof getIndicatorDef>,
): void {
const plotDefs = sortDualLinePlotDefs(config.plots ?? def?.plots ?? [])
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false);
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
for (const plotDef of plotDefs) {
const plotData = result.plots[plotDef.id] 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
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
: config.plotVisibility?.[plotDef.id] !== false);
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
const showSeriesTitle = pane < 2 && showPriceLabel;
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
const series = this.chart.addSeries(LineSeries, {
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 } : {}),
}, 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 });
}
}
private _appendMissingPlotSeriesToLists(
@@ -831,7 +885,7 @@ export class ChartManager {
try {
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
this._appendMissingPlotSeriesToLists(
this._addDualLinePlotSeries(
entry.seriesList,
entry.seriesMeta,
config,
@@ -861,8 +915,7 @@ export class ChartManager {
if (!def) return;
if (this.rawBars.length === 0) return;
const missing = this._dualLineMissingPlotIds(entry, config, def);
if (missing.length > 0) {
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
return;
}
@@ -921,52 +974,9 @@ export class ChartManager {
}
const plotDefsRaw = config.plots ?? def?.plots ?? [];
const plotDefs = DUAL_LINE_INDICATOR_TYPES.has(config.type)
? sortPlotDefsForSeriesAdd(config.type, plotDefsRaw)
: plotDefsRaw;
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>;
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
if (plotDef.type === 'histogram') {
series = this.chart.addSeries(HistogramSeries, {
color: plotDef.color,
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
}, pane);
series.setData(mapHistogramSeriesData(filtered, plotDef));
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
} else {
const isPlotVisible = !indicatorHidden
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
: config.plotVisibility?.[plotDef.id] !== false);
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
const showSeriesTitle = pane < 2 && showPriceLabel;
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
series = this.chart.addSeries(LineSeries, {
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 } : {}),
}, pane);
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
}
seriesList.push(series);
}
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
this._appendMissingPlotSeriesToLists(
this._addDualLinePlotSeries(
seriesList,
seriesMeta,
config,
@@ -975,6 +985,46 @@ export class ChartManager {
indicatorHidden,
def,
);
} else {
for (const plotDef of plotDefsRaw) {
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>;
const indicatorPriceFormat = priceFormatForIndicatorPane(pane);
if (plotDef.type === 'histogram') {
series = this.chart.addSeries(HistogramSeries, {
color: plotDef.color,
...(indicatorPriceFormat ? { priceFormat: indicatorPriceFormat } : {}),
}, pane);
series.setData(mapHistogramSeriesData(filtered, plotDef));
seriesMeta.push({ plotId: plotDef.id, isHistogram: true, color: plotDef.color });
} else {
const isPlotVisible = !indicatorHidden
&& (this._isCustomOverlayApplied() && (config.type === 'SMA' || config.type === 'BollingerBands' || config.type === 'IchimokuCloud')
? isCustomPlotSelected(this._getAppliedCustomOverlaySelection(), config.type, plotDef.id)
: config.plotVisibility?.[plotDef.id] !== false);
const showPriceLabel = this.shouldShowSeriesPriceLabel(config, true, pane);
const showSeriesTitle = pane < 2 && showPriceLabel;
const regPlot = def?.plots?.find(p => p.id === plotDef.id);
series = this.chart.addSeries(LineSeries, {
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 } : {}),
}, pane);
series.setData(filtered.map(p => ({ time: p.time as Time, value: p.value })));
seriesMeta.push({ plotId: plotDef.id, isHistogram: false, color: plotDef.color });
}
seriesList.push(series);
}
}
if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) {
@@ -1105,8 +1155,7 @@ export class ChartManager {
const def = getIndicatorDef(entry.type);
if (!def) continue;
const missing = this._dualLineMissingPlotIds(entry, config, def);
if (missing.length > 0) {
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
} else if (entry.seriesList.length > 0 && this.rawBars.length > 0) {
try {
@@ -1923,7 +1972,7 @@ export class ChartManager {
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
const config = enrichIndicatorConfig(entry.config);
const def = getIndicatorDef(entry.type);
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) {
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
await this._ensureDualLinePlotSeriesForEntry(entry);
}
}
@@ -2155,7 +2204,7 @@ export class ChartManager {
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
const config = enrichIndicatorConfig(entry.config);
const def = getIndicatorDef(entry.type);
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) {
if (this._dualLineSeriesOutOfSync(entry, config, def)) {
await this._ensureDualLinePlotSeriesForEntry(entry);
}
}
@@ -3857,6 +3906,11 @@ export class ChartManager {
return ids;
}
/** 지표에 붙은 plotId 목록 (OBV plot0/plot1 누락 진단용) */
getIndicatorSeriesPlotIds(id: string): string[] {
return this.indicators.get(id)?.seriesMeta.map(m => m.plotId) ?? [];
}
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */
getRawBarsLength(): number {
return this.rawBars.length;