阅读提示
这篇是实战调试手册,目标是让你在回测结果不符合预期时,能快速定位问题。核心是 BacktestBase 的四大诊断点和对应的修复思路。
导言
跑完第一个回测,你可能遇到几种”不对劲”的情况:
- 净值曲线是平的,像一根直线。
- 明明设了 100 万资金,但
actual_weight全是 0。 - 策略跑了两天就停了,后面的日期都没数据。
- 报告里的收益统计看起来不错,但自己算了一下对不上。
这些问题的根源大多在同一个地方:权重矩阵到实际成交之间的损耗。Betalens 提供了一套诊断顺序,按部就班查下去,通常能在 5 分钟内找到原因。
诊断流程总览
当回测结果不符合预期时,按这个顺序查:
1 | ① actual_weight ──→ 有没有真实的股票权重? |
诊断点详解
① 检查 engine.actual_weight
actual_weight 是经过停牌处理、整数手取整之后,真正参与净值计算的权重。如果它和输入的 weight 差异很大,说明回测引擎做了大幅调整。
1 | import pandas as pd |
常见问题:actual_weight 接近 0,但 weight 有值。这通常是因为 position 全是 0(见诊断点②)。
② 检查 engine.position
position 是每日持仓(股数),由 actual_weight 和当日收盘价计算得出。如果它是 0,说明资金买不了任何整数手。
1 | # 看第一天的持仓 |
常见原因:
amount太小。例如 100 万买 A 股,每手 100 股,最便宜的股票也要几百元一手,1 万元买不了几手。- 分组太细,导致每组只有很少的股票,而每只需要买 1 手,资金分散后每只都买不了。
解决思路:
1 | # 方法1:提高初始资金 |
③ 检查 engine.trade_status_matrix
trade_status_matrix 记录每天每只股票的交易状态还原值:1 正常、0 停牌、-1 未上市。如果大量股票在某些日期是 0,BacktestBase 会按 trade_status_mode 处理。
1 | import numpy as np |
trade_status_mode 的四种策略:
| 模式 | 行为 | 适用场景 |
|---|---|---|
to_cash(默认) |
停牌股票转现金,按当日现金比例分配 | 保守、模拟真实交易 |
hold |
继续持有,停牌日不操作 | 长期持有、减少换手 |
redistribute |
停牌股票的权重重新分配给其他正常交易的股票 | 希望满仓运行 |
as_normal |
忽略停牌状态,按正常价格成交(不推荐) | 回测对照实验 |
如果想查看具体某只股票的停牌情况:
1 | # 看"平安银行"(000001.SZ)的停牌日期 |
④ 检查 engine.rebalance_log
rebalance_log 是调仓审计日志,记录每次调仓的详情:日期、股票代码、成交价、买入/卖出股数、成交手数、成交金额。
1 | log = engine.rebalance_log |
常见问题:
- 交易费用太高:可能是调仓太频繁(
rebal_freq设成了"D"每天调仓)。A 股默认印花税 + 佣金对日内交易非常不友好。 - 买入/卖出数量是 0:说明该股票当天无法成交(可能是停牌日)。
综合诊断脚本
把以上四点打包成一个函数,方便每次回测后调用:
1 | def diagnose_backtest(engine, top_n=10): |
在回测后调用:
1 | engine = BacktestBase(weight=weights, symbol="Dividend", amount=1_000_000, ...) |
开发者侧:整数手约束的工程实现
从源码角度看,BacktestBase 的成交逻辑在 betalens/backtest/backtest.py 里,核心步骤是:
1 | # 伪代码 |
lot_size默认 100(A 股)。floor(向下取整)是整数手约束的核心——永远买不到零头,这个零头差就是”碎片化损耗”。- 碎片化损耗在小资金、多持仓时尤其明显。100 万分成 20 只股票,每只 5 万,买 3 元股能买约 166 手(满足),但买 20 元股只能买约 250 手,两者的碎片损耗差异巨大。
常见问题速查
| 症状 | 最可能的原因 | 修复方向 |
|---|---|---|
| 净值曲线平直 | actual_weight 全 0 |
提高 amount,减少持仓数 |
| 回测中途净值下跌 | 停牌导致被动转现金 | 检查 trade_status_matrix,用 trade_status_mode="redistribute" |
| 收益和报告不一致 | 用了输入 weight 而非 actual_weight |
始终以 engine.actual_weight 为准 |
| 每天都在交易 | rebal_freq="D" |
改成 "W"(每周)或 "M"(每月) |
| 某只股票一直没成交 | 停牌日期太多 | 用 get_tradable_pool 时设 include_abnormal=True 观察,或看 trade_status_matrix |
延伸阅读
- Betalens 新手系列 · 02:五分钟跑通股息率多空——完整流水线。
- Betalens 新手系列 · 14:BacktestBase 全参数拆解——深入理解
check_trade_status/trade_status_mode/lot_size。 - Betalens 新手系列 · 15:调仓审计与现金管理——
rebalance_log的完整字段说明。 docs/guide/backtest.rst——官方回测文档。