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

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
@@ -2,14 +2,20 @@
* 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스)
*/
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 { normalizeConditionRow } from '../utils/virtualSignalMetrics';
import {
resolveNotificationStrategy,
normalizeStrategyId,
} from '../utils/resolveNotificationStrategy';
import {
fetchLiveSnapshot,
type VirtualIndicatorSnapshot,
} from './useVirtualIndicatorSnapshots';
const MAX_EMPTY_RETRIES = 10;
function staticFallback(
market: string,
strategyId: number,
@@ -31,7 +37,9 @@ export function useTradeNotificationSignalSnapshot(
market: string,
strategyId: number | null | undefined,
strategy: StrategyDto | undefined,
strategyName: string | null | undefined,
enabled: boolean,
prefetchedStrategies?: StrategyDto[],
pollMs = 3000,
): { snapshot: VirtualIndicatorSnapshot | undefined; loading: boolean } {
const [snapshot, setSnapshot] = useState<VirtualIndicatorSnapshot | undefined>();
@@ -39,13 +47,15 @@ export function useTradeNotificationSignalSnapshot(
const emptyRetryRef = useRef(0);
const genRef = useRef(0);
const refreshInFlightRef = useRef(false);
/** strategy prop 이 undefined 일 때 직접 로드한 전략 캐시 */
const fallbackStrategyRef = useRef<StrategyDto | null>(null);
const resolvedStrategyRef = useRef<StrategyDto | undefined>();
const effectiveStrategyIdRef = useRef<number | null>(null);
const refresh = useCallback(async () => {
if (!enabled || strategyId == null) {
const rawId = normalizeStrategyId(strategyId);
if (!enabled || rawId == null) {
setSnapshot(undefined);
setLoading(false);
emptyRetryRef.current = 0;
return;
}
if (refreshInFlightRef.current) return;
@@ -55,28 +65,32 @@ export function useTradeNotificationSignalSnapshot(
setLoading(true);
try {
let effectiveStrategy = strategy;
if (!effectiveStrategy) {
if (fallbackStrategyRef.current) {
effectiveStrategy = fallbackStrategyRef.current;
} else if (!isStrategyMissingOnServer(strategyId)) {
try {
const loaded = await loadStrategy(strategyId);
if (loaded) {
fallbackStrategyRef.current = loaded;
effectiveStrategy = loaded;
}
} catch { /* ignore */ }
if (gen !== genRef.current) return;
let effectiveStrategy = strategy ?? resolvedStrategyRef.current;
let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId;
if (!effectiveStrategy || effectiveStrategy.id !== rawId) {
const resolved = await resolveNotificationStrategy(
rawId,
strategyName,
prefetchedStrategies,
);
if (gen !== genRef.current) return;
if (resolved.strategy) {
effectiveStrategy = resolved.strategy;
resolvedStrategyRef.current = resolved.strategy;
}
if (resolved.strategyId != null) {
effectiveId = resolved.strategyId;
effectiveStrategyIdRef.current = resolved.strategyId;
}
}
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 (!snap && effectiveStrategy) {
snap = staticFallback(market, strategyId, effectiveStrategy) ?? null;
snap = staticFallback(market, effectiveId, effectiveStrategy) ?? null;
}
if (snap && snap.rows.length > 0) {
@@ -87,24 +101,28 @@ export function useTradeNotificationSignalSnapshot(
if (gen === genRef.current) {
setSnapshot(snap ?? undefined);
const stillCollecting = (!snap || snap.rows.length === 0)
&& emptyRetryRef.current < MAX_EMPTY_RETRIES;
setLoading(stillCollecting);
}
} finally {
refreshInFlightRef.current = false;
if (gen === genRef.current) {
setLoading(false);
}
}
}, [market, strategyId, strategy, enabled]);
}, [market, strategyId, strategy, strategyName, enabled, prefetchedStrategies]);
useEffect(() => {
if (!enabled || strategyId == null) {
const rawId = normalizeStrategyId(strategyId);
if (!enabled || rawId == null) {
setSnapshot(undefined);
setLoading(false);
emptyRetryRef.current = 0;
resolvedStrategyRef.current = undefined;
effectiveStrategyIdRef.current = null;
return;
}
emptyRetryRef.current = 0;
fallbackStrategyRef.current = null;
resolvedStrategyRef.current = strategy;
effectiveStrategyIdRef.current = strategy?.id ?? rawId;
setLoading(true);
void refresh();
const id = window.setInterval(() => void refresh(), pollMs);
@@ -113,7 +131,7 @@ export function useTradeNotificationSignalSnapshot(
genRef.current += 1;
refreshInFlightRef.current = false;
};
}, [market, strategyId, enabled, pollMs, refresh]);
}, [market, strategyId, strategyName, enabled, pollMs, refresh, strategy?.id]);
return { snapshot, loading };
}
@@ -17,6 +17,7 @@ import {
import { pinStrategyEvaluationTimeframes } from '../utils/strategyTimeframePin';
import { formatIndicatorDisplayLabel } from '../utils/indicatorRegistry';
import { coerceFiniteNumber } from '../utils/safeFormat';
import { normalizeStrategyId } from '../utils/resolveNotificationStrategy';
import {
extractVirtualConditions,
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;
const sid = coerceFiniteNumber(status.strategyId);
return sid === strategyId;
const sid = normalizeStrategyId(status.strategyId);
const expected = normalizeStrategyId(strategyId);
return sid != null && expected != null && sid === expected;
}
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */