feat(web): M8.6b 回测页做实 + 首页入口更新(零新后端 API)
- backtest 页:成本参数表单(手续费/印花税/滑点 %)+ 初始资金 → ResearchSpec.costs/ initial_capital;结果新增年度收益卡与成交明细表(买/卖日、买/卖价、收益、累计换手) - Dashboard 入口卡加入「股票筛选」「交易信号」(6 张) - lib/types.ts:ResearchSpec 增加 price_adjustment/costs/portfolio/initial_capital, Trade 补 entry_price/exit_price - 页面 200 冒烟 + tsc --noEmit 通过
This commit is contained in:
@@ -28,6 +28,10 @@ export default function BacktestPage() {
|
|||||||
const [start, setStart] = useState("");
|
const [start, setStart] = useState("");
|
||||||
const [end, setEnd] = useState("");
|
const [end, setEnd] = useState("");
|
||||||
const [excludeSt, setExcludeSt] = useState(true);
|
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<BacktestResult | null>(null);
|
const [result, setResult] = useState<BacktestResult | null>(null);
|
||||||
const [running, setRunning] = useState(false);
|
const [running, setRunning] = useState(false);
|
||||||
const [jobId, setJobId] = useState("");
|
const [jobId, setJobId] = useState("");
|
||||||
@@ -65,6 +69,12 @@ export default function BacktestPage() {
|
|||||||
factors: [{ name: factor, weight: 1 }],
|
factors: [{ name: factor, weight: 1 }],
|
||||||
selection: { top_n: topN },
|
selection: { top_n: topN },
|
||||||
rebalance,
|
rebalance,
|
||||||
|
costs: {
|
||||||
|
commission_rate: commission / 100,
|
||||||
|
stamp_tax_rate: stamp / 100,
|
||||||
|
slippage_rate: slippage / 100,
|
||||||
|
},
|
||||||
|
initial_capital: capital,
|
||||||
period: [start, end],
|
period: [start, end],
|
||||||
};
|
};
|
||||||
// 异步 Job:后台执行(全市场可能数十秒),轮询到终态
|
// 异步 Job:后台执行(全市场可能数十秒),轮询到终态
|
||||||
@@ -133,6 +143,22 @@ export default function BacktestPage() {
|
|||||||
<option value="weekly">周度</option>
|
<option value="weekly">周度</option>
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field label="手续费率 %">
|
||||||
|
<input className="input" type="number" step="0.01" min={0} value={commission}
|
||||||
|
onChange={(e) => setCommission(Number(e.target.value))} />
|
||||||
|
</Field>
|
||||||
|
<Field label="印花税率 %">
|
||||||
|
<input className="input" type="number" step="0.01" min={0} value={stamp}
|
||||||
|
onChange={(e) => setStamp(Number(e.target.value))} />
|
||||||
|
</Field>
|
||||||
|
<Field label="滑点率 %">
|
||||||
|
<input className="input" type="number" step="0.01" min={0} value={slippage}
|
||||||
|
onChange={(e) => setSlippage(Number(e.target.value))} />
|
||||||
|
</Field>
|
||||||
|
<Field label="初始资金(元)">
|
||||||
|
<input className="input" type="number" step={100000} min={10000} value={capital}
|
||||||
|
onChange={(e) => setCapital(Number(e.target.value))} />
|
||||||
|
</Field>
|
||||||
<Field label="开始日期">
|
<Field label="开始日期">
|
||||||
<input type="date" className="input" value={start} onChange={(e) => setStart(e.target.value)} />
|
<input type="date" className="input" value={start} onChange={(e) => setStart(e.target.value)} />
|
||||||
</Field>
|
</Field>
|
||||||
@@ -228,6 +254,58 @@ function ResultView({ result }: { result: BacktestResult }) {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{result.yearly_returns.length ? (
|
||||||
|
<Card icon="calendar" title="年度收益(%)" tools={<Pill>均值 {(result.yearly_returns.reduce((a, y) => a + y.return_pct, 0) / result.yearly_returns.length).toFixed(2)}%</Pill>}>
|
||||||
|
<div className="chips">
|
||||||
|
{result.yearly_returns.map((y) => (
|
||||||
|
<span className="chip" key={y.year}>
|
||||||
|
<b>{y.year}</b>
|
||||||
|
<span className={y.return_pct >= 0 ? "tone-pos" : "tone-neg"}>
|
||||||
|
{y.return_pct.toFixed(2)}%
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{result.trades.length ? (
|
||||||
|
<Card
|
||||||
|
icon="scale"
|
||||||
|
title={`成交明细 · ${result.trades.length} 笔`}
|
||||||
|
tools={<Pill>{result.turnover_pct.toFixed(2)}% 累计换手</Pill>}
|
||||||
|
>
|
||||||
|
<div className="table-wrap">
|
||||||
|
<table className="tbl">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>买入日</th>
|
||||||
|
<th>卖出日</th>
|
||||||
|
<th>代码</th>
|
||||||
|
<th>买价</th>
|
||||||
|
<th>卖价</th>
|
||||||
|
<th>收益</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{result.trades.map((t) => (
|
||||||
|
<tr key={`${t.symbol}-${t.entry_date}-${t.exit_date}`}>
|
||||||
|
<td className="mono dim">{t.entry_date}</td>
|
||||||
|
<td className="mono dim">{t.exit_date}</td>
|
||||||
|
<td className="mono"><b>{t.symbol}</b></td>
|
||||||
|
<td className="mono">{t.entry_price?.toFixed(2) ?? "-"}</td>
|
||||||
|
<td className="mono">{t.exit_price?.toFixed(2) ?? "-"}</td>
|
||||||
|
<td className={t.return_pct >= 0 ? "tone-pos" : "tone-neg"}>
|
||||||
|
{t.return_pct.toFixed(2)}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<UnimplementedNote items={result.unimplemented} />
|
<UnimplementedNote items={result.unimplemented} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,20 @@ const LINKS = [
|
|||||||
icon: "gauge" as const,
|
icon: "gauge" as const,
|
||||||
tone: "quick-card--warn",
|
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",
|
href: "/stocks",
|
||||||
title: "股票池",
|
title: "股票池",
|
||||||
|
|||||||
@@ -27,9 +27,18 @@ export interface ResearchSpec {
|
|||||||
min_listing_days?: number;
|
min_listing_days?: number;
|
||||||
symbols?: string[];
|
symbols?: string[];
|
||||||
};
|
};
|
||||||
|
price_adjustment?: "none" | "qfq";
|
||||||
factors: { name: string; weight: number }[];
|
factors: { name: string; weight: number }[];
|
||||||
selection: { top_n: number };
|
selection: { top_n: number };
|
||||||
rebalance: "weekly" | "monthly";
|
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];
|
period: [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +69,7 @@ export interface BacktestResult {
|
|||||||
monthly_returns: { year: number; month: number; return_pct: number }[];
|
monthly_returns: { year: number; month: number; return_pct: number }[];
|
||||||
yearly_returns: { year: number; return_pct: number }[];
|
yearly_returns: { year: number; return_pct: number }[];
|
||||||
positions: { date: string; symbol: string; weight: 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;
|
turnover_pct: number;
|
||||||
unimplemented: string[];
|
unimplemented: string[];
|
||||||
config_snapshot: Record<string, unknown>;
|
config_snapshot: Record<string, unknown>;
|
||||||
|
|||||||
Reference in New Issue
Block a user