feat: Add backtest UI with ML training data generation and strategy testing
This commit is contained in:
417
frontend/src/pages/Backtest.tsx
Normal file
417
frontend/src/pages/Backtest.tsx
Normal file
@@ -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<any>(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 (
|
||||
<div className="space-y-5">
|
||||
{/* 설정 */}
|
||||
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-5">
|
||||
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<Database size={20} className="text-blue-400" />
|
||||
ML 학습 데이터 생성 설정
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">종목 코드 (쉼표 구분)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stockCodes}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">시작일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">종료일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mt-4">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">미래 예측 기간 (N일 후)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={forwardDays}
|
||||
onChange={(e) => setForwardDays(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">UP 레이블 임계값 (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={upThreshold}
|
||||
onChange={(e) => setUpThreshold(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">DOWN 레이블 임계값 (%)</label>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={downThreshold}
|
||||
onChange={(e) => setDownThreshold(Number(e.target.value))}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-5">
|
||||
<button
|
||||
onClick={() => generateMutation.mutate()}
|
||||
disabled={generateMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium disabled:opacity-50"
|
||||
>
|
||||
<Play size={16} />
|
||||
{generateMutation.isPending ? '생성 중...' : '데이터 생성'}
|
||||
</button>
|
||||
{result && (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-medium"
|
||||
>
|
||||
<Download size={16} />
|
||||
CSV 다운로드
|
||||
</button>
|
||||
<button
|
||||
onClick={() => trainMutation.mutate()}
|
||||
disabled={trainMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium disabled:opacity-50"
|
||||
>
|
||||
<TrendingUp size={16} />
|
||||
{trainMutation.isPending ? '학습 중...' : '모델 학습 시작'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 결과 */}
|
||||
{result && (
|
||||
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-5">
|
||||
<h3 className="text-lg font-bold text-white mb-4">생성 결과</h3>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-slate-800 rounded-lg p-4">
|
||||
<p className="text-xs text-slate-400">총 샘플 수</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{result.total_samples?.toLocaleString()}</p>
|
||||
</div>
|
||||
{Object.entries(result.label_distribution || {}).map(([label, count]: any) => (
|
||||
<div key={label} className="bg-slate-800 rounded-lg p-4">
|
||||
<p className="text-xs text-slate-400">{label}</p>
|
||||
<p className="text-2xl font-bold text-white mt-1">{count.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{((count / result.total_samples) * 100).toFixed(1)}%
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-sm text-blue-400">
|
||||
<AlertCircle size={14} className="inline mr-2" />
|
||||
{result.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 전략 성과 테스트 탭 ──────────────────────────────────────────────────────
|
||||
|
||||
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<any>(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 (
|
||||
<div className="space-y-5">
|
||||
{/* 설정 */}
|
||||
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-5">
|
||||
<h3 className="text-lg font-bold text-white mb-4 flex items-center gap-2">
|
||||
<BarChart3 size={20} className="text-purple-400" />
|
||||
프리셋 비교 백테스트
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">종목 코드</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stockCodes}
|
||||
onChange={(e) => setStockCodes(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">시작일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-400 mb-1">종료일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => compareMutation.mutate()}
|
||||
disabled={compareMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium mt-4 disabled:opacity-50"
|
||||
>
|
||||
<Play size={16} />
|
||||
{compareMutation.isPending ? '실행 중...' : '백테스트 실행'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 결과 비교 */}
|
||||
{compareResults && (
|
||||
<>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{Object.entries(compareResults.presets || {}).map(([key, result]: any) => (
|
||||
<div key={key} className="bg-slate-900/60 border border-slate-800 rounded-xl p-5">
|
||||
<h4 className="text-lg font-bold text-white mb-4">{result.name}</h4>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<p className="text-xs text-slate-400">수익률</p>
|
||||
<p className={`text-2xl font-bold ${
|
||||
result.total_return_pct > 0 ? 'text-green-400' : 'text-red-400'
|
||||
}`}>
|
||||
{(result.total_return_pct * 100).toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-400">MDD</p>
|
||||
<p className="text-lg text-red-400">{(result.mdd_pct * 100).toFixed(2)}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-400">승률</p>
|
||||
<p className="text-lg text-white">{(result.win_rate * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-400">Sharpe</p>
|
||||
<p className="text-lg text-white">{result.sharpe_ratio.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-400">거래 횟수</p>
|
||||
<p className="text-lg text-white">{result.total_trades}회</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 차트 */}
|
||||
{chartData && (
|
||||
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-5">
|
||||
<h3 className="text-lg font-bold text-white mb-4">수익 곱선 비교</h3>
|
||||
<Line
|
||||
data={chartData}
|
||||
options={{
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'top' },
|
||||
title: { display: false },
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
title: { display: true, text: '수익률 (%)' },
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── 메인 컴포넌트 ────────────────────────────────────────────────────────────────
|
||||
|
||||
const Backtest = () => {
|
||||
const [activeTab, setActiveTab] = useState<'ml' | 'strategy'>('ml')
|
||||
|
||||
return (
|
||||
<HtsPage>
|
||||
<div className="space-y-5">
|
||||
{/* 헤더 */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">백테스트</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
ML 학습 데이터 생성 및 전략 성과 검증
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 탭 */}
|
||||
<div className="flex gap-2 border-b border-slate-800">
|
||||
<button
|
||||
onClick={() => setActiveTab('ml')}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'ml'
|
||||
? 'text-blue-400 border-b-2 border-blue-400'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Database size={16} className="inline mr-2" />
|
||||
ML 학습 데이터 생성
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('strategy')}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'strategy'
|
||||
? 'text-purple-400 border-b-2 border-purple-400'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<TrendingUp size={16} className="inline mr-2" />
|
||||
전략 성과 테스트
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 컨텐츠 */}
|
||||
{activeTab === 'ml' ? <MLTrainingTab /> : <StrategyTestTab />}
|
||||
</div>
|
||||
</HtsPage>
|
||||
)
|
||||
}
|
||||
|
||||
export default Backtest
|
||||
Reference in New Issue
Block a user