"use client"; import { useEffect, useState } from "react"; import { apiGet } from "@/lib/api"; import { LineChart } from "@/components/LineChart"; import { submitJob, waitJob } from "@/lib/jobs"; import type { BacktestResult, FactorMeta, ResearchSpec } from "@/lib/types"; import { recentRange } from "@/lib/dates"; interface Pick { name: string; weight: number; } export default function ComposePage() { const [factors, setFactors] = useState([]); const [picks, setPicks] = useState([]); const [topN, setTopN] = useState(10); const [rebalance, setRebalance] = useState<"monthly" | "weekly">("monthly"); const [excludeSt, setExcludeSt] = useState(true); const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const [jobId, setJobId] = useState(""); const [error, setError] = useState(""); useEffect(() => { apiGet("/factors") .then((list) => { setFactors(list); // 默认示例组合:中期动量(重)+ 低波动(防) setPicks([ { name: "momentum_60", weight: 1.5 }, { name: "volatility_60", weight: 1.0 }, ]); }) .catch((e: Error) => setError(e.message)); const { start: s, end: e } = recentRange(); setStart(s); setEnd(e); }, []); function togglePick(name: string) { setPicks((prev) => prev.some((p) => p.name === name) ? prev.filter((p) => p.name !== name) : [...prev, { name, weight: 1 }], ); } function setWeight(name: string, weight: number) { setPicks((prev) => prev.map((p) => (p.name === name ? { ...p, weight } : p))); } async function run() { const valid = picks.filter((p) => Number.isFinite(p.weight) && p.weight > 0); if (valid.length === 0) { setError("请选择至少一个因子并设置大于 0 的权重"); return; } setRunning(true); setError(""); setJobId(""); setResult(null); try { const spec: ResearchSpec = { type: "backtest", universe: { exclude_st: excludeSt, min_listing_days: 0 }, factors: valid.map((p) => ({ name: p.name, weight: p.weight })), selection: { top_n: topN }, rebalance, period: [start, end], }; const { job_id } = await submitJob(spec); setJobId(job_id); const out = await waitJob(job_id); if (out.status === "success" && out.result) { setResult(out.result); } else { setError(`任务${out.status}${out.error ? `:${out.error}` : ""}`); } } catch (e) { setError((e as Error).message); } finally { setRunning(false); setJobId(""); } } return ( <>

因子组合

组合方式(如何合成一个分数)

  1. 选择若干因子并设置权重(默认 1;低为好方向由引擎自动反向)。
  2. 每个交易日:每个因子在全体股票横截面上做 z-score 标准化 (该日缺失因子值的股票不参与该因子得分)。
  3. 综合得分 = Σ(权重 × 标准化后因子得分),得分越高越优先。
  4. 每期按综合得分取 Top N 等权持有,到下一调仓日再平衡(含成本 / 涨跌停 / 停牌近似)。

选因子 + 设权重

{factors.map((f) => { const pick = picks.find((p) => p.name === f.name); return ( ); })}
因子 方向 权重 简介
togglePick(f.name)} /> {f.name} {f.direction === "higher_is_better" ? "高为好" : "低为好"} setWeight(f.name, Number(e.target.value))} /> {f.brief ?? f.description}

回测条件

{running && jobId &&
任务 {jobId} 后台执行中,请稍候…
} {error &&
{error}
}
{result && } ); } function ResultView({ result }: { result: BacktestResult }) { const s = result.summary; return ( <>

结果 · {s.total_return_pct.toFixed(2)}%{" "} = 0 ? "good" : "bad"}> {s.total_return_pct >= 0 ? "▲" : "▼"}

期末净值
{s.final_equity.toLocaleString()}
年化收益
{s.annual_return_pct.toFixed(2)}%
Sharpe
{s.sharpe.toFixed(2)}
最大回撤
{s.max_drawdown_pct.toFixed(2)}%
胜率
{s.win_rate_pct.toFixed(1)}%
交易 / 换手
{s.total_trades} / {s.avg_turnover_pct.toFixed(0)}%
{result.monthly_returns.length > 0 && (

月度收益(%)

{result.monthly_returns.map((m) => ( ))}
月份 收益
{m.year}-{String(m.month).padStart(2, "0")} = 0 ? "good" : "bad"}>{m.return_pct.toFixed(2)}%
)}

未建模约束(如实标注)

    {result.unimplemented.map((u, i) =>
  • {u}
  • )}
); }