bt / algorithm tree
bt 算法树:Algo、AlgoStack 与 Strategy 怎么串起来
bt 最独特的地方不是「能回测」,而是把回测过程拆成可复用的算法对象:调度算法决定什么时候动手,选券算法决定动谁,权重算法决定分多少,再平衡算法决定怎么成交。这些算法按顺序组成一棵策略树,改一个节点就得到一套新策略。
算法树的三层
单个动作
串成链
算法链 + 证券
策略装策略
Algo、AlgoStack、Strategy 分别是什么?
三个类名都从 bt 顶层公开导出(bt/__init__.py),下表给出职责、适用场景与注意点。
| 类 | 职责 | 适用场景 | 注意点 |
|---|---|---|---|
Algo | 最小动作单元:被调用时做一件事并返回 True / False(__call__(target, data)) | 把一段判断逻辑抽出来复用 | 单个算法不产生交易,只改状态或目标权重 |
AlgoStack | 把多个算法按顺序串成一个整体,遇到 False 就短路 | 需要「条件不满足就跳过本次调仓」 | 顺序写反会得到完全不同的行为 |
Strategy | 策略树:持有算法栈与证券/子策略,按数据时间轴推进 | 定义单个策略或嵌套子策略 | 只写选券不写再平衡不会有交易 |
Security 系列 | 树的叶子:普通证券、固收、对冲等不同证券类型 | 多资产组合、固收与对冲场景 | 不同证券类型的字段要求不同 |
Backtest | 把策略与价格数据绑定成一次回测任务 | 策略 × 数据 × 成本假设 | 数据是「日期 × 标的」的收盘价矩阵 |
bt.run | 执行一个或多个 Backtest 并汇总结果 | 多策略横向对比 | 传多个回测时统计口径会合并,注意可比性 |
四个环节为什么要按这个顺序?
算法链是一条流水线,每一步都为下一步准备输入;下表说明各环的输入输出契约(取自 bt/algos.py 的类 docstring)。
| 环节 | 代表算法 | 它写入什么 | 下一步依赖什么 |
|---|---|---|---|
| 调度 | RunMonthly / RunDaily / RunQuarterly | 决定本次是否执行(返回 True/False) | 返回 False 时后面全部跳过 |
| 选券 | SelectAll / SelectN / SelectMomentum | temp['selected'] | 权重算法依赖 selected |
| 权重 | WeighEqually / WeighInvVol / WeighERC | weights(目标权重) | 再平衡算法依赖 weights |
| 再平衡 | Rebalance / RebalanceOverTime | 交易(把差额变成成交) | 通常放在链尾 |
| 限额与风控 | LimitWeights / LimitDeltas / TargetVol | 修改目标权重 | 放在权重之后、再平衡之前 |
| 记录与调试 | Debug / PrintInfo / PrintTempData | 控制台输出中间状态 | 不改变交易结果,只用于排查 |
怎么用逻辑算法做条件组合?
bt.algos 里提供了 Require、Not、Or 三个逻辑类,配合自定义 Algo 可以表达「满足条件才调仓」。
| 需求 | 写法思路 | 适用场景 | 注意点 |
|---|---|---|---|
| 只有在某条件成立时调仓 | 把条件算法与 Require(...) 组合 | 择时开关、风险过滤 | 条件算法必须返回 True/False |
| 反向条件 | 用 Not(algo) 取反 | 「不满足某条件才执行」 | 嵌套过深会让算法链难读,建议拆函数 |
| 多条件任一满足 | 用 Or(...) 合并 | 多套择时规则并联 | 短路语义意味着顺序会影响计算量 |
| 记录自定义状态 | 用 SetStat 写入统计量 | 输出中间指标供后续算法判断 | 统计量不是绩效指标,位置要在结果对象里读 |
| 多期统计 | StatTotalReturn / StatMultiPeriodReturn | 按多期收益做判断 | 窗口长度需与调度频率匹配 |
| 自定义 Algo | 继承 bt.Algo 并实现 __call__ | 库内算法表达不了的规则 | 返回值必须为布尔,且不要在里面直接下单 |
策略可以嵌套成组合吗?
README 把「把策略建成组合树」列为四大特性之一:策略与证券可以嵌进同一棵树,因此「先分策略、再合并成组合」可以写成一次回测。
| 结构 | 含义 | 适用场景 | 注意点 |
|---|---|---|---|
| 单策略多证券 | 一个 Strategy 里挂多个 Security | 等权或按规则分配的多资产组合 | 最常见结构,先从这里开始 |
| 多策略嵌套 | 父策略里挂子策略,各自有算法链 | 把不同逻辑拼成一个组合 | 嵌套层级的权重分配要明确 |
| 并行回测对比 | bt.run 传多个 Backtest | 横向比较几套算法组合 | 结果对象是组合统计,注意口径一致 |
| 随机基准对照 | benchmark_random(backtest, random_strategy, nsim=100) | 判断策略是否只是运气 | 模拟次数越多越慢,先小样本试 |
| 固收与对冲证券 | 使用固收 / 对冲类 Security 与对应 Strategy | 固收与对冲研究 | 字段要求更多,见「成本与固收」页 |
| 树的可读性 | 层级越深越难排查 | 建议每层只做一件事 | 用 PrintInfo/Debug 观察每层输入 |
算法树最容易写错的地方有哪些?
下表六种结构性错误都会「跑得通但结论错」,属于最难发现的一类问题。
| 错误 | 表现 | 排查方式 |
|---|---|---|
| 顺序写反 | 有调仓日但权重异常、交易稀少 | 按调度 → 选券 → 权重 → 再平衡逐项核对 |
| 缺再平衡算法 | 完全没有交易,结果等于买入持有 | 检查算法链最后是否有 Rebalance 类算法 |
| 把 AlgoStack 当 Strategy 用 | 无法绑定数据或缺少证券树 | 区分「算法集合」与「策略容器」两种用法 |
| 在 Algo 里直接下单 | 绕过权重与再平衡机制,结果不可解释 | Algo 只改状态与目标权重 |
| 调度与统计窗口不匹配 | 多期统计量失真 | 让统计窗口是调度频率的整数倍 |
| 一上来就跑全区间全标的 | 出错时无法定位是哪一环 | 先用小样本验证逻辑,再放大规模 |
bt 算法树常见问题
bt 里的「策略」和别的回测库有什么不一样?
别的库常见做法是写一个策略类、在里面用 if 描述买卖条件;bt 把回测过程拆成四类算法对象,用「算法链」表达同一件事。好处是每个环节都能单独替换做对照实验,代价是要先理解这条流水线的顺序。
必须按调度 → 选券 → 权重 → 再平衡写吗?
这是库设计的自然顺序:调度决定是否执行、选券写入 temp['selected']、权重算法依赖 selected 写入 weights、再平衡算法依赖 weights 成交。顺序被打破时,后一步读到的是上一次的中间状态,结果难以解释。
能不能自己写算法?
可以,继承 bt.Algo 并实现 __call__(target, data),返回 True / False。建议先在 bt.algos 的 59 个内置类里找有没有现成的(如 SelectWhere、SetStat),能复用就不要自写。
策略嵌套会不会影响统计口径?
会。多策略嵌套后统计是组合层结果,与单策略结果不是同一口径;要比较不同结构,请固定区间、数据与成本假设,只改变量。口径以官方文档与 ffn 文档为准。
算法链很长会不会很慢?
AlgoStack 是短路求值:前面的算法返回 False 时,后面的算法不会执行。因此把「最可能过滤掉本次调仓」的算法放前面,通常更快,也更易读。
怎么确认算法真的按预期触发了?
用 Debug、PrintInfo、PrintTempData 这类打印算法观察中间状态,或直接读 result.get_weights() 与 result.get_transactions() 核对调仓频率。