160 lines
5.1 KiB
TypeScript
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 };
|
|
}
|