bt / algorithm tree

bt 算法树:Algo、AlgoStack 与 Strategy 怎么串起来

bt 最独特的地方不是「能回测」,而是把回测过程拆成可复用的算法对象:调度算法决定什么时候动手,选券算法决定动谁,权重算法决定分多少,再平衡算法决定怎么成交。这些算法按顺序组成一棵策略树,改一个节点就得到一套新策略。

核心:Algo / AlgoStack / Strategy顺序即语义:先调度后选券基于包源码 bt 1.2.3

算法树的三层

Algo
单个动作
AlgoStack
串成链
Strategy
算法链 + 证券
嵌套
策略装策略
Algo 是最小动作单元,AlgoStack 把多个 Algo 串成一条链,Strategy 把链与证券组合成一棵树,树还可以再嵌套。
59 个算法bt.algos 内置类总数(1.2.3)
短路语义AlgoStack 遇 False 即停止后续
可复用换一个算法 = 一套新策略
证据bt/core.py 与 bt/algos.py
Three layers

Algo、AlgoStack、Strategy 分别是什么?

三个类名都从 bt 顶层公开导出(bt/__init__.py),下表给出职责、适用场景与注意点。

职责适用场景注意点
Algo最小动作单元:被调用时做一件事并返回 True / False(__call__(target, data)把一段判断逻辑抽出来复用单个算法不产生交易,只改状态或目标权重
AlgoStack把多个算法按顺序串成一个整体,遇到 False 就短路需要「条件不满足就跳过本次调仓」顺序写反会得到完全不同的行为
Strategy策略树:持有算法栈与证券/子策略,按数据时间轴推进定义单个策略或嵌套子策略只写选券不写再平衡不会有交易
Security 系列树的叶子:普通证券、固收、对冲等不同证券类型多资产组合、固收与对冲场景不同证券类型的字段要求不同
Backtest把策略与价格数据绑定成一次回测任务策略 × 数据 × 成本假设数据是「日期 × 标的」的收盘价矩阵
bt.run执行一个或多个 Backtest 并汇总结果多策略横向对比传多个回测时统计口径会合并,注意可比性
Execution order

四个环节为什么要按这个顺序?

算法链是一条流水线,每一步都为下一步准备输入;下表说明各环的输入输出契约(取自 bt/algos.py 的类 docstring)。

环节代表算法它写入什么下一步依赖什么
调度RunMonthly / RunDaily / RunQuarterly决定本次是否执行(返回 True/False)返回 False 时后面全部跳过
选券SelectAll / SelectN / SelectMomentumtemp['selected']权重算法依赖 selected
权重WeighEqually / WeighInvVol / WeighERCweights(目标权重)再平衡算法依赖 weights
再平衡Rebalance / RebalanceOverTime交易(把差额变成成交)通常放在链尾
限额与风控LimitWeights / LimitDeltas / TargetVol修改目标权重放在权重之后、再平衡之前
记录与调试Debug / PrintInfo / PrintTempData控制台输出中间状态不改变交易结果,只用于排查
顺序不改的后果:把权重算法放到选券之前,权重算的是「上一次的选券结果」甚至空集合,表现就是「有调仓日但权重异常、交易很少」;把再平衡算法放中间,后半段的权重改动不会被成交。
Composition

怎么用逻辑算法做条件组合?

bt.algos 里提供了 RequireNotOr 三个逻辑类,配合自定义 Algo 可以表达「满足条件才调仓」。

需求写法思路适用场景注意点
只有在某条件成立时调仓把条件算法与 Require(...) 组合择时开关、风险过滤条件算法必须返回 True/False
反向条件Not(algo) 取反「不满足某条件才执行」嵌套过深会让算法链难读,建议拆函数
多条件任一满足Or(...) 合并多套择时规则并联短路语义意味着顺序会影响计算量
记录自定义状态SetStat 写入统计量输出中间指标供后续算法判断统计量不是绩效指标,位置要在结果对象里读
多期统计StatTotalReturn / StatMultiPeriodReturn按多期收益做判断窗口长度需与调度频率匹配
自定义 Algo继承 bt.Algo 并实现 __call__库内算法表达不了的规则返回值必须为布尔,且不要在里面直接下单
组合顺序建议:先用最小样本(2 个标的、3 个月)验证逻辑是否按预期触发,再放到完整区间上跑。
Nesting

策略可以嵌套成组合吗?

README 把「把策略建成组合树」列为四大特性之一:策略与证券可以嵌进同一棵树,因此「先分策略、再合并成组合」可以写成一次回测。

结构含义适用场景注意点
单策略多证券一个 Strategy 里挂多个 Security等权或按规则分配的多资产组合最常见结构,先从这里开始
多策略嵌套父策略里挂子策略,各自有算法链把不同逻辑拼成一个组合嵌套层级的权重分配要明确
并行回测对比bt.run 传多个 Backtest横向比较几套算法组合结果对象是组合统计,注意口径一致
随机基准对照benchmark_random(backtest, random_strategy, nsim=100)判断策略是否只是运气模拟次数越多越慢,先小样本试
固收与对冲证券使用固收 / 对冲类 Security 与对应 Strategy固收与对冲研究字段要求更多,见「成本与固收」页
树的可读性层级越深越难排查建议每层只做一件事用 PrintInfo/Debug 观察每层输入
Mistakes

算法树最容易写错的地方有哪些?

下表六种结构性错误都会「跑得通但结论错」,属于最难发现的一类问题。

错误表现排查方式
顺序写反有调仓日但权重异常、交易稀少按调度 → 选券 → 权重 → 再平衡逐项核对
缺再平衡算法完全没有交易,结果等于买入持有检查算法链最后是否有 Rebalance 类算法
把 AlgoStack 当 Strategy 用无法绑定数据或缺少证券树区分「算法集合」与「策略容器」两种用法
在 Algo 里直接下单绕过权重与再平衡机制,结果不可解释Algo 只改状态与目标权重
调度与统计窗口不匹配多期统计量失真让统计窗口是调度频率的整数倍
一上来就跑全区间全标的出错时无法定位是哪一环先用小样本验证逻辑,再放大规模
FAQ

bt 算法树常见问题

bt 里的「策略」和别的回测库有什么不一样?

别的库常见做法是写一个策略类、在里面用 if 描述买卖条件;bt 把回测过程拆成四类算法对象,用「算法链」表达同一件事。好处是每个环节都能单独替换做对照实验,代价是要先理解这条流水线的顺序。

必须按调度 → 选券 → 权重 → 再平衡写吗?

这是库设计的自然顺序:调度决定是否执行、选券写入 temp['selected']、权重算法依赖 selected 写入 weights、再平衡算法依赖 weights 成交。顺序被打破时,后一步读到的是上一次的中间状态,结果难以解释。

能不能自己写算法?

可以,继承 bt.Algo 并实现 __call__(target, data),返回 True / False。建议先在 bt.algos 的 59 个内置类里找有没有现成的(如 SelectWhereSetStat),能复用就不要自写。

策略嵌套会不会影响统计口径?

会。多策略嵌套后统计是组合层结果,与单策略结果不是同一口径;要比较不同结构,请固定区间、数据与成本假设,只改变量。口径以官方文档与 ffn 文档为准。

算法链很长会不会很慢?

AlgoStack 是短路求值:前面的算法返回 False 时,后面的算法不会执行。因此把「最可能过滤掉本次调仓」的算法放前面,通常更快,也更易读。

怎么确认算法真的按预期触发了?

DebugPrintInfoPrintTempData 这类打印算法观察中间状态,或直接读 result.get_weights()result.get_transactions() 核对调仓频率。

下一步:继续写策略还是先看数据?

策略写好后,准备自己的 OHLCV 数据接入回测,或了解完整功能特性;两页都会给出可以照着核对的步骤。