obv 그래프 오류

This commit is contained in:
Macbook
2026-06-09 02:25:59 +09:00
parent 8670e58c78
commit 7c1cc2ee4b
4 changed files with 58 additions and 13 deletions
+4 -1
View File
@@ -1989,7 +1989,10 @@ function barsKey(bars: OHLCVBar[], market = ''): string {
if (bars.length === 0) return ''; if (bars.length === 0) return '';
const last = bars[bars.length - 1]; const last = bars[bars.length - 1];
// last.close 제외 — 틱마다 full reload 방지 (실시간은 updateBar/appendBar 경로) // last.close 제외 — 틱마다 full reload 방지 (실시간은 updateBar/appendBar 경로)
return `${market}:${bars.length}:${bars[0].time}:${last.time}`; // volume 합계 — STOMP volume=0 → Upbit 폴백 후 OBV 등 지표 재계산 유도
const volSum = bars.reduce((s, b) => s + (Number(b.volume) || 0), 0);
const volFlag = volSum > 0 ? 'v' : '0';
return `${market}:${bars.length}:${bars[0].time}:${last.time}:${volFlag}`;
} }
/** /**
@@ -54,7 +54,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
}) => { }) => {
const { getParams, getVisualConfig } = useIndicatorSettings(); const { getParams, getVisualConfig } = useIndicatorSettings();
const { defaults: appDefaults } = useAppSettings(); const { defaults: appDefaults } = useAppSettings();
const chartRealtimeSource = (appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP') as ChartRealtimeSource; const appChartSource = (appDefaults.chartRealtimeSource ?? 'BACKEND_STOMP') as ChartRealtimeSource;
const managerRef = useRef<ChartManager | null>(null); const managerRef = useRef<ChartManager | null>(null);
const canvasWrapRef = useRef<HTMLDivElement | null>(null); const canvasWrapRef = useRef<HTMLDivElement | null>(null);
const [chartReloadTick, setChartReloadTick] = useState(0); const [chartReloadTick, setChartReloadTick] = useState(0);
@@ -77,6 +77,9 @@ const TradeSignalMiniChart: React.FC<Props> = ({
[baseIndicators], [baseIndicators],
); );
/** OBV 누적값 — Upbit REST 직접 (STOMP 백엔드 volume=0 이면 본선 미표시) */
const chartRealtimeSource: ChartRealtimeSource = needsDeepObv ? 'UPBIT_DIRECT' : appChartSource;
const { const {
indicators: chartIndicators, indicators: chartIndicators,
candleOverlayToggles, candleOverlayToggles,
@@ -166,6 +169,14 @@ const TradeSignalMiniChart: React.FC<Props> = ({
barsMarketRef.current = barsMarket; barsMarketRef.current = barsMarket;
const barsReady = bars.length >= 2 && barsMarket === market; const barsReady = bars.length >= 2 && barsMarket === market;
const obvBarsReady = useMemo(() => {
if (!needsDeepObv) return true;
if (bars.length < 2) return false;
return bars.some(b => (Number(b.volume) || 0) > 0);
}, [needsDeepObv, bars]);
const chartDataReady = barsReady && obvBarsReady;
useLayoutEffect(() => { useLayoutEffect(() => {
chartLiveReadyRef.current = false; chartLiveReadyRef.current = false;
pendingBarRef.current = null; pendingBarRef.current = null;
@@ -191,7 +202,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
if (!canvasWrapRef.current) return; if (!canvasWrapRef.current) return;
if (managerRef.current?.hasMainSeries()) { if (managerRef.current?.hasMainSeries()) {
syncChartLayout(); syncChartLayout();
} else if (!chartReloadTriggeredRef.current && delay >= 600 && barsReady) { } else if (!chartReloadTriggeredRef.current && delay >= 600 && chartDataReady) {
chartReloadTriggeredRef.current = true; chartReloadTriggeredRef.current = true;
setChartReloadTick(t => t + 1); setChartReloadTick(t => t + 1);
} }
@@ -204,15 +215,15 @@ const TradeSignalMiniChart: React.FC<Props> = ({
timers.forEach(clearTimeout); timers.forEach(clearTimeout);
ro.disconnect(); ro.disconnect();
}; };
}, [market, timeframe, enabled, barsReady, syncChartLayout, fillHeight]); }, [market, timeframe, enabled, chartDataReady, syncChartLayout, fillHeight]);
useEffect(() => { useEffect(() => {
if (!enabled || !barsReady) return; if (!enabled || !chartDataReady) return;
if (!managerRef.current?.hasMainSeries() && !chartReloadTriggeredRef.current) { if (!managerRef.current?.hasMainSeries() && !chartReloadTriggeredRef.current) {
chartReloadTriggeredRef.current = true; chartReloadTriggeredRef.current = true;
setChartReloadTick(t => t + 1); setChartReloadTick(t => t + 1);
} }
}, [enabled, barsReady]); }, [enabled, chartDataReady]);
useEffect(() => { useEffect(() => {
if (!enabled) return; if (!enabled) return;
@@ -246,13 +257,17 @@ const TradeSignalMiniChart: React.FC<Props> = ({
].filter(Boolean).join(' ')} ].filter(Boolean).join(' ')}
> >
{isLoading && <div className="tnl-mini-chart-loading"></div>} {isLoading && <div className="tnl-mini-chart-loading"></div>}
{needsDeepObv && !isLoading && !obvBarsReady && (
<div className="tnl-mini-chart-loading">OBV </div>
)}
{isLoadingMore && ( {isLoadingMore && (
<div className="chart-history-loading"> <div className="chart-history-loading">
<div className="loading-spinner" style={{ width: 12, height: 12 }} /> <div className="loading-spinner" style={{ width: 12, height: 12 }} />
</div> </div>
)} )}
{chartDataReady && (
<TradingChart <TradingChart
key={`${market}-${timeframe}-${chartReloadTick}-${chartIndicators.map(i => i.id).join(',')}`} key={`${market}-${timeframe}-${chartReloadTick}-${chartRealtimeSource}-${needsDeepObv ? 'obv' : 'std'}-${chartIndicators.map(i => i.id).join(',')}`}
chartVisible chartVisible
bars={bars} bars={bars}
barsMarket={barsMarket} barsMarket={barsMarket}
@@ -283,6 +298,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
onToggleCandleOverlay={handleToggleCandleOverlay} onToggleCandleOverlay={handleToggleCandleOverlay}
crosshairInfoVisible={false} crosshairInfoVisible={false}
/> />
)}
</div> </div>
); );
}; };
+25 -5
View File
@@ -15,6 +15,10 @@ import { fetchUpbitCandlesCached } from '../utils/requestCache';
/** 초기 렌더 최소 봉 수 — 미만이면 업비트 REST 폴백 */ /** 초기 렌더 최소 봉 수 — 미만이면 업비트 REST 폴백 */
const MIN_INITIAL_BARS = Math.min(50, Math.floor(DISPLAY_COUNT / 4)); const MIN_INITIAL_BARS = Math.min(50, Math.floor(DISPLAY_COUNT / 4));
function barsHaveVolume(bars: OHLCVBar[]): boolean {
return bars.some(b => (Number(b.volume) || 0) > 0);
}
/** 백엔드 BarBuilder 는 틱마다 1m 만 STOMP 발행 — 차트 분봉은 클라이언트에서 집계 */ /** 백엔드 BarBuilder 는 틱마다 1m 만 STOMP 발행 — 차트 분봉은 클라이언트에서 집계 */
const STOMP_TICK_CANDLE_TYPE = '1m'; const STOMP_TICK_CANDLE_TYPE = '1m';
@@ -106,29 +110,45 @@ async function fetchInitialBarsWithRetry(
throw lastErr ?? new Error('history fetch failed'); throw lastErr ?? new Error('history fetch failed');
} }
/** 백엔드 history → 부족·실패 시 업비트 REST 폴백 (종목별 Ta4j 미적재 대응) */ /** 백엔드 history → 부족·거래량 0·실패 시 업비트 REST 폴백 */
async function loadInitialBars( async function loadInitialBars(
market: string, market: string,
timeframe: Timeframe, timeframe: Timeframe,
count: number, count: number,
marketChanged: boolean, marketChanged: boolean,
preferUpbit = false,
): Promise<OHLCVBar[]> { ): Promise<OHLCVBar[]> {
const candleType = timeframeToCandleType(timeframe); const candleType = timeframeToCandleType(timeframe);
const upbitCount = Math.min(count, OBV_DEEP_HISTORY);
if (preferUpbit) {
try {
const upbit = await fetchUpbitCandlesCached(market, timeframe, upbitCount, marketChanged);
if (upbit.length >= MIN_INITIAL_BARS && barsHaveVolume(upbit)) return upbit;
} catch {
/* Upbit 실패 시 backend 시도 */
}
}
let bars: OHLCVBar[] = []; let bars: OHLCVBar[] = [];
try { try {
bars = await fetchInitialBarsWithRetry(market, candleType, count); bars = await fetchInitialBarsWithRetry(market, candleType, count);
} catch { } catch {
bars = []; bars = [];
} }
if (bars.length < MIN_INITIAL_BARS) {
const needsUpbitFallback = bars.length < MIN_INITIAL_BARS || !barsHaveVolume(bars);
if (needsUpbitFallback) {
try { try {
const fallback = await fetchUpbitCandlesCached( const fallback = await fetchUpbitCandlesCached(
market, market,
timeframe, timeframe,
Math.min(count, 200), upbitCount,
marketChanged, marketChanged,
); );
if (fallback.length > bars.length) bars = fallback; if (fallback.length > bars.length || (barsHaveVolume(fallback) && !barsHaveVolume(bars))) {
bars = fallback;
}
} catch { } catch {
/* 폴백 실패 시 backend 결과 유지 */ /* 폴백 실패 시 backend 결과 유지 */
} }
@@ -182,7 +202,7 @@ export function useStompChartData(
const count = opts.deepObvHistory const count = opts.deepObvHistory
? OBV_DEEP_HISTORY ? OBV_DEEP_HISTORY
: DISPLAY_COUNT + WARMUP_COUNT; : DISPLAY_COUNT + WARMUP_COUNT;
loadInitialBars(market, timeframe, count, marketChanged) loadInitialBars(market, timeframe, count, marketChanged, opts.deepObvHistory === true)
.then(newBars => { .then(newBars => {
if (cancelled) return; if (cancelled) return;
barsRef.current = newBars; barsRef.current = newBars;
+7 -1
View File
@@ -472,7 +472,13 @@ function normalizeSignalLineIndicatorPlots(
if (type === 'OBV') { if (type === 'OBV') {
const nextPlots = merged.map(p => { const nextPlots = merged.map(p => {
const reg = registryPlots.find(r => r.id === p.id); const reg = registryPlots.find(r => r.id === p.id);
return reg ? normalizeDualLinePlotDef(p, reg) : p; if (!reg) return p;
const normalized = normalizeDualLinePlotDef(p, reg);
// OBV 본선 — DB 저장색(투명·신호선과 동일) 무시, registry 기본색 유지
if (p.id === 'plot0') {
return { ...normalized, color: reg.color };
}
return normalized;
}); });
return { return {
plots: nextPlots, plots: nextPlots,