obv 수정

This commit is contained in:
Macbook
2026-06-09 22:04:30 +09:00
parent 0a7d547ea9
commit 27c60be1e9
3 changed files with 74 additions and 42 deletions
@@ -123,7 +123,12 @@ const TradeSignalMiniChart: React.FC<Props> = ({
} }
syncChartLayout(); syncChartLayout();
applySignalMarkers(); applySignalMarkers();
void mgr?.repairDualLineSeries(); if (!mgr) return;
[0, 100, 500, 1200].forEach(delay => {
setTimeout(() => {
void managerRef.current?.repairDualLineSeries();
}, delay);
});
}, [syncChartLayout, applySignalMarkers]); }, [syncChartLayout, applySignalMarkers]);
const handleManagerReady = useCallback((mgr: ChartManager) => { const handleManagerReady = useCallback((mgr: ChartManager) => {
@@ -185,6 +190,16 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const chartDataReady = barsReady && obvBarsReady; const chartDataReady = barsReady && obvBarsReady;
useEffect(() => {
if (!enabled || !chartDataReady) return;
const mgr = managerRef.current;
if (!mgr?.hasMainSeries()) return;
const timers = [200, 800, 1600].map(delay =>
setTimeout(() => void mgr.repairDualLineSeries(), delay),
);
return () => timers.forEach(clearTimeout);
}, [enabled, chartDataReady, chartIndicators, market, timeframe]);
useLayoutEffect(() => { useLayoutEffect(() => {
chartLiveReadyRef.current = false; chartLiveReadyRef.current = false;
pendingBarRef.current = null; pendingBarRef.current = null;
+54 -40
View File
@@ -748,23 +748,18 @@ export class ChartManager {
return loadGen !== this._dataGeneration; return loadGen !== this._dataGeneration;
} }
/** OBV — RSI/CCI와 동일한 공유 price scale, 이전 autoscale 오버라이드 제거 */ /** 이중 plot 지표에서 누락된 plotId 목록 (series 개수만으로는 plot0 누락 감지 불가) */
private _resetObvSharedScale(entry: IndicatorEntry): void { private _dualLineMissingPlotIds(
if (entry.type !== 'OBV') return; entry: IndicatorEntry,
for (const s of entry.seriesList) { config: IndicatorConfig,
try { def: ReturnType<typeof getIndicatorDef> | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any ): string[] {
(s as ISeriesApi<any>).applyOptions({ autoscaleInfoProvider: undefined }); const plotDefs = config.plots ?? def?.plots ?? [];
} catch { /* ok */ } const existing = new Set(entry.seriesMeta.map(m => m.plotId));
} return plotDefs
const anchor = entry.seriesList[0]; .filter(p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false)
if (!anchor) return; .map(p => p.id)
try { .filter(id => !existing.has(id));
anchor.priceScale().applyOptions({
visible: true,
scaleMargins: { top: 0.1, bottom: 0.1 },
});
} catch { /* ok */ }
} }
private _appendMissingPlotSeriesToLists( private _appendMissingPlotSeriesToLists(
@@ -813,17 +808,13 @@ export class ChartManager {
} }
} }
/** 스타일 갱신·초기 추가 후 OBV 등 본선(plot0) 시리즈 누락 보정 */ /** 스타일 갱신·초기 추가 후 OBV 등 본선(plot0) 시리즈 누락 보정 + 전체 setData */
private async _ensureDualLinePlotSeriesForEntry(entry: IndicatorEntry): Promise<void> { private async _ensureDualLinePlotSeriesForEntry(entry: IndicatorEntry): Promise<void> {
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) return; if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) return;
const config = enrichIndicatorConfig(entry.config!); const config = enrichIndicatorConfig(entry.config!);
const def = getIndicatorDef(config.type); const def = getIndicatorDef(config.type);
if (!def) return; if (!def) return;
const plotDefs = config.plots ?? def.plots ?? []; if (this._dualLineMissingPlotIds(entry, config, def).length === 0) return;
const expectedCount = plotDefs.filter(
p => p.type !== 'histogram' && config.plotVisibility?.[p.id] !== false,
).length;
if (entry.seriesList.length >= expectedCount) return;
if (this.rawBars.length === 0) return; if (this.rawBars.length === 0) return;
const pane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? 2; const pane = entry.paneIndex ?? this._readSeriesPaneIndex(entry.seriesList[0]) ?? 2;
@@ -839,8 +830,8 @@ export class ChartManager {
indicatorHidden, indicatorHidden,
def, def,
); );
if (config.type === 'OBV' && !indicatorHidden && entry.seriesList.length > 0) { if (entry.seriesList.length > 0) {
this._resetObvSharedScale(entry); this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
} }
entry.config = config; entry.config = config;
if (entry.seriesList.length > 0) { if (entry.seriesList.length > 0) {
@@ -952,12 +943,12 @@ export class ChartManager {
); );
} }
if (config.type === 'OBV' && !indicatorHidden && seriesList.length > 0) { if (DUAL_LINE_INDICATOR_TYPES.has(config.type) && !indicatorHidden && seriesList.length > 0) {
const obvEntry: IndicatorEntry = { const dualEntry: IndicatorEntry = {
id: config.id, type: config.type, seriesList, seriesMeta, id: config.id, type: config.type, seriesList, seriesMeta,
paneIndex: pane, alertLines: [], hlineRefs: [], config, paneIndex: pane, alertLines: [], hlineRefs: [], config,
}; };
this._resetObvSharedScale(obvEntry); this._setIndicatorPlotsFullData(dualEntry, result.plots as Record<string, PlotData>, config);
} }
// hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지) // hlines(과열선/중앙선/침체선)는 첫 번째 시리즈에만 한 번 생성 (중복 방지)
@@ -1013,6 +1004,9 @@ export class ChartManager {
this.indicators.set(config.id, entry); this.indicators.set(config.id, entry);
if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) { if (DUAL_LINE_INDICATOR_TYPES.has(config.type)) {
await this._ensureDualLinePlotSeriesForEntry(entry); await this._ensureDualLinePlotSeriesForEntry(entry);
if (entry.seriesList.length > 0 && !indicatorHidden) {
this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
}
} }
this.applyIndicatorStyle(config); this.applyIndicatorStyle(config);
if (config.type === 'BollingerBands' && !indicatorHidden) { if (config.type === 'BollingerBands' && !indicatorHidden) {
@@ -1069,15 +1063,20 @@ export class ChartManager {
this._notifyPaneLayout(); this._notifyPaneLayout();
} }
/** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 (미니 차트·스타일 갱신 후) */ /** OBV·RSI 등 본선(plot0) 시리즈 누락 시 재생성 + 전체 plot setData */
async repairDualLineSeries(): Promise<void> { async repairDualLineSeries(): Promise<void> {
for (const entry of this.indicators.values()) { for (const entry of this.indicators.values()) {
if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type)) continue; if (!DUAL_LINE_INDICATOR_TYPES.has(entry.type) || !entry.config) continue;
await this._ensureDualLinePlotSeriesForEntry(entry); await this._ensureDualLinePlotSeriesForEntry(entry);
if (entry.config) this.applyIndicatorStyle(entry.config); if (entry.seriesList.length === 0 || this.rawBars.length === 0) continue;
if (entry.type === 'OBV' && entry.seriesList.length > 0) { try {
this._resetObvSharedScale(entry); const config = enrichIndicatorConfig(entry.config);
} const result = await calculateIndicator(
config.type, this.rawBars.slice(), config.params ?? {},
);
this._setIndicatorPlotsFullData(entry, result.plots as Record<string, PlotData>, config);
this.applyIndicatorStyle(config);
} catch { /* ok */ }
} }
} }
@@ -1087,6 +1086,7 @@ export class ChartManager {
await this.addIndicator(config, { skipLayout: true }); await this.addIndicator(config, { skipLayout: true });
} }
if (configs.length > 0) { if (configs.length > 0) {
await this.repairDualLineSeries();
this._finalizeIndicatorPaneLayout(); this._finalizeIndicatorPaneLayout();
this._refreshAllIchimokuClouds(); this._refreshAllIchimokuClouds();
this.syncChartOverlayVisibility(); this.syncChartOverlayVisibility();
@@ -1876,8 +1876,17 @@ export class ChartManager {
for (const [, entry] of this.indicators.entries()) { for (const [, entry] of this.indicators.entries()) {
if (dataGenAtStart !== this._dataGeneration) return; if (dataGenAtStart !== this._dataGeneration) return;
if (!entry.config || entry.seriesList.length === 0) continue; if (!entry.config) continue;
try { try {
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) {
await this._ensureDualLinePlotSeriesForEntry(entry);
}
}
if (entry.seriesList.length === 0) continue;
const { plots } = await calculateIndicator( const { plots } = await calculateIndicator(
entry.config.type, barsSnapshot, entry.config.params ?? {} entry.config.type, barsSnapshot, entry.config.params ?? {}
); );
@@ -2099,8 +2108,17 @@ export class ChartManager {
const barsSnapshot = this.rawBars.slice(); const barsSnapshot = this.rawBars.slice();
for (const [, entry] of this.indicators.entries()) { for (const [, entry] of this.indicators.entries()) {
if (dataGenAtStart !== this._dataGeneration) return; if (dataGenAtStart !== this._dataGeneration) return;
if (!entry.config || entry.seriesList.length === 0) continue; if (!entry.config) continue;
try { try {
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) {
await this._ensureDualLinePlotSeriesForEntry(entry);
}
}
if (entry.seriesList.length === 0) continue;
const { plots } = await calculateIndicator( const { plots } = await calculateIndicator(
entry.config.type, barsSnapshot, entry.config.params ?? {} entry.config.type, barsSnapshot, entry.config.params ?? {}
); );
@@ -2984,10 +3002,6 @@ export class ChartManager {
} }
} }
if (entry.type === 'OBV' && entry.seriesList.length > 0) {
this._resetObvSharedScale(entry);
}
if (entry.type === 'BollingerBands') { if (entry.type === 'BollingerBands') {
detachBbBandFill(entry); detachBbBandFill(entry);
if (this._shouldShowBbBandFillForConfig(config)) { if (this._shouldShowBbBandFillForConfig(config)) {
+4 -1
View File
@@ -479,7 +479,10 @@ function normalizeSignalLineIndicatorPlots(
const order = ['plot0', 'plot1']; const order = ['plot0', 'plot1'];
return order.indexOf(a.id) - order.indexOf(b.id); return order.indexOf(a.id) - order.indexOf(b.id);
}); });
return { plots: nextPlots, plotVisibility: { ...plotVisibility } }; const nextVis = { ...plotVisibility };
if (nextVis.plot0 !== false) nextVis.plot0 = true;
if (nextVis.plot1 !== false) nextVis.plot1 = true;
return { plots: nextPlots, plotVisibility: nextVis };
} }
const regSignal = registryPlots.find(p => p.id === 'plot1'); const regSignal = registryPlots.find(p => p.id === 'plot1');