import { useEffect, useState } from "react"; import { Banner, Button, Empty, InputNumber, Tag, Toast, } from "@douyinfe/semi-ui-19"; import { api, formatNumber, formatTime, post } from "../api"; import { ResearchSelect } from "./ResearchSelect"; export type Evaluation = { id: string; created_at: string; report: { verdict: string; rules: { version: string; sharpe_min: number; fitness_min: number; turnover_max: number; }; records: { alpha_id: string | null; client_item_id: string | null; verdict: string; missing: string[]; failed: string[]; evidence: { metric: string; value: number | null; bound: number; direction: string; status: string; }[]; snapshot: unknown; }[]; model_advice: { summary: string; risks: string[]; suggestions: string[]; } | null; previous_evaluation_id?: string; }; }; const labels: Record = { pass: "通过本地筛选", review: "数据不足 / 待核实", block: "未通过规则", }; export function EvaluationPanel({ alphaId, experimentId, backtestRunId, }: { alphaId?: string; experimentId?: string; backtestRunId?: string; }) { const [items, setItems] = useState([]); const [selected, setSelected] = useState(null); const [rules, setRules] = useState({ version: "research-v1", sharpe_min: 1, fitness_min: 0.5, turnover_max: 0.7, }); const [busy, setBusy] = useState(""); const [page, setPage] = useState(1); const [revision, setRevision] = useState(0); useEffect(() => { const controller = new AbortController(); api<{ items: Evaluation[] }>( `/research/evaluations?${alphaId ? `alpha_id=${encodeURIComponent(alphaId)}` : `experiment_id=${encodeURIComponent(experimentId || "")}`}&offset=${(page - 1) * 25}`, { signal: controller.signal }, ) .then((r) => setItems(r.items)) .catch((e) => { if (!controller.signal.aborted) Toast.error(e.message); }); return () => controller.abort(); }, [alphaId, experimentId, page, revision]); async function perform(name: string, work: () => Promise) { setBusy(name); try { setSelected(await work()); setPage(1); setRevision((n) => n + 1); } catch (e) { Toast.error((e as Error).message); } finally { setBusy(""); } } return (

版本化研究评估

每份报告固定输入快照、规则与指标证据。模型建议单独保存,不改变规则判断或本地研究状态。

{( [ ["sharpe_min", "Sharpe 下限"], ["fitness_min", "Fitness 下限"], ["turnover_max", "Turnover 上限"], ] as const ).map(([key, label]) => ( ))}
({ value: i.id, label: `${formatTime(i.created_at)} · ${labels[i.report.verdict]}`, }))} onChange={(v) => setSelected(items.find((i) => i.id === v) || null)} />
{!selected ? ( ) : ( <>
{labels[selected.report.verdict]} {formatTime(selected.created_at)} ·{" "} {selected.report.rules.version}

规则:Sharpe ≥ {selected.report.rules.sharpe_min},Fitness ≥{" "} {selected.report.rules.fitness_min},Turnover ≤{" "} {selected.report.rules.turnover_max}

{selected.report.records.map((r, index) => (

{r.alpha_id || r.client_item_id || "未返回 Alpha"} ·{" "} {labels[r.verdict]}

{r.evidence.map((e) => (
{e.metric}: {formatNumber(e.value)} {e.direction === "min" ? "≥" : "≤"} {e.bound} {e.status === "unknown" ? "缺失" : e.status === "pass" ? "通过" : "失败"}
))} {r.missing.length > 0 && (

缺失或未决:{r.missing.join("、")}

)} {r.failed.length > 0 &&

未通过:{r.failed.join("、")}

}
当时的输入快照
                  {JSON.stringify(r.snapshot, null, 2)}
                
))} {selected.report.model_advice && (

模型建议

{selected.report.model_advice.summary}

风险: {selected.report.model_advice.risks.join(";") || "未提供"}

建议: {selected.report.model_advice.suggestions.join(";") || "未提供"}

)} )}
); }