"use client"; import { useEffect, useState } from "react"; import { apiGet } from "@/lib/api"; import { submitJob, waitJob } from "@/lib/jobs"; import type { BacktestResult, FactorMeta, ResearchSpec } from "@/lib/types"; import { recentRange } from "@/lib/dates"; import { LineChart } from "@/components/LineChart"; export default function BacktestPage() { const [factors, setFactors] = useState([]); const [factor, setFactor] = useState("momentum_60"); const [topN, setTopN] = useState(5); const [rebalance, setRebalance] = useState<"monthly" | "weekly">("monthly"); const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const [excludeSt, setExcludeSt] = useState(true); const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const [jobId, setJobId] = useState(""); const [error, setError] = useState(""); useEffect(() => { apiGet("/factors") .then(setFactors) .catch((e: Error) => setError(e.message)); const { start: s, end: e } = recentRange(); setStart(s); setEnd(e); }, []); async function run() { setRunning(true); setError(""); setJobId(""); setResult(null); try { const spec: ResearchSpec = { type: "backtest", universe: { exclude_st: excludeSt, min_listing_days: 0 }, factors: [{ name: factor, weight: 1 }], selection: { top_n: topN }, rebalance, period: [start, end], }; // 异步 Job:后台执行(全市场可能数十秒),轮询到终态 const { job_id } = await submitJob(spec); setJobId(job_id); const out = await waitJob(job_id); if (out.status === "success" && out.result) { setResult(out.result); } else { setError(`任务${out.status}${out.error ? `:${out.error}` : ""}`); } } catch (e) { setError((e as Error).message); } finally { setRunning(false); } } const s = result?.summary; return ( <>

选股回测

研究参数(Research Specification)

{running && jobId &&
任务 {jobId} 后台执行中,请稍候…
} {error &&
{error}
}
{result && s && ( <>

结果 · {s.total_return_pct.toFixed(2)}%{" "} = 0 ? "good" : "bad"}> {s.total_return_pct >= 0 ? "▲" : "▼"}

期末净值
{s.final_equity.toLocaleString()}
年化收益
{s.annual_return_pct.toFixed(2)}%
Sharpe
{s.sharpe.toFixed(2)}
最大回撤
{s.max_drawdown_pct.toFixed(2)}%
胜率(按交易)
{s.win_rate_pct.toFixed(1)}%
交易笔数 / 换手
{s.total_trades} / {s.avg_turnover_pct.toFixed(0)}%

月度收益(%)

{result.monthly_returns.map((m) => ( ))}
月份 收益
{m.year}-{String(m.month).padStart(2, "0")} = 0 ? "good" : "bad"}> {m.return_pct.toFixed(2)}%

最新持仓

{(() => { const last = new Map(); for (const p of result.positions) last.set(p.date, 0); const lastDate = [...last.keys()].at(-1); const pos = result.positions.filter((p) => p.date === lastDate); return (
{pos.map((p) => ( {p.symbol} · {(p.weight * 100).toFixed(0)}% ))}
); })()}

未建模约束(如实标注)

    {result.unimplemented.map((u, i) => (
  • {u}
  • ))}
)} ); }