알림목록 obv 그래프 문제 최종수정

This commit is contained in:
Macbook
2026-06-10 09:57:38 +09:00
parent 490db298b6
commit 1ec22d6ea2
12 changed files with 745 additions and 162 deletions
@@ -2,7 +2,7 @@
* 매매 시그널 알림 목록 행 — 4칸이 목록 너비를 꽉 채움, 보조지표 2개↑ 시 우측 슬롯만 스크롤
*/
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
import type { Theme } from '../../types';
import type { Theme, IndicatorConfig } from '../../types';
import type { TickerData } from '../../hooks/useMarketTicker';
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
@@ -23,8 +23,10 @@ import {
buildStrategyChartIndicators,
candleTypeToTimeframe,
} from '../../utils/strategyToChartIndicators';
import { buildNotificationChartIndicators } from '../../utils/notificationChartIndicators';
import { formatIndicatorDisplayLabel } from '../../utils/indicatorRegistry';
import { useIndicatorSettings } from '../../hooks/useIndicatorSettings';
import { ensureWorkspaceChartIndicators, invalidateWorkspaceChartIndicatorsCache } from '../../utils/workspaceChartIndicatorsCache';
import TradeSignalChartCard from './TradeSignalChartCard';
import TradeNotificationSignalCard from './TradeNotificationSignalCard';
import TradeNotificationHScrollPane from './TradeNotificationHScrollPane';
@@ -109,7 +111,9 @@ const TradeNotificationListRow: React.FC<Props> = ({
const chartTf = candleTypeToTimeframe(item.candleType ?? '1m');
const candleKo = formatCandleTypeKo(item.candleType);
const { getParams, getVisualConfig } = useIndicatorSettings();
const { getParams, getVisualConfig, settingsRevision } = useIndicatorSettings();
const [wsIndicators, setWsIndicators] = useState<IndicatorConfig[]>([]);
const [wsRevision, setWsRevision] = useState(0);
const [strategy, setStrategy] = useState<StrategyDto | undefined>();
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const strategyLabel = item.strategyName?.trim()
@@ -166,16 +170,28 @@ const TradeNotificationListRow: React.FC<Props> = ({
setExpandedChartKey(null);
}, [item.id, layoutMode]);
useEffect(() => {
let cancelled = false;
invalidateWorkspaceChartIndicatorsCache();
void ensureWorkspaceChartIndicators().then(inds => {
if (cancelled) return;
setWsIndicators(inds);
setWsRevision(r => r + 1);
});
return () => { cancelled = true; };
}, [settingsRevision]);
const indicatorCards = useMemo(() => {
if (!isListLayout || !strategy) return [];
const all = buildStrategyChartIndicators(strategy, getParams, getVisualConfig);
const strategyBuilt = buildStrategyChartIndicators(strategy, getParams, getVisualConfig, wsIndicators);
const all = buildNotificationChartIndicators(strategyBuilt, wsIndicators);
return all.slice(0, MAX_INDICATOR_CARDS).map(ind => ({
id: ind.id,
type: ind.type,
label: formatIndicatorDisplayLabel(ind.type),
config: [ind],
}));
}, [strategy, getParams, getVisualConfig, isListLayout]);
}, [strategy, getParams, getVisualConfig, settingsRevision, wsRevision, wsIndicators, isListLayout]);
/** 통합 차트용: 전략의 모든 보조지표를 하나의 차트에 넘긴다 */
const allIndicatorConfigs = useMemo(
@@ -17,13 +17,13 @@ import { useAppSettings } from '../../hooks/useAppSettings';
import type { ChartManager } from '../../utils/ChartManager';
import { pinCandleWatch } from '../../utils/backendApi';
import { timeframeToCandleType } from '../../utils/chartCandleType';
import { DEFAULT_DISPLAY_TIMEZONE } from '../../utils/timezone';
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 { buildNotificationChartIndicators } from '../../utils/notificationChartIndicators';
import { ensurePaperChartOverlays } from '../../utils/strategyToChartIndicators';
import { ensureWorkspaceChartIndicators, invalidateWorkspaceChartIndicatorsCache } from '../../utils/workspaceChartIndicatorsCache';
import { indicatorDefaultsFingerprint } from '../../utils/indicatorDefaultsFingerprint';
import {
applyNotificationSignalMarkers,
type TradeSignalChartMarker,
@@ -70,21 +70,42 @@ const TradeSignalMiniChart: React.FC<Props> = ({
const signalMarkerRef = useRef(signalMarker);
signalMarkerRef.current = signalMarker;
const baseIndicators = useMemo(
() => mergeGlobalDefaultsOntoChartIndicators(
const [wsIndicators, setWsIndicators] = useState<IndicatorConfig[]>([]);
const [wsRevision, setWsRevision] = useState(0);
useEffect(() => {
let cancelled = false;
invalidateWorkspaceChartIndicatorsCache();
void ensureWorkspaceChartIndicators().then(inds => {
if (cancelled) return;
setWsIndicators(inds);
setWsRevision(r => r + 1);
});
return () => { cancelled = true; };
}, [settingsRevision]);
const baseIndicators = useMemo(() => {
const strategyMerged = mergeGlobalDefaultsOntoChartIndicators(
ensurePaperChartOverlays(indicators, getParams, getVisualConfig),
getParams,
getVisualConfig,
).map(enrichIndicatorConfig),
[indicators, getParams, getVisualConfig, settingsRevision],
wsIndicators,
);
return buildNotificationChartIndicators(strategyMerged, wsIndicators);
}, [indicators, getParams, getVisualConfig, settingsRevision, wsRevision, wsIndicators]);
const indicatorStyleKey = useMemo(
() => baseIndicators.map(i => indicatorDefaultsFingerprint(i)).join('|'),
[baseIndicators],
);
const needsDeepObv = useMemo(
/** OBV 등 워크스페이스 지표가 있으면 ws config 로드 완료 후 렌더 (maLength=1 플래시 방지) */
const needsWorkspaceConfig = useMemo(
() => baseIndicators.some(i => i.type === 'OBV'),
[baseIndicators],
);
/** 메인 차트와 동일 소스 — STOMP(+ volume 없으면 Upbit 폴백). deepObvHistory 로 장기 봉 확보 */
/** 실시간(메인) 차트와 동일한 데이터 소스 사용 */
const chartRealtimeSource: ChartRealtimeSource = appChartSource;
const {
@@ -162,28 +183,23 @@ const TradeSignalMiniChart: React.FC<Props> = ({
onNewCandle: handleNewCandle,
enabled: enabled && useUpbit,
source: chartRealtimeSource,
deepObvHistory: needsDeepObv,
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit, needsDeepObv, chartRealtimeSource]),
}), [handleTickUpdate, handleNewCandle, enabled, useUpbit, chartRealtimeSource]),
);
const { isLoadingMore } = useHistoryLoader({
symbol: market,
timeframe,
/** OBV — 백엔드 history volume=0 봉 prepend 시 본선 왜곡 방지 */
isUpbit: useUpbit && enabled && !needsDeepObv,
isUpbit: useUpbit && enabled,
managerRef,
});
barsMarketRef.current = barsMarket;
const barsReady = bars.length >= 2 && barsMarket === market;
const obvBarsReady = useMemo(() => {
if (!needsDeepObv) return true;
if (bars.length < 2) return false;
return barsMeetObvVolumeRequirement(bars);
}, [needsDeepObv, bars]);
// 워크스페이스 지표(OBV maLength 등) 로드 완료 전에는 렌더 보류 → 잘못된 기본값 플래시 방지
const wsConfigReady = !needsWorkspaceConfig || wsRevision > 0;
const chartDataReady = barsReady && obvBarsReady && settingsLoaded;
const chartDataReady = barsReady && wsConfigReady && settingsLoaded;
useLayoutEffect(() => {
chartLiveReadyRef.current = false;
@@ -265,9 +281,6 @@ const TradeSignalMiniChart: React.FC<Props> = ({
].filter(Boolean).join(' ')}
>
{(!settingsLoaded || isLoading) && <div className="tnl-mini-chart-loading"></div>}
{needsDeepObv && !isLoading && !obvBarsReady && (
<div className="tnl-mini-chart-loading">OBV </div>
)}
{isLoadingMore && (
<div className="chart-history-loading">
<div className="loading-spinner" style={{ width: 12, height: 12 }} />
@@ -275,7 +288,7 @@ const TradeSignalMiniChart: React.FC<Props> = ({
)}
{chartDataReady && (
<TradingChart
key={`${market}-${timeframe}-${chartReloadTick}-${chartRealtimeSource}-${needsDeepObv ? 'obv' : 'std'}-${settingsRevision}-${chartIndicators.map(i => i.id).join(',')}`}
key={`${market}-${timeframe}-${chartReloadTick}-${chartRealtimeSource}-${settingsRevision}-${wsRevision}-${indicatorStyleKey}-${chartIndicators.map(i => i.id).join(',')}`}
chartVisible
bars={bars}
barsMarket={barsMarket}