/** * 归档结果的**纯派生逻辑**(无 React、无 "use client")。 * * 为什么单独放 `lib/`:归档详情页是 Server Component(只读内容必须能 SSR 直出), * 而 Server Component **不能调用** `"use client"` 模块里导出的普通函数 * (`Attempted to call X() from the server but X is on the client` → 整页 500, * 实测踩过一次)。所以「按 kind 派生展示口径」这类纯函数必须待在非客户端模块里, * 客户端组件与服务端组件都从 `lib/` 引用。 */ import type { BacktestResult, ExperimentDetail, FactorTestReport, SelectionResult, } from "@/lib/types"; /** 归档结果的联合类型(结构与 `kind` 一一对应)。 */ export type AnyArchiveResult = | BacktestResult | FactorTestReport | SelectionResult; /** 「带回测结果」的归档详情:对比视图与指标表只接受这一种。 */ export type BacktestDetail = ExperimentDetail & { result: BacktestResult }; /** 判断某条归档详情是否带回测结果(对比/指标只能用在回测归档上)。 */ export function isBacktestDetail(d: ExperimentDetail): d is BacktestDetail { return d.kind === "backtest" && !!d.result && "equity_curve" in d.result; } /** * 归档结果的**关键字段计数**,按 `kind` 给不同口径。 * * 用回测字段(净值/曲线/成交)去数因子测试或选股结果会得到 0 或 undefined, * 显示成「净值 0 点」属于误导;这里按类型分别统计,未知类型返回 null(不编造)。 */ export function archivedFieldSummary( kind: string, result: AnyArchiveResult | null ): string | null { if (!result) return null; if (kind === "backtest") { const r = result as BacktestResult; return `结果字段:净值 ${r.equity_curve?.length ?? 0} 点 · 个股曲线 ${ r.symbol_curves?.length ?? 0 } 条 · 成交 ${r.trades?.length ?? 0} 笔`; } if (kind === "factor_test") { const r = result as FactorTestReport; return `结果字段:因子 ${r.factor_name} · 样本 ${r.sample_days} 日 · 分层 ${ r.quantile_returns?.length ?? 0 } 层`; } if (kind === "selection") { const r = result as SelectionResult; return `结果字段:as_of ${r.as_of_date} · 候选 ${ r.candidates?.length ?? 0 } 只 · 评估 ${r.statistics?.evaluated ?? 0} 只`; } return null; }