feat: persist feature research and versioned alpha assessments
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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<string, string> = {
|
||||
pass: "通过本地筛选",
|
||||
review: "数据不足 / 待核实",
|
||||
block: "未通过规则",
|
||||
};
|
||||
export function EvaluationPanel({
|
||||
alphaId,
|
||||
experimentId,
|
||||
backtestRunId,
|
||||
}: {
|
||||
alphaId?: string;
|
||||
experimentId?: string;
|
||||
backtestRunId?: string;
|
||||
}) {
|
||||
const [items, setItems] = useState<Evaluation[]>([]);
|
||||
const [selected, setSelected] = useState<Evaluation | null>(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<Evaluation>) {
|
||||
setBusy(name);
|
||||
try {
|
||||
setSelected(await work());
|
||||
setPage(1);
|
||||
setRevision((n) => n + 1);
|
||||
} catch (e) {
|
||||
Toast.error((e as Error).message);
|
||||
} finally {
|
||||
setBusy("");
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<h3>版本化研究评估</h3>
|
||||
<p className="muted">
|
||||
每份报告固定输入快照、规则与指标证据。模型建议单独保存,不改变规则判断或本地研究状态。
|
||||
</p>
|
||||
<div className="research-form-grid">
|
||||
{(
|
||||
[
|
||||
["sharpe_min", "Sharpe 下限"],
|
||||
["fitness_min", "Fitness 下限"],
|
||||
["turnover_max", "Turnover 上限"],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<label key={key}>
|
||||
{label}
|
||||
<InputNumber
|
||||
aria-label={label}
|
||||
value={rules[key]}
|
||||
step={0.1}
|
||||
onChange={(v) => {
|
||||
if (typeof v === "number") setRules({ ...rules, [key]: v });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="inline-actions">
|
||||
<Button
|
||||
disabled={Boolean(busy) || (!alphaId && !backtestRunId)}
|
||||
loading={busy === "evaluate"}
|
||||
onClick={() =>
|
||||
void perform("evaluate", () =>
|
||||
post("/research/evaluations", {
|
||||
alpha_id: alphaId,
|
||||
experiment_id: experimentId,
|
||||
backtest_run_id: backtestRunId,
|
||||
rules,
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
保存规则评估
|
||||
</Button>
|
||||
<ResearchSelect
|
||||
label="历史评估报告"
|
||||
placeholder="查看历史报告"
|
||||
value={selected?.id}
|
||||
optionList={items.map((i) => ({
|
||||
value: i.id,
|
||||
label: `${formatTime(i.created_at)} · ${labels[i.report.verdict]}`,
|
||||
}))}
|
||||
onChange={(v) => setSelected(items.find((i) => i.id === v) || null)}
|
||||
/>
|
||||
<Button disabled={page <= 1} onClick={() => setPage((n) => n - 1)}>
|
||||
上一页
|
||||
</Button>
|
||||
<Button
|
||||
disabled={items.length < 25}
|
||||
onClick={() => setPage((n) => n + 1)}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
{!selected ? (
|
||||
<Empty description="选择历史报告,或按当前规则保存新评估" />
|
||||
) : (
|
||||
<>
|
||||
<div className="section-toolbar">
|
||||
<Tag
|
||||
color={
|
||||
selected.report.verdict === "pass"
|
||||
? "green"
|
||||
: selected.report.verdict === "block"
|
||||
? "red"
|
||||
: "orange"
|
||||
}
|
||||
>
|
||||
{labels[selected.report.verdict]}
|
||||
</Tag>
|
||||
<span className="muted">
|
||||
{formatTime(selected.created_at)} ·{" "}
|
||||
{selected.report.rules.version}
|
||||
</span>
|
||||
</div>
|
||||
<p>
|
||||
规则:Sharpe ≥ {selected.report.rules.sharpe_min},Fitness ≥{" "}
|
||||
{selected.report.rules.fitness_min},Turnover ≤{" "}
|
||||
{selected.report.rules.turnover_max}
|
||||
</p>
|
||||
{selected.report.records.map((r, index) => (
|
||||
<div className="research-card" key={index}>
|
||||
<h4>
|
||||
{r.alpha_id || r.client_item_id || "未返回 Alpha"} ·{" "}
|
||||
{labels[r.verdict]}
|
||||
</h4>
|
||||
{r.evidence.map((e) => (
|
||||
<div className="check-row" key={e.metric}>
|
||||
<span>
|
||||
{e.metric}: {formatNumber(e.value)}
|
||||
</span>
|
||||
<span>
|
||||
{e.direction === "min" ? "≥" : "≤"} {e.bound}
|
||||
</span>
|
||||
<Tag>
|
||||
{e.status === "unknown"
|
||||
? "缺失"
|
||||
: e.status === "pass"
|
||||
? "通过"
|
||||
: "失败"}
|
||||
</Tag>
|
||||
</div>
|
||||
))}
|
||||
{r.missing.length > 0 && (
|
||||
<p>缺失或未决:{r.missing.join("、")}</p>
|
||||
)}
|
||||
{r.failed.length > 0 && <p>未通过:{r.failed.join("、")}</p>}
|
||||
<details>
|
||||
<summary>当时的输入快照</summary>
|
||||
<pre className="code-block">
|
||||
{JSON.stringify(r.snapshot, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
</div>
|
||||
))}
|
||||
<Banner
|
||||
type="info"
|
||||
description="这是本地研究筛选,不代表官方提交资格。已有平台检查缺失时保留为待核实。"
|
||||
/>
|
||||
{selected.report.model_advice && (
|
||||
<div className="research-card">
|
||||
<h4>模型建议</h4>
|
||||
<p>{selected.report.model_advice.summary}</p>
|
||||
<p>
|
||||
风险:
|
||||
{selected.report.model_advice.risks.join(";") || "未提供"}
|
||||
</p>
|
||||
<p>
|
||||
建议:
|
||||
{selected.report.model_advice.suggestions.join(";") ||
|
||||
"未提供"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
disabled={Boolean(busy)}
|
||||
loading={busy === "advice"}
|
||||
onClick={() =>
|
||||
void perform("advice", () =>
|
||||
post(`/research/evaluations/${selected.id}/advice`, {}),
|
||||
)
|
||||
}
|
||||
>
|
||||
生成建议并保存为新报告
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user