feat: Add bulk universe backtest UI tab
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user