추가수정

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
@@ -103,6 +103,26 @@ const TradeSignalMiniChart: React.FC<Props> = ({
applyNotificationSignalMarkers(mgr, signalMarkerRef.current, false);
}, []);
/** OBV plot0(본선) 누락 시 refreshIndicators 로 강제 재생성 */
const ensureObvDualLine = useCallback(async (attempt = 0) => {
const mgr = managerRef.current;
if (!mgr?.hasMainSeries() || !needsDeepObv) return;
const obvCfg = chartIndicators.find(i => i.type === 'OBV');
if (!obvCfg) return;
await mgr.repairDualLineSeries();
const plotIds = mgr.getIndicatorSeriesPlotIds(obvCfg.id);
const needsBoth = plotIds.length < 2 || !plotIds.includes('plot0');
if (needsBoth && attempt < 4) {
await mgr.refreshIndicators([obvCfg]);
await mgr.repairDualLineSeries();
const after = mgr.getIndicatorSeriesPlotIds(obvCfg.id);
if ((after.length < 2 || !after.includes('plot0')) && attempt < 3) {
setTimeout(() => void ensureObvDualLine(attempt + 1), 500 * (attempt + 1));
}
}
}, [needsDeepObv, chartIndicators]);
const syncChartLayout = useCallback(() => {
const wrap = canvasWrapRef.current;
const mgr = managerRef.current;
@@ -125,21 +145,23 @@ const TradeSignalMiniChart: React.FC<Props> = ({
syncChartLayout();
applySignalMarkers();
if (!mgr) return;
void ensureObvDualLine();
[0, 100, 500, 1200].forEach(delay => {
setTimeout(() => {
void managerRef.current?.repairDualLineSeries();
void ensureObvDualLine();
}, delay);
});
}, [syncChartLayout, applySignalMarkers]);
}, [syncChartLayout, applySignalMarkers, ensureObvDualLine]);
const handleManagerReady = useCallback((mgr: ChartManager) => {
managerRef.current = mgr;
syncToManager(mgr);
applySignalMarkers();
if (needsDeepObv) {
void mgr.repairDualLineSeries();
void mgr.repairDualLineSeries().then(() => ensureObvDualLine());
}
}, [syncToManager, applySignalMarkers, needsDeepObv]);
}, [syncToManager, applySignalMarkers, needsDeepObv, ensureObvDualLine]);
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
if (barsMarketRef.current !== marketRef.current) return;
@@ -199,10 +221,13 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const mgr = managerRef.current;
if (!mgr?.hasMainSeries()) return;
const timers = [200, 800, 1600].map(delay =>
setTimeout(() => void mgr.repairDualLineSeries(), delay),
setTimeout(() => {
void mgr.repairDualLineSeries();
void ensureObvDualLine();
}, delay),
);
return () => timers.forEach(clearTimeout);
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision]);
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision, ensureObvDualLine]);
useLayoutEffect(() => {
chartLiveReadyRef.current = false;
+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;