"use client"; import { useCallback, useEffect, useState } from "react"; import { apiGet, apiPost } from "@/lib/api"; import type { BacktestResult } from "@/lib/types"; import { PageHeader, Card, Pill, Btn, Banner, Empty, Metric, SkeletonLines, } from "@/components/ui"; import { Icon } from "@/components/icons"; interface ExperimentMeta { id: string; kind: string; factors: string[]; period?: [string, string] | null; rebalance?: string; top_n?: number | null; summary_text?: string | null; code_version?: string | null; created_at?: string; } interface ExperimentDetail extends ExperimentMeta { spec: { universe?: Record }; result: BacktestResult | null; } const KIND_MAP: Record = { backtest: { label: "回测", tone: "accent" }, factor_test: { label: "因子测试", tone: "violet" }, }; function kindView(kind: string) { const k = KIND_MAP[kind] ?? { label: kind, tone: "default" as const }; return {k.label}; } function statusView(status: string) { switch (status) { case "success": return ( 完成 ); case "running": return ( 运行中 ); case "queued": return ( 排队 ); case "failed": return ( 失败 ); case "cancelled": return 已取消; default: return {status}; } } export default function ExperimentsPage() { const [exps, setExps] = useState([]); const [loading, setLoading] = useState(true); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [jobId, setJobId] = useState(""); const [jobStatus, setJobStatus] = useState(""); const [error, setError] = useState(""); const load = useCallback(() => { apiGet("/experiments") .then(setExps) .catch((e: Error) => setError(e.message)) .finally(() => setLoading(false)); }, []); useEffect(load, [load]); function open(id: string) { setDetailLoading(true); setDetail(null); apiGet(`/experiments/${id}`) .then((d) => { setDetail(d); }) .catch((e: Error) => setError(e.message)) .finally(() => setDetailLoading(false)); } function rerun(exp: ExperimentMeta) { setError(""); setJobId(""); setJobStatus("submitting"); apiPost<{ job_id: string; status: string }>(`/experiments/${exp.id}/rerun`, {}) .then((r) => { setJobId(r.job_id); setJobStatus(r.status); poll(r.job_id); }) .catch((e: Error) => setError(e.message)); } function poll(id: string) { let tries = 0; const timer = window.setInterval(() => { tries += 1; apiGet<{ status: string }>(`/jobs/${id}`) .then((j) => { setJobStatus(j.status); if (j.status === "success" || j.status === "failed" || tries > 60) { window.clearInterval(timer); load(); setDetail(null); } }) .catch(() => window.clearInterval(timer)); }, 1200); } const s = detail?.result?.summary; return ( <> 0 ? 共 {exps.length} 个实验 : undefined} /> {error ? {error} : null} {jobId ? (
任务状态实时轮询;完成后列表自动刷新,可直接展开查看最新结果。
) : null} {loading ? (
) : exps.length === 0 ? ( ) : (
{exps.map((e) => ( ))}
ID 类型 因子 区间 摘要 代码版本 操作
{e.id} {kindView(e.kind)} {e.factors.map((f) => ( {f} ))} {e.period ? `${e.period[0]}~${e.period[1]}` : "—"} {e.summary_text ?? "—"} {e.code_version ? {e.code_version} : —} open(e.id)}> 详情 rerun(e)} disabled={jobStatus === "running" || jobStatus === "queued" || jobStatus === "submitting"}> 复跑
)}
{detail ? ( setDetail(null)}> 关闭 } > {detailLoading ? ( ) : ( <> {s ? (
= 0 ? "pos" : "neg"} value={`${s.total_return_pct >= 0 ? "+" : ""}${s.total_return_pct.toFixed(2)}%`} /> = 0 ? "pos" : "neg"} value={`${s.annual_return_pct >= 0 ? "+" : ""}${s.annual_return_pct.toFixed(2)}%`} />
) : (
该实验没有可展示的汇总结果(可能是因子测试或历史版本)。
)} {detail.spec ? (
spec(可复现输入):{JSON.stringify(detail.spec).slice(0, 500)} {JSON.stringify(detail.spec).length > 500 ? "…" : ""}
) : null} )}
) : null} ); }