diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1d04d78..e9ef29c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@stomp/stompjs": "^7.3.0", + "@tanstack/react-query": "^5.101.0", "@tanstack/react-virtual": "^3.14.2", "@xyflow/react": "^12.10.2", "lightweight-charts": "^5.2.0", @@ -1155,6 +1156,32 @@ "integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==", "license": "Apache-2.0" }, + "node_modules/@tanstack/query-core": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.0.tgz", + "integrity": "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.0.tgz", + "integrity": "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@tanstack/react-virtual": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 102b2e8..57b1930 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@stomp/stompjs": "^7.3.0", + "@tanstack/react-query": "^5.101.0", "@tanstack/react-virtual": "^3.14.2", "@xyflow/react": "^12.10.2", "lightweight-charts": "^5.2.0", diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index 3d541f9..e0ece66 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -7,6 +7,7 @@ import '../styles/strategyEditorTheme.css'; import '../styles/strategyEditor.css'; import '../styles/strategyEvaluation.css'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { Theme, Timeframe } from '../types'; import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnapshots'; import { @@ -72,6 +73,8 @@ import { buildAiPanelSelectionSummary, } from '../utils/strategyEvaluationAiVerify'; import type { StrategyEvaluationAiVerifyResponse } from '../utils/backendApi'; +import { useStrategyBulkEvaluation } from '../hooks/useStrategyBulkEvaluation'; +import StrategyBulkEvalBadge, { StrategyBulkEvalToolbar } from './strategyEvaluation/StrategyBulkEvalControls'; const LEFT_KEY = 'seval-left-width'; const LEFT_OPEN_KEY = 'seval-left-open'; @@ -91,7 +94,26 @@ interface Props { theme?: Theme; } -export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { +const strategyEvaluationQueryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60_000, + gcTime: 10 * 60_000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); + +export default function StrategyEvaluationPage(props: Props) { + return ( + + + + ); +} + +function StrategyEvaluationPageInner({ theme = 'dark' }: Props) { const { getParams: baseGetParams, getVisualConfig, @@ -413,6 +435,72 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { }); }, [selectedStrategy, backtestSignals, initialCapital, market]); + const bulkEval = useStrategyBulkEvaluation({ + strategies, + market, + timeframe: chartTimeframe, + bars, + evaluationBarCount: evalWindowMeta.evaluationBarCount, + windowEndTimeSec: evalWindowMeta.windowEndTimeSec, + initialCapital, + selectedStrategyId, + getParams: getEvalParams, + appliedIndicatorParams, + }); + + const autoBulkEvalTriggerKey = useMemo(() => { + const lastBarTime = bars[bars.length - 1]?.time ?? 0; + const strategyIds = strategies.map(s => s.id).filter(Boolean).join(','); + return [ + market, + chartTimeframe, + selectedStrategyId ?? 0, + evalWindowMeta.windowEndTimeSec ?? 'latest', + evalWindowMeta.evaluationBarCount, + bars.length, + lastBarTime, + paramsRevision, + strategyIds, + ].join('|'); + }, [ + market, + chartTimeframe, + selectedStrategyId, + evalWindowMeta.windowEndTimeSec, + evalWindowMeta.evaluationBarCount, + bars, + paramsRevision, + strategies, + ]); + + const lastAutoBulkEvalKeyRef = useRef(null); + + useEffect(() => { + if (chartLoading) { + bulkEval.cancelBulkEvaluation(); + } + }, [chartLoading, bulkEval.cancelBulkEvaluation]); + + useEffect(() => { + const waitingForSelectedSignals = selectedStrategyId != null && signalScanRunning; + if (chartLoading || waitingForSelectedSignals) return; + if (bars.length < 10 || !strategies.some(s => s.id != null)) return; + + const key = autoBulkEvalTriggerKey; + if (lastAutoBulkEvalKeyRef.current === key) return; + lastAutoBulkEvalKeyRef.current = key; + + void bulkEval.startBulkEvaluation({ restart: true }); + }, [ + autoBulkEvalTriggerKey, + chartLoading, + signalScanRunning, + selectedStrategyId, + bars.length, + strategies, + bulkEval.startBulkEvaluation, + ]); + const refreshEvaluation = useCallback(async () => { if (!selectedStrategyId || !selectedStrategy) { setSnapshot(undefined); @@ -687,11 +775,8 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { return (
-
-
-

전략 평가

- Strategy Match Analysis -
+ {selectedStrategy && (signalScanRunning || analysisSummary) && ( +
{selectedStrategy && signalScanRunning && ( 시그널 계산 중… @@ -732,6 +817,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { )}
+ )}