알림목록 지표없음 문제 수정

This commit is contained in:
Macbook
2026-06-08 01:23:49 +09:00
parent 7055748b34
commit 0229799383
8 changed files with 166 additions and 39 deletions
@@ -75,9 +75,9 @@ public class LiveConditionStatusService {
for (PendingCond p : pending) { for (PendingCond p : pending) {
String tf = LiveStrategyTimeframeService.normalize(p.timeframe()); String tf = LiveStrategyTimeframeService.normalize(p.timeframe());
timeframes.add(tf); timeframes.add(tf);
subscriptionManager.ensureMarketPinned(market, tf); subscriptionManager.ensureBarsReadySync(market, tf, 60);
} }
subscriptionManager.ensureMarketPinned(market, "1m"); subscriptionManager.ensureBarsReadySync(market, "1m", 60);
List<LiveConditionRowDto> rows = new ArrayList<>(); List<LiveConditionRowDto> rows = new ArrayList<>();
int met = 0; int met = 0;
@@ -239,6 +239,13 @@ public class LiveConditionStatusService {
if (seen.add(key)) { if (seen.add(key)) {
out.add(new PendingCond(id, cond, timeframe, side)); out.add(new PendingCond(id, cond, timeframe, side));
} }
return;
}
// START·구버전 DSL 등 — 자식 노드 재귀 (프론트 extractVirtualConditions 와 동일)
JsonNode children = node.path("children");
if (children.isArray()) {
for (JsonNode c : children) walk(c, timeframe, side, out, seen);
} }
} }
@@ -11,7 +11,9 @@ import {
loadStrategy, loadStrategy,
runBacktest, runBacktest,
type PaperSummaryDto, type PaperSummaryDto,
type StrategyDto,
} from '../utils/backendApi'; } from '../utils/backendApi';
import { prefetchNotificationStrategies } from '../utils/resolveNotificationStrategy';
import { loadAnalysisCandles } from '../utils/analysisChartData'; import { loadAnalysisCandles } from '../utils/analysisChartData';
import { buildChartBacktestReportModel } from '../utils/backtestReportModel'; import { buildChartBacktestReportModel } from '../utils/backtestReportModel';
import { candleTypeToTimeframe } from '../utils/strategyToChartIndicators'; import { candleTypeToTimeframe } from '../utils/strategyToChartIndicators';
@@ -145,6 +147,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
const [gridPreset, setGridPreset] = useState<TradeNotificationGridPresetId>( const [gridPreset, setGridPreset] = useState<TradeNotificationGridPresetId>(
() => loadTradeNotificationGridPreset(), () => loadTradeNotificationGridPreset(),
); );
const [strategies, setStrategies] = useState<StrategyDto[]>([]);
const [selectedMarket, setSelectedMarket] = useState(defaultMarket); const [selectedMarket, setSelectedMarket] = useState(defaultMarket);
const [selectedId, setSelectedId] = useState<string | null>(null); const [selectedId, setSelectedId] = useState<string | null>(null);
const [reportOpen, setReportOpen] = useState(false); const [reportOpen, setReportOpen] = useState(false);
@@ -177,6 +180,14 @@ export const TradeNotificationListPage: React.FC<Props> = ({
void refreshPaperData(); void refreshPaperData();
}, [refreshPaperData]); }, [refreshPaperData]);
useEffect(() => {
let cancelled = false;
void prefetchNotificationStrategies().then(list => {
if (!cancelled) setStrategies(list);
});
return () => { cancelled = true; };
}, []);
const tradePrice = useMemo( const tradePrice = useMemo(
() => coerceFiniteNumber(tickers?.get(selectedMarket)?.tradePrice), () => coerceFiniteNumber(tickers?.get(selectedMarket)?.tradePrice),
[tickers, selectedMarket], [tickers, selectedMarket],
@@ -433,6 +444,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
theme={theme} theme={theme}
layoutMode={listLayout} layoutMode={listLayout}
itemTag="div" itemTag="div"
prefetchedStrategies={strategies}
isSelected={selectedId === item.id} isSelected={selectedId === item.id}
chartsEnabled={isListView} chartsEnabled={isListView}
chartLiveReceiveHighlight={chartLiveReceiveHighlight} chartLiveReceiveHighlight={chartLiveReceiveHighlight}
@@ -465,6 +477,7 @@ export const TradeNotificationListPage: React.FC<Props> = ({
handleReportFromAlert, handleReportFromAlert,
reportLoadingId, reportLoadingId,
tickers, tickers,
strategies,
]); ]);
return ( return (
@@ -7,7 +7,7 @@ import type { TickerData } from '../../hooks/useMarketTicker';
import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash'; import { useLiveReceiveFlash } from '../../hooks/useLiveReceiveFlash';
import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot'; import { useTradeNotificationSignalSnapshot } from '../../hooks/useTradeNotificationSignalSnapshot';
import type { StrategyDto } from '../../utils/backendApi'; import type { StrategyDto } from '../../utils/backendApi';
import { isStrategyMissingOnServer, loadStrategies, loadStrategy } from '../../utils/backendApi'; import { resolveNotificationStrategy } from '../../utils/resolveNotificationStrategy';
import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext'; import type { TradeNotificationItem } from '../../contexts/TradeNotificationContext';
import { import {
buildSignalDetailRows, buildSignalDetailRows,
@@ -68,6 +68,8 @@ interface Props {
reportLoading?: boolean; reportLoading?: boolean;
/** 가상 스크롤 목록에서는 div + role=listitem */ /** 가상 스크롤 목록에서는 div + role=listitem */
itemTag?: 'li' | 'div'; itemTag?: 'li' | 'div';
/** 목록 화면에서 선로드한 전략 목록 (ID·이름 fallback) */
prefetchedStrategies?: StrategyDto[];
} }
const MAX_INDICATOR_CARDS = 8; const MAX_INDICATOR_CARDS = 8;
@@ -88,6 +90,7 @@ const TradeNotificationListRow: React.FC<Props> = ({
onReport, onReport,
reportLoading = false, reportLoading = false,
itemTag = 'li', itemTag = 'li',
prefetchedStrategies = [],
}) => { }) => {
useTradeAlertTimeFormat(); useTradeAlertTimeFormat();
const isBuy = item.signalType === 'BUY'; const isBuy = item.signalType === 'BUY';
@@ -137,17 +140,20 @@ const TradeNotificationListRow: React.FC<Props> = ({
return; return;
} }
let cancelled = false; let cancelled = false;
void loadStrategies().then(list => { setStrategies(prefetchedStrategies);
if (!cancelled) setStrategies(list ?? []); if (!item.strategyId && !item.strategyName?.trim()) {
});
if (!item.strategyId || isStrategyMissingOnServer(item.strategyId)) {
return () => { cancelled = true; }; return () => { cancelled = true; };
} }
void loadStrategy(item.strategyId).then(s => { void resolveNotificationStrategy(
item.strategyId,
item.strategyName,
prefetchedStrategies,
).then(({ strategy: s, strategyId: sid }) => {
if (!cancelled && s) setStrategy(s); if (!cancelled && s) setStrategy(s);
else if (!cancelled && !s && sid == null) setStrategy(undefined);
}); });
return () => { cancelled = true; }; return () => { cancelled = true; };
}, [item.strategyId, panelActive, isListLayout]); }, [item.strategyId, item.strategyName, panelActive, isListLayout, prefetchedStrategies]);
useEffect(() => { useEffect(() => {
setExpandedChartKey(null); setExpandedChartKey(null);
@@ -181,7 +187,9 @@ const TradeNotificationListRow: React.FC<Props> = ({
item.market, item.market,
item.strategyId, item.strategyId,
strategy, strategy,
item.strategyName,
signalSnapshotEnabled, signalSnapshotEnabled,
prefetchedStrategies,
); );
const [chartActivitySeq, setChartActivitySeq] = useState(0); const [chartActivitySeq, setChartActivitySeq] = useState(0);
@@ -52,7 +52,9 @@ const TradeNotificationSignalSummary: React.FC<Props> = ({
market, market,
strategyId, strategyId,
strategy, strategy,
strategyLabel,
active && snapshotProp === undefined, active && snapshotProp === undefined,
strategies,
); );
const snapshot = snapshotProp ?? internal.snapshot; const snapshot = snapshotProp ?? internal.snapshot;
const loading = signalLoadingProp ?? internal.loading; const loading = signalLoadingProp ?? internal.loading;
@@ -26,6 +26,7 @@ import {
playTradeAlertSound, playTradeAlertSound,
type TradeAlertSoundId, type TradeAlertSoundId,
} from '../utils/tradeAlertSound'; } from '../utils/tradeAlertSound';
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
import { getUiPreferences, patchUiPreferences } from '../utils/uiPreferencesDb'; import { getUiPreferences, patchUiPreferences } from '../utils/uiPreferencesDb';
import { useAppSettings } from '../hooks/useAppSettings'; import { useAppSettings } from '../hooks/useAppSettings';
@@ -87,7 +88,7 @@ function dtoToItem(dto: TradeSignalDto, readIds: Set<string>): TradeNotification
price: dto.price, price: dto.price,
candleTime: dto.candleTime, candleTime: dto.candleTime,
strategyName: dto.strategyName, strategyName: dto.strategyName,
strategyId: dto.strategyId, strategyId: normalizeStrategyId(dto.strategyId) ?? undefined,
executionType: dto.executionType, executionType: dto.executionType,
candleType: dto.candleType, candleType: dto.candleType,
isRead: readIds.has(id), isRead: readIds.has(id),
@@ -2,14 +2,20 @@
* 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스) * 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스)
*/ */
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { isStrategyMissingOnServer, loadStrategy, type StrategyDto } from '../utils/backendApi'; import type { StrategyDto } from '../utils/backendApi';
import { extractVirtualConditions } from '../utils/virtualStrategyConditions'; import { extractVirtualConditions } from '../utils/virtualStrategyConditions';
import { normalizeConditionRow } from '../utils/virtualSignalMetrics'; import { normalizeConditionRow } from '../utils/virtualSignalMetrics';
import {
resolveNotificationStrategy,
normalizeStrategyId,
} from '../utils/resolveNotificationStrategy';
import { import {
fetchLiveSnapshot, fetchLiveSnapshot,
type VirtualIndicatorSnapshot, type VirtualIndicatorSnapshot,
} from './useVirtualIndicatorSnapshots'; } from './useVirtualIndicatorSnapshots';
const MAX_EMPTY_RETRIES = 10;
function staticFallback( function staticFallback(
market: string, market: string,
strategyId: number, strategyId: number,
@@ -31,7 +37,9 @@ export function useTradeNotificationSignalSnapshot(
market: string, market: string,
strategyId: number | null | undefined, strategyId: number | null | undefined,
strategy: StrategyDto | undefined, strategy: StrategyDto | undefined,
strategyName: string | null | undefined,
enabled: boolean, enabled: boolean,
prefetchedStrategies?: StrategyDto[],
pollMs = 3000, pollMs = 3000,
): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } { ): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } {
const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>(); const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>();
@@ -39,13 +47,15 @@ export function useTradeNotificationSignalSnapshot(
const emptyRetryRef = useRef(0); const emptyRetryRef = useRef(0);
const genRef = useRef(0); const genRef = useRef(0);
const refreshInFlightRef = useRef(false); const refreshInFlightRef = useRef(false);
/** strategy prop 이 undefined 일 때 직접 로드한 전략 캐시 */ const resolvedStrategyRef = useRef<StrategyDto | undefined>();
const fallbackStrategyRef = useRef<StrategyDto | null>(null); const effectiveStrategyIdRef = useRef<number | null>(null);
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
if (!enabled || strategyId == null) { const rawId = normalizeStrategyId(strategyId);
if (!enabled || rawId == null) {
setSnapshot(undefined); setSnapshot(undefined);
setLoading(false); setLoading(false);
emptyRetryRef.current = 0;
return; return;
} }
if (refreshInFlightRef.current) return; if (refreshInFlightRef.current) return;
@@ -55,28 +65,32 @@ export function useTradeNotificationSignalSnapshot(
setLoading(true); setLoading(true);
try { try {
let effectiveStrategy = strategy; let effectiveStrategy = strategy ?? resolvedStrategyRef.current;
if (!effectiveStrategy) { let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId;
if (fallbackStrategyRef.current) {
effectiveStrategy = fallbackStrategyRef.current; if (!effectiveStrategy || effectiveStrategy.id !== rawId) {
} else if (!isStrategyMissingOnServer(strategyId)) { const resolved = await resolveNotificationStrategy(
try { rawId,
const loaded = await loadStrategy(strategyId); strategyName,
if (loaded) { prefetchedStrategies,
fallbackStrategyRef.current = loaded; );
effectiveStrategy = loaded; if (gen !== genRef.current) return;
} if (resolved.strategy) {
} catch { /* ignore */ } effectiveStrategy = resolved.strategy;
if (gen !== genRef.current) return; resolvedStrategyRef.current = resolved.strategy;
}
if (resolved.strategyId != null) {
effectiveId = resolved.strategyId;
effectiveStrategyIdRef.current = resolved.strategyId;
} }
} }
const pinFirst = emptyRetryRef.current === 0; const pinFirst = emptyRetryRef.current === 0;
let snap = await fetchLiveSnapshot(market, strategyId, effectiveStrategy, pinFirst); let snap = await fetchLiveSnapshot(market, effectiveId, effectiveStrategy, pinFirst);
if (gen !== genRef.current) return; if (gen !== genRef.current) return;
if (!snap && effectiveStrategy) { if (!snap && effectiveStrategy) {
snap = staticFallback(market, strategyId, effectiveStrategy) ?? null; snap = staticFallback(market, effectiveId, effectiveStrategy) ?? null;
} }
if (snap && snap.rows.length > 0) { if (snap && snap.rows.length > 0) {
@@ -87,24 +101,28 @@ export function useTradeNotificationSignalSnapshot(
if (gen === genRef.current) { if (gen === genRef.current) {
setSnapshot(snap ?? undefined); setSnapshot(snap ?? undefined);
const stillCollecting = (!snap || snap.rows.length === 0)
&& emptyRetryRef.current < MAX_EMPTY_RETRIES;
setLoading(stillCollecting);
} }
} finally { } finally {
refreshInFlightRef.current = false; refreshInFlightRef.current = false;
if (gen === genRef.current) {
setLoading(false);
}
} }
}, [market, strategyId, strategy, enabled]); }, [market, strategyId, strategy, strategyName, enabled, prefetchedStrategies]);
useEffect(() => { useEffect(() => {
if (!enabled || strategyId == null) { const rawId = normalizeStrategyId(strategyId);
if (!enabled || rawId == null) {
setSnapshot(undefined); setSnapshot(undefined);
setLoading(false); setLoading(false);
emptyRetryRef.current = 0; emptyRetryRef.current = 0;
resolvedStrategyRef.current = undefined;
effectiveStrategyIdRef.current = null;
return; return;
} }
emptyRetryRef.current = 0; emptyRetryRef.current = 0;
fallbackStrategyRef.current = null; resolvedStrategyRef.current = strategy;
effectiveStrategyIdRef.current = strategy?.id ?? rawId;
setLoading(true); setLoading(true);
void refresh(); void refresh();
const id = window.setInterval(() => void refresh(), pollMs); const id = window.setInterval(() => void refresh(), pollMs);
@@ -113,7 +131,7 @@ export function useTradeNotificationSignalSnapshot(
genRef.current += 1; genRef.current += 1;
refreshInFlightRef.current = false; refreshInFlightRef.current = false;
}; };
}, [market, strategyId, enabled, pollMs, refresh]); }, [market, strategyId, strategyName, enabled, pollMs, refresh, strategy?.id]);
return { snapshot, loading }; return { snapshot, loading };
} }
@@ -17,6 +17,7 @@ import {
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin'; import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry'; import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
import { coerceFiniteNumber } from '../utils/safeFormat'; import { coerceFiniteNumber } from '../utils/safeFormat';
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
import { import {
extractVirtualConditions, extractVirtualConditions,
type VirtualConditionRow, type VirtualConditionRow,
@@ -111,10 +112,11 @@ function staticSnapshot(
}; };
} }
function statusMatches(status: { market?: string; strategyId?: number }, market: string, strategyId: number): boolean { function statusMatches(status: { market?: string; strategyId?: unknown }, market: string, strategyId: number): boolean {
if (!status?.market || status.market !== market) return false; if (!status?.market || status.market !== market) return false;
const sid = coerceFiniteNumber(status.strategyId); const sid = normalizeStrategyId(status.strategyId);
return sid === strategyId; const expected = normalizeStrategyId(strategyId);
return sid != null && expected != null && sid === expected;
} }
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */ /** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
@@ -0,0 +1,76 @@
/**
* 매매 시그널 알림 — strategyId·전략명으로 StrategyDto 해석
* (서버 DB와 알림 이력의 strategyId 불일치 시 전략명 fallback)
*/
import {
isStrategyMissingOnServer,
loadStrategies,
loadStrategy,
type StrategyDto,
} from './backendApi';
import { reconcileStrategyMissingWithValidIds } from './strategyMissingCache';
import { coerceFiniteNumber } from './safeFormat';
let strategiesCache: StrategyDto[] | null = null;
let strategiesCacheAt = 0;
const CACHE_MS = 30_000;
export function normalizeStrategyId(id: unknown): number | null {
const n = coerceFiniteNumber(id);
return n != null && n > 0 ? Math.trunc(n) : null;
}
async function getStrategiesList(force = false): Promise<StrategyDto[]> {
const now = Date.now();
if (!force && strategiesCache && now - strategiesCacheAt < CACHE_MS) {
return strategiesCache;
}
const list = await loadStrategies();
strategiesCache = list ?? [];
strategiesCacheAt = now;
reconcileStrategyMissingWithValidIds(
strategiesCache.map(s => s.id).filter((id): id is number => id != null && id > 0),
);
return strategiesCache;
}
export interface ResolvedNotificationStrategy {
strategy: StrategyDto | undefined;
/** live-conditions·차트에 사용할 유효 strategyId */
strategyId: number | null;
}
export async function resolveNotificationStrategy(
strategyId: unknown,
strategyName?: string | null,
prefetched?: StrategyDto[],
): Promise<ResolvedNotificationStrategy> {
const normalizedId = normalizeStrategyId(strategyId);
const list = prefetched ?? await getStrategiesList();
if (normalizedId != null && !isStrategyMissingOnServer(normalizedId)) {
const fromList = list.find(s => s.id === normalizedId);
if (fromList) {
return { strategy: fromList, strategyId: normalizedId };
}
const loaded = await loadStrategy(normalizedId);
if (loaded) {
return { strategy: loaded, strategyId: normalizedId };
}
}
const name = strategyName?.trim();
if (name) {
const byName = list.find(s => s.name?.trim() === name);
if (byName?.id != null && byName.id > 0) {
return { strategy: byName, strategyId: byName.id };
}
}
return { strategy: undefined, strategyId: normalizedId };
}
/** 알림 목록 진입 시 전략 목록 선로드 */
export async function prefetchNotificationStrategies(): Promise<StrategyDto[]> {
return getStrategiesList(true);
}