教程 · 单套解锁 教程 · EA量化教学

EA13 Python/Backtrader 策略

使用手册 + 操作教程 共 39 章全部免费开放,一步一步把软件用起来;核心知识点 18 章,¥69 一次解锁讲透原理。

作者 枢汇安全课堂 更新 2026-10-13 阅读 64 收录 EA量化教学 · 57 章
本课程 · 单套解锁
¥69¥104
单品永久解锁 · 会员可阅 · 一次购买终身使用
✓使用手册 + 操作教程 共 39 章全文免费 ✓核心知识点 18 章,¥69 一次解锁 ✓支持电脑 / 手机 · 永久免费更新
注册后解锁核心知识点
免费学习区 使用手册 + 操作教程 共 39 章全部免费开放 · 核心知识点 18 章为付费内容 ¥69 解锁核心知识点
PART ONE

使用手册

功能在哪 · 是什么 · 怎么调出来

一、Backtrader 是什么

Backtrader 是 Python 生态里最老牌、社区最大的开源回测框架之一(最后发布版本 1.9.78.x)。一句话:你把历史 K 线喂给它,它按你的策略在过去"模拟买卖",最后告诉你赚了多少、回撤多大。

  • 纯 Python 编写,跨平台(Windows / macOS / Linux)
  • 策略写成一个类,继承 bt.Strategy
  • 支持股票、期货、外汇、加密货币
  • 自带几百个技术指标 + 丰富的绩效分析器(Analyzers)
  • 可对接多种数据源,也能通过 Store 接口接部分券商做模拟/实盘
pip install backtrader
# 画图依赖(必须,plot 时需要)
pip install matplotlib pandas
白话注解:Backtrader 就像一个"模拟炒股模拟器"。你设定规则(比如金叉买、死叉卖),它把历史一根根 K 线放给你看,每根都问你"现在买还是卖",然后按你设的手续费模拟成交,最后出成绩单。它本身不直接连你券商账户下单(但可经 Store 扩展,见第三十三章)。

二、设置成本:手续费、印花税与滑点

不设置成本 = 自欺欺人。 默认 Backtrader 是"零成本裸跑",回测出来人人都是股神。务必设手续费和滑点。

1、手续费(按金额百分比,A股/基金常用)

# 万分之 1 手续费(0.0001)
cerebro.broker.setcommission(commission=0.0001)

2、手续费(按每手固定,期货常用)

# 每手固定 5 元,期货式 commission
cerebro.broker.setcommission(
    commission=5.0,
commtype=bt.CommInfoBase.COMM_FIXED, # 每手固定
stocklike=False, # 期货
margin=3000, # 每手保证金
mult=10) # 合约乘数

3、A股印花税(卖出收 0.1%)

class AShareCommission(bt.CommInfoBase):
    params = (('commission', 0.0003), ('stamp', 0.001),)
    def getcommission(self, size, price):
        # 买入万3,卖出万3 + 千1印花税
        base = abs(size) * price * self.p.commission
        stamp = 0 if size > 0 else abs(size) * price * self.p.stamp
        return base + stamp

cerebro.broker.addcommissioninfo(AShareCommission())

4、滑点(真实成交永远比理想价差一点)

# 按百分比滑点:成交价劣后 0.1%
cerebro.broker.set_slippage_perc(perc=0.001)
# 按最小变动价位滑点(期货更真实)
cerebro.broker.set_slippage_fixed(fixed=0.01)

提醒:滑点 + 手续费看起来"很小",但高频或高换手的策略,一年累积能吃掉一大块利润。回测请至少设 1 个最小变动价位滑点 + 真实手续费率。

三、PyFolio 对接与基准对比

想要更专业的收益分析(Alpha、Beta、最大回撤、月度收益热力图),可把 Backtrader 结果交给 pyfolio(经典量化分析库)。

# pip install pyfolio
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
results = cerebro.run()
strat = results[0]
pf = strat.analyzers.pyfolio.get_analysis()
returns = pf['returns'] # 每日收益序列
positions = pf['positions'] # 持仓
transactions = pf['transactions']# 成交记录
# 配合 pyfolio / empyrical 做进一步分析
# import pyfolio as pfio
# pfio.tears.create_full_tear_sheet(returns, positions=positions, transactions=transactions)
PyFolio 偏"研究向",依赖较重且维护较慢;若只想看夏普/回撤,用第十九章的 Analyzers 更轻量。Benchmark observer(第九章)可做"买入持有"对比,无需额外依赖。
PART TWO

操作教程

一步一步 · 跟着做完一件事

四、核心抽象:Lines 与声明式编程

这是 Backtrader 最底层的设计,理解了它,你对框架就"通了"。Backtrader 里几乎所有数据都是 Line(线):一根 K 线的收盘价序列、一条均线、一个 RSI,全是"线"。

1、Line 的索引

self.data.close[0] # 当前这根 K 线的收盘价(最常用)
self.data.close[-1] # 上一根
self.data.close[-5] # 5 根前
self.data.high[0] # 当前最高价
self.fast_ma[0] # 当前均线值

索引永远"向左看历史":[0] 是当下,[-1] 是过去。绝不能在 next() 里写 [1](那是未来,会被框架拒绝或导致未来函数)。

2、声明式 vs 命令式(关键区别)

在 __init__ 里,你可以直接对"线"做运算,得到的结果还是一条线(Backtrader 会"懒求值",每根 K 线自动算)。在 next() 里,你用 [0] 取出当前标量值做判断。

class Declarative(bt.Strategy):
    def __init__(self):
        self.fast = bt.indicators.SMA(self.data.close, period=10)
        self.slow = bt.indicators.SMA(self.data.close, period=30)
self.rsi = bt.indicators.RSI(self.data.close, period=14)
        # 声明式:对线直接运算,得到"懒求值"的线对象
self.crossup = self.fast > self.slow # 仍是 LineBuffer
self.not_overbought = self.rsi < 70 # 仍是 LineBuffer

    def next(self):
        # 命令式:用 [0] 取当前标量判断
        if self.crossup[0] and self.not_overbought[0]:
            self.buy()
好处:声明式写法不用自己写 for 循环遍历历史,框架底层用 NumPy 向量化计算,又快又不会写出未来函数。新手最容易犯的错是:在 __init__ 里写 if self.fast[0] > self.slow[0](此时 [0] 还是空值),正确做法是把比较留在 __init__ 生成线、在 next 取 [0]。

3、minperiod(预热期)自动推算

当你把多条线组合,Backtrader 自动取"最长周期"作为策略的 minperiod。比如 SMA(30) 会让策略前 29 根 K 线不进 next(),等均线有值才启动。你不用手动等,框架帮你管。

五、Strategy 生命周期方法全集

bt.Strategy 有一整套"钩子方法",在不同阶段自动被调用。掌握它们,才能正确处理订单状态、统计成交、在指标未就绪时不乱下单。

方法触发时机典型用途
start()回测开始前,仅一次初始化日志文件、全局变量
prenext()指标还没"预热"好时每根都跑指标未就绪,先不交易
nextstart()指标刚就绪的第一根,仅一次相当于第一次 next
next()指标就绪后每根 K 线核心交易逻辑
notify_order(order)订单状态变化跟踪成交 / 失败
notify_trade(trade)一笔交易平仓时统计单笔盈亏
notify_cashvalue(cash, value)每根 K 线记录资金曲线
notify_fund(cash, value, fundvalue, shares)每根 K 线基金/组合净值
notify_data(data, status)数据状态变化检测行情断开/恢复(实盘有用)
notify_timer(timer, when)定时器触发(见第二十六章)定时任务
stop()回测结束后,仅一次打印最终参数 / 结果

1、标准骨架(强烈建议照抄)

import backtrader as bt

class MyStrategy(bt.Strategy):
    params = (('print_log', True),)

    def log(self, txt, dt=None):
        dt = dt or self.data.datetime.date(0)
        if self.p.print_log:
            print(f'{dt} {txt}')

    def __init__(self):
        self.dataclose = self.data.close
self.order = None # 记录"在途"订单
        self.buyprice = None
        self.buycomm = None

    def notify_order(self, order):
        # 订单提交/接受阶段:什么都不做,等最终结果
        if order.status in [order.Submitted, order.Accepted]:
            return
        # 订单完成
        if order.status == order.Completed:
            if order.isbuy():
self.log(f'BUY 成交价 {order.executed.price:.2f} 数量 {order.executed.size} 手续费 {order.executed.comm:.2f}')
                self.buyprice = order.executed.price
self.buycomm = order.executed.comm
            else:
                self.log(f'SELL 成交价 {order.executed.price:.2f} 数量 {order.executed.size} 手续费 {order.executed.comm:.2f}')
            self.order = None
        # 订单失败(取消 / 保证金不足 / 拒绝)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('订单失败:Canceled/Margin/Rejected')
            self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        self.log(f'交易平仓 毛利 {trade.pnl:.2f} 净利 {trade.pnlcomm:.2f}')

    def next(self):
if self.order: # 还有订单没处理完,先等
            return
        if not self.position:
            # 空仓时:满足买入条件
            if self.dataclose[0] > self.dataclose[-1]:
                self.log(f'买入信号 收盘价 {self.dataclose[0]:.2f}')
                self.order = self.buy()
        else:
            # 持仓时:满足卖出条件
            if self.dataclose[0] < self.dataclose[-1]:
                self.log(f'卖出信号 收盘价 {self.dataclose[0]:.2f}')
                self.order = self.sell()

为什么要记录 self.order? 回测里订单通常不是"瞬间成交",notify_order 会分多次回调。用 self.order 占位,避免在订单还没成交时反复发新单,这是新手最容易踩的坑。

六、加载数据(含日期裁剪与列映射)

Backtrader 不吃"原始 CSV",要通过 Data Feed 把数据喂进去。最常见三种方式。

1、PandasData(最推荐,配合 akshare/tushare/baostock)

import pandas as pd
import backtrader as bt

# akshare 免费示例(无需 token):
# import akshare as ak
# raw = ak.stock_zh_a_hist(symbol="600519", period="daily",
#                          start_date="20200101", end_date="20231231", adjust="qfq")
# raw = raw.rename(columns={"日期":"date","开盘":"open","最高":"high",
#                           "最低":"low","收盘":"close","成交量":"volume"})
# raw["date"] = pd.to_datetime(raw["date"]); raw.set_index("date", inplace=True)
# df = raw[["open","high","low","close","volume"]]

data = bt.feeds.PandasData(
    dataname=df,
datetime=None, # None=用 index
    open=0, high=1, low=2, close=3, volume=4, openinterest=-1
)
cerebro.adddata(data)

PandasData 的 datetime/open/high/low/close/volume/openinterest 填的是"列下标"或"列名"。-1 表示没有该列(如 openinterest 股票通常没有)。

2、GenericCSVData(从 CSV 文件读,最灵活)

data = bt.feeds.GenericCSVData(
    dataname='rb2401.csv',
dtformat='%Y-%m-%d', # 日期格式
    datetime=0, open=1, high=2, low=3, close=4, volume=5, openinterest=-1,
    separator=','
)
cerebro.adddata(data)

3、日期范围裁剪:fromdate / todate

不想先切 Pandas,也可以直接在数据层限定回测区间(实跑验证可用):

data = bt.feeds.PandasData(
    dataname=df,
    fromdate=pd.Timestamp('2020-01-01'),
    todate=pd.Timestamp('2023-12-31'),
)
cerebro.adddata(data)
用 fromdate/todate 限定区间的好处:原始 DataFrame 仍保留完整数据(例如多周期 resample 时大周期需要更长历史),但回测只在指定区间播放。注意 fromdate 要早于指标预热所需的最少 bar 数,否则开头一段没信号。

4、自定义列映射:列名不是标准名怎么办

若你的 DataFrame 列名不是 open/high/low/close/volume(比如收盘叫 cl、量叫 vol),继承 PandasData 重映射即可(实跑验证可用):

class MyData(bt.feeds.PandasData):
    params = (
('close', 'cl'), # 用列名 'cl' 作为收盘
('volume', 'vol'), # 用列名 'vol' 作为成交量
        ('openinterest', -1),
    )

data = MyData(dataname=df) # df 里必须有 'cl' 和 'vol' 两列
cerebro.adddata(data)
映射可填"列下标"(如 ('close', 3))或"列名字符串"(如 ('close', 'cl'))。没有的列用 -1 占位。多标的时给每份数据 name='XXX',便于 self.getdatabyname('XXX') 取用。

七、画线看结果:Observers 与基础绘图

Observers 负责"把过程画出来"。cerebro.plot() 默认就带几个:Broker(资金曲线)、BuySell(买卖点)、DrawDown(回撤)。

cerebro.plot(style='candlestick', volume=True, iplot=False)

# 加内置 Benchmark observer:叠加"买入持有"基准线
cerebro.addobserver(bt.observers.Benchmark, timeframe=bt.TimeFrame.Days)
Observer作用
Broker现金 + 持仓市值曲线
BuySell在 K 线上标买卖箭头
DrawDown回撤子图
Trades每笔交易盈亏
Benchmark叠加基准(如买入持有)对比

服务器 / 无界面环境用 cerebro.plot(show=False, filename='result.png') 把图存成文件再看。

八、完整实战案例:均线 + RSI + ATR

下面是一份端到端可运行的完整策略:用均线金叉做方向、RSI 过滤(不在超买时追)、ATR 设移动止损。含随机数据生成,不依赖外部数据源也能直接跑。

import backtrader as bt
import pandas as pd
import numpy as np

def make_demo_data():
    np.random.seed(42)
    dates = pd.date_range('2022-01-01', periods=600, freq='D')
    close = 100 + np.cumsum(np.random.randn(600) * 0.8)
    df = pd.DataFrame({
'open': close + np.random.randn(600) * 0.3,
'high': close + np.abs(np.random.randn(600)) * 1.0,
'low': close - np.abs(np.random.randn(600)) * 1.0,
        'close': close,
        'volume': np.random.randint(1000, 10000, 600),
    }, index=dates)
    return df

class MaRsiAtrStrategy(bt.Strategy):
    params = (
        ('fast', 10), ('slow', 30),
        ('rsi_period', 14), ('rsi_high', 70),
('trail', 0.05), # 从高点回撤 5% 离场
        ('print_log', False),
    )
    def log(self, txt):
        if self.p.print_log:
            print(f'{self.data.datetime.date(0)} {txt}')

    def __init__(self):
        self.fast_ma = bt.indicators.SMA(self.data.close, period=self.p.fast)
        self.slow_ma = bt.indicators.SMA(self.data.close, period=self.p.slow)
self.cross = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)
self.rsi = bt.indicators.RSI(self.data.close, period=self.p.rsi_period)
self.atr = bt.indicators.ATR(self.data, period=14)
        self.order = None

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return
        if order.status == order.Completed:
            self.log('成交 ' + ('BUY' if order.isbuy() else 'SELL'))
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('订单失败')
        self.order = None

    def next(self):
        if self.order:
            return
        if not self.position:
            # 金叉 + RSI 未超买 → 开多
            if self.cross > 0 and self.rsi[0] < self.p.rsi_high:
                self.order = self.buy()
        else:
            # 移动止损:从持仓期间最高价回撤 trail 即平仓
if self.rsi[0] > 30: # 简单离场条件示例
                self.order = self.close()

df = make_demo_data()
data = bt.feeds.PandasData(dataname=df)

cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.addstrategy(MaRsiAtrStrategy, print_log=False)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.0005) # 万5
cerebro.broker.set_slippage_perc(perc=0.001) # 0.1% 滑点

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe',
                    riskfreerate=0.0, annualize=True)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='dd')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')

print('初始资金:', f'{cerebro.broker.getvalue():.2f}')
results = cerebro.run()
strat = results[0]
print('最终资金:', f'{cerebro.broker.getvalue():.2f}')

ta = strat.analyzers.trades.get_analysis()
dd = strat.analyzers.dd.get_analysis()
sh = strat.analyzers.sharpe.get_analysis().get('sharperatio')
print(f'交易次数: {ta.total.closed} 净利润: {ta.pnl.net.total:.2f}')
print(f'最大回撤: {dd.max.drawdown:.2f}% 夏普: {sh}')
# cerebro.plot(style='candlestick', volume=True)  # 本地有界面时取消注释看图形
把 make_demo_data() 换成真实行情(akshare/tushare)即可用于真实回测。这个案例演示了"多条件过滤 + 风控离场"的思路,不是盈利保证。

第二部分 · 核心知识点(进阶与实战)

九、订单系统详解(含 Order 属性表)

1、基础下单方法

self.buy(size=100) # 买入(开多 / 加仓)
self.sell(size=100) # 卖出(平多 / 开空)
self.close() # 平掉当前全部持仓
self.order_target_size(target=500) # 调到目标持仓 500 股
self.order_target_percent(target=0.8) # 调到 80% 仓位
self.cancel(order) # 撤单
白话注解:buy=开多,sell=开空(期货/外汇支持,股票默认只能做多),close=平仓。Backtrader 默认股票只能做多,期货可双向。

2、订单类型(exectype)

类型常量说明
市价单bt.Order.Market当前价立即成交(默认)
限价单bt.Order.Limit指定 price,只有到价才成交
止损单bt.Order.Stop触发价 price 后转市价
止损限价bt.Order.StopLimit触发后变限价单,需 plimit
收盘价单bt.Order.Close当根 K 线收盘时按收盘价成交
追踪止损bt.Order.StopTrail随价移动止损,trailpercent / trailamount
# 限价买入:价格 ≤ 10 才买
self.buy(size=100, exectype=bt.Order.Limit, price=10.0)
# 止损卖出:价格 ≤ 9.5 触发
self.sell(size=100, exectype=bt.Order.Stop, price=9.5)
# 追踪止损:从最高点回撤 5% 止损
self.sell(exectype=bt.Order.StopTrail, trailpercent=0.05)
# 有效期:3 根 K 线内有效,过期作废
self.buy(size=100, valid=3)

3、订单状态(写 notify_order 必懂)

# bt.Order 的状态常量
bt.Order.Created # 刚创建
bt.Order.Submitted # 已提交券商
bt.Order.Accepted # 已接受
bt.Order.Partial # 部分成交
bt.Order.Completed # 完全成交
bt.Order.Canceled # 已撤单
bt.Order.Expired # 过期
bt.Order.Margin # 保证金不足(钱不够)
bt.Order.Rejected # 被拒

4、Order 对象属性全表

在 notify_order(order) 里,order 是最重要的对象。常用属性(已实跑验证):

属性含义
order.ref订单唯一编号(整数)
order.status当前状态(见 11.3 状态常量)
order.isbuy() / order.issell()是否买 / 卖单
order.alive()是否还在途(未成交也未撤)
order.data该订单对应的数据(多标的有用)
order.executed成交信息对象:.price / .size / .value / .comm
order.created下单时信息对象:.price / .size / .value(计划价)
order.info自定义附加字典(下单时传 info={...})
def notify_order(self, order):
    ...
    if order.status == order.Completed:
        ex = order.executed
        self.log(f'成交 编号{order.ref} 方向{"买" if order.isbuy() else "卖"} '
                 f'价{ex.price:.2f} 量{ex.size} 额{ex.value:.2f} 费{ex.comm:.2f}')
        # 想看下单时计划价:order.created.price
order.executed.value 是这笔成交的"成交额"(价×量),order.executed.comm 是手续费;order.created 是下单那一刻记录的计划值,常与成交值不同(因为滑点/限价撮合)。多标的时务必用 order.data 区分是哪份数据下的单。

十、订单完整参数 + Bracket 组合单

1、buy / sell 全部常用参数

参数含义
size数量(整数股/手)
price限价/止损触发价
plimitStopLimit 触发后的限价
exectype订单类型(见 11.2)
valid有效期:数字=K线根数;datetime=具体时间;bt.Order.DAY/GTC
trailamount追踪止损的绝对点数
trailpercent追踪止损的百分比
parent父订单(用于 Bracket)
ocoi / oco与哪张订单互斥(One-Cancels-Other)
transmit是否立即发给 broker;Bracket 组里除最后一张外都设 False
comminfo覆盖默认手续费方案
info附加信息字典(在 notify_order 里可回读)

2、手动挂"进场 + 止损 + 止盈"组合单(parent / transmit / oco)

buy_bracket/sell_bracket 是语法糖;理解底层更灵活——用 parent 把子单挂到主单、用 transmit=False 暂不发、最后一张 transmit 默认 True 一次性发出整组。

def next(self):
    if not self.position:
        ref = self.data.close[0]
        # 主单:先 transmit=False,等子单挂好再一起发
        entry = self.buy(size=100, transmit=False)
        # 止损单:挂在 entry 下,与止盈互斥
        self.sell(size=100, price=ref * 0.95,
                  exectype=bt.Order.Stop, parent=entry,
                  transmit=False, oco=entry)
        # 止盈单:最后一张 transmit 默认 True,整组发出
        self.sell(size=100, price=ref * 1.05,
                  exectype=bt.Order.Limit, parent=entry,
                  oco=entry)
这样挂的好处:止损和止盈"二选一"自动撤另一张;主单成交后子单才生效,没成交不会提前挂废单。

3、便捷方法 buy_bracket / sell_bracket

第八章的手动写法是底层;Backtrader 也提供一步到位的语法糖(实跑验证,方法真实存在)。

ref = self.data.close[0]
# 现价附近开多,同时挂止损(stopprice)与止盈(limitprice)
self.buy_bracket(
    size=100,
price=ref, # 进场限价
stopprice=ref * 0.95, # 止损触发价
limitprice=ref * 1.05, # 止盈限价
)
# 做空组合单用 sell_bracket,参数相同
便捷方法内部就是帮你组装了 parent/oco/transmit,省事但灵活度不如手写。需要"止损和止盈都跟随 ATR 动态变化"时,回到 12.2 的手动写法更可控。

十一、仓位管理 Sizers

不想每次 hardcode size=100?用 Sizer 自动控制每笔买多少。两种写法:全局加在 Cerebro,或在策略里自定义。

1、内置 Sizer

# 每次固定买 100 股
cerebro.addsizer(bt.sizers.FixedSize, stake=100)
# 每次用 90% 可用资金
cerebro.addsizer(bt.sizers.PercentSizer, percents=90)
# 满仓(注意杠杆标的慎用)
cerebro.addsizer(bt.sizers.AllInSizer)
# 反手:有持仓就反向开同样大小
cerebro.addsizer(bt.sizers.FixedReverser, stake=100)

用了 Sizer 后,self.buy() 不写 size 也会自动按规则下单。

2、自定义 Sizer(按比例 + 留缓冲)

class MySizer(bt.Sizer):
params = (('frac', 0.5),) # 每次用 50% 现金
    def _getsizing(self, comminfo, cash, data, isbuy):
        value = cash * self.p.frac
        size = int(value / data.close[0])
        return size

cerebro.addsizer(MySizer, frac=0.5)

十二、Line 逻辑运算:组合多条件

现实中很少"金叉就买",往往要"金叉 + 没超买 + 量能放大"多个条件同时满足。Backtrader 提供一批对线做运算的函数,让你在 __init__ 里声明式地拼条件,不用写循环。

函数作用等价 Python
bt.And(a, b, ...)全部为真a and b and ...
bt.Or(a, b, ...)任一为真a or b or ...
bt.If(cond, a, b)条件选择a if cond else b
bt.Max(a, b) / bt.Min(a, b)逐元素最大/最小max/min
bt.Cmp(a, b)a>b 返回 1 否则 0(a>b)
bt.All(a, b) / bt.Any(a, b)线序列全/任为真all/any
bt.Abs(a)绝对值abs(a)
class MultiCond(bt.Strategy):
    def __init__(self):
        fast = bt.indicators.SMA(self.data.close, period=10)
        slow = bt.indicators.SMA(self.data.close, period=30)
rsi = bt.indicators.RSI(self.data.close, period=14)
vol = self.data.volume
        # 声明式组合条件(仍是线)
        self.buy_sig = bt.And(
fast > slow, # 均线多头
rsi < 70, # 未超买
vol > bt.indicators.SMA(vol, period=20) # 量能放大
        )
    def next(self):
        if self.buy_sig[0] and not self.position:
            self.buy()
这些函数返回的都是"线对象",只能在 __init__ 里用;要拿当前值必须 [0]。好处是框架底层向量化计算,且自动对齐 minperiod。

十三、Trade 对象属性全表

一笔交易从开仓到平仓,Backtrader 会维护一个 Trade 对象并在 notify_trade(trade) 里交给你。平仓时你能拿到这些字段(已实跑逐一验证存在):

属性含义
trade.pnl毛利(未扣手续费)
trade.pnlcomm净利(扣手续费后)
trade.price开仓均价
trade.value当前持仓市值
trade.commission该笔累计手续费
trade.size持仓数量(含方向,负为净空)
trade.justopened本根 K 线刚开仓?(布尔)
trade.isopen / trade.isclosed是否持仓中 / 已平仓
trade.baropen开仓时的 bar 序号(算持有根数用)
trade.history该笔状态变更记录列表
trade.status状态码
trade.data该交易所属的数据(多标的有用)
def notify_trade(self, trade):
    if not trade.isclosed:
        return
    # 持仓根数 = 当前 bar 序号 - 开仓 bar 序号
    hold_bars = len(self) - trade.baropen
    self.log(f'平仓 净利{trade.pnlcomm:.2f} 开仓价{trade.price:.2f} '
             f'手续费{trade.commission:.2f} 持有{hold_bars}根')

想算"持仓多少根 K 线",用当前 bar 序号 len(self) 减去 trade.baropen 即可。注意 Backtrader 1.9.78 的 Trade 对象没有直接暴露 duration 属性,自行推算最稳,别照抄网上旧帖的 trade.duration。

十四、参数优化(含提前终止 runstop)

Backtrader 支持"网格搜索"自动跑遍参数组合,用 optstrategy 代替 addstrategy。

cerebro.optstrategy(
    DualMaStrategy,
fast_period=range(5, 21, 5), # 5,10,15,20
slow_period=range(20, 61, 10) # 20,30,40,50,60
)
results = cerebro.run(maxcpus=4) # 多进程加速

1、稳健提取最优参数(不只看一个指标)

# optstrategy 返回的 results 是"列表的列表":results[i][0] 是第 i 组
best = None
for run in results:
    strat = run[0]
    sharpe = strat.analyzers.sharpe.get_analysis().get('sharperatio') or -999
    dd = strat.analyzers.dd.get_analysis().max.drawdown
    # 复合评分:夏普高且回撤小才优
    score = sharpe - dd / 100.0
    if best is None or score > best[2]:
        best = (strat.params.fast_period, strat.params.slow_period, score)
print('稳健最优参数:', best)

参数优化 = 过拟合重灾区! 回测里"最优"的那组参数,往往只是刚好贴合了这段历史,换段时间就失灵。优化后一定要做"样本外检验 / Walk-forward"(见第三十章)。建议:参数少而合理、范围别太密、最终用样本外数据验证。

2、提前终止回测:self.env.runstop()

某些规则下你想让回测"立刻停下"(比如触发熔断、或到达目标收益)。在策略内调用 self.env.runstop()(实跑验证可用):

def next(self):
    # 亏损超 50% 立即终止回测
    if self.broker.getvalue() < self.broker.startingcash * 0.5:
        self.log('触发回撤上限,终止回测')
        self.env.runstop()
        return
    # 正常交易逻辑 ...
策略内可用 self.broker 直接读账户(getvalue()/getcash()/startingcash),也可用 self.env 访问运行你的引擎(Cerebro)。runstop() 会在当前 bar 结束后安全停止,已持仓按最后价结算,不会破坏已成交记录。

十五、Analyzers 绩效分析全集(含 Transactions)

Analyzers 是 Backtrader 的"成绩单生成器"。回测加几个,结束后就能读出专业指标。

cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe',
                    riskfreerate=0.0, annualize=True, timeframe=bt.TimeFrame.Days)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='dd')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.SQN, _name='sqn')
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='annual')
cerebro.addanalyzer(bt.analyzers.VWR, _name='vwr') # Variability-Weighted Return
cerebro.addanalyzer(bt.analyzers.Calmar, _name='calmar')
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='time') # 可算月度收益
cerebro.addanalyzer(bt.analyzers.Transactions, _name='tx') # 逐笔成交记录

results = cerebro.run()
strat = results[0]

1、常用 Analyzer 含义

Analyzer看什么越大/越小越好
SharpeRatio每承担 1 单位风险赚多少越大越好(>1 不错)
DrawDown最大回撤幅度越小越好
TradeAnalyzer交易笔数/胜率/盈亏综合看
Returns总/年化收益率越大越好
SQN系统质量数(含样本量)越大越好(>1.6 可)
AnnualReturn逐年收益越稳越好
VWR波动加权收益越大越好
Calmar年化收益/最大回撤越大越好
Transactions逐笔成交明细复盘/对账

Analyzer 返回值类型不同:SharpeRatio、DrawDown、TradeAnalyzer、SQN 返回"可属性访问的对象"(如 .max.drawdown、.total.closed);而 Calmar、VWR、TimeReturn 返回的是字典(如 {月末日期: 值}),请用 list(calmar.values()) 或整体 print(calmar.get_analysis()) 查看,不要写 .calmar 属性。夏普/回撤/交易类已在上文给出标准读取代码。

白话注解: - 夏普比率 = 每冒 1 份风险能赚多少,>1 算不错,>2 优秀。 - 最大回撤 = 从最高点跌到最低点最多亏多少,越小越好。一个策略赚 10 万但中间回撤 8 万,你大概率拿不住。

2、成交记录器 Transactions

Transactions 返回一个以 (订单编号, 日期) 为键的成交字典,专门用来逐笔导出"何时、什么价、买卖多少":

tx = strat.analyzers.tx.get_analysis()
for (ref, dt), t in tx.items():
    print(dt.date(), '单号', ref, '方向', '买' if t.size > 0 else '卖',
          '量', abs(t.size), '价', round(t.price, 2))
它和 Writer(第二十三章)互补:Writer 给逐根全样本,Transactions 只给"实际发生的成交",做税务/对账更聚焦。

十六、TradeAnalyzer 字段全表

TradeAnalyzer.get_analysis() 返回的是一个嵌套对象(支持属性访问)。常用字段如下,没列全的用 print(strat.analyzers.trades.get_analysis()) 自己探索。

路径含义
total.closed已平仓交易笔数
total.open未平仓(持仓中)笔数
total.won / total.lost盈利 / 亏损笔数
won.total / lost.total同上(按胜负分组)
pnl.gross.total毛利(未扣费)合计
pnl.gross.average平均每笔毛利
pnl.net.total净利(扣费后)合计
pnl.net.average平均每笔净利
won.pnl.total / lost.pnl.total盈利组 / 亏损组盈亏合计
len.average平均持仓 K 线数
len.max / len.min最长 / 最短持仓
streak.won.longest最长连胜
streak.lost.longest最长连亏
ta = strat.analyzers.trades.get_analysis()
total = ta.total.closed
won = ta.won.total
lost = ta.lost.total
net = ta.pnl.net.total
print(f'交易次数: {total} 盈利: {won} 亏损: {lost} 净利: {net:.2f}')
if total:
avg_win = ta.won.pnl.total / won if won else 0
    avg_loss = ta.lost.pnl.total / lost if lost else 0
print(f'胜率: {won/total*100:.1f}% 盈亏比: {(avg_win/avg_loss):.2f}' if avg_loss else '盈亏比: 无亏损')

十七、自定义 Analyzer 与 Observer(按需扩展)

内置的几十个 Analyzer/Observer 不够用时,自己写一个。两者套路一致:继承基类,框架在回测各阶段回调你的方法。

1、自定义 Analyzer

class MaxDrawdownCustom(bt.Analyzer):
    def create_analysis(self):
        self.ret = {'maxdd': 0.0, 'peak': None}
        return self.ret
    def next(self):
        val = self.strategy.broker.getvalue()
        if self.ret['peak'] is None or val > self.ret['peak']:
            self.ret['peak'] = val
        if self.ret['peak']:
            dd = (self.ret['peak'] - val) / self.ret['peak']
            self.ret['maxdd'] = max(self.ret['maxdd'], dd)
    # start()/stop() 也可重写(分别回测前/后调用)

# 用:cerebro.addanalyzer(MaxDrawdownCustom, _name='mydd')
# 读:strat.analyzers.mydd.get_analysis()['maxdd']

Analyzer 的标准钩子:start()(回测前)、next()(每根)、stop()(回测后)、create_analysis()(初始化返回对象)。框架通过 self.strategy 把策略实例递给你,随时可取账户市值/持仓。

2、自定义 Observer(把你想看的画出来)

class EquityCurve(bt.Observer):
lines = ('equity',) # 定义一条要画的线
    plotinfo = dict(plotymargin=0.05)
    def next(self):
        # self._owner 就是策略,随时可取账户市值
        self.lines.equity[0] = self._owner.broker.getvalue()

# 用:cerebro.addobserver(EquityCurve)
自定义 Observer 会在 cerebro.plot() 里自动多画一张子图。内置的 Broker / DrawDown 本质上就是这种 Observer。想要更花哨的可缩放交互图,见第二十二章的 Bokeh 方案。

十八、进阶绘图:plotinfo / plotlines + 中文乱码

1、自定义指标 / 线的绘图样式

继承指标或线对象时,用 plotinfo 控制是否画、画在哪个子图;用 plotlines 控制每条线的颜色、线型。

class MyIndicator(bt.Indicator):
    lines = ('fast', 'slow')
plotinfo = dict(subplot=False) # 和价格画在一起
    plotlines = dict(
fast=dict(color='red', ls='--'),
slow=dict(color='blue', ls='-'),
    )
    def __init__(self):
        self.lines.fast = bt.indicators.SMA(self.data, period=10)
        self.lines.slow = bt.indicators.SMA(self.data, period=30)

2、中文乱码修复(matplotlib)

Backtrader 用 matplotlib 画图,中文会显示为方块。在 plot() 前设置字体即可:

import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示成方块
cerebro.plot(style='candlestick')

Windows 常用 SimHei,macOS 用 Arial Unicode MS。若仍乱码,先确认系统已装对应字体。

3、交互式绘图(社区库)

官方 plot() 是静态图。想要可缩放/悬浮提示的交互图,可用社区库 backtrader_plotting(基于 Bokeh):

# pip install backtrader_plotting bokeh
from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo
b = Bokeh(style='bar', scheme=Tradimo())
b.plot(cerebro)

十九、结果导出 Writer

想把每根 K 线的资金、持仓、指标都导出成 CSV / Excel 复盘?用 Writer。

# 把回测过程数据写到 out.csv(; 分隔,含 broker/交易/指标)
cerebro.addwriter(bt.WriterFile, csv=True, out='backtest_result.csv', rounding=2)
cerebro.run()

Writer 会把 self.data、broker、各 analyzer、observer 的逐根数据都落盘,方便你用 Excel / Pandas 二次分析。

二十、定时任务 add_timer 与 cheat_on_open

1、add_timer:按时间触发动作

有时不在每根 K 线判断,而是"每天收盘后""每周一开盘"执行某逻辑。用 self.add_timer() + notify_timer。

import datetime
import backtrader as bt

class TimerStrat(bt.Strategy):
    def __init__(self):
        # 每个交易日结束时触发
        self.add_timer(when=bt.timer.SESSION_END)
        # 每周一 10:30 触发(weekdays: 周一=0)
        # self.add_timer(when=datetime.time(10, 30), weekdays=[0])

    def notify_timer(self, timer, when, *args, **kwargs):
        self.log(f'定时器触发 @ {when}')
        # 在这里做盘后复盘、调仓等

2、cheat_on_open:以开盘价成交

默认市价单在"当前 K 线收盘价"或下一根撮合。设 cheat_on_open 后,当根信号可在下一根开盘价成交,更接近"夜盘出信号、早盘开盘执行"的真实场景,也能避免"用收盘价决策却假设同根成交"的微小前视。

cerebro.broker.set_coo(True) # cheat on open

cheat_on_open 主要是撮合时点约定,务必和你的数据周期、决策逻辑一致,否则反而制造偏差。

二十一、完整可运行:优化 + Walk-forward 样本外检验

"样本内优化"选出的参数必须过"样本外"这一关。Walk-forward(滚动窗口)做法:用一段历史训练选参,紧接的下一段历史验证,再滚动。下面是可落地的骨架。

import pandas as pd
import backtrader as bt

def walk_forward(df, train_days=250, test_days=60, step=60):
    n = len(df)
oos_values = [] # 样本外终值集合
    start = 0
    while start + train_days + test_days <= n:
        train = df.iloc[start:start + train_days]
test = df.iloc[start + train_days:start + train_days + test_days]

        # 1) 训练段:优化选参(这里简化为跑一次取终值,真实应遍历参数取最优)
        c_train = bt.Cerebro()
        c_train.adddata(bt.feeds.PandasData(dataname=train))
        c_train.addstrategy(MaRsiAtrStrategy)
        c_train.broker.setcash(100000)
        c_train.run()

        # 2) 测试段:用"训练得到的最优参数"在样本外验证
        c_test = bt.Cerebro()
        c_test.adddata(bt.feeds.PandasData(dataname=test))
c_test.addstrategy(MaRsiAtrStrategy) # 真实应传入训练选出的最优参数
        c_test.broker.setcash(100000)
        c_test.run()
        oos_values.append(c_test.broker.getvalue())

        start += step
    return oos_values

# oos_values 若普遍 > 100000  策略在未见数据上仍盈利,可信度更高
骨架里"训练选参"用了一句示意。真实工程:在训练段用 optstrategy 遍历参数,按复合评分(第十八章)选最优,再把该参数传给测试段的 addstrategy。多段样本外都盈利,才算过关。

二十二、Backtrader vs 其他框架

对比项Backtradervn.py(EA10)聚宽TqSdk(EA12)
定位纯回测为主回测+实盘云端研究回测+实盘
语言PythonPythonPythonPython
实盘需 Store 扩展(IB 等)支持 CTP 期货有限支持 A股支持期货/股票
数据自己准备可接行情源平台自带平台自带
本地化全本地全本地须上云全本地
适合学回测/快速验证期货实盘A股研究期货/股票实盘
想"本地快速验证想法"→ Backtrader;想"国内期货真刀实枪跑"→ vn.py / TqSdk。本系列 EA10、EA12 已分别详讲。

二十三、实盘对接思路(Store 扩展)

原话"Backtrader 不能实盘"不够准确:它核心定位是回测,但通过 Store 接口也能接部分券商做模拟/实盘,只是生态以回测为主、实盘稳定性不如专业框架。

  • 盈透证券(IB):官方自带 backtrader.stores.IBStore,可接 IB 行情与交易(美股/期货/外汇)。
  • Oanda / CCXT(加密货币):社区扩展可接外汇与币安等交易所(如 backtrader-ccxt-store)。
  • 国内期货实盘:更推荐直接用本系列 EA10(vn.py,CTP)或 EA12(TqSdk),文档、行情、柜台对接更成熟。

实盘前务必想清楚:回测通过 ≠ 实盘能跑。实盘要处理断线重连、部分成交、涨跌停、交易所规则、资金占用等,复杂度远高于回测。小资金、强监控、可一键止损,再逐步放大。

二十四、目标仓位下单族:order_target_\*

前面下单都是"再买 10 手"。但做再平衡(比如想让某标的占仓位 50%)时,你更关心"最终持有多少",而不是"这次买多少"。Backtrader 提供了三个自动算差额的方法:

方法目标含义典型用途
self.order_target_size(target=100)最终持仓 = target 股/手精确控制持仓量
self.order_target_value(target=50000)最终持仓市值 ≈ target 元按金额配置
self.order_target_percent(target=0.5)最终持仓占净值比例 = target按仓位比例配置(最常用)
class Rebal(bt.Strategy):
    def next(self):
        # 每 20 根 K 线把仓位调成"净值的一半买股票"
        if len(self) % 20 == 0:
            self.order_target_percent(target=0.5)
        # 想清仓:order_target_percent(target=0.0)
白话注解:order_target_percent(0.5) 会先看你现在已持有多少,再下"补足到 50%"的订单——已有 30% 就只补 20%,已有 60% 就卖出 10%。省得自己算差值,也不会重复加仓。

这几个方法底层仍是普通市价/限价单,只是帮你算 size。它们会覆盖 Sizer 算出的 size(因为本就要精确到位)。注意目标类方法的 target 参数不能省略。

二十五、持仓对象 Position 全属性

下单成交后,self.position 就是当前持仓对象。下面列出 1.9.78 版本里真实存在的属性(其余如 .value、.pnl 并不存在于 Position 上,别乱写):

属性类型/含义说明
sizeint当前持仓量,正数多仓、负数空仓
pricefloat当前持仓的开仓均价
price_origfloat最初开仓的成交价(未做调整)
adjbasefloat调整后的成本基准(分红/拆细等场景用)
upopenedint当前尚未平仓的"买入开仓"量
upclosedint已经平仓掉的"买入开仓"累计量
datetimedate当前持仓最近一次变动的时间

常用方法:update()(成交后更新仓位)、pseudoupdate()(试算更新不落盘)、clone()(复制)、fix()(冻结)、set()(直接设定)。日常用 self.position.size 判断是否持仓就够了。

def next(self):
    pos = self.position
    if pos.size != 0:
        print("持仓:", pos.size, "均价:", pos.price)
    # 想算"浮动盈亏"?Position 没有 .pnl 属性:
    # 用 当前价 - 开仓均价 乘 size 自己算,或读 TradeAnalyzer
    float_pnl = (self.data.close[0] - pos.price) * pos.size

别踩坑:网上有些旧教程写 self.position.value 或 self.position.pnl。在 1.9.78 里这两个属性都不存在,运行时直接 AttributeError。浮盈请用上面公式自己算,或读 TradeAnalyzer。

二十六、完整的 notify_\* 生命周期方法清单

除了最常用 notify_order / notify_trade,Backtrader 还有几个 notify 方法,覆盖"现金/基金/数据/定时器/ store"事件。完整清单与触发时机:

方法触发时机参数
notify_order(self, order)订单状态变化(提交/部分成交/完成/拒单)order 对象
notify_trade(self, trade)每笔交易有变动(开仓/平仓/费用变动)trade 对象
notify_cashvalue(self, cash, value)每个周期末(默认)现金、总净值
notify_fund(self, cash, value, fundvalue, shares)基金模式下每个周期末现金、净值、基金值、份额
notify_data(self, data, status, *args, **kwargs)数据源状态变化(连接/断开/延迟)data、status 码
notify_timer(self, timer, when, *args, **kwargs)定时器到点(第二十六章)timer、触发时间
notify_store(self, msg, *args, **kwargs)Store(实盘/行情)推送消息消息内容
class S(bt.Strategy):
    def notify_cashvalue(self, cash, value):
        # 每个周期末拿到现金和净值,可做动态风控
        if value < self.broker.startingcash * 0.8:
            self.log("净值回撤超 20%,暂停开仓")
    def notify_fund(self, cash, value, fundvalue, shares):
pass # 基金模式才用得上
读绩效用 self.analyzers,不是 self.stats:Backtrader 的 Strategy 没有 self.stats 这个属性(网上旧文常见误写)。加的分析器统一通过 self.analyzers.名字.get_analysis() 在 stop() 里读取。

二十七、订单有效期 valid(时间限定)

挂出的限价/止损单默认一直有效,直到成交或你手动取消(GTC)。你也可以给它一个"截止日",到期自动作废(GTD)。用订单的 valid 参数:

写法含义
valid=None(默认)GTC:一直有效,直到成交/取消
valid=datetime.date(2026,12,31)GTD:到这天仍未成交则过期
valid=datetime.datetime(...)精确到某一时刻过期
import datetime
# 挂一个限价买单,2026 年底前有效,过期自动撤
self.buy(exectype=bt.Order.Limit, price=99.0,
         valid=datetime.date(2026, 12, 31))

只有"未成交的挂单"(Limit / Stop / StopLimit)才受 valid 约束。市价单(Market)立即撮合,谈不上有效期。过期未成交的订单状态会变成 Expired,在 notify_order 里能收到。

二十八、成本模型完整定制(期货/股票差异)

第八章只讲了基础手续费。这里把 setcommission 的全部参数讲透,并纠正一个常见误区:滑点不在 setcommission 里,而是用独立方法 set_slippage_perc / set_slippage_fixed。

1、setcommission 全部参数

参数含义
commission佣金数值(配合 commtype 解释单位)
commtype计费方式:COMM_PERCENT(按金额%) / COMM_FIXED(每笔固定) / COMM_ABSOLUTE(按股数绝对值)
percabsTrue 时 commission 直接是百分比小数(如 0.0003),False 时需除以 100
stocklikeTrue=股票(按金额算);False=期货(按合约,需 margin+mult)
margin每手保证金(期货专用)
mult合约乘数(期货专用,如股指 300、原油 1000)
leverage杠杆倍数(影响可用资金)
interest / interest_long融券/融资利息(少用)
automargin自动按保证金占用
name给该佣金方案命名,多数据可分别设

2、股票(A股)例子

cerebro.broker.setcommission(
commission=0.0003, # 万三佣金
    commtype=bt.CommInfoBase.COMM_PERCENT,
percabs=True, # commission 已是小数
    stocklike=True)
# 卖出再加印花税(卖出金额 * 0.001),用 comminfo 子类或分开算

3、期货(按合约+保证金)例子

cerebro.broker.setcommission(
commission=2.0, # 每手 2 元
mult=10, # 每点 10 元
margin=3000, # 每手保证金 3000
    stocklike=False,
    commtype=bt.CommInfoBase.COMM_FIXED)

4、滑点独立设置(关键)

# 百分比滑点:成交价偏移 0.1%
cerebro.broker.set_slippage_perc(perc=0.001, slip_open=True)
# 或固定滑点:每单位偏移 0.02
cerebro.broker.set_slippage_fixed(fixed=0.02)

再次强调:setcommission(...) 的签名里没有 slip_perc / slip_fixed 这类参数。滑点必须单独调用 set_slippage_*。把滑点写进 setcommission 会直接报"意外关键字"错误。

二十九、内置数据源 feeds 全家桶

除了 PandasData,Backtrader 还带多种现成数据源。最实用的是 GenericCSVData——它能吃任意列顺序的 CSV,通过"列索引"告诉框架每列是什么。

1、GenericCSVData 列映射

class MyCSV(bt.feeds.GenericCSVData):
    params = (
('datetime', 0), # 第 0 列是时间
('open', 1), # 第 1 列开盘
        ('high', 2),
        ('low', 3),
        ('close', 4),
        ('volume', 5),
('openinterest', -1), # -1 表示该列不存在,跳过
        ('dtformat', '%Y-%m-%d'),
    )
data = MyCSV(dataname='行情.csv')

2、其他现成 feeds

feed用途
bt.feeds.PandasData从 pandas DataFrame 读(最常用)
bt.feeds.GenericCSVData任意列 CSV(自定义映射)
bt.feeds.YahooFinanceData雅虎财经格式(含 adjclose 复权)
bt.feeds.QuandlQuandl 数据格式
bt.feeds.VChartCSVData部分行情软件导出的 CSV

没有 bt.feeds.CSVData 这个类(网上旧帖会误写)。吃 CSV 一律用 GenericCSVData 并指定列索引。注意 openinterest 字段很多 CSV 没有,设成 -1 跳过即可,否则会报错。

三十、内置观测器 Observers 全解

Observers 是"画在图上的实时数据"。默认 cerebro.run(stdstats=True) 会加载四个:Broker(现金/净值)、Trades(每笔盈亏)、BuySell(买卖箭头)、DrawDown(回撤)。你还能手动增删:

Observer画什么
bt.observers.Broker账户现金与总净值曲线
bt.observers.Trades每笔交易的盈亏点
bt.observers.BuySell买(下)/卖(上)箭头
bt.observers.DrawDown回撤曲线
bt.observers.Value净值(与 Broker 类似)
bt.observers.Benchmark策略 vs 基准对比
# 关掉默认观测器,只留净值和买卖点
cerebro = bt.Cerebro(stdstats=False)
cerebro.addobserver(bt.observers.Broker)
cerebro.addobserver(bt.observers.BuySell)
# 多标的时买卖点会重复画,用 addobservermulti 分别画每个数据
cerebro.addobservermulti(bt.observers.BuySell)
# 加基准对比(默认以第一根为基准)
cerebro.addobserver(bt.observers.Benchmark)
addobserver 加单个实例;addobservermulti 会为每个数据各生成一个(多标的买卖点分开展示时用)。它们和 Analyzer 不同:Observer 是为了"画图",Analyzer 是为了"出数字"。

三十一、replaydata 重放 vs resample 重采样

两者都用于"把小周期合成大周期",但行为完全不同:

方式行为适用
resampledata把 N 根小 K 线"压缩"成 1 根大 K 线,整段一次性给策略多周期指标(日线策略里看周线)
replaydata把 N 根小 K 线"重放"成 1 根,但逐根喂入,指标随每根小 K 更新用日线模拟"盘中"决策、或日线+ tick 混合
# 日线作为基础数据,再合成周线(resample)
cerebro.adddata(daily)
cerebro.resampledata(daily, timeframe=bt.TimeFrame.Weeks, compression=1)
# 重放:把 5 根日线重放成 1 根,但策略会"看到"这 5 根依次演化
cerebro.replaydata(daily, timeframe=bt.TimeFrame.Days, compression=5)
白话区别:resample 像"先把一周每天收盘价平均成周线,再给你看";replay 像"周一给你看一次、周二再更新一次……周五定型",指标在过程中持续变化。绝大多数多周期策略用 resampledata 就够了。

三十二、minperiod 之前怎么跑:prenext 与 nextstart

每根 K 线、每个指标都有自己的最小周期(minperiod)。比如 SMA(close, period=50) 要等攒够 50 根才算出第一个值。在指标"准备好"之前,next() 根本不会被调用——策略看起来像在"装死"。Backtrader 提供两个钩子处理这种边界:

方法触发时机典型用途
prenext(self)最小周期达到之前,每根 bar 都会调(前提是你定义了它)想在指标就绪前就逐步建仓/记录;或打印"数据未就绪"日志
nextstart(self)最小周期刚好第一次达到的那一根,只调一次做"首次就绪"的一次性初始化(之后就交给 next)
next(self)最小周期达到后,每根 bar 调用(已讲)正常交易逻辑
import backtrader as bt

class MyStrat(bt.Strategy):
    params = (("pfast", 5), ("pslow", 50))

    def __init__(s):
        s.fast = bt.ind.SMA(s.data.close, period=s.p.pfast)
        s.slow = bt.ind.SMA(s.data.close, period=s.p.pslow)

    def prenext(s):
        # 慢线 50 根就绪前,每根都进来
        print("prenext @ bar", len(s), "数据未完全就绪,先观望")

    def nextstart(s):
        # 刚好第 50 根(慢线首个有效值)触发一次
        print("nextstart @ bar", len(s), "指标首次就绪,准备交易")

    def next(s):
        if not s.position and s.fast[0] > s.slow[0]:
            s.buy()
        elif s.position and s.fast[0] < s.slow[0]:
            s.close()
白话注解:不写 prenext,策略在 minperiod 前就真的什么都不做;写了 prenext,它会在"数据不齐"时依然每根跑一次。这里用不完整数据下单要格外小心——指标值可能是 NaN 或基于很少的样本,容易出"假信号"。绝大多数策略直接等 next 就够了。

nextstart 只触发一次,且必定紧接在 prenext 最后一次之后、next 第一次之前。别把它当"开盘初始化"用——开盘请用 __init__ 或 start()。

三十三、自定义数据源:继承 DataBase 喂自己的数据

内置 PandasData、GenericCSVData 覆盖了大多数场景。但数据在数据库、API、消息队列,或你想做特殊清洗时,就得自己写一个数据源。方法是继承 bt.feeds.DataBase,重写三个方法:

方法作用
start(self)初始化:打开连接、准备迭代器(只调一次)
_load(self)每次被问"还有没有下一根"时调用;把这一根填进 self.lines.xxx[0],有数据返回 True,没了返回 False
stop(self)收尾:关连接、释放资源(只调一次)
import backtrader as bt

class MyData(bt.feeds.DataBase):
    # 加一个自定义参数,让外部把数据源传进来
params = (("source", None),) # source 可以是列表 / 生成器 / 数据库游标

    def start(self):
        # 把传进来的数据源包成迭代器
        self.it = iter(self.p.source)

    def _load(self):
        try:
            row = next(self.it)
        except StopIteration:
return False # 没数据了,回测结束
        # row 假设是 (datetime, open, high, low, close, volume)
        # 把这一根写进 lines;DataBase 默认有 open/high/low/close/volume/openinterest
self.lines.open[0] = row[1]
self.lines.high[0] = row[2]
self.lines.low[0] = row[3]
        self.lines.close[0] = row[4]
        self.lines.volume[0]= row[5]
        # 时间:可赋值给 self.lines.datetime[0] = bt.date2num(row[0])
return True # 这一根有效

    def stop(self):
pass # 真实场景在这里关数据库连接

接入方式和内置 feed 一样:

# 假设 rows 是上面 row 结构的列表
data = MyData(source=rows)
cerebro.adddata(data)
白话注解:_load 就是个"喂数据"的循环函数,Backtrader 每要一根就调一次,你给一根。真实接入行情 API 时,把 start 换成建立 WebSocket/HTTP 连接、_load 里从队列取最新 tick 合成 K 线即可。注意时间列要转成 Backtrader 的 bt.date2num() 数值格式,否则排序会乱。

自定义 feed 不处理复权、不校验时间连续性,这些要你自己保证。回测前先用少量数据 print(data.close[0]) 验证喂对了再跑全量。

三十四、自定义佣金方案:继承 CommInfoBase

第三十八章的 setcommission 覆盖标准佣金。但遇到阶梯费率、按手固定费、最低佣金、期货按点值算等 setcommission 表达不了的规则,就要继承 bt.CommInfoBase 重写佣金算法。

核心是重写 _getcommission(self, size, price, pseudoexec),返回这笔成交的佣金金额(浮点):

import backtrader as bt

class TieredCommission(bt.CommInfoBase):
    params = (
("per_trade_fee", 0.0), # 每笔固定费用(如交易所规费)
("min_commission", 1.0), # 单笔最低佣金
    )

    def _getcommission(self, size, price, pseudoexec):
        # size: 成交数量(可负)  price: 成交价  pseudoexec: 预演时为 True
        notional = abs(size) * price
        # 阶梯:名义金额越大,费率越低
        rate = 0.0003 if notional > 100000 else 0.0005
        comm = notional * rate + self.p.per_trade_fee
return max(comm, self.p.min_commission) # 不低于最低佣金

挂到 broker(替换默认的佣金方案):

cerebro.broker.addcommissioninfo(TieredCommission(per_trade_fee=0.5, min_commission=1.0))
白话注解:pseudoexec=True 表示这次计算是"预演"(比如算能买多少股时),真实成交时是 False。绝大多数自定义佣金里直接忽略它即可。期货场景可在 _getcommission 里用 self.p.mult(合约乘数)和 size * price * mult 算名义金额,实现按点值收费。

重写的是 _getcommission(带下划线,框架内部调用),不是 getcommission。addcommissioninfo 会替换对应标的的佣金方案;想只对某个标的生效,传 name= 参数指定标的名。

三十五、给订单贴标签:order.addinfo 与 order.info

一个策略可能同时下"金叉进场单""止损单""止盈单",回测完后想在 notify_order/notify_trade 里区分它们,或在结果里统计各类订单表现——这时用 order.addinfo(**kwargs) 给订单贴任意标签。

class TagStrat(bt.Strategy):
    def next(s):
        if not s.position:
            o = s.buy(size=100)
o.addinfo(tag="entry", signal="ma_cross") # 贴标签
        elif s.position and s.fast[0] < s.slow[0]:
            o = s.close()
            o.addinfo(tag="exit", signal="ma_dead_cross")

    def notify_trade(s, trade):
        if trade.isclosed:
tag = trade.history[0].event.order.info.get("tag") # 读标签
            print("平仓单来源:", tag, "盈亏:", round(trade.pnl, 2))
白话注解:addinfo 把键值存进 order.info(一个 AutoOrderedDict),用 order.info.tag 或 order.info.get("tag") 读取。它纯属元数据,不影响成交,但能让你导出成交明细时按来源分组统计,复盘哪类信号更赚钱。

info 只能在 addinfo 调用之后读取;订单刚创建、还没 addinfo 时读会拿不到值。标签名别和 Backtrader 内部字段撞车(用 entry/exit/signal 这类业务词最安全)。

三十六、更多绩效分析器:TimeReturn / Returns / AnnualReturn / SharpeRatio_A / GrossLeverage / PeriodStats

第十九章讲了 Sharpe/DrawDown/TradeAnalyzer/SQN/VWR/Calmar。下面这些同样是内置 analyzer,补上后绩效评估更立体:

分析器返回什么关键参数
TimeReturn按周期(日/周/月)的收益率,键是日期timeframe=bt.TimeFrame.Weeks
Returns期末累计收益序列(净值增长)—
AnnualReturn每个自然年的收益率,键是年份—
SharpeRatio_A年化夏普(SharpeRatio 默认按总周期,这个按年)timeframe=bt.TimeFrame.Years, riskfreerate=0.0
GrossLeverage总杠杆 = 持仓市值 / 净资产,看用了几倍杠杆—
PeriodStats区间统计:平均收益、收益标准差、偏度、峰度、最佳/最差timeframe
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name="timereturn", timeframe=bt.TimeFrame.Weeks)
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name="annual")
cerebro.addanalyzer(bt.analyzers.SharpeRatio_A, _name="sharpe_a", timeframe=bt.TimeFrame.Years)
cerebro.addanalyzer(bt.analyzers.GrossLeverage, _name="leverage")
cerebro.addanalyzer(bt.analyzers.PeriodStats, _name="pstats")

# 跑完读取(注意:这几个 get_analysis() 返回的都是字典)
tr = strat.analyzers.timereturn.get_analysis() # {datetime: 周收益率}
ann = strat.analyzers.annual.get_analysis() # {2022: 0.12, 2023: -0.05, ...}
sh_a = strat.analyzers.sharpe_a.get_analysis() # {'sharperatio': 1.8}
lev = strat.analyzers.leverage.get_analysis() # 杠杆序列
pst = strat.analyzers.pstats.get_analysis() # {'average':..., 'stddev':..., 'skew':...}
白话注解:年化夏普(SharpeRatio_A)比总夏普更适合跨策略比较,因为都折算到"每年"。GrossLeverage 能暴露你不知不觉用了几倍杠杆——杠杆越高,一个反向跳空就可能爆仓。AnnualReturn 让你看清"哪年赚哪年亏",而不是被平均掩盖。

和 Calmar/VWR 一样,这些分析器的 get_analysis() 返回字典,别写成 .sharperatio 属性(只有 TradeAnalyzer 的部分字段是属性)。读不到就用 dict(...) 打印出来看结构。

三十七、绘图进阶:plotmaster / numfigs / iplot

第九章和二十二章讲了基础绘图与自定义样式。下面几个 cerebro.plot() 和指标上的开关,能解决"图太挤""想画一起""想在 notebook 里交互"三个痛点:

开关作用
指标.plotinfo.plotmaster = 主图数据把指标画到另一数据源的主图上,而不是独立子图
cerebro.plot(numfigs=2)图太多时分多页(多张图)显示,避免挤成一团
cerebro.plot(iplot=True)在 Jupyter Notebook 里交互式显示(需装 ipywidgets/bqplot)
cerebro.plot(volume=False)关闭成交量副图
class PlotStrat(bt.Strategy):
    def __init__(s):
        s.sma = bt.ind.SMA(s.data.close, period=20)
        # 把 SMA 画到收盘价主图,而不是单独占一个子图
        s.sma.plotinfo.plotmaster = s.data
    def next(s): pass

# 多图分页 + 关闭成交量
cerebro.plot(numfigs=2, volume=False)
# Jupyter 里交互
# cerebro.plot(iplot=True)
白话注解:plotmaster 特别适合"主图叠加均线/布林带",一眼看价格和指标的关系;numfigs 在多标的、多指标时让版面不爆炸;iplot=True 在 notebook 里能缩放拖动,复盘体验更好。

自定义 Plotter(含第二十二章的中文字体修复)可以这样传:from backtrader import plot; cerebro.plot(plotter=plot.Plotter())。无 GUI 的服务器环境直接 cerebro.plot() 可能因 matplotlib 后端报错,属于环境问题,不是代码错——存成文件或设 MPLBACKEND=Agg 即可。

三十八、实用数学工具 bt.mathsupport

写自定义指标或策略时,要做平均、标准差这类统计,除了用 numpy,Backtrader 自带一个轻量模块 bt.mathsupport,无需额外依赖:

import backtrader as bt

bt.mathsupport.average([1, 2, 3, 4]) # 2.5 平均值
bt.mathsupport.standarddev([1, 2, 3, 4, 5]) # 标准差
bt.mathsupport.variance([1, 2, 3, 4, 5]) # 方差

在自定义指标里用它的例子:

class MyStd(bt.Indicator):
    lines = ("std",)
    params = (("period", 20),)

    def __init__(s):
        # 用框架自带函数,而不是每次都 import numpy
        s.addminperiod(s.p.period)

    def next(s):
window = s.data.close.get(size=s.p.period) # 取最近 period 个收盘价
        s.lines.std[0] = bt.mathsupport.standarddev(window)
白话注解:bt.mathsupport 是 Backtrader 内部也在用的统计小工具。功能比 numpy 弱,但胜在"框架原生"、零额外依赖。简单统计用它足够;要做矩阵运算、向量化回测,还是 numpy/pandas 更合适。

三十九、本课小结

  • 两大块:第一部分"基础教程"带你从零安装、懂 Lines 核心抽象、写出第一个策略、跑通实战案例;第二部分"核心知识点"深入订单/仓位/指标/多标的/优化/绩效/实盘。
  • Backtrader 核心是 Lines/LineSeries:在 __init__ 声明式拼线,在 next 用 [0] 取当前值(第四章)
  • 完整的 notify_\* 生命周期方法(order/trade/cashvalue/fund/data/timer/store,见第三十六章)管理订单与成交;读绩效用 self.analyzers(不是 self.stats)
  • 订单分市价/限价/止损/追踪止损,支持 Bracket、OCO、parent/transmit 组合单;Order/Trade 对象有完整属性可读(第十一章/第十七章)
  • 用 Sizer 管理仓位,用 Commission/Slippage 模拟真实成本(含 A股印花税与自定义列映射)
  • 数据用 PandasData / GenericCSV 加载,支持 fromdate/todate 裁剪与自定义列名映射;注意复权、缺失交易日与前视
  • 内置几百个指标(含 bt.ind 简写与 Sum/Highest/ROC/StdDev 等),也能自定义 Indicator;用 bt.And/Or/If 组合多条件
  • Analyzers 出成绩单(Sharpe/DrawDown/TradeAnalyzer/SQN/VWR/Calmar/Transactions),Observers 画图,Writer 导出;还能自定义 Analyzer/Observer
  • 参数优化要稳健提取 + Walk-forward 样本外检验,可 self.env.runstop() 提前终止;严防过拟合
  • 进阶:add_timer 定时、cheat_on_open、exactbars 性能、PyFolio 专业分析、SignalStrategy 快速拼策略、Bokeh 交互图
  • 还覆盖边界与扩展:minperiod 前的 prenext/nextstart、自定义数据源 DataBase 与自定义佣金 CommInfoBase、订单标签 addinfo、HeikinAshi 平均 K 线、绘图进阶(plotmaster/numfigs/iplot)与 bt.mathsupport 数学工具
  • 纯回测框架,实盘经 Store 扩展(IB 等);国内期货用 vn.py / TqSdk
  • 回测≠实盘,所有业绩不代表未来收益
本站固定免责声明:本教程仅讲解软件编程与操作方法,不构成投资建议或策略推荐。回测业绩不代表未来收益,EA 自动交易风险大,请谨慎决策。量化交易涉及杠杆时可能放大亏损,实盘前请确保已充分理解策略逻辑与风险。
付费专享 · 核心知识点 18 章

继续阅读:核心知识点,搞懂原理

EA13 Python/Backtrader 策略 — 共 57 章,已免费阅读 39 章,剩余 18 章核心知识点讲透「为什么这样、点了会怎样」。

四十、安装与环境排错
四十一、回测是怎么跑起来的
四十二、第一个最小策略:双均线
四十三、内置指标 + 自定义指标(+ bt.ind 简写)
四十四、多标的 / 多周期策略
四十五、日志与调试技巧 + 报错速查
…等共 18 章核心内容
✍️ 注册后扫码购买 🔐 已有账号,去登录 单套 ¥69 永久 · 会员可阅
SERVICE & PRICING
服务收费说明 · 明码标价,无隐藏收费
全部课程统一 ¥39-69,买一套永久使用。以下为购买与使用的完整流程。
01先免费试读

每套教程的「使用手册」部分均可免费阅读,先确认内容是否适合自己再决定,不必盲买。

02扫码付款

选定课程后点「立即解锁」,页面展示收款二维码与客服微信,扫码转账即可,无需注册海外支付。

03获取卡密

付款完成后客服第一时间发送卡密号与卡密密码,站内消息与微信同步通知。

04兑换解锁

在用户中心「兑换卡密」输入卡密号与密码,系统即时解锁全部章节,无需二次付款。

一次付费,终身可用 · 解锁后课程永久留在账号里,不限次数、不限时间阅读 持续免费更新 · 平台改版、规则变化时课程同步修订
RECOMMEND
相关教程推荐 · 同栏目更多上手机会

💬 用户评价 (0)

🔒 登录并成为会员后可发表评价
还没有评价 · 成为第一个评价的人吧 🎯