obv 그래프 오류
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user