From 38ba5bfbb0b3f2ed6cab30758b2ae0fe22383e1e Mon Sep 17 00:00:00 2001 From: YoungWooSeo Date: Fri, 6 Mar 2026 21:28:16 +0900 Subject: [PATCH] feat: Add backtest UI with ML training data generation and strategy testing --- frontend/src/pages/Backtest.tsx | 417 ++++++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 frontend/src/pages/Backtest.tsx diff --git a/frontend/src/pages/Backtest.tsx b/frontend/src/pages/Backtest.tsx new file mode 100644 index 0000000..e9b6ac5 --- /dev/null +++ b/frontend/src/pages/Backtest.tsx @@ -0,0 +1,417 @@ +import { useState } from 'react' +import { useMutation } from '@tanstack/react-query' +import api from '../lib/api' +import HtsPage from '../components/HtsPage' +import { Download, Play, TrendingUp, Database, BarChart3, AlertCircle } from 'lucide-react' +import { Line } from 'react-chartjs-2' +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js' + +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend) + +// ── 타입 ────────────────────────────────────────────────────────────────── + +interface BacktestResult { + total_return_pct: number + mdd_pct: number + win_rate: number + sharpe_ratio: number + total_trades: number + equity_curve: Array<{ date: string; value: number; return_pct: number }> +} + +// ── ML 학습 데이터 생성 탭 ─────────────────────────────────────────────── + +function MLTrainingTab() { + const [stockCodes, setStockCodes] = useState('005930,000660,035720') // 삼성전자, SK하이닉스, 카카오 + const [startDate, setStartDate] = useState('2023-01-01') + const [endDate, setEndDate] = useState('2025-12-31') + const [forwardDays, setForwardDays] = useState(5) + const [upThreshold, setUpThreshold] = useState(3.0) + const [downThreshold, setDownThreshold] = useState(-3.0) + const [result, setResult] = useState(null) + + const generateMutation = useMutation({ + mutationFn: async () => { + const res = await api.post('/backtest/generate-training-data', { + stock_codes: stockCodes.split(',').map(c => c.trim()), + start_date: startDate, + end_date: endDate, + forward_days: forwardDays, + up_threshold: upThreshold / 100, + down_threshold: downThreshold / 100, + }) + return res.data + }, + onSuccess: (data) => { + setResult(data) + }, + }) + + const trainMutation = useMutation({ + mutationFn: async () => { + const res = await api.post('/backtest/train-model') + return res.data + }, + }) + + const handleDownload = async () => { + const res = await api.get('/backtest/download-training-data', { responseType: 'blob' }) + const url = window.URL.createObjectURL(new Blob([res.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', 'training_data.csv') + document.body.appendChild(link) + link.click() + link.remove() + } + + return ( +
+ {/* 설정 */} +
+

+ + ML 학습 데이터 생성 설정 +

+
+
+ + setStockCodes(e.target.value)} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + placeholder="005930,000660,035720" + /> +
+
+
+ + setStartDate(e.target.value)} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ + setEndDate(e.target.value)} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+
+ +
+
+ + setForwardDays(Number(e.target.value))} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ + setUpThreshold(Number(e.target.value))} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ + setDownThreshold(Number(e.target.value))} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ +
+ + {result && ( + <> + + + + )} +
+
+ + {/* 결과 */} + {result && ( +
+

생성 결과

+
+
+

총 샘플 수

+

{result.total_samples?.toLocaleString()}

+
+ {Object.entries(result.label_distribution || {}).map(([label, count]: any) => ( +
+

{label}

+

{count.toLocaleString()}

+

+ {((count / result.total_samples) * 100).toFixed(1)}% +

+
+ ))} +
+
+ + {result.message} +
+
+ )} +
+ ) +} + +// ── 전략 성과 테스트 탭 ────────────────────────────────────────────────────── + +function StrategyTestTab() { + const [stockCodes, setStockCodes] = useState('005930,000660,035720') + const [startDate, setStartDate] = useState('2024-01-01') + const [endDate, setEndDate] = useState('2024-12-31') + const [compareResults, setCompareResults] = useState(null) + + const compareMutation = useMutation({ + mutationFn: async () => { + const res = await api.post('/backtest/compare-presets', { + stock_codes: stockCodes.split(',').map(c => c.trim()), + start_date: startDate, + end_date: endDate, + presets: ['conservative', 'balanced', 'aggressive'], + }) + return res.data + }, + onSuccess: (data) => { + setCompareResults(data) + }, + }) + + const chartData = compareResults ? { + labels: compareResults.presets?.conservative?.equity_curve?.map((p: any) => p.date) || [], + datasets: [ + { + label: '보수적', + data: compareResults.presets?.conservative?.equity_curve?.map((p: any) => p.return_pct * 100) || [], + borderColor: 'rgb(34, 197, 94)', + backgroundColor: 'rgba(34, 197, 94, 0.1)', + }, + { + label: '균형형', + data: compareResults.presets?.balanced?.equity_curve?.map((p: any) => p.return_pct * 100) || [], + borderColor: 'rgb(59, 130, 246)', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + }, + { + label: '공격적', + data: compareResults.presets?.aggressive?.equity_curve?.map((p: any) => p.return_pct * 100) || [], + borderColor: 'rgb(239, 68, 68)', + backgroundColor: 'rgba(239, 68, 68, 0.1)', + }, + ], + } : null + + return ( +
+ {/* 설정 */} +
+

+ + 프리셋 비교 백테스트 +

+
+
+ + setStockCodes(e.target.value)} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ + setStartDate(e.target.value)} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ + setEndDate(e.target.value)} + className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm" + /> +
+
+ +
+ + {/* 결과 비교 */} + {compareResults && ( + <> +
+ {Object.entries(compareResults.presets || {}).map(([key, result]: any) => ( +
+

{result.name}

+
+
+

수익률

+

0 ? 'text-green-400' : 'text-red-400' + }`}> + {(result.total_return_pct * 100).toFixed(2)}% +

+
+
+

MDD

+

{(result.mdd_pct * 100).toFixed(2)}%

+
+
+

승률

+

{(result.win_rate * 100).toFixed(1)}%

+
+
+

Sharpe

+

{result.sharpe_ratio.toFixed(2)}

+
+
+

거래 횟수

+

{result.total_trades}회

+
+
+
+ ))} +
+ + {/* 차트 */} + {chartData && ( +
+

수익 곱선 비교

+ +
+ )} + + )} +
+ ) +} + +// ── 메인 컴포넌트 ──────────────────────────────────────────────────────────────── + +const Backtest = () => { + const [activeTab, setActiveTab] = useState<'ml' | 'strategy'>('ml') + + return ( + +
+ {/* 헤더 */} +
+

백테스트

+

+ ML 학습 데이터 생성 및 전략 성과 검증 +

+
+ + {/* 탭 */} +
+ + +
+ + {/* 컨텐츠 */} + {activeTab === 'ml' ? : } +
+
+ ) +} + +export default Backtest