"use client"; import { useEffect, useState } from "react"; import { apiGet } from "@/lib/api"; import { submitJob, waitJob } from "@/lib/jobs"; import type { FactorMeta, FactorTestReport, ResearchSpec } from "@/lib/types"; import { recentRange } from "@/lib/dates"; export default function FactorsPage() { const [factors, setFactors] = useState([]); const [checked, setChecked] = useState>(new Set()); const [expanded, setExpanded] = useState>(new Set()); const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const [reports, setReports] = useState>({}); const [running, setRunning] = useState(false); const [current, setCurrent] = useState<{ idx: number; total: number; name: string } | null>(null); const [jobId, setJobId] = useState(""); const [error, setError] = useState(""); useEffect(() => { apiGet("/factors") .then((list) => { setFactors(list); // 默认勾选两个代表性因子 const picks = ["momentum_60", "volatility_60"].filter((n) => list.some((f) => f.name === n)); setChecked(new Set(picks)); }) .catch((e: Error) => setError(e.message)); // 区间默认:近 6 个月(客户端设置,避免 SSR 日期不一致) const { start: s, end: e } = recentRange(); setStart(s); setEnd(e); }, []); function toggleCheck(name: string) { setChecked((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); else next.add(name); return next; }); } function toggleExpand(name: string) { setExpanded((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); else next.add(name); return next; }); } async function run() { const names = factors.map((f) => f.name).filter((n) => checked.has(n)); if (names.length === 0) { setError("请至少选择一个因子"); return; } setRunning(true); setError(""); setJobId(""); setReports({}); try { let index = 0; for (const name of names) { index += 1; setCurrent({ idx: index, total: names.length, name }); const spec: ResearchSpec = { type: "factor_test", universe: { exclude_st: true, min_listing_days: 0 }, factors: [{ name, weight: 1 }], selection: { top_n: 10 }, rebalance: "monthly", period: [start, end], }; const { job_id } = await submitJob(spec); setJobId(job_id); const out = await waitJob(job_id); if (out.status === "success" && out.result) { const report = out.result; setReports((prev) => ({ ...prev, [name]: report })); } else { const msg = `因子 ${name}:${out.status}${out.error ? `:${out.error}` : ""}`; setError((prev) => (prev ? `${prev}\n${msg}` : msg)); } } } catch (e) { setError((e as Error).message); } finally { setRunning(false); setCurrent(null); setJobId(""); } } const selectedCount = checked.size; const doneCount = Object.keys(reports).length; return ( <>

因子研究

因子目录(行情因子 · 无未来函数)

勾选因子后可多选;点击行展开详细说明。每个因子运行一次独立的 IC/RankIC 测试并归档。 用这些因子搭建选股组合 → 因子组合
{factors.map((f) => ( toggleCheck(f.name)} onToggleExpand={() => toggleExpand(f.name)} /> ))}
因子 回看 方向 简介(用法 / 何时有效)
计分规则:每因子在每日横截面上 z-score 标准化(低为好自动取负),组合页可按权重叠加得分选股; 本页单个因子测试各自归档为 Experiment,可去「实验」页查看与复跑。

运行单因子测试(IC / RankIC / 分层)

{running && (
正在运行:{current?.name}({current?.idx}/{current?.total})—— 后台任务 {jobId},请稍候…
)} {error &&
{error}
}
{Object.entries(reports).map(([name, report]) => { const meta = factors.find((f) => f.name === name); return (

{name} · 测试报告 {meta?.brief ? ( {" "}— {meta.brief} ) : null}

); })} ); } function FactorRow(props: { factor: FactorMeta; checked: boolean; expanded: boolean; onToggleCheck: () => void; onToggleExpand: () => void; }) { const { factor: f } = props; return ( <> { e.stopPropagation(); props.onToggleCheck(); }}> {f.name} {f.lookback} {f.direction === "higher_is_better" ? "高为好" : "低为好"} {f.brief ?? f.description} {props.expanded && (
{f.name} · {f.description}
公式:{f.formula};回看 {f.lookback} 个交易日;频率 {f.frequency}; 方向:{f.direction === "higher_is_better" ? "因子值越高得分越高" : "因子值越低得分越高(引擎自动反向)"}
{f.brief}
)} ); } function ReportView({ report }: { report: FactorTestReport }) { return ( <>
IC 均值
{report.ic_mean.toFixed(4)}
RankIC 均值
{report.rank_ic_mean.toFixed(4)}
ICIR
{report.icir.toFixed(3)}
正收益占比
{report.positive_ratio_pct.toFixed(1)}%
样本日数
{report.sample_days}
{report.quantile_returns.length > 0 && ( {report.quantile_returns.map((q) => ( ))} {report.quantile_returns.map((q) => ( ))}
分层(1 最低 → 5 最高)Q{q.quantile + 1}
未来 21 日平均收益{q.return_pct.toFixed(2)}%
)}
读数:IC/RankIC 为正表示与未来收益正相关;ICIR 越大越稳定;分层收益若高分层显著高于低分层说明单调性好。 单因子测试≠策略有效,需结合样本外与稳健性分析。
); }