diff --git a/frontend/web/app/backtest/page.tsx b/frontend/web/app/backtest/page.tsx index 9c2e5ab..2893966 100644 --- a/frontend/web/app/backtest/page.tsx +++ b/frontend/web/app/backtest/page.tsx @@ -28,6 +28,10 @@ export default function BacktestPage() { const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const [excludeSt, setExcludeSt] = useState(true); + const [commission, setCommission] = useState(0.03); // % + const [stamp, setStamp] = useState(0.05); + const [slippage, setSlippage] = useState(0.1); + const [capital, setCapital] = useState(1_000_000); const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const [jobId, setJobId] = useState(""); @@ -65,6 +69,12 @@ export default function BacktestPage() { factors: [{ name: factor, weight: 1 }], selection: { top_n: topN }, rebalance, + costs: { + commission_rate: commission / 100, + stamp_tax_rate: stamp / 100, + slippage_rate: slippage / 100, + }, + initial_capital: capital, period: [start, end], }; // 异步 Job:后台执行(全市场可能数十秒),轮询到终态 @@ -133,6 +143,22 @@ export default function BacktestPage() { + + setCommission(Number(e.target.value))} /> + + + setStamp(Number(e.target.value))} /> + + + setSlippage(Number(e.target.value))} /> + + + setCapital(Number(e.target.value))} /> + setStart(e.target.value)} /> @@ -228,6 +254,58 @@ function ResultView({ result }: { result: BacktestResult }) { )} + {result.yearly_returns.length ? ( + 均值 {(result.yearly_returns.reduce((a, y) => a + y.return_pct, 0) / result.yearly_returns.length).toFixed(2)}%}> +
+ {result.yearly_returns.map((y) => ( + + {y.year} + = 0 ? "tone-pos" : "tone-neg"}> + {y.return_pct.toFixed(2)}% + + + ))} +
+
+ ) : null} + + {result.trades.length ? ( + {result.turnover_pct.toFixed(2)}% 累计换手} + > +
+ + + + + + + + + + + + + {result.trades.map((t) => ( + + + + + + + + + ))} + +
买入日卖出日代码买价卖价收益
{t.entry_date}{t.exit_date}{t.symbol}{t.entry_price?.toFixed(2) ?? "-"}{t.exit_price?.toFixed(2) ?? "-"}= 0 ? "tone-pos" : "tone-neg"}> + {t.return_pct.toFixed(2)}% +
+
+
+ ) : null} + ); diff --git a/frontend/web/app/page.tsx b/frontend/web/app/page.tsx index 5b3dd5d..0004b85 100644 --- a/frontend/web/app/page.tsx +++ b/frontend/web/app/page.tsx @@ -36,6 +36,20 @@ const LINKS = [ icon: "gauge" as const, tone: "quick-card--warn", }, + { + href: "/selection", + title: "股票筛选", + desc: "因子评分 TopN 或条件选股,支持当前/历史时点,结果可解释", + icon: "target" as const, + tone: "quick-card--violet", + }, + { + href: "/signals", + title: "交易信号", + desc: "按评分排名 + 趋势规则输出 BUY / WATCH / SELL 及理由", + icon: "scale" as const, + tone: "quick-card--pos", + }, { href: "/stocks", title: "股票池", diff --git a/frontend/web/lib/types.ts b/frontend/web/lib/types.ts index 6afba02..0856f0f 100644 --- a/frontend/web/lib/types.ts +++ b/frontend/web/lib/types.ts @@ -27,9 +27,18 @@ export interface ResearchSpec { min_listing_days?: number; symbols?: string[]; }; + price_adjustment?: "none" | "qfq"; factors: { name: string; weight: number }[]; selection: { top_n: number }; rebalance: "weekly" | "monthly"; + costs?: { + commission_rate?: number; + stamp_tax_rate?: number; + slippage_rate?: number; + benchmark?: string; + }; + portfolio?: { weighting?: string; max_position_pct?: number | null; max_industry_weight_pct?: number | null }; + initial_capital?: number; period: [string, string]; } @@ -60,7 +69,7 @@ export interface BacktestResult { monthly_returns: { year: number; month: number; return_pct: number }[]; yearly_returns: { year: number; return_pct: number }[]; positions: { date: string; symbol: string; weight: number }[]; - trades: { entry_date: string; exit_date: string; symbol: string; return_pct: number }[]; + trades: { entry_date: string; exit_date: string; symbol: string; entry_price?: number; exit_price?: number; return_pct: number }[]; turnover_pct: number; unimplemented: string[]; config_snapshot: Record;