"use client"; /** * 归档结果的**按类型分发**视图:归档页只依赖这一个入口,不在页面里猜结果结构。 * * 为什么必须存在(真实缺陷驱动):归档详情页原先在 `result` 非空时**无条件**渲染 * `BacktestResultView`,而 `factor_test` / `selection` 归档的结果结构与回测完全不同 * (没有 `summary` / `equity_curve` / `positions`)。实测后果:库里 4 条因子测试归档 * 与 1 条选股归档,从列表点「打开归档」**整页 500**(SSR 阶段 `result.positions.at(-1)` * 抛 TypeError,且没有 error.tsx 兜底)——列表对每一行都给链接,所以这是必现路径。 * * 现在按后端 `kind` 显式分发: * - `backtest` → `BacktestResultView`(净值/回撤/个股曲线/持仓/成交/月度年度/未建模) * - `factor_test` → IC / RankIC / ICIR / 正收益占比 / 样本日数 + **分层收益柱状图** * - `selection` → 选股统计 + 候选明细表(代码**带名称**且可点击进个股页) * - 其它未知类型 → 如实说明「暂不支持预览」并引导导出 JSON,**不假装能渲染** * * 纯派生逻辑(字段计数)不在这里,而在 `lib/archive.ts` —— Server Component 不能调用 * `"use client"` 模块导出的普通函数,放错位置会让归档页整页 500(已实测踩过)。 * * 与 `/factors`、`/selection` 页面各自的渲染器并存是刻意的:那两个页面带交互 * (运行、直通回测、导出),归档页是**只读快照**,两类用途的文案与动作不同。 */ import type { BacktestResult, FactorTestReport, SelectionResult } from "@/lib/types"; import type { AnyArchiveResult } from "@/lib/archive"; import { BacktestResultView } from "@/components/BacktestResultView"; import { RichText } from "@/components/RichText"; import { Card, Empty, Metric, UnimplementedNote } from "@/components/ui"; import { SymbolLink } from "@/lib/symbols"; export interface ArchiveInfo { id: string; created_at?: string | null; code_version?: string | null; data_version?: string | null; job_id?: string | null; } export function ArchiveResultView({ kind, result, archive, }: { kind: string; result: AnyArchiveResult; archive: ArchiveInfo; }) { if (kind === "backtest") { return ; } if (kind === "factor_test") { return ; } if (kind === "selection") { return ; } return ( ); } /* ------------------------------------------------------------------ */ function FactorReportView({ report }: { report: FactorTestReport }) { const qs = report.quantile_returns ?? []; const maxAbs = Math.max(0.001, ...qs.map((q) => Math.abs(q.return_pct))); return (
归档快照,只读} >
= 0 ? "pos" : "neg"} /> = 0 ? "pos" : "neg"} /> = 1 ? "pos" : "plain"} sub="越大越稳定" /> = 50 ? "pos" : "warn"} />
{qs.length > 0 ? (
分层表现(Q1 最低因子值 → Q{qs.length} 最高)
{qs.map((q) => { const v = q.return_pct; const h = Math.max(4, Math.round((Math.abs(v) / maxAbs) * 72)); return (
{v >= 0 ? "+" : ""} {v.toFixed(2)}%
= 0 ? "is-pos" : "is-neg"}`} style={{ height: h }} /> Q{q.quantile + 1}
); })}
) : null}
); } function SelectionSnapshotView({ result }: { result: SelectionResult }) { const c = result.candidates ?? []; return (
归档快照,只读} >
{c.length > 0 ? (
{c.map((row) => ( ))}
# 代码 / 名称 得分 因子值 入选理由
{row.rank} {row.score.toFixed(4)} {Object.entries(row.factor_values ?? {}) .map(([k, v]) => `${k}=${typeof v === "number" ? v.toFixed(4) : v}` ) .join(" ")}
{(row.selection_reason ?? []).map((r) => ( {r.slice(0, 40)} ))}
) : (
该归档没有候选明细(结果为空或历史版本)。
)}
); }