Backtrader是用于量化回测的python框架,作者是德国人Daniel Rodriguez。相比于zipline等量化回测平台,backtrader是一个易懂、易上手的量化投资框架,今天我们试着用Backtrader进行简单的均线买入卖出量化策略回溯,即5日均线上穿20日均线,则表示股票处于强势,买入。反之,处于弱势,卖出。


下载

点击下载本文代码数据


安装

pip3 install backtrader

快速入门


  • 买入:MA5上穿MA20, 即五日价格移动平均线(MA5)和二十日价格移动平均线(MA20), 最近处于涨势

  • 卖出:MA20下穿MA5, 即五日价格移动平均线(MA5)和二十日价格移动平均线(MA20), 最近处于涨势


import datetime

import backtrader as bt

if __name__ == "__main__":
    #初始化
    cerebro = bt.Cerebro()

    #设定初始资金
    cerebro.broker.setcash(100000.0)

    #策略执行前的资金
    print('启动资金: {}'.format(cerebro.broker.getvalue()))

    #策略执行
    cerebro.run()

    #策略执行前的资金
    print('启动资金: {}'.format(cerebro.broker.getvalue()))
启动资金: 100000.0
启动资金: 100000.0

每次股票交易,证券经纪人会收取一定的佣金,如万三(每一万元交易收三元)即0.003

cerebro.broker.setcommission(0.003)

交易会有最小的购买/卖出份额,一般一手100股

cerebro.addsizer(bt.sizers.FixedSize, stake=100)

加载数据

  • 前复权:保持当前价格不变,将历史价格进行增减,从而使股价连续。 前复权用来看盘非常方便,能一眼看出股价的历史走势,叠加各种技术指标也比较顺畅,是各种行情软件默认的复权方式。 这种方法虽然很常见,但也有两个缺陷需要注意。

    • 为了保证当前价格不变,每次股票除权除息,均需要重新调整历史价格,因此其历史价格是时变的。 这会导致在不同时点看到的历史前复权价可能出现差异。
    • 对于有持续分红的公司来说,前复权价可能出现负值。
  • 后复权 :保证历史价格不变,在每次股票权益事件发生后,调整当前的股票价格。 后复权价格和真实股票价格可能差别较大,不适合用来看盘。 其优点在于,可以被看作投资者的长期财富增长曲线,反映投资者的真实收益率情况。

在量化投资研究中普遍采用后复权数据,使用 https://github.com/mpquant/Ashare 下载的股票数据

Backtrader将数据集称作数据流Data Feeds, 默认数据集是yahoo的股票数据,通过以下代码即可加载:

# 创建数据
data = bt.feeds.YahooFinanceCSVData(
    dataname='sz000725.csv',
    datetime=0,
    open=1,
    high=2,
    low=3,
    close=4,
    volume=5,
    dtformat=('%Y-%m-%d'),
    fromdate = datetime.datetime(2014, 7, 11),
    todate = datetime.datetime(2021, 12, 1)
)

添加指标

backtrader中内置了许多计算值表,比如移动平滑线、MACD、RSI等等, 我们这一篇文章仅需要移动平均线MA, 设置方法如下

self.sma5 = bt.indicators.MovingAverageSimple(self.datas[0], period=5)
self.sma20 = bt.indicators.MovingAverageSimple(self.datas[0], period=20)

datas[0]是第一个数据集, period是指多少天的移动平均线,比如5,则返回MA5的相关数据。


构建策略

使用backtrader构建策略是一件很简单的事情, 继承backtrader的策略类,并重写部分方法,就能实现策略。比如

  • 重写属于我们自己的log函数
  • 均线金叉死叉策略
class TestStrategy(bt.Strategy):
    """
    继承并构建自己的策略
    """
    def log(self, txt, dt=None, doprint=False):
        "日志函数,用于统一输出日志格式"
        if doprint:
            dt = dt or self.datas[0].datetime.date(0)
            print('{}, {}'.format(dt.isoformat(), txt))

    def __init__(self):
        # 初始化相关数据
        self.dataclose = self.datas[0].close
        self.order = None
        self.buyprice = None
        self.buycomm = None
        # 移动平均线初始化
        self.sma5 = bt.indicators.MovingAverageSimple(self.datas[0], period=5)
        self.sma20 = bt.indicators.MovingAverageSimple(self.datas[0], period=20)

    def notify_order(self, order):
        """
        订单状态处理
        Arguments:
            order {object} -- 订单状态
        """
        if order.status in [order.Submitted, order.Accepted]:
            # 如订单已被处理,则不用做任何事情
            return

        # 检查订单是否完成
        if order.status in [order.Completed]:
            if order.isbuy():
                self.buyprice = order.executed.price
                self.buycomm = order.executed.comm
            self.bar_executed = len(self)

        # 订单因为缺少资金之类的原因被拒绝执行
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')

        # 订单状态处理完成,设为空
        self.order = None

    def notify_trade(self, trade):
        """
        交易成果
        Arguments:
            trade {object} -- 交易状态
        """
        if not trade.isclosed:
            return

        # 显示交易的毛利率和净利润
        self.log('OPERATION PROFIT, GROSS {}, NET {}'.format(trade.pnl, trade.pnlcomm), doprint=True)

    def next(self):
        ''' 下一次执行 '''

        # 记录收盘价
        self.log('Close, {}'.format(self.dataclose[0]))

        # 是否正在下单,如果是的话不能提交第二次订单
        if self.order:
            return

        # 是否已经买入
        if not self.position:
            # 还没买,如果 MA5 > MA10 说明涨势,买入
            if self.sma5[0] > self.sma20[0]:
                self.order = self.buy()
        else:
            # 已经买了,如果 MA5 < MA10 ,说明跌势,卖出
            if self.sma5[0] < self.sma20[0]:
                self.order = self.sell()

    #def stop(self):
        #self.log(u'(金叉死叉有用吗) Ending Value {}'.format(self.broker.getvalue()), doprint=True)

策略回测

为了验证我们开头提到的策略,咱使用了 京东方sz000725 在2014年7月11日至今2021年12月3日的股票数据,将数据命名为sz000725.csv, 我们先用pandas审查下csv

import pandas as pd

df = pd.read_csv('data/sz000725.csv')
df.head()
|    |   Unnamed: 0 |        date |   open |   high |   low |   close |      volume |
|---:|-------------:|------------:|-------:|-------:|------:|--------:|------------:|
|  0 |            0 | 2.01407e+07 |   2.17 |   2.2  |  2.16 |    2.19 | 7.49341e+07 |
|  1 |            1 | 2.01407e+07 |   2.18 |   2.2  |  2.17 |    2.2  | 8.10931e+07 |
|  2 |            2 | 2.01407e+07 |   2.19 |   2.21 |  2.18 |    2.2  | 8.19694e+07 |
|  3 |            3 | 2.01407e+07 |   2.2  |   2.21 |  2.19 |    2.21 | 7.96481e+07 |
|  4 |            4 | 2.01407e+07 |   2.2  |   2.21 |  2.19 |    2.21 | 8.75106e+07 |

在backtrader中,使用GenericCSVData函数来加载csv,需要注明日期始末、open/high/low/close/volume等字段在csv中的列数(第几列,从0开始,0表示第一列)

import backtrader as bt
import datetime

if __name__ == "__main__":
    # 初始化模型
    cerebro = bt.Cerebro()
    init_cash = 100000.0
    fromdate = datetime.datetime(2014, 7, 11)
    todate = datetime.datetime(2021, 12, 3)

    #构建策略
    strategy = cerebro.addstrategy(TestStrategy)

    #每次买100股
    cerebro.addsizer(bt.sizers.FixedSize, stake=100)

    #加载数据到模型
    data = bt.feeds.GenericCSVData(
        dataname='data/sz000725.csv',
        fromdate=fromdate,
        todate=todate,
        dtformat='%Y%m%d',
        datetime=1,
        open=2,
        high=3,
        low=4,
        close=5,
        volume=6
    )

    cerebro.adddata(data)

    # 设定初始资金和佣金
    cerebro.broker.setcash(init_cash)
    cerebro.broker.setcommission(0.003)

    print('会不会玩了个寂寞?')
    #策略执行前的资金
    print('启动资金: {}'.format(cerebro.broker.getvalue()))

    #策略执行
    cerebro.run()

    #策略结束时的资金
    print('策略结束时资金: {}'.format(cerebro.broker.getvalue()))

    duration_year = (todate-fromdate).days/360
    end_value = cerebro.broker.getvalue()
    roi = pow(end_value/init_cash, 1/duration_year)-1
    print('策略年华收益率: {}%'.format(roi*100))

会不会玩了个寂寞?
启动资金: 100000.0
2014-08-27, OPERATION PROFIT, GROSS -3.000000000000025, NET -4.365000000000025
2014-10-28, OPERATION PROFIT, GROSS 10.999999999999988, NET 9.568999999999988
2014-11-24, OPERATION PROFIT, GROSS -4.0000000000000036, NET -5.584000000000003
2015-01-15, OPERATION PROFIT, GROSS 52.0, NET 50.242
2015-05-08, OPERATION PROFIT, GROSS 113.00000000000003, NET 110.82500000000003
2015-07-02, OPERATION PROFIT, GROSS 25.0, NET 22.075
2015-08-25, OPERATION PROFIT, GROSS -96.0, NET -98.076
2015-11-03, OPERATION PROFIT, GROSS -8.999999999999986, NET -10.760999999999985
2015-11-30, OPERATION PROFIT, GROSS -16.000000000000014, NET -17.812000000000015
2015-12-31, OPERATION PROFIT, GROSS -8.999999999999986, NET -10.820999999999986
2016-03-14, OPERATION PROFIT, GROSS -10.999999999999988, NET -12.514999999999988
2016-04-14, OPERATION PROFIT, GROSS 0.0, NET -1.548
2016-06-16, OPERATION PROFIT, GROSS -6.000000000000005, NET -7.404000000000005
2016-07-28, OPERATION PROFIT, GROSS 0.0, NET -1.404
2016-09-08, OPERATION PROFIT, GROSS 8.000000000000007, NET 6.566000000000007
2016-12-19, OPERATION PROFIT, GROSS 31.999999999999986, NET 30.421999999999986
2017-02-10, OPERATION PROFIT, GROSS 14.000000000000012, NET 12.110000000000012
2017-02-20, OPERATION PROFIT, GROSS -4.999999999999982, NET -6.940999999999982
2017-03-06, OPERATION PROFIT, GROSS -10.999999999999988, NET -12.976999999999988
2017-05-12, OPERATION PROFIT, GROSS 44.99999999999997, NET 42.86699999999997
2017-06-01, OPERATION PROFIT, GROSS -22.00000000000002, NET -24.38200000000002
2017-07-13, OPERATION PROFIT, GROSS -14.000000000000012, NET -16.406000000000013
2017-09-18, OPERATION PROFIT, GROSS -2.9999999999999805, NET -5.31899999999998
2017-11-27, OPERATION PROFIT, GROSS 153.00000000000003, NET 150.12900000000002
2018-01-08, OPERATION PROFIT, GROSS -8.000000000000007, NET -11.360000000000007
2018-02-01, OPERATION PROFIT, GROSS 2.000000000000046, NET -1.6419999999999533
2018-03-23, OPERATION PROFIT, GROSS -25.0, NET -28.303
2018-08-07, OPERATION PROFIT, GROSS -12.00000000000001, NET -14.124000000000011
2018-09-04, OPERATION PROFIT, GROSS -27.0, NET -29.115000000000002
2018-11-26, OPERATION PROFIT, GROSS -14.000000000000012, NET -15.668000000000012
2019-01-29, OPERATION PROFIT, GROSS -4.999999999999982, NET -6.598999999999982
2019-03-15, OPERATION PROFIT, GROSS 70.00000000000001, NET 67.90000000000002
2019-04-12, OPERATION PROFIT, GROSS -15.99999999999997, NET -18.35799999999997
2019-04-29, OPERATION PROFIT, GROSS -14.999999999999991, NET -17.34299999999999
2019-06-06, OPERATION PROFIT, GROSS -5.000000000000027, NET -7.043000000000027
2019-06-19, OPERATION PROFIT, GROSS 4.999999999999982, NET 2.9509999999999823
2019-08-07, OPERATION PROFIT, GROSS 36.999999999999964, NET 34.84299999999996
2019-08-30, OPERATION PROFIT, GROSS -10.999999999999988, NET -13.282999999999987
2019-09-27, OPERATION PROFIT, GROSS -41.99999999999995, NET -44.35799999999995
2020-02-04, OPERATION PROFIT, GROSS 30.00000000000003, NET 27.67200000000003
2020-03-06, OPERATION PROFIT, GROSS 9.999999999999964, NET 7.017999999999965
2020-05-28, OPERATION PROFIT, GROSS -25.0, NET -27.301000000000002
2020-07-21, OPERATION PROFIT, GROSS 54.999999999999986, NET 52.25499999999999
2020-09-14, OPERATION PROFIT, GROSS 52.999999999999936, NET 50.002999999999936
2020-10-19, OPERATION PROFIT, GROSS -5.999999999999961, NET -8.98199999999996
2020-12-10, OPERATION PROFIT, GROSS 2.000000000000046, NET -1.083999999999954
2021-02-02, OPERATION PROFIT, GROSS 66.00000000000001, NET 62.454000000000015
2021-03-03, OPERATION PROFIT, GROSS -13.000000000000078, NET -16.67500000000008
2021-03-11, OPERATION PROFIT, GROSS -33.999999999999986, NET -37.64199999999999
2021-05-12, OPERATION PROFIT, GROSS 31.00000000000005, NET 27.12700000000005
2021-07-06, OPERATION PROFIT, GROSS -33.00000000000001, NET -36.717000000000006
2021-07-26, OPERATION PROFIT, GROSS -34.999999999999964, NET -38.70499999999996
2021-09-17, OPERATION PROFIT, GROSS -21.999999999999975, NET -25.467999999999975
2021-11-22, OPERATION PROFIT, GROSS -7.000000000000028, NET -9.979000000000028
策略结束时资金: 100120.964
策略年华收益率: 0.016108152552885002%

策略可视化

from backtrader_plotting import Bokeh
from backtrader_plotting.schemes import Tradimo

b = Bokeh(style='bar', plot_mode='single', scheme=Tradimo())
cerebro.plot(b)

广而告之