목록 로딩 로직 개선
This commit is contained in:
@@ -3,11 +3,30 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { isStrategyMissingOnServer, loadStrategy, type StrategyDto } from '../utils/backendApi';
|
||||
import { extractVirtualConditions } from '../utils/virtualStrategyConditions';
|
||||
import { normalizeConditionRow } from '../utils/virtualSignalMetrics';
|
||||
import {
|
||||
fetchLiveSnapshot,
|
||||
type VirtualIndicatorSnapshot,
|
||||
} from './useVirtualIndicatorSnapshots';
|
||||
|
||||
function staticFallback(
|
||||
market: string,
|
||||
strategyId: number,
|
||||
strategy: StrategyDto,
|
||||
): VirtualIndicatorSnapshot | undefined {
|
||||
const baseRows = extractVirtualConditions(strategy);
|
||||
if (baseRows.length === 0) return undefined;
|
||||
return {
|
||||
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,
|
||||
@@ -19,6 +38,7 @@ export function useTradeNotificationSignalSnapshot(
|
||||
const [loading, setLoading] = useState(false);
|
||||
const emptyRetryRef = useRef(0);
|
||||
const genRef = useRef(0);
|
||||
const refreshInFlightRef = useRef(false);
|
||||
/** strategy prop 이 undefined 일 때 직접 로드한 전략 캐시 */
|
||||
const fallbackStrategyRef = useRef<StrategyDto | null>(null);
|
||||
|
||||
@@ -28,40 +48,52 @@ export function useTradeNotificationSignalSnapshot(
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (refreshInFlightRef.current) return;
|
||||
|
||||
const gen = ++genRef.current;
|
||||
refreshInFlightRef.current = true;
|
||||
setLoading(true);
|
||||
|
||||
// strategy prop 이 undefined 면 내부에서 직접 로드해 baseRows 를 확보한다.
|
||||
// 이렇게 하면 strategy 가 늦게 주입되어도 조건 목록이 즉시 표시된다.
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const pinFirst = emptyRetryRef.current === 0;
|
||||
let snap = await fetchLiveSnapshot(market, strategyId, effectiveStrategy, pinFirst);
|
||||
if (gen !== genRef.current) return;
|
||||
|
||||
if (!snap && effectiveStrategy) {
|
||||
snap = staticFallback(market, strategyId, effectiveStrategy) ?? null;
|
||||
}
|
||||
|
||||
if (snap && snap.rows.length > 0) {
|
||||
emptyRetryRef.current = 0;
|
||||
} else {
|
||||
emptyRetryRef.current += 1;
|
||||
}
|
||||
|
||||
if (gen === genRef.current) {
|
||||
setSnapshot(snap ?? undefined);
|
||||
}
|
||||
} finally {
|
||||
refreshInFlightRef.current = false;
|
||||
if (gen === genRef.current) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const pinFirst = emptyRetryRef.current === 0;
|
||||
const snap = await fetchLiveSnapshot(market, strategyId, effectiveStrategy, pinFirst);
|
||||
if (gen !== genRef.current) return;
|
||||
|
||||
if (snap && snap.rows.length > 0) {
|
||||
emptyRetryRef.current = 0;
|
||||
} else {
|
||||
emptyRetryRef.current += 1;
|
||||
}
|
||||
|
||||
setSnapshot(snap ?? undefined);
|
||||
setLoading(false);
|
||||
}, [market, strategyId, strategy, enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -72,13 +104,14 @@ export function useTradeNotificationSignalSnapshot(
|
||||
return;
|
||||
}
|
||||
emptyRetryRef.current = 0;
|
||||
fallbackStrategyRef.current = null; // strategy prop 변경 시 캐시 초기화
|
||||
setLoading(true); // 활성화 즉시 로딩 표시
|
||||
fallbackStrategyRef.current = null;
|
||||
setLoading(true);
|
||||
void refresh();
|
||||
const id = window.setInterval(() => void refresh(), pollMs);
|
||||
return () => {
|
||||
clearInterval(id);
|
||||
genRef.current += 1;
|
||||
refreshInFlightRef.current = false;
|
||||
};
|
||||
}, [market, strategyId, enabled, pollMs, refresh]);
|
||||
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
* running 시 3초 폴링, 미수집 시 재시도
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { StrategyDto } from '../utils/backendApi';
|
||||
import {
|
||||
isStrategyMissingOnServer,
|
||||
loadStrategy,
|
||||
type StrategyDto,
|
||||
} from '../utils/backendApi';
|
||||
import {
|
||||
fetchLiveConditionStatus,
|
||||
pinCandleWatch,
|
||||
@@ -107,6 +111,12 @@ function staticSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
function statusMatches(status: { market?: string; strategyId?: number }, market: string, strategyId: number): boolean {
|
||||
if (!status?.market || status.market !== market) return false;
|
||||
const sid = coerceFiniteNumber(status.strategyId);
|
||||
return sid === strategyId;
|
||||
}
|
||||
|
||||
/** 백엔드가 업비트 REST·WS로 캔들 warm-up 후 Ta4j 평가 */
|
||||
export async function fetchLiveSnapshot(
|
||||
market: string,
|
||||
@@ -133,13 +143,16 @@ export async function fetchLiveSnapshot(
|
||||
status = null;
|
||||
}
|
||||
|
||||
if (!status || status.market !== market || status.strategyId !== strategyId) {
|
||||
if (!status || !statusMatches(status, market, strategyId)) {
|
||||
if (baseRows.length === 0) return null;
|
||||
return staticSnapshot(market, strategyId, baseRows);
|
||||
}
|
||||
|
||||
const rows = mergeRows(baseRows, status.rows);
|
||||
if (rows.length === 0) return null;
|
||||
const rows = mergeRows(baseRows, status.rows ?? []);
|
||||
if (rows.length === 0) {
|
||||
if (baseRows.length === 0) return null;
|
||||
return staticSnapshot(market, strategyId, baseRows);
|
||||
}
|
||||
|
||||
return {
|
||||
market,
|
||||
@@ -151,6 +164,34 @@ export async function fetchLiveSnapshot(
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveStrategy(
|
||||
strategyId: number,
|
||||
strategies: StrategyDto[],
|
||||
cache: Map<number, StrategyDto | null>,
|
||||
): Promise<StrategyDto | undefined> {
|
||||
const cached = strategies.find(s => s.id === strategyId);
|
||||
if (cached) return cached;
|
||||
|
||||
if (cache.has(strategyId)) {
|
||||
const hit = cache.get(strategyId);
|
||||
return hit ?? undefined;
|
||||
}
|
||||
|
||||
if (isStrategyMissingOnServer(strategyId)) {
|
||||
cache.set(strategyId, null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const loaded = await loadStrategy(strategyId);
|
||||
cache.set(strategyId, loaded);
|
||||
return loaded ?? undefined;
|
||||
} catch {
|
||||
cache.set(strategyId, null);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface TargetRef {
|
||||
market: string;
|
||||
strategyId: number | null;
|
||||
@@ -166,47 +207,16 @@ export function useVirtualIndicatorSnapshots(
|
||||
const [loadingByMarket, setLoadingByMarket] = useState<Record<string, boolean>>({});
|
||||
const strategiesRef = useRef(strategies);
|
||||
strategiesRef.current = strategies;
|
||||
const pinnedRef = useRef<Set<string>>(new Set());
|
||||
const strategyLoadCacheRef = useRef<Map<number, StrategyDto | null>>(new Map());
|
||||
const refreshGenRef = useRef<Record<string, number>>({});
|
||||
const emptyRetryRef = useRef<Record<string, number>>({});
|
||||
const refreshInFlightRef = useRef(false);
|
||||
const targetsKey = targets.map(t => `${t.market}:${t.strategyId ?? ''}`).join('|');
|
||||
|
||||
const pinTargets = useCallback(async () => {
|
||||
const strats = strategiesRef.current;
|
||||
const pins = new Set<string>();
|
||||
for (const t of targets) {
|
||||
if (t.strategyId == null) continue;
|
||||
try {
|
||||
const tfs = await pinStrategyEvaluationTimeframes(t.market, t.strategyId);
|
||||
for (const tf of tfs) pins.add(`${t.market}:${tf}`);
|
||||
} catch {
|
||||
const strat = strats.find(s => s.id === t.strategyId);
|
||||
const conditions = strat ? extractVirtualConditions(strat) : [];
|
||||
const tfs = conditions.length > 0
|
||||
? [...new Set(conditions.map(c => c.timeframe))]
|
||||
: ['1m'];
|
||||
for (const tf of tfs) {
|
||||
const key = `${t.market}:${tf}`;
|
||||
pins.add(key);
|
||||
if (!pinnedRef.current.has(key)) {
|
||||
await pinCandleWatch(t.market, tf);
|
||||
pinnedRef.current.add(key);
|
||||
}
|
||||
}
|
||||
const k1 = `${t.market}:1m`;
|
||||
pins.add(k1);
|
||||
if (!pinnedRef.current.has(k1)) {
|
||||
await pinCandleWatch(t.market, '1m');
|
||||
pinnedRef.current.add(k1);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const k of pinnedRef.current) {
|
||||
if (!pins.has(k)) pinnedRef.current.delete(k);
|
||||
}
|
||||
}, [targets]);
|
||||
const strategiesKey = strategies.map(s => s.id).join('|');
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (refreshInFlightRef.current) return;
|
||||
|
||||
const strats = strategiesRef.current;
|
||||
const activeTargets = targets.filter(t => t.strategyId != null);
|
||||
if (activeTargets.length === 0) {
|
||||
@@ -215,69 +225,78 @@ export function useVirtualIndicatorSnapshots(
|
||||
return;
|
||||
}
|
||||
|
||||
refreshInFlightRef.current = true;
|
||||
setLoadingByMarket(prev => {
|
||||
const next = { ...prev };
|
||||
for (const t of activeTargets) next[t.market] = true;
|
||||
return next;
|
||||
});
|
||||
|
||||
await pinTargets();
|
||||
try {
|
||||
const jobs = activeTargets.map(async t => {
|
||||
const market = t.market;
|
||||
const strategyId = t.strategyId!;
|
||||
const gen = (refreshGenRef.current[market] ?? 0) + 1;
|
||||
refreshGenRef.current[market] = gen;
|
||||
|
||||
const jobs = activeTargets.map(async t => {
|
||||
const gen = (refreshGenRef.current[t.market] ?? 0) + 1;
|
||||
refreshGenRef.current[t.market] = gen;
|
||||
const strat = strats.find(s => s.id === t.strategyId);
|
||||
const baseRows = strat ? extractVirtualConditions(strat) : [];
|
||||
const needsLiveApi = running || baseRows.length === 0;
|
||||
try {
|
||||
const strat = await resolveStrategy(strategyId, strats, strategyLoadCacheRef.current);
|
||||
if (refreshGenRef.current[market] !== gen) return;
|
||||
|
||||
let snap: VirtualIndicatorSnapshot | null = null;
|
||||
if (needsLiveApi) {
|
||||
const retry = emptyRetryRef.current[t.market] ?? 0;
|
||||
snap = await fetchLiveSnapshot(t.market, t.strategyId!, strat, retry === 0);
|
||||
} else {
|
||||
snap = staticSnapshot(t.market, t.strategyId!, baseRows);
|
||||
}
|
||||
const baseRows = strat ? extractVirtualConditions(strat) : [];
|
||||
const retry = emptyRetryRef.current[market] ?? 0;
|
||||
let snap = await fetchLiveSnapshot(market, strategyId, strat, retry === 0);
|
||||
|
||||
if (!snap || refreshGenRef.current[t.market] !== gen) return snap;
|
||||
if (snap.strategyId !== t.strategyId) return snap;
|
||||
if (!snap && baseRows.length > 0) {
|
||||
snap = staticSnapshot(market, strategyId, baseRows);
|
||||
}
|
||||
|
||||
if (snap.rows.length === 0) {
|
||||
const n = (emptyRetryRef.current[t.market] ?? 0) + 1;
|
||||
emptyRetryRef.current[t.market] = n;
|
||||
} else {
|
||||
emptyRetryRef.current[t.market] = 0;
|
||||
}
|
||||
if (!snap || refreshGenRef.current[market] !== gen) return;
|
||||
if (snap.strategyId !== strategyId) return;
|
||||
|
||||
setSnapshots(prev => ({ ...prev, [snap.market]: snap }));
|
||||
setLoadingByMarket(prev => ({ ...prev, [t.market]: false }));
|
||||
return snap;
|
||||
});
|
||||
await Promise.all(jobs);
|
||||
if (snap.rows.length === 0) {
|
||||
emptyRetryRef.current[market] = (emptyRetryRef.current[market] ?? 0) + 1;
|
||||
} else {
|
||||
emptyRetryRef.current[market] = 0;
|
||||
}
|
||||
|
||||
const activeMarkets = new Set(targets.map(t => t.market));
|
||||
setSnapshots(prev => {
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
for (const key of Object.keys(next)) {
|
||||
if (!activeMarkets.has(key)) {
|
||||
delete next[key];
|
||||
changed = true;
|
||||
setSnapshots(prev => ({ ...prev, [snap!.market]: snap! }));
|
||||
} finally {
|
||||
if (refreshGenRef.current[market] === gen) {
|
||||
setLoadingByMarket(prev => ({ ...prev, [market]: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
setLoadingByMarket(prev => {
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
for (const key of Object.keys(next)) {
|
||||
if (!activeMarkets.has(key)) {
|
||||
delete next[key];
|
||||
changed = true;
|
||||
});
|
||||
|
||||
await Promise.all(jobs);
|
||||
|
||||
const activeMarkets = new Set(targets.map(t => t.market));
|
||||
setSnapshots(prev => {
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
for (const key of Object.keys(next)) {
|
||||
if (!activeMarkets.has(key)) {
|
||||
delete next[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [targets, running, pinTargets]);
|
||||
return changed ? next : prev;
|
||||
});
|
||||
setLoadingByMarket(prev => {
|
||||
const next = { ...prev };
|
||||
let changed = false;
|
||||
for (const key of Object.keys(next)) {
|
||||
if (!activeMarkets.has(key)) {
|
||||
delete next[key];
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
} finally {
|
||||
refreshInFlightRef.current = false;
|
||||
}
|
||||
}, [targets, running]);
|
||||
|
||||
const hasStrategyTargets = targets.some(t => t.strategyId != null);
|
||||
|
||||
@@ -285,17 +304,17 @@ export function useVirtualIndicatorSnapshots(
|
||||
if (targets.length === 0) {
|
||||
setSnapshots({});
|
||||
setLoadingByMarket({});
|
||||
pinnedRef.current.clear();
|
||||
refreshGenRef.current = {};
|
||||
emptyRetryRef.current = {};
|
||||
strategyLoadCacheRef.current.clear();
|
||||
return;
|
||||
}
|
||||
void refresh();
|
||||
if (!running && !hasStrategyTargets) return;
|
||||
if (!hasStrategyTargets) return;
|
||||
const intervalMs = running ? pollMs : 5000;
|
||||
const id = window.setInterval(() => void refresh(), intervalMs);
|
||||
return () => clearInterval(id);
|
||||
}, [targetsKey, running, hasStrategyTargets, pollMs, refresh]);
|
||||
}, [targetsKey, strategiesKey, running, hasStrategyTargets, pollMs, refresh]);
|
||||
|
||||
return { snapshots, loadingByMarket };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user