/** * Admin 后台 - 回测管理页面(报刊风) * * 功能: * - 回测配置: 联赛(下拉)、日期范围、场数、模式 * - 结果汇总: 已评分数、1X2 准确率、比分 RMSE、平均置信度 * - 逐场明细: 实际比分 vs 预测比分,正误标记 * - 模型评估: 各模型历史准确率(/eval/summary) * * 注意: 回测会对每场完赛比赛各发起一次 LLM 预测,成本高,需管理员密钥。 */ import { useEffect, useState } from 'react' import { triggerBacktest, fetchEvalSummary, fetchLeagues } from '../dal' import type { BacktestRequest, EvalSummary, League } from '../types' import { Card, CardBody, CardHeader, Badge, SectionHeader, Alert, Spinner } from '../components' interface BacktestResultRow { match_id: number league_code?: string | null home_team: string away_team: string match_date?: string | null actual_score: string actual_1x2?: string pred_home?: number | null pred_away?: number | null pred_1x2?: string | null subjective_confidence?: number | null correct_1x2: boolean } interface BacktestResponse { summary: { total: number scored: number accuracy_1x2?: number avg_score_rmse?: number avg_subjective_confidence?: number } results: BacktestResultRow[] } const OUTCOME_LABEL: Record = { '1': '主胜', X: '平局', '2': '客胜' } function fmtDate(s?: string | null): string { if (!s) return '—' return s.slice(0, 10) } export default function BacktestPage() { const [leagues, setLeagues] = useState([]) const [leagueId, setLeagueId] = useState('') const [dateFrom, setDateFrom] = useState('') const [dateTo, setDateTo] = useState('') const [limit, setLimit] = useState(20) const [mode, setMode] = useState<'single' | 'multi'>('single') const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [result, setResult] = useState(null) const [evalSummary, setEvalSummary] = useState(null) const [evalLoading, setEvalLoading] = useState(false) useEffect(() => { fetchLeagues().then(setLeagues) loadEval() }, []) async function loadEval() { setEvalLoading(true) try { setEvalSummary(await fetchEvalSummary()) } finally { setEvalLoading(false) } } async function handleBacktest(e: React.FormEvent) { e.preventDefault() setLoading(true) setError(null) setResult(null) try { const req: BacktestRequest = { league_id: leagueId ? parseInt(leagueId) : undefined, date_from: dateFrom || undefined, date_to: dateTo || undefined, limit, mode, } const res = await triggerBacktest(req as BacktestRequest) setResult(res as unknown as BacktestResponse) } catch (err: unknown) { setError(err instanceof Error ? err.message : '回测失败') } finally { setLoading(false) } } const summary = result?.summary return (
{/* 回测配置 */}
setDateFrom(e.target.value)} className="field w-full" />
setDateTo(e.target.value)} className="field w-full" />
setLimit(parseInt(e.target.value) || 20)} className="field w-full" />
{error && setError(null)} />}
{/* 结果汇总 */}
{summary && (
{summary.scored}/{summary.total}
已评分 / 总场数
{summary.accuracy_1x2 !== undefined && summary.accuracy_1x2 !== null ? `${summary.accuracy_1x2.toFixed(1)}%` : '—'}
1X2 准确率
{summary.avg_score_rmse !== undefined && summary.avg_score_rmse !== null ? summary.avg_score_rmse.toFixed(2) : '—'}
比分 RMSE
{summary.avg_subjective_confidence !== undefined && summary.avg_subjective_confidence !== null ? `${Math.round(summary.avg_subjective_confidence * 100)}%` : '—'}
平均置信度
)} {/* 模型评估 */} {evalLoading ? (<> 加载中) : '刷新'} } /> {evalSummary && evalSummary.summary?.length > 0 ? (
{evalSummary.summary.map((s, i) => (
{s.provider}/{s.model} 准确率 {s.accuracy_1x2 !== undefined && s.accuracy_1x2 !== null ? `${s.accuracy_1x2.toFixed(1)}%` : '—'} {s.total} 场 {s.avg_score_rmse != null && RMSE {s.avg_score_rmse.toFixed(2)}}
))}
) : (

暂无评估数据。到「预测管理」完成结算后,这里会给出各模型准确率。

)}
{/* 逐场明细 */} {result && result.results?.length > 0 && (
{result.results.map(r => (
{fmtDate(r.match_date)} {r.home_team} vs {r.away_team} 实际 {r.actual_score} | 预测 {r.pred_home ?? '-'}:{r.pred_away ?? '-'} {r.correct_1x2 ? 命中 : 未中}
))}
)}
) }