추가수정

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); 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 syncChartLayout = useCallback(() => {
const wrap = canvasWrapRef.current; const wrap = canvasWrapRef.current;
const mgr = managerRef.current; const mgr = managerRef.current;
@@ -125,21 +145,23 @@ const TradeSignalMiniChart: React.FC<Props> = ({
syncChartLayout(); syncChartLayout();
applySignalMarkers(); applySignalMarkers();
if (!mgr) return; if (!mgr) return;
void ensureObvDualLine();
[0, 100, 500, 1200].forEach(delay => { [0, 100, 500, 1200].forEach(delay => {
setTimeout(() => { setTimeout(() => {
void managerRef.current?.repairDualLineSeries(); void managerRef.current?.repairDualLineSeries();
void ensureObvDualLine();
}, delay); }, delay);
}); });
}, [syncChartLayout, applySignalMarkers]); }, [syncChartLayout, applySignalMarkers, ensureObvDualLine]);
const handleManagerReady = useCallback((mgr: ChartManager) => { const handleManagerReady = useCallback((mgr: ChartManager) => {
managerRef.current = mgr; managerRef.current = mgr;
syncToManager(mgr); syncToManager(mgr);
applySignalMarkers(); applySignalMarkers();
if (needsDeepObv) { if (needsDeepObv) {
void mgr.repairDualLineSeries(); void mgr.repairDualLineSeries().then(() => ensureObvDualLine());
} }
}, [syncToManager, applySignalMarkers, needsDeepObv]); }, [syncToManager, applySignalMarkers, needsDeepObv, ensureObvDualLine]);
const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => { const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => {
if (barsMarketRef.current !== marketRef.current) return; if (barsMarketRef.current !== marketRef.current) return;
@@ -199,10 +221,13 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const mgr = managerRef.current; const mgr = managerRef.current;
if (!mgr?.hasMainSeries()) return; if (!mgr?.hasMainSeries()) return;
const timers = [200, 800, 1600].map(delay => const timers = [200, 800, 1600].map(delay =>
setTimeout(() => void mgr.repairDualLineSeries(), delay), setTimeout(() => {
void mgr.repairDualLineSeries();
void ensureObvDualLine();
}, delay),
); );
return () => timers.forEach(clearTimeout); return () => timers.forEach(clearTimeout);
}, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision]); }, [enabled, chartDataReady, chartIndicators, market, timeframe, settingsRevision, ensureObvDualLine]);
useLayoutEffect(() => { useLayoutEffect(() => {
chartLiveReadyRef.current = false; chartLiveReadyRef.current = false;
+82 -28
View File
@@ -748,18 +748,72 @@ export class ChartManager {
return loadGen !== this._dataGeneration; return loadGen !== this._dataGeneration;
} }
/** 이중 plot 지표에서 누락된 plotId 목록 (series 개수만으로는 plot0 누락 감지 불가) */ /** 이중 plot 지표에서 표시해야 할 plotId 목록 */
private _dualLineMissingPlotIds( private _expectedDualLinePlotIds(
entry: IndicatorEntry,
config: IndicatorConfig, config: IndicatorConfig,
def: ReturnType<typeof getIndicatorDef> | undefined, def: ReturnType<typeof getIndicatorDef> | undefined,
): string[] { ): string[] {
const plotDefs = config.plots ?? def?.plots ?? []; const plotDefs = config.plots ?? def?.plots ?? [];
const existing = new Set(entry.seriesMeta.map(m => m.plotId));
return plotDefs return plotDefs
.filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false) .filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false)
.map(p => p.id) .map(p => p.id);
.filter(id => !existing.has(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( private _appendMissingPlotSeriesToLists(
@@ -831,7 +885,7 @@ export class ChartManager {
try { try {
const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params); const result = await calculateIndicator(config.type, this.rawBars.slice(), config.params);
this._appendMissingPlotSeriesToLists( this._addDualLinePlotSeries(
entry.seriesList, entry.seriesList,
entry.seriesMeta, entry.seriesMeta,
config, config,
@@ -861,8 +915,7 @@ export class ChartManager {
if (!def) return; if (!def) return;
if (this.rawBars.length === 0) return; if (this.rawBars.length === 0) return;
const missing = this._dualLineMissingPlotIds(entry, config, def); if (this._dualLineSeriesOutOfSync(entry, config, def)) {
if (missing.length > 0) {
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def); await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
return; return;
} }
@@ -921,11 +974,19 @@ export class ChartManager {
} }
const plotDefsRaw = config.plots ?? def?.plots ?? []; 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) { if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
this._addDualLinePlotSeries(
seriesList,
seriesMeta,
config,
result,
pane,
indicatorHidden,
def,
);
} else {
for (const plotDef of plotDefsRaw) {
const plotData = result.plots[plotDef.id] as PlotData | undefined; const plotData = result.plots[plotDef.id] as PlotData | undefined;
if (!plotData || plotData.length === 0) continue; if (!plotData || plotData.length === 0) continue;
const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value)); const filtered = plotData.filter(p => p.value !== null && !isNaN(p.value));
@@ -964,17 +1025,6 @@ export class ChartManager {
seriesList.push(series); seriesList.push(series);
} }
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
this._appendMissingPlotSeriesToLists(
seriesList,
seriesMeta,
config,
result,
pane,
indicatorHidden,
def,
);
} }
if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) { if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) {
@@ -1105,8 +1155,7 @@ export class ChartManager {
const def = getIndicatorDef(entry.type); const def = getIndicatorDef(entry.type);
if (!def) continue; if (!def) continue;
const missing = this._dualLineMissingPlotIds(entry, config, def); if (this._dualLineSeriesOutOfSync(entry, config, def)) {
if (missing.length > 0) {
await this._rebuildDualLinePlotSeriesForEntry(entry, config, def); await this._rebuildDualLinePlotSeriesForEntry(entry, config, def);
} else if (entry.seriesList.length > 0 && this.rawBars.length > 0) { } else if (entry.seriesList.length > 0 && this.rawBars.length > 0) {
try { try {
@@ -1923,7 +1972,7 @@ export class ChartManager {
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) { if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
const config = enrichIndicatorConfig(entry.config); const config = enrichIndicatorConfig(entry.config);
const def = getIndicatorDef(entry.type); const def = getIndicatorDef(entry.type);
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) { if (this._dualLineSeriesOutOfSync(entry, config, def)) {
await this._ensureDualLinePlotSeriesForEntry(entry); await this._ensureDualLinePlotSeriesForEntry(entry);
} }
} }
@@ -2155,7 +2204,7 @@ export class ChartManager {
if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) { if (DUAL_LINE_INDICATOR_TYPES.has(entry.type)) {
const config = enrichIndicatorConfig(entry.config); const config = enrichIndicatorConfig(entry.config);
const def = getIndicatorDef(entry.type); const def = getIndicatorDef(entry.type);
if (this._dualLineMissingPlotIds(entry, config, def).length > 0) { if (this._dualLineSeriesOutOfSync(entry, config, def)) {
await this._ensureDualLinePlotSeriesForEntry(entry); await this._ensureDualLinePlotSeriesForEntry(entry);
} }
} }
@@ -3857,6 +3906,11 @@ export class ChartManager {
return ids; return ids;
} }
/** 지표에 붙은 plotId 목록 (OBV plot0/plot1 누락 진단용) */
getIndicatorSeriesPlotIds(id: string): string[] {
return this.indicators.get(id)?.seriesMeta.map(m => m.plotId) ?? [];
}
/** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */ /** 현재 로드된 rawBars 개수 반환 (데이터 로드 여부 확인용) */
getRawBarsLength(): number { getRawBarsLength(): number {
return this.rawBars.length; return this.rawBars.length;