From 5acaaabc76fa1ac7d7b3153912f2eecfba67106c Mon Sep 17 00:00:00 2001 From: Macbook Date: Tue, 16 Jun 2026 23:28:23 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A0=84=EB=9E=B5=ED=8F=89=EA=B0=80=20?= =?UTF-8?q?=EB=B6=84=EC=84=9D=EB=A0=88=ED=8F=AC=ED=8A=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/StrategyEvaluationPage.tsx | 109 +++++++++++++++++- .../StrategyEvaluationChart.tsx | 28 +++++ frontend/src/styles/strategyEvaluation.css | 16 +++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/StrategyEvaluationPage.tsx b/frontend/src/components/StrategyEvaluationPage.tsx index 7623404..f67a9b9 100644 --- a/frontend/src/components/StrategyEvaluationPage.tsx +++ b/frontend/src/components/StrategyEvaluationPage.tsx @@ -12,6 +12,8 @@ import type { VirtualIndicatorSnapshot } from '../hooks/useVirtualIndicatorSnaps import { loadStrategies, loadStrategy, + loadBacktestSettings, + runBacktest, type BacktestSignal, type StrategyDto, } from '../utils/backendApi'; @@ -41,6 +43,10 @@ import { } from '../utils/backtestRunTimeframe'; import { resolveBarSignalHighlight } from '../utils/strategyEvaluationBarSignals'; import type { OHLCVBar } from '../types'; +import BacktestAnalysisReportModal from './backtest/BacktestAnalysisReportModal'; +import type { BacktestAnalysisReportModel } from './backtest/BacktestAnalysisReportModal'; +import { buildChartBacktestReportModel } from '../utils/backtestReportModel'; +import { BACKTEST_DISPLAY_BAR_COUNT } from '../utils/backtestWarmup'; const LEFT_KEY = 'seval-left-width'; const LEFT_OPEN_KEY = 'seval-left-open'; @@ -80,6 +86,11 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { const [chartLoading, setChartLoading] = useState(false); const [backtestSignals, setBacktestSignals] = useState([]); const [editorModalOpen, setEditorModalOpen] = useState(false); + const [reportOpen, setReportOpen] = useState(false); + const [reportModel, setReportModel] = useState(null); + const [reportLoading, setReportLoading] = useState(false); + const reportCacheKeyRef = useRef(null); + const reportGenRef = useRef(0); const evalSessionRef = useRef(0); const paramsRevisionRef = useRef(paramsRevision); @@ -162,7 +173,7 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { [strategyPrimaryTimeframe], ); - /** 종목·시간봉·전략 변경 — 차트·평가 컨텍스트 초기화 */ + /** 종목·시간봉·전략 변경 — 차트·평가·레포트 컨텍스트 초기화 */ useEffect(() => { evalSessionRef.current += 1; setSnapshot(undefined); @@ -170,6 +181,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { setEvalLoading(true); setBars([]); setSelectedBarIndex(0); + reportCacheKeyRef.current = null; + setReportModel(null); + setReportOpen(false); }, [market, chartTimeframe, selectedStrategyId]); /** 전략설정 파라미터 저장·초기화 */ @@ -181,6 +195,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { setSnapshot(undefined); setEvalError(null); setEvalLoading(true); + reportCacheKeyRef.current = null; + setReportModel(null); + setReportOpen(false); }, [paramsRevision]); useEffect(() => { @@ -331,6 +348,87 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { ? repairUtf8Mojibake(selectedStrategy.name) : '전략 미선택'; + const reportCacheKey = useMemo(() => { + if (!selectedStrategyId || bars.length < 10) return null; + const lastBarTime = bars[bars.length - 1]?.time ?? 0; + return `${selectedStrategyId}|${market}|${chartTimeframe}|${paramsRevision}|${bars.length}|${lastBarTime}`; + }, [selectedStrategyId, market, chartTimeframe, paramsRevision, bars]); + + const reportDisabled = !selectedStrategyId || !selectedStrategy || bars.length < 10 || chartLoading; + + const handleOpenReport = useCallback(async () => { + if (reportDisabled || !selectedStrategyId || !selectedStrategy) return; + + if (reportCacheKey && reportCacheKeyRef.current === reportCacheKey && reportModel) { + setReportOpen(true); + return; + } + + const gen = ++reportGenRef.current; + setReportLoading(true); + try { + const [btSettings] = await Promise.all([loadBacktestSettings()]); + const indicatorParams = buildEvalParamsFromStrategy(selectedStrategy, getEvalParams); + const evaluationBarCount = Math.min(BACKTEST_DISPLAY_BAR_COUNT, bars.length); + const res = await runBacktest({ + strategyId: selectedStrategyId, + bars: bars.map(b => ({ + time: b.time, + open: b.open, + high: b.high, + low: b.low, + close: b.close, + volume: b.volume, + })), + timeframe: chartTimeframe, + symbol: market, + strategyName: selectedStrategy.name, + settings: btSettings, + indicatorParams, + evaluationBarCount, + }); + if (gen !== reportGenRef.current) return; + if (!res) { + window.alert('분석 레포트 생성에 실패했습니다.'); + return; + } + + const model = buildChartBacktestReportModel({ + analysis: res.analysis ?? null, + stats: res.stats ?? null, + signals: res.signals, + strategyName: selectedStrategy.name ?? strategyLabel, + symbol: market.replace(/^KRW-/, ''), + timeframe: chartTimeframe, + barCount: bars.length, + }); + if (!model) { + window.alert('분석 데이터를 생성할 수 없습니다.'); + return; + } + + reportCacheKeyRef.current = reportCacheKey; + setReportModel(model); + setReportOpen(true); + } catch { + if (gen !== reportGenRef.current) return; + window.alert('상세분석 레포트 생성 중 오류가 발생했습니다.'); + } finally { + if (gen === reportGenRef.current) setReportLoading(false); + } + }, [ + reportDisabled, + selectedStrategyId, + selectedStrategy, + reportCacheKey, + reportModel, + getEvalParams, + bars, + chartTimeframe, + market, + strategyLabel, + ]); + return (
@@ -485,6 +583,9 @@ export default function StrategyEvaluationPage({ theme = 'dark' }: Props) { getVisualConfigOverride={getVisualConfig} evalLoading={evalLoading} onSignalsChange={handleSignalsChange} + onOpenReport={() => { void handleOpenReport(); }} + reportDisabled={reportDisabled} + reportLoading={reportLoading} /> { void refreshStrategies(); }} /> )} + + setReportOpen(false)} + model={reportModel} + />
); } diff --git a/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx b/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx index 9724add..e630b69 100644 --- a/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx +++ b/frontend/src/components/strategyEvaluation/StrategyEvaluationChart.tsx @@ -69,6 +69,10 @@ interface Props { evalLoading?: boolean; /** 백테스트 시그널 목록 변경 시 (우측 패널 하이라이트 등) */ onSignalsChange?: (signals: BacktestSignal[]) => void; + /** 투자분석 상세 레포트 (인쇄·PDF) */ + onOpenReport?: () => void; + reportDisabled?: boolean; + reportLoading?: boolean; } const isOverlayType = (type: string) => getIndicatorDef(type)?.overlay === true; @@ -102,6 +106,9 @@ const StrategyEvaluationChart: React.FC = ({ getVisualConfigOverride, evalLoading = false, onSignalsChange, + onOpenReport, + reportDisabled = false, + reportLoading = false, }) => { const { getParams: baseGetParams, getVisualConfig: baseGetVisual, settingsRevision } = useIndicatorSettings(); const getParams = getParamsOverride ?? baseGetParams; @@ -462,6 +469,27 @@ const StrategyEvaluationChart: React.FC = ({ )}
+ {onOpenReport && ( + + )}