Files
goldenChart/frontend/src/hooks/useTradeNotificationSignalSnapshot.ts
T
2026-06-08 01:31:02 +09:00

160 lines
5.1 KiB
TypeScript

/**
* 매매 시그널 알림 목록 행 — 종목×전략 실시간 신호 스냅샷 (가상매매 카드 요약형과 동일 소스)
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import type { StrategyDto } from '../utils/backendApi';
import { extractVirtualConditions } from '../utils/virtualStrategyConditions';
import { normalizeConditionRow } from '../utils/virtualSignalMetrics';
import {
resolveNotificationStrategy,
normalizeStrategyId,
} from '../utils/resolveNotificationStrategy';
import { hydrateStrategyDto, normalizeMarketCode } from '../utils/strategyHydrate';
import {
fetchLiveSnapshot,
type VirtualIndicatorSnapshot,
} from './useVirtualIndicatorSnapshots';
const MAX_EMPTY_RETRIES = 15;
function staticFallback(
market: string,
strategyId: number,
strategy: StrategyDto,
): VirtualIndicatorSnapshot | undefined {
const hydrated = hydrateStrategyDto(strategy);
const baseRows = extractVirtualConditions(hydrated);
if (baseRows.length === 0) return undefined;
return {
market: normalizeMarketCode(market),
strategyId,
timeframe: [...new Set(baseRows.map(r => r.timeframe))].join(', '),
rows: baseRows.map(r => normalizeConditionRow({ ...r, currentValue: null, satisfied: null })),
updatedAt: Date.now(),
matchRate: 0,
};
}
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>();
const [loading, setLoading] = useState(false);
const emptyRetryRef = useRef(0);
const genRef = useRef(0);
const refreshInFlightRef = useRef(false);
const resolvedStrategyRef = useRef<StrategyDto | undefined>();
const effectiveStrategyIdRef = useRef<number | null>(null);
const normalizedMarket = normalizeMarketCode(market);
const refresh = useCallback(async () => {
const rawId = normalizeStrategyId(strategyId);
if (!enabled || rawId == null) {
setSnapshot(undefined);
setLoading(false);
emptyRetryRef.current = 0;
return;
}
if (refreshInFlightRef.current) return;
const gen = ++genRef.current;
refreshInFlightRef.current = true;
setLoading(true);
try {
let effectiveStrategy = strategy ? hydrateStrategyDto(strategy) : resolvedStrategyRef.current;
let effectiveId = effectiveStrategy?.id ?? effectiveStrategyIdRef.current ?? rawId;
const needsResolve = !effectiveStrategy
|| extractVirtualConditions(effectiveStrategy).length === 0
|| effectiveStrategy.id !== rawId;
if (needsResolve) {
const resolved = await resolveNotificationStrategy(
rawId,
strategyName,
prefetchedStrategies,
normalizedMarket,
);
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(
normalizedMarket,
effectiveId,
effectiveStrategy,
pinFirst,
{ readOnlyPin: true },
);
if (gen !== genRef.current) return;
if ((!snap || snap.rows.length === 0) && effectiveStrategy) {
snap = staticFallback(normalizedMarket, effectiveId, effectiveStrategy) ?? snap;
}
if (snap && snap.rows.length > 0) {
emptyRetryRef.current = 0;
} else {
emptyRetryRef.current += 1;
}
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;
}
}, [
market,
normalizedMarket,
strategyId,
strategy,
strategyName,
enabled,
prefetchedStrategies,
]);
useEffect(() => {
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;
resolvedStrategyRef.current = strategy ? hydrateStrategyDto(strategy) : undefined;
effectiveStrategyIdRef.current = strategy?.id ?? rawId;
setLoading(true);
void refresh();
const id = window.setInterval(() => void refresh(), pollMs);
return () => {
clearInterval(id);
genRef.current += 1;
refreshInFlightRef.current = false;
};
}, [market, strategyId, strategyName, enabled, pollMs, refresh, strategy?.id]);
return { snapshot, loading };
}