feat: Add bulk universe backtest UI tab
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useMutation } from '@tanstack/react-query'
|
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||||
import api from '../lib/api'
|
import api from '../lib/api'
|
||||||
import HtsPage from '../components/HtsPage'
|
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 { Line } from 'react-chartjs-2'
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
@@ -31,7 +31,7 @@ interface BacktestResult {
|
|||||||
// ── ML 학습 데이터 생성 탭 ───────────────────────────────────────────────
|
// ── ML 학습 데이터 생성 탭 ───────────────────────────────────────────────
|
||||||
|
|
||||||
function MLTrainingTab() {
|
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 [startDate, setStartDate] = useState('2023-01-01')
|
||||||
const [endDate, setEndDate] = useState('2025-12-31')
|
const [endDate, setEndDate] = useState('2025-12-31')
|
||||||
const [forwardDays, setForwardDays] = useState(5)
|
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 Backtest = () => {
|
||||||
const [activeTab, setActiveTab] = useState<'ml' | 'strategy'>('ml')
|
const [activeTab, setActiveTab] = useState<'ml' | 'strategy' | 'universe'>('ml')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HtsPage>
|
<HtsPage>
|
||||||
@@ -392,7 +580,7 @@ const Backtest = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Database size={16} className="inline mr-2" />
|
<Database size={16} className="inline mr-2" />
|
||||||
ML 학습 데이터 생성
|
ML 학습 데이터
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('strategy')}
|
onClick={() => setActiveTab('strategy')}
|
||||||
@@ -403,12 +591,25 @@ const Backtest = () => {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<TrendingUp size={16} className="inline mr-2" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 컨텐츠 */}
|
{/* 컨텐츠 */}
|
||||||
{activeTab === 'ml' ? <MLTrainingTab /> : <StrategyTestTab />}
|
{activeTab === 'ml' && <MLTrainingTab />}
|
||||||
|
{activeTab === 'strategy' && <StrategyTestTab />}
|
||||||
|
{activeTab === 'universe' && <UniverseBacktestTab />}
|
||||||
</div>
|
</div>
|
||||||
</HtsPage>
|
</HtsPage>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user