feat: Add bulk universe backtest UI tab

This commit is contained in:
2026-03-06 21:57:15 +09:00
parent 87b4205fb6
commit 19711705b3

View File

@@ -1,8 +1,8 @@
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { useMutation, useQuery } 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 { Download, Play, TrendingUp, Database, BarChart3, AlertCircle, Globe } from 'lucide-react'
import { Line } from 'react-chartjs-2'
import {
Chart as ChartJS,
@@ -31,7 +31,7 @@ interface BacktestResult {
// ── ML 학습 데이터 생성 탭 ───────────────────────────────────────────────
function MLTrainingTab() {
const [stockCodes, setStockCodes] = useState('005930,000660,035720') // 삼성전자, SK하이닉스, 카카오
const [stockCodes, setStockCodes] = useState('005930,000660,035720')
const [startDate, setStartDate] = useState('2023-01-01')
const [endDate, setEndDate] = useState('2025-12-31')
const [forwardDays, setForwardDays] = useState(5)
@@ -365,10 +365,198 @@ function StrategyTestTab() {
)
}
// ── 전체 종목 백테스트 탭 ─────────────────────────────────────────────────────────
function UniverseBacktestTab() {
const [universePreset, setUniversePreset] = useState('large_cap')
const [strategyPreset, setStrategyPreset] = useState('balanced')
const [startDate, setStartDate] = useState('2024-01-01')
const [endDate, setEndDate] = useState('2024-12-31')
const [topN, setTopN] = useState(20)
const [result, setResult] = useState<any>(null)
// 유니버스 프리셋 목록 조회
const { data: universePresets } = useQuery({
queryKey: ['universe-presets'],
queryFn: async () => (await api.get('/backtest/universe-presets')).data,
})
const runMutation = useMutation({
mutationFn: async () => {
const res = await api.post('/backtest/run-universe', {
universe_preset: universePreset,
strategy_preset: strategyPreset,
start_date: startDate,
end_date: endDate,
top_n_results: topN,
})
return res.data
},
onSuccess: (data) => {
setResult(data)
},
})
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">
<Globe size={20} className="text-orange-400" />
전체 종목 백테스트
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-slate-400 mb-1">종목 유니버스</label>
<select
value={universePreset}
onChange={(e) => setUniversePreset(e.target.value)}
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
>
{universePresets && Object.entries(universePresets).map(([key, preset]: any) => (
<option key={key} value={key}>
{preset.description || key}
</option>
))}
</select>
</div>
<div>
<label className="block text-xs text-slate-400 mb-1">전략 프리셋</label>
<select
value={strategyPreset}
onChange={(e) => setStrategyPreset(e.target.value)}
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-white text-sm"
>
<option value="conservative">보수적</option>
<option value="balanced">균형형</option>
<option value="aggressive">공격적</option>
</select>
</div>
</div>
<div className="grid grid-cols-3 gap-4 mt-4">
<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>
<label className="block text-xs text-slate-400 mb-1">결과 개수 (Top N)</label>
<input
type="number"
value={topN}
onChange={(e) => setTopN(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>
<button
onClick={() => runMutation.mutate()}
disabled={runMutation.isPending}
className="flex items-center gap-2 px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg font-medium mt-4 disabled:opacity-50"
>
<Play size={16} />
{runMutation.isPending ? '실행 중...' : '백테스트 실행'}
</button>
</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_tested}개</p>
</div>
<div className="bg-slate-800 rounded-lg p-4">
<p className="text-xs text-slate-400">평균 수익률</p>
<p className={`text-2xl font-bold mt-1 ${
result.average_return > 0 ? 'text-green-400' : 'text-red-400'
}`}>
{(result.average_return * 100).toFixed(2)}%
</p>
</div>
<div className="bg-slate-800 rounded-lg p-4">
<p className="text-xs text-slate-400">유니버스</p>
<p className="text-lg font-bold text-white mt-1">{result.universe_preset}</p>
</div>
<div className="bg-slate-800 rounded-lg p-4">
<p className="text-xs text-slate-400">전략</p>
<p className="text-lg font-bold text-white mt-1">{result.strategy_preset}</p>
</div>
</div>
</div>
{/* Top N 결과 */}
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-5">
<h3 className="text-lg font-bold text-white mb-4">수익률 상위 {topN}종목</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-700">
<th className="text-left py-2 text-slate-400 font-medium">순위</th>
<th className="text-left py-2 text-slate-400 font-medium">종목코드</th>
<th className="text-right py-2 text-slate-400 font-medium">수익률</th>
<th className="text-right py-2 text-slate-400 font-medium">MDD</th>
<th className="text-right py-2 text-slate-400 font-medium">승률</th>
<th className="text-right py-2 text-slate-400 font-medium">Sharpe</th>
<th className="text-right py-2 text-slate-400 font-medium">거래수</th>
</tr>
</thead>
<tbody>
{result.top_performers?.map((item: any, idx: number) => (
<tr key={item.stock_code} className="border-b border-slate-800 hover:bg-slate-800/50">
<td className="py-3 text-slate-300">{idx + 1}</td>
<td className="py-3 text-white font-mono">{item.stock_code}</td>
<td className={`py-3 text-right font-bold ${
item.total_return_pct > 0 ? 'text-green-400' : 'text-red-400'
}`}>
{(item.total_return_pct * 100).toFixed(2)}%
</td>
<td className="py-3 text-right text-red-400">
{(item.mdd_pct * 100).toFixed(2)}%
</td>
<td className="py-3 text-right text-slate-300">
{(item.win_rate * 100).toFixed(1)}%
</td>
<td className="py-3 text-right text-slate-300">
{item.sharpe_ratio.toFixed(2)}
</td>
<td className="py-3 text-right text-slate-400">
{item.total_trades}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
)}
</div>
)
}
// ── 메인 컴포넌트 ────────────────────────────────────────────────────────────────
const Backtest = () => {
const [activeTab, setActiveTab] = useState<'ml' | 'strategy'>('ml')
const [activeTab, setActiveTab] = useState<'ml' | 'strategy' | 'universe'>('ml')
return (
<HtsPage>
@@ -392,7 +580,7 @@ const Backtest = () => {
}`}
>
<Database size={16} className="inline mr-2" />
ML 학습 데이터 생성
ML 학습 데이터
</button>
<button
onClick={() => setActiveTab('strategy')}
@@ -403,12 +591,25 @@ const Backtest = () => {
}`}
>
<TrendingUp size={16} className="inline mr-2" />
전략 성과 테스트
프리셋 비교
</button>
<button
onClick={() => setActiveTab('universe')}
className={`px-4 py-2 text-sm font-medium transition-colors ${
activeTab === 'universe'
? 'text-orange-400 border-b-2 border-orange-400'
: 'text-slate-400 hover:text-white'
}`}
>
<Globe size={16} className="inline mr-2" />
전체 종목 테스트
</button>
</div>
{/* 컨텐츠 */}
{activeTab === 'ml' ? <MLTrainingTab /> : <StrategyTestTab />}
{activeTab === 'ml' && <MLTrainingTab />}
{activeTab === 'strategy' && <StrategyTestTab />}
{activeTab === 'universe' && <UniverseBacktestTab />}
</div>
</HtsPage>
)