/** * Admin 后台 - 回测管理页面 * * 功能: * - 配置回测参数(策略、联赛、日期范围、初始资金) * - 触发回测任务 * - 查看回测结果(收益率、最大回撤、夏普比率等) * - 交易记录明细 */ import { useEffect, useState } from 'react' import { triggerBacktest, fetchBacktestHistory, fetchLeagues } from '../dal' import type { BacktestRequest, BacktestResult, BacktestTrade, League } from '../types' import { Card, CardBody, CardHeader, Badge, ProgressBar, DataTable, EmptyState, } from '../components' import { SectionHeader } from '../components' const STRATEGIES = [ { value: 'confidence_weighted', label: '置信度加权', desc: '按预测置信度调整仓位' }, { value: 'kelly', label: 'Kelly 准则', desc: 'Kelly 公式优化投注比例' }, { value: 'flat', label: '固定金额', desc: '每次固定金额投注' }, ] export default function BacktestPage() { const [leagues, setLeagues] = useState([]) const [tasks, setTasks] = useState([]) const [strategy, setStrategy] = useState('confidence_weighted') const [selectedLeagues, setSelectedLeagues] = useState(['E0']) const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [bankroll, setBankroll] = useState(1000) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [successMsg, setSuccessMsg] = useState(null) useEffect(() => { Promise.all([fetchLeagues(), fetchBacktestHistory()]).then(([lg, ts]) => { setLeagues(lg) setTasks(ts) }) }, []) async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError(null) setSuccessMsg(null) setLoading(true) try { const body: BacktestRequest = { strategy, league_codes: selectedLeagues, date_from: dateFrom || '2024-01-01', date_to: dateTo || new Date().toISOString().split('T')[0], initial_bankroll: bankroll, } const res = await triggerBacktest(body) setSuccessMsg(`回测任务已启动: ${res.task_id}`) // 刷新列表 const ts = await fetchBacktestHistory() setTasks(ts) } catch (err: unknown) { setError(err instanceof Error ? err.message : '回测触发失败') } finally { setLoading(false) } } function toggleLeague(code: string) { setSelectedLeagues(prev => prev.includes(code) ? prev.filter(c => c !== code) : [...prev, code], ) } return (
{/* ── 回测配置表单 ── */}
{/* 策略选择 */}
{STRATEGIES.map(s => ( ))}
{/* 联赛多选 */}
{leagues.map(l => ( ))}
{/* 日期范围 */}
setDateFrom(e.target.value)} className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none" />
setDateTo(e.target.value)} className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none" />
{/* 初始资金 */}
setBankroll(Number(e.target.value))} min={100} step={100} className="w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none" />
{error && (
{error}
)} {successMsg && (
{successMsg}
)}
{/* ── 回测任务与结果 ── */}
{tasks.length === 0 ? ( ) : (
{tasks.map(task => ( ))}
)}
) } function BacktestResultRow({ task }: { task: BacktestResult }) { const m = task.metrics return (
{/* 头部 */}
{task.task_id.slice(0, 8)} {task.status}
{m && ( = 0 ? 'text-emerald-400' : 'text-red-400' }`} > {m.profit_loss >= 0 ? '+' : ''} {m.profit_loss.toFixed(0)} ({(m.roi * 100).toFixed(1)}%) )}
{/* 进度条 */} {task.status === 'running' && (
)} {/* 指标网格 */} {m && (
)} {/* 错误 */} {task.error_message && (

{task.error_message}

)} {/* 交易明细 */} {task.trades && task.trades.length > 0 && (
查看交易明细 ({task.trades.length} 笔)
`${row.home_team} vs ${row.away_team}`, }, { key: 'bet_type', label: '类型', width: '60px' }, { key: 'stake', label: '投注', width: '70px' }, { key: 'odds', label: '赔率', width: '60px' }, { key: 'result', label: '结果', width: '60px', render: (row: BacktestTrade) => {row.result}, }, { key: 'profit', label: '盈亏', width: '80px', render: (row: BacktestTrade) => ( = 0 ? 'text-emerald-400' : 'text-red-400' } > {row.profit >= 0 ? '+' : ''} {row.profit.toFixed(1)} ), }, ]} data={task.trades} rowKey={(row: BacktestTrade) => row.match_id} />
)}
) } function MiniStat({ label, value }: { label: string; value: string | number }) { return (
{label}
{value}
) }