전략평가 화면 전략 일괄평가 기능

This commit is contained in:
Macbook
2026-06-19 20:26:59 +09:00
parent 994a2024c7
commit cc30d317ec
8 changed files with 683 additions and 21 deletions
@@ -0,0 +1,145 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { OHLCVBar } from '../types';
import type { StrategyDto } from '../utils/backendApi';
import {
buildBarsFingerprint,
buildStrategyBulkEvalScanKey,
evaluateStrategyBulkReturns,
strategyBulkEvalKeys,
type StrategyBulkEvalResult,
} from '../utils/strategyBulkEvaluation';
import { mergeEvalGetParams } from '../utils/strategyEvaluationParams';
interface UseStrategyBulkEvaluationOpts {
strategies: StrategyDto[];
market: string;
timeframe: string;
bars: OHLCVBar[];
evaluationBarCount: number;
windowEndTimeSec: number | null;
initialCapital: number;
selectedStrategyId: number | null;
getParams: (type: string) => Record<string, number | string | boolean>;
appliedIndicatorParams: Record<string, Record<string, number | string | boolean>> | null;
}
export function useStrategyBulkEvaluation({
strategies,
market,
timeframe,
bars,
evaluationBarCount,
windowEndTimeSec,
initialCapital,
selectedStrategyId,
getParams,
appliedIndicatorParams,
}: UseStrategyBulkEvaluationOpts) {
const queryClient = useQueryClient();
const [isRunning, setIsRunning] = useState(false);
const [currentStrategyId, setCurrentStrategyId] = useState<number | null>(null);
const runGenRef = useRef(0);
const scanKey = useMemo(
() => buildStrategyBulkEvalScanKey({
market,
timeframe,
windowEndTimeSec,
evaluationBarCount,
barsFingerprint: buildBarsFingerprint(bars),
}),
[market, timeframe, windowEndTimeSec, evaluationBarCount, bars],
);
const getParamsForStrategy = useCallback((strategy: StrategyDto) => {
if (strategy.id === selectedStrategyId && appliedIndicatorParams) {
return mergeEvalGetParams(getParams, appliedIndicatorParams);
}
return getParams;
}, [selectedStrategyId, appliedIndicatorParams, getParams]);
const startBulkEvaluation = useCallback(async (options?: { restart?: boolean }) => {
if (!options?.restart && isRunning) return;
if (bars.length < 10) return;
const list = strategies.filter((s): s is StrategyDto & { id: number } => s.id != null);
if (list.length === 0) return;
const gen = ++runGenRef.current;
setIsRunning(true);
setCurrentStrategyId(null);
for (const strategy of list) {
if (gen !== runGenRef.current) break;
setCurrentStrategyId(strategy.id);
const queryKey = strategyBulkEvalKeys.returns(scanKey, strategy.id);
await queryClient.fetchQuery({
queryKey,
queryFn: () => evaluateStrategyBulkReturns({
strategy,
market,
timeframe,
bars,
evaluationBarCount,
initialCapital,
getParams: getParamsForStrategy(strategy),
}),
staleTime: 0,
gcTime: 10 * 60_000,
}).catch(() => {
queryClient.setQueryData(queryKey, null);
});
await new Promise<void>(resolve => { setTimeout(resolve, 0); });
}
if (gen === runGenRef.current) {
setCurrentStrategyId(null);
setIsRunning(false);
}
}, [
isRunning,
bars,
strategies,
queryClient,
scanKey,
market,
timeframe,
evaluationBarCount,
initialCapital,
getParamsForStrategy,
]);
const cancelBulkEvaluation = useCallback(() => {
runGenRef.current += 1;
setCurrentStrategyId(null);
setIsRunning(false);
}, []);
return {
isRunning,
currentStrategyId,
scanKey,
startBulkEvaluation,
cancelBulkEvaluation,
canStart: bars.length >= 10 && strategies.some(s => s.id != null),
};
}
/** 목록 아이템 — React Query 캐시 구독 */
export function useStrategyBulkEvalReturns(
strategyId: number | null | undefined,
scanKey: string,
) {
return useQuery<StrategyBulkEvalResult | null>({
queryKey: strategyId != null
? strategyBulkEvalKeys.returns(scanKey, strategyId)
: ['strategyBulkEval', 'idle'],
queryFn: async () => null,
enabled: false,
staleTime: 60_000,
gcTime: 10 * 60_000,
placeholderData: (prev) => prev,
});
}