From 420050dfb749001a7d0dd582601d2c658d12a3d1 Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 9 Jun 2026 22:30:19 +0900 Subject: [PATCH] =?UTF-8?q?obv=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tradeNotification/TradeSignalMiniChart.tsx | 8 ++++++-- frontend/src/utils/ChartManager.ts | 2 ++ frontend/src/utils/obvChartBars.ts | 10 ++++++++++ .../src/utils/strategyToChartIndicators.ts | 18 +++++++++++++----- 4 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 frontend/src/utils/obvChartBars.ts diff --git a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx index a4b6bf2..c4ad025 100644 --- a/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx +++ b/frontend/src/components/tradeNotification/TradeSignalMiniChart.tsx @@ -22,6 +22,7 @@ import { useIndicatorSettings } from '../../hooks/useIndicatorSettings'; import { useSessionCandleOverlayControls } from '../../hooks/useSessionCandleOverlayControls'; import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators'; import { mergeGlobalDefaultsOntoChartIndicators } from '../../utils/indicatorMainConfig'; +import { barsMeetObvVolumeRequirement } from '../../utils/obvChartBars'; import { enrichIndicatorConfig } from '../../utils/indicatorRegistry'; import { applyNotificationSignalMarkers, @@ -135,7 +136,10 @@ const TradeSignalMiniChart: React.FC = ({ managerRef.current = mgr; syncToManager(mgr); applySignalMarkers(); - }, [syncToManager, applySignalMarkers]); + if (needsDeepObv) { + void mgr.repairDualLineSeries(); + } + }, [syncToManager, applySignalMarkers, needsDeepObv]); const applyRealtimeBar = useCallback((bar: OHLCVBar, append: boolean) => { if (barsMarketRef.current !== marketRef.current) return; @@ -185,7 +189,7 @@ const TradeSignalMiniChart: React.FC = ({ const obvBarsReady = useMemo(() => { if (!needsDeepObv) return true; if (bars.length < 2) return false; - return bars.some(b => (Number(b.volume) || 0) > 0); + return barsMeetObvVolumeRequirement(bars); }, [needsDeepObv, bars]); const chartDataReady = barsReady && obvBarsReady && settingsLoaded; diff --git a/frontend/src/utils/ChartManager.ts b/frontend/src/utils/ChartManager.ts index b1c1c50..ee23d46 100644 --- a/frontend/src/utils/ChartManager.ts +++ b/frontend/src/utils/ChartManager.ts @@ -1203,6 +1203,7 @@ export class ChartManager { if (this._indicatorLoadStale(loadGen)) return; + await this.repairDualLineSeries(); this._finalizeIndicatorPaneLayout(); this._refreshAllIchimokuClouds(); this.syncChartOverlayVisibility(); @@ -1250,6 +1251,7 @@ export class ChartManager { } if (this._indicatorLoadStale(loadGen)) return; + await this.repairDualLineSeries(); this._finalizeIndicatorPaneLayout(); } diff --git a/frontend/src/utils/obvChartBars.ts b/frontend/src/utils/obvChartBars.ts new file mode 100644 index 0000000..39fb19b --- /dev/null +++ b/frontend/src/utils/obvChartBars.ts @@ -0,0 +1,10 @@ +import type { OHLCVBar } from '../types'; + +/** OBV 본선 계산에 필요한 최소 거래량 품질 (some>0 만으로는 STOMP volume=0 history 통과) */ +export function barsMeetObvVolumeRequirement(bars: OHLCVBar[]): boolean { + if (bars.length < 2) return false; + const withVol = bars.filter(b => (Number(b.volume) || 0) > 0).length; + const volSum = bars.reduce((s, b) => s + (Number(b.volume) || 0), 0); + const minBarsWithVol = Math.max(20, Math.floor(bars.length * 0.5)); + return volSum > 0 && withVol >= minBarsWithVol; +} diff --git a/frontend/src/utils/strategyToChartIndicators.ts b/frontend/src/utils/strategyToChartIndicators.ts index 5d6d4e9..6f518c3 100644 --- a/frontend/src/utils/strategyToChartIndicators.ts +++ b/frontend/src/utils/strategyToChartIndicators.ts @@ -68,10 +68,14 @@ type GetVisual = ( cloudColors?: import('./ichimokuConfig').IchimokuCloudColors; }; -let _idSeq = 0; -function newIndId(): string { - _idSeq += 1; - return `vstrat_${_idSeq}_${Date.now()}`; +/** 전략 DSL 기준 안정 id — getVisualConfig/settingsRevision 변경 시 id 재발급 방지 */ +export function stableStrategyIndicatorId(ref: IndicatorRef): string { + const parts = [ref.registryType]; + if (ref.period != null) parts.push(`p${ref.period}`); + if (ref.leftPeriod != null) parts.push(`l${ref.leftPeriod}`); + if (ref.rightPeriod != null) parts.push(`r${ref.rightPeriod}`); + if (ref.targetValue != null) parts.push(`t${ref.targetValue}`); + return `strat_${parts.join('_')}`; } export function candleTypeToTimeframe(candleType: string): Timeframe { @@ -171,6 +175,7 @@ function buildOneIndicator( ref: IndicatorRef, getParams: GetParams, getVisual: GetVisual, + stableId?: string, ): IndicatorConfig | null { const def = getIndicatorDef(ref.registryType); if (!def) return null; @@ -179,7 +184,7 @@ function buildOneIndicator( const { plots, hlines, cloudColors } = getVisual(ref.registryType, def.plots, def.hlines ?? []); let cfg: IndicatorConfig = { - id: newIndId(), + id: stableId ?? stableStrategyIndicatorId(ref), type: def.type, params: { ...params }, plots, @@ -295,6 +300,7 @@ export function ensurePaperChartOverlays( { dslType: 'MA', registryType: 'SMA' }, getParams, getVisual, + 'strat_overlay_SMA', ); if (sma) prepend.push(sma); } @@ -303,6 +309,7 @@ export function ensurePaperChartOverlays( { dslType: 'ICHIMOKU', registryType: 'IchimokuCloud' }, getParams, getVisual, + 'strat_overlay_IchimokuCloud', ); if (ichimoku) prepend.push(ichimoku); } @@ -311,6 +318,7 @@ export function ensurePaperChartOverlays( { dslType: 'BOLLINGER', registryType: 'BollingerBands' }, getParams, getVisual, + 'strat_overlay_BollingerBands', ); if (bb) prepend.push(bb); }