From ba592b0789241aebf5b17d1ce767f4ead7a2facc Mon Sep 17 00:00:00 2001 From: Macbook Date: Wed, 3 Jun 2026 12:19:03 +0900 Subject: [PATCH] =?UTF-8?q?=EC=8B=A4=EC=8B=9C=EA=B0=84=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=B1=ED=85=8C=EC=8A=A4=ED=8C=85=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=9D=B8=EC=87=84=20=EA=B8=B0=EB=8A=A5=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 53 ++++++++++++++++------ frontend/src/utils/backtestReportModel.ts | 55 +++++++++++++++++++++++ 2 files changed, 94 insertions(+), 14 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 89ef9a0..b9ca182 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -111,7 +111,9 @@ import { import { NotifyTopMenuBar, ChartToolbarNotify } from './components/NotifyUiBindings'; import { AppNotificationLayer } from './components/AppNotificationLayer'; import BacktestSettingsModal from './components/BacktestSettingsModal'; -import { BacktestResultModal } from './components/BacktestResultModal'; +import BacktestAnalysisReportModal from './components/backtest/BacktestAnalysisReportModal'; +import { buildChartBacktestReportModel } from './utils/backtestReportModel'; +import { fmtListTimestamp } from './utils/backtestUiUtils'; const DashboardPage = lazy(() => import('./components/DashboardPage')); const StrategyPage = lazy(() => import('./components/StrategyPage')); @@ -760,6 +762,7 @@ function App() { const [showBtSettings, setShowBtSettings] = useState(false); const [showBtResult, setShowBtResult] = useState(false); const [btStrategyName, setBtStrategyName] = useState('전략'); + const [btReportCreatedAt, setBtReportCreatedAt] = useState(); /** 현재 차트와 일치하는 백테스팅 결과만 UI·마커에 반영 */ const btMatchesChart = useMemo( @@ -1228,6 +1231,34 @@ function App() { const barsMarket = useUpbit ? upbitBarsMarket : symbol; barsMarketRef.current = barsMarket; + const chartBtReportModel = useMemo(() => { + if (!btMatchesChart) return null; + const reportSymbol = btSymbol ?? symbol; + const reportTf = btTimeframe ?? timeframe; + return buildChartBacktestReportModel({ + analysis: btAnalysis, + stats: btStats, + signals: btSignals, + strategyName: btStrategyName, + symbol: reportSymbol, + timeframe: reportTf, + barCount: bars.length, + createdAt: btReportCreatedAt, + }); + }, [ + btMatchesChart, + btAnalysis, + btStats, + btSignals, + btStrategyName, + btSymbol, + btTimeframe, + symbol, + timeframe, + bars.length, + btReportCreatedAt, + ]); + /** 종목·타임프레임 전환 시 이전 틱 버퍼·실시간 허용 플래그 초기화 (paint 전에 실행) */ useLayoutEffect(() => { chartLiveReadyRef.current = false; @@ -2315,6 +2346,7 @@ function App() { }; btMarkerPayloadRef.current = payload; applyBacktestMarkersWhenReady(payload); + setBtReportCreatedAt(fmtListTimestamp(new Date().toISOString())); } if (appDefaults.btAutoPopup) setShowBtResult(true); } @@ -2677,19 +2709,12 @@ function App() { /> )} - {/* 백테스팅 결과 모달 */} - {showBtResult && ( - setShowBtResult(false)} - /> - )} + {/* 백테스팅 결과 — 투자분석 상세 레포트 (인쇄·PDF) */} + setShowBtResult(false)} + model={chartBtReportModel} + /> {/* 시리즈 클릭/호버 컨텍스트 툴바 */} {ctxToolbar && (() => { diff --git a/frontend/src/utils/backtestReportModel.ts b/frontend/src/utils/backtestReportModel.ts index b26a2a7..3651bf2 100644 --- a/frontend/src/utils/backtestReportModel.ts +++ b/frontend/src/utils/backtestReportModel.ts @@ -2,6 +2,7 @@ import type { BacktestAnalysis, BacktestResultRecord, BacktestSignal, + BacktestStats, } from './backendApi'; import type { BacktestAnalysisReportModel } from '../components/backtest/BacktestAnalysisReportModal'; import type { LiveExecutionItem } from './liveExecutionGroups'; @@ -42,6 +43,60 @@ export function buildAnalysisFromLive( }; } +function buildAnalysisFromStats(s: BacktestStats): BacktestAnalysis { + return { + initialCapital: 10_000_000, + finalEquity: s.finalEquity ?? 0, + totalReturnPct: s.totalReturn ?? 0, + totalProfitLoss: (s.finalEquity ?? 0) - 10_000_000, + grossProfit: 0, + grossLoss: 0, + avgReturnPct: s.avgReturn ?? 0, + profitLossRatio: 0, + numberOfPositions: s.totalTrades ?? 0, + numberOfWinning: s.winTrades ?? 0, + numberOfLosing: (s.totalTrades ?? 0) - (s.winTrades ?? 0), + numberOfBreakEven: 0, + winRate: s.winRate ?? 0, + maxDrawdownPct: s.maxDrawdown ?? 0, + maxRunupPct: 0, + sharpeRatio: 0, + sortinoRatio: 0, + calmarRatio: 0, + valueAtRisk95: 0, + expectedShortfall: 0, + buyAndHoldReturnPct: 0, + vsBuyAndHold: 0, + }; +} + +/** 실시간 차트에서 실행한 백테스트 → 투자분석 상세 레포트 모델 */ +export function buildChartBacktestReportModel(opts: { + analysis: BacktestAnalysis | null; + stats: BacktestStats | null; + signals: BacktestSignal[]; + strategyName: string; + symbol: string; + timeframe: string; + barCount: number; + createdAt?: string; +}): BacktestAnalysisReportModel | null { + const analysis = opts.analysis ?? (opts.stats ? buildAnalysisFromStats(opts.stats) : null); + if (!analysis) return null; + const market = toUpbitMarket(opts.symbol); + return { + analysis, + signals: opts.signals, + strategyName: repairUtf8Mojibake(opts.strategyName || '전략'), + symbol: opts.symbol, + symbolKo: repairUtf8Mojibake(getKoreanName(market)), + timeframe: opts.timeframe, + barCount: opts.barCount || 300, + createdAt: opts.createdAt, + reportKind: 'backtest', + }; +} + export function buildBacktestReportModel( record: BacktestResultRecord, analysis: BacktestAnalysis,