Backtesting.py / Strategy

Backtesting.py 策略循环:init 与 next 的写法、指标接入与参数优化

策略开发是回测的核心:继承 Strategy 基类、在 init() 里注册指标、在 next() 里写买卖逻辑,再用内置优化器做参数网格搜索与热力图分析。官方 README 给的是一段完整示例,但真正卡人的是「每一步在做什么、写错会怎样」——本页把这条链路拆成可复制的步骤,并补上参数可信度判断与结构性错误排查。

Strategy:策略基类指标:BYO / talib优化:optimize + 热力图
Strategy继承基类
init()注册指标
next()买卖逻辑
optimize()参数搜索
Backtesting.py 策略开发与回测流程示意(基于官方 README)。
Write Strategy

Backtesting.py 策略类怎么写:五步从空文件到能跑通

Backtesting.py 的策略骨架只有两个方法:init() 算指标、next() 做决策。下面五步按顺序做,每步给出真实代码、一句话说明和可以核对的预期输出。跑不到预期结果时就停在这一步排查——报错信息往往指向最后一行,而真正的起因经常在前面几步。

  1. 导入两个类

    从包顶层取 Backtest(回测器)与 Strategy(策略基类);策略必须继承 Strategy,回测器才能调度它。这三行同时验证了包是否装在当前解释器里——「装错环境」比「策略写错」更常见。

    from backtesting import Backtest, Strategy
    from backtesting.lib import crossover
    from backtesting.test import SMA, GOOG

    预期输出:无报错。若提示 ModuleNotFoundError,说明当前解释器没装包,先回下载部署页的自检清单。

  2. 在 init() 里注册指标

    init() 只在回测开始时执行一次,负责把指标算好并注册到策略上;self.I() 的返回值之后可以在 next() 里按序列取用。窗口写成类属性(n1 / n2)而不是写死数字,是为了后面能直接做参数搜索。

    class SmaCross(Strategy):
        n1 = 10
        n2 = 20
    
        def init(self):
            price = self.data.Close
            self.ma1 = self.I(SMA, price, self.n1)
            self.ma2 = self.I(SMA, price, self.n2)

    预期输出:无输出。指标序列与数据等长;长度不匹配会在 run() 阶段才报错,所以别在这一步就以为「没问题了」。

  3. 在 next() 里写交易规则

    next() 按 bar 逐个调用,这里用 crossover() 判断两条均线谁上穿谁:快线上穿慢线买入,慢线上穿快线卖出。规则要写成「事件」(交叉那一刻),不要写成「状态比较」(快线一直在慢线上方就一直买入),否则会反复下单。

        def next(self):
            if crossover(self.ma1, self.ma2):
                self.buy()
            elif crossover(self.ma2, self.ma1):
                self.sell()

    预期输出:无输出。真正的成交记录要等 run() 之后才看得到。

  4. 处理持仓与平仓

    self.position 能读到当前持仓(is_long / is_short),self.position.close() 平掉现有仓位。多空混合的策略里,平仓条件与开仓条件要成对写,避免方向叠加或反复开平。

        def next(self):
            if self.position.is_long and crossover(self.ma2, self.ma1):
                self.position.close()

    预期输出:无输出。跑完后结果里的交易笔数应与你预期的量级一致;差得离谱通常是条件没写成对。

  5. 创建回测并读结果

    把数据、策略类、资金与成本参数交给回测器,run() 返回统计结果;exclusive_orders=True 让每个新信号先平掉反向仓。commission 一定要显式给,默认值不等于你真实市场的摩擦成本。

    bt = Backtest(GOOG, SmaCross, cash=10_000, commission=.002, exclusive_orders=True)
    stats = bt.run()
    print(stats)

    预期输出:打印一份以 Return [%]、Max. Drawdown [%]、Sharpe Ratio、# Trades 等字段开头的结果面板;字段名与口径以实际安装版本为准。

操作写法适用场景注意点
开多self.buy()趋势跟随、突破入场资金或成交量不足时会被拒单,先确认 cash 与标的最小交易单位
开空self.sell()单标的做空、对冲先确认标的是否允许做空,不然结果没有实际意义
平仓self.position.close()反向信号、固定持有期与开仓条件成对写,否则容易出现反复开平
读持仓self.position.is_long / is_short条件分支判断不要用「价格涨了」代替方向判断,两者不等价
读行情self.data.Close / self.data.Open计算信号与过滤条件数据只要求 OHLC(V) 列,其它字段要自己准备并对齐
读权益self.equity动态仓位、按权益止损逐 bar 变化,不要拿它当静态阈值
Indicators

Backtesting.py 指标库无关怎么落地:三条接入路径对照

官方 README 把「指标库无关(BYO)」写成一个特性,落地时要在三条路径里选:内置工具、第三方库、自己用 pandas / NumPy 算。三条都能跑,差别在依赖、可控性与可复现性。

路径写法示例适用场景注意点
内置 libself.I(SMA, self.data.Close, 10)快速验证均线类想法只提供基础工具,复杂指标要自己写
talibself.I(talib.RSI, self.data.Close, 14)需要成熟技术指标talib 要单独安装且依赖系统库,装不上时先查系统依赖再看 Python 侧
pandas 滚动self.I(lambda c: c.rolling(20).mean(), self.data.Close)自定义指标、快速试验返回序列长度与索引要与数据对齐,否则 run() 阶段报错
NumPy 向量化自己写函数后交给 self.I()计算量大的指标返回值必须与原序列等长,长度算错会很难定位
指标参数化把窗口写成类属性 n1 / n2准备做参数搜索的策略属性名不要与基类已有属性冲突
多指标组合init() 里依次注册需要多个条件确认指标越多越容易过拟合,先各自单独看再组合
预热期处理前 N 根 bar 没有值所有均线类与长周期指标预热期内不产生信号,别误判成逻辑写错
说明:self.I() 会缓存指标计算结果,next() 中可按索引取用;talib 的安装方式与支持的指标集以官方文档为准。指标本身不是策略,把指标阈值直接当买卖信号前,先想清楚它在什么行情里会失效。
Optimize

Backtesting.py 参数优化:从参数网格到热力图

Backtesting.py 的内置优化器让「一组参数」变成「一片参数」,能一次看到参数的稳定区间。但同一套机制也会一次制造大量「偶然看起来很好」的组合,所以这一步的重点不是找峰值,而是判断哪片区域可信。

  1. 把要搜索的参数提成类属性

    只有写成类属性的参数才能被优化器扫描;写死在 init() 里的数字对优化器不可见。

    class SmaCross(Strategy):
        n1 = 10
        n2 = 20
    
        def init(self):
            self.ma1 = self.I(SMA, self.data.Close, self.n1)
            self.ma2 = self.I(SMA, self.data.Close, self.n2)

    预期输出:无输出。可以先手动改 n1 跑两次 run(),确认结果确实随参数变化。

  2. 调用 optimize() 做网格搜索

    把参数取值(通常是 range)、优化目标 maximize 与可选约束 constraint 传给优化器;约束用来排除无意义的组合,例如快线窗口不小于慢线。

    stats, heatmap = bt.optimize(
        n1=range(5, 30, 5),
        n2=range(10, 70, 10),
        maximize='Equity Final [$]',
        constraint=lambda p: p.n1 < p.n2)
    print(heatmap)

    预期输出:返回优化后的结果与一张热力图对象(具体返回顺序与字段以官方文档为准);打印时会看到以参数为行列表头的数值矩阵。

  3. 读热力图:看邻域,不看单点

    热力图的价值在于形态:稳定区是一片连续的高值,偶然值是一个孤立的亮点。只盯那个亮点,等于把噪声当规律。

    print(heatmap)              # 打印参数矩阵
    print(stats['# Trades'])    # 顺带看这组参数产生了多少笔交易

    打印出来的矩阵行与列分别对应两组参数,先看高值是否连续成片,再看笔数是否够多。

    预期输出:能指出一片「参数变动、表现也不差」的区域;如果全程都是棋盘状的花纹,说明这组指标在这段数据上没有稳定结构。

  4. 用未参与优化的区间复核

    把数据切成两段:一段用来搜索参数,另一段只做验证。样本内挑出来的参数在样本外经常直接失效,这一步是筛掉大部分假结论的地方。

    SmaCross.n1, SmaCross.n2 = 10, 20          # 换成你搜索得到的那组参数
    train, test = data.iloc[:-252], data.iloc[-252:]
    print(Backtest(test, SmaCross, cash=10_000, commission=.002).run()['Return [%]'])

    参数赋值方式与区间划分方法只是示意,写法与返回字段以官方文档为准。

    预期输出:样本外区间的结果明显弱于样本内是常态;如果样本外直接反向,通常说明参数在拟合噪声。

环节怎么设适用场景注意点
搜索范围n1=range(5, 30, 5)首次扫某组参数范围先粗后细,一上来就细分会把网格撑得很大
优化目标maximize='Equity Final [$]'需要单一排序指标用哪个指标优化,结果就偏向哪个指标,回撤与交易次数要另外看
约束条件constraint=lambda p: p.n1 < p.n2参数之间有大小关系约束写错会静默筛掉大量组合,看到结果稀疏先回来检查它
优化后端方法参数取值以官方文档为准网格很大或要加速不同后端的结果可能不完全一致,换后端要重跑对照
结果读取同时看结果对象与热力图任何参数搜索只看单个数字会漏掉邻域信息
成本假设显式设置 commission,必要时加滑点任何打算落地的研究不设成本会系统性高估所有组合,且高估幅度对高频策略更大
样本外复核留出未参与优化的区间所有要写进结论的参数样本外表现明显变差属正常,关键是别把它藏起来
合规与边界:参数优化只是把历史区间上的指标排了个序,不代表未来表现;任何一组参数都可能在换市场、换区间后失效。回测结果仅供研究,不构成投资建议。
Patterns

Backtesting.py 常用策略模式与参数起点

下面三类是单标的回测里最常见、也最容易验证的起步模式。它们的共同点是逻辑不复杂、参数少,适合先把「回测链路跑通 + 结果解释得清」这两件事做扎实,再往上加条件。

均线交叉

快慢两条均线的交叉作为买卖事件,快线上穿慢线进场、下穿离场。它的优点是逻辑可复述、参数只有窗口长度;缺点是震荡行情里交叉频繁,成本会吃掉大部分理论收益,所以成本假设必须显式设置。

RSI 超买超卖

用 RSI 的阈值(常见 30 / 70 一类的分界)判断超买超卖并反向操作。它适合区间震荡行情,但趋势行情里会持续给出反向信号;单看 RSI 容易忽略趋势过滤,建议配合均线或波动率条件一起看。

通道突破

突破最近 N 根 bar 的高低点入场,属于动量类思路。它对参数(回看窗口)与前视偏差都敏感:突破位必须用「当时已经知道」的价格区间来计算,否则回测会异常好看而无法复现。

研究场景常见做法参数起点思路注意点
趋势跟随双均线交叉先看 10 / 20 一类短中期组合,再按标的波动调整震荡期回撤集中,需同时看回撤幅度与交易次数
震荡反转RSI 或通道内反向阈值与回看窗口都要与标的波动幅度匹配趋势行情里反向信号会连续出现
突破动量N 日高低点突破窗口从较短周期起步,避免信号过稀严格检查突破位是否用了未来信息
多指标确认趋势指标 + 震荡指标同向才交易先各自单独跑,再合并条件条件越多信号越少,容易变成过拟合
控制回撤加止损或按权益比例减仓止损阈值与标的日常波动相关止损过紧会被噪声触发,反而放大成本
降低交易频率加过滤条件或拉长持有期先用较长窗口做基线频率降低会让统计样本变少,需要更长区间
Pitfalls

Backtesting.py 策略里最常见的结构性错误

这类问题的共同点是:代码能跑、不报错,结果还很好看,但结论是错的。它们大多不是语法问题,而是「你假设了自己在某一刻知道多少信息」这件事没对齐。

现象可能成因处理方式提前规避
回测很漂亮,实盘差很多前视偏差:用了当根 bar 收盘后的信息做决策明确成交时点假设,按下一根 bar 成交或统一用收盘成交口径写策略时先写下「这条信息我在哪一刻知道」
换一段区间结论就翻转参数是在同一段数据上挑出来的切出样本外区间复核,只把稳得住的结论留下来先划分区间,再动参数
收益明显偏高没有设置手续费与滑点,或按理想价格成交按真实费率与成交价假设重跑一遍做对照把成本当默认假设写进代码,而不是最后补
交易次数异常多把「状态」当成「事件」来判断改用交叉一类的事件判断,或加确认条件先看结果里的交易笔数是否与预期量级一致
某段区间完全没有交易指标预热期没值,或数据有缺口检查前 N 根 bar 是否有值、数据是否连续把数据质检放在策略之前
做空结果与预期相反仓位方向与平仓条件没配对用持仓状态明确判断方向,再决定开平多空混合策略先用单方向跑通再加另一边
一组参数扫出来全是同款结果搜索范围过窄,或约束把组合筛得只剩几个放大范围、放宽约束,先看单参数的敏感度曲线动手前先想清楚每个参数的合理数量级
排查顺序建议:先看成交时点与成本假设,再看参数是否样本内挑的,最后才怀疑指标实现。多数「策略失效」其实发生在前面两步。
Trust Check

优化完,怎么判断这组参数能不能信

Backtesting.py 里搜索结束只代表你拿到了一批候选,不代表其中任何一个可以写进结论。下面六项是判断参数可信度时最实际的检查点,顺序也建议照这个来。

看什么怎么判断常见误读下一步
邻域稳定性看热力图上高值区域是否成片把孤立亮点当成较优解取成片区域的中心参数,而不是尖峰
样本外表现在未参与搜索的区间重跑同一组参数把样本内指标当成预期收益把样本外结果作为主要依据,样本内只做参考
交易笔数看笔数是否足以支撑统计判断认为笔数多就一定稳健结合平均持有期一起看,笔数太少就延长区间
成本敏感性提高手续费与滑点假设后结果是否崩塌忽略摩擦成本对高频策略的影响用偏高的成本假设做压力测试
时间分布收益是否集中在少数几段行情里只看总收益,不看分布分段看每段表现,判断依赖什么行情
参数可解释性参数值能否用交易含义解释清楚把随机波动当成规律优先选能复述、能在别的区间站住的参数
边界说明:这些判断标准来自常见的回测实践,不是官方结论;具体实现的字段与口径以官方 README 与文档为准。任何一组参数的历史表现都不能推导出未来收益。
Best Practice

Backtesting.py 策略开发实践清单

把前面几节压缩成一张可勾选的清单:每次改完策略,按这六项过一遍,能挡掉大部分「结果看起来不对但找不到原因」的情况。这些建议属于经验层面的检查项,不替代官方文档。

环节建议为什么注意点
数据质量确认 OHLCV 无缺漏、复权方式一致数据口径不同会让同一策略跑出不同结论跨数据源对比前先统一复权与时间区间
成本假设显式设置 commission,必要时加滑点成本是高频策略的主要侵蚀项不要用「先跑通再看成本」的顺序做结论
成交时点明确默认成交与 trade_on_close 的差别成交时点决定你有没有用到未来信息换成交口径后结论变化大,说明策略对执行很敏感
样本外验证参数搜索与验证用不同区间样本内挑参数几乎必然乐观样本外不要反复调参,否则它也会被用成样本内
风险指标同时看收益、回撤幅度、夏普与胜率单一指标容易被局部行情诱导指标口径以实际返回为准,跨框架比较前先统一
可复现性把数据区间、参数与成本一起记录半年后要能重跑出同样的结论只记「用了双均线」是不够的
合规:回测结果仅代表历史表现,不构成投资建议;是否可投入实盘还需结合合规、资金与风险承受能力自行判断。
FAQ

策略开发常见问题

怎么接入 talib 指标?

先在环境里装好 talib,再在 init() 中用 self.I(talib.RSI, self.data.Close, 14) 这类写法注册即可。talib 需要单独安装并依赖系统库,装不上时先排查系统依赖;支持的指标集与安装方式以官方 README 与文档为准。

optimize() 会不会过拟合?

会,而且网格越大越容易。参数是在同一段历史上挑出来的,样本内表现好并不奇怪。可行的做法是把搜索与验证分成不同区间,并在热力图上优先选成片区域而不是孤立高点;具体的优化参数取值以官方文档为准。

能同时回测多个标的吗?

Backtesting.py 以单标的回测为主,不支持多资产组合与分数股交易,这一点官方 README 有明确说明。多标的要分别回测,或改用支持组合的框架;跨框架比较前先统一成本与成交口径。

buy() 是按什么价格成交的?

默认按下一根 bar 的开盘价成交,这样避免用到当时还不知道的价格;设置 trade_on_close 可改为收盘价成交。两种口径的结果会有差异,写法与默认行为以官方 README 为准。

策略里能直接用 pandas 算指标吗?

能。Backtesting.py 里把计算结果交给 self.I() 注册即可,例如用滚动窗口写成一行 lambda 或单独的函数。要注意返回序列的长度与索引必须与数据对齐,否则在 run() 阶段才会报错;细节以官方文档为准。

优化出来的参数能直接拿去实盘吗?

不建议。参数优化只说明它在某段历史上表现较好,不代表未来有效;资金规模、成交时点与滑点都不同。参数命名与返回字段以官方文档为准。较稳妥的顺序是先做样本外复核,再用小规模验证,最终是否投入实盘需自行评估风险。

下一步

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