backtrader 使用笔记

Backtrader 简介

Backtrader 是由 Python 实现的一个开源的量化回测工具,配合 Akshare 可以获取数据并进行投资策略的实现。

Backtrader 中增加基准收益

基准收益

看到许多回测的框架都附带一个基准收益,一般设置为上证指数或者沪深 300,在 backtrader 中有类似的方法可以添加一个基准方案。

这里我选择的是使用 observers 来观测基准收益和策略的收益区别。

观察周期

观察器指定基准的时候,需要指定数据和观察的周期,这里我们观察的是整个过程,也可以选择传入 bt.TimeFrame.Years 或者其它的周期值。

title: backtrader 时间周期 bt.TimeFrame.Days, bt.TimeFrame.Weeks, bt.TimeFrame.Months, bt.TimeFrame.Years, bt.TimeFrame.NoTimeFrame,

代码实现

添加基准收益主要在三步:

  1. 增加对应的数据,这里用的是沪深 300。
  2. 将数据添加到回测的数据池中,指定名称为 benchmark ,这里如果是股票池之外的数据,需要在策略中,处理数据池的时候,排除掉作为基准的数据,以防止除现误差。
  3. 指定一个观察器,通过观察器可以实现类似的基准的方案。
benchmark_path = os.path.join(modpath, f'datas/indexs/sh000300.csv')
benchmark_data = ETFCsvData(dataname=benchmark_path, fromdate=start_date, todate=end_date)
cerebro.adddata(benchmark_data, name='benchmark')
cerebro.addobserver(bt.observers.Benchmark, data=benchmark_data, timeframe=bt.TimeFrame.NoTimeFrame)

Backtrader 常见量化策略实现

基准基金

「基准基金」的是作为个人量化策略测试用的标杆基金。选择的是收益、回撤和风险的对照组基金,主要是一些场外的比较优秀的基金。

这次选择的基准基金有 2 只,第一只是『富国天惠成长混合 161005』,近乎神话的基金,16 年 21 倍增长;第二只是我比较关注的『广发多因子混合 002943』。

投资策略

目前主要用于实盘的策略是「MOM 动量策略」和「开弓策略」 。

『因为一般比较关注的是近三年的收益情况,所以回测主要以近三年的数据进行比较。』

还有一些做了回测但是数据不够好或者是很差的策略,仅做为介绍,未进行实盘。

周内策略

  • 背景

    在学习回测技术的时候,看到了一篇「周内效应」的文章,主要说的是关于国内大 A 的一个效应,看起来就一周内的每天都有涨跌的趋势,可以利用这个趋势进行买卖股票。

    文章中给的这个策略的净值达到了 41.45,年化率是 28.78%,最大回撤 34.99% 。

  • 结论

    在文章的最终,作者给出了一个周内效应的归纳,如下:

    若今日收盘价高于 20 日均线则为牛市,否则为熊市。

    牛市中,周一和周五表现较好,周二和周四表现较差。

    熊市中,周二和周三表现较好,周一和周四表现较差。

    然后依据这个效应,作者进行了一个针对沪深 300 的回测,年化达到了 28.7% ,而回撤 34.99% ,这个是完全高于沪深 300 的指数的,而且于长红的 富国天惠成长混合 161005 差不多了,可能回撤相对高一些。

    具体的操作思路如下:

    牛市下个交易日为周一或周五则买入下个交易日为周二或周四则卖出熊市下个交易日为周二或周三则买入下个交易日为周一或周四则卖出

  • 回测

    因为是针对沪深 300 的操作,所以选择的是 510300 这支基金,看一下开始的时间为 2012 年 5 月 4 日。

    其它还有一些基金的规模不够大,或者是时间相对比较短,选就要选最大的一支。

    为了复测这个效果是否合理,再选择两支基金进行测试,一支选择与深沪 300 同源的中证 500 的指数,510500 ,另外一支选择创业板 159915 这支 ETF。

    首先准备数据,从 Akshare 上下载对应的数据文件。

    fund_detail = ak.fund_etf_hist_sina(symbol=etf_fund_code)
    

    然后将三支基金的数据文件下载下来,分别是「sz159915 创业板」、「sh510300 沪深 300」和「sh510500 中证 500」三只基金。

    然后编写回测用的程序。

    def next(self):
        if self.price[0] >= self.sma[0]:
            "牛市"
            if self.weekday(self.datas[0].datetime.date(0)) in (4, 3):
                self.order = self.order_target_percent(data=self.datas[0],
                                                       target=0.98)
            if self.weekday(self.datas[0].datetime.date(0)) in (0, 2):
                self.order_target_percent(data=self.datas[0], target=0)
        else:
            "熊市"
            if self.weekday(self.datas[0].datetime.date(0)) in (0, 1):
                self.order = self.order_target_percent(data=self.datas[0],
                                                       target=0.98)
            if self.weekday(self.datas[0].datetime.date(0)) in (4, 2):
                self.order = self.order_target_percent(data=self.datas[0],
                                                       target=0)
    

    因为示例给的是从 2006 年开始做的测试,但是时间太久了,可能更希望看最近几年的简略如何,将开始时间修改到 2015 年 4 月 15 日测试了一下,不是很好,至少比我之前使用的动量策略、动量钟摆策略都要差。

    为什么选择 2015 年 4 月 15 日 ?

    因为 akshare 上的中证 500 的数据在这个时间点之前是有问题的,回测之后发现收益率达到了 35 倍,这显然是不可能的。所以查看了一下数据,可能是中间编制修改过,但是价格没有重新计算导致有问题。后面把这三支基金都换成指数来测试一下,应该会更加准确。

    当然也有可能我的选择不是特别合理,可以把开始时间拉到 2006 年开始,但是这就需要按指数进行测试,不能使用场内基金。

    初始资金 140000.00 。

    场内基金 资产总值 总利润 投资回报率 夏普指数 最大回撤周期 最大回撤 最大回撤资金
    创业板 176533.98 36533.98 0.26 0.22 1124 42.99 68328.84
    沪深 300 212969.75 72969.75 0.52 0.46 320 29.83 51248.03
    中证 500 198141.46 58141.46 0.42 0.35 1131 35.6 61531.16

    又修改了一下代码,增加了一些功能,实现了指数数据的回测,还是用的这三个指数,时间范围从 2010 年 6 月 1 日到 2021 年 7 月 8 日的数据。

    指数 资产总值 总利润 投资回报率 夏普指数 最大回撤周期 最大回撤 最大回撤资金
    创业板 1254578.27 1114578.27 7.96 0.78 916 27.75 312852.16
    沪深 300 701822.57 561822.57 4.01 0.80 312 22.72 118498.77
    中证 500 932812.03 792812.03 5.66 0.87 1126 34.79 261791.33

    看数据要比使用 ETF 基金进行回测要好太多了,这个就有点奇怪了,于是把时间拉到和 ETF 相同的时间,结果就基本上差不多了,可以发现,到后面之后,指数的周内效应收益就很一般了。

动量策略

  • 理论

    动量策略的理论基础是:『股票的收益率有延续原来的运动方向的趋势。——强者恒强。』

    依据这个理论,还有一个延伸的出来的策略——动量反转策略:『表现差的股票在其后的一段时间内有强烈的趋势经历相当大的逆转。——弱者转强。』

  • 策略

    依据这两个理论进行操作的交易策略,也就是在一段时间内,选择收益「最好或者最差」的进行操作。

  • 回测

    选择一个基金组合,从中选择动量最好的一支基金买入持有至下一个周期。

    def next(self):
        buy_id = 999
        c = [
            i.momentum[0]  #/ (i.datas[0].close + i.datas[0].open) * 2
            for i in self.mom
        ]
        index, value = c.index(max(c)), max(c)
        if value > 0:
            buy_id = index
    
        for i in range(0, len(c)):
            if i != buy_id:
                position_size = self.broker.getposition(
                    data=self.datas[i]).size
                if position_size != 0:
                    self.order_target_percent(data=self.datas[i], target=0)
    
        if buy_id != 999:
            position_size = self.broker.getposition(
                data=self.datas[buy_id]).size
            if position_size == 0:
                self.order_target_percent(data=self.datas[buy_id], target=0.98)
    

    实现的思路非常简单,就只是计算出「基金组合」中所有的基金的「动量值」,然后取最高的一只买入,然后每天都依据收盘价计算出「动量最好」的一只基金,如果不再是最好的,则清掉当前持仓的基金并买入最好。

    基金组合选择的是「沪深 300」,「中证 500」 和「创业板」三支。

    从 2015 年 4 月 15 日开始回测,起始资金为 140000.00 元。与上次的数据做一下比较,可以说是非常好的,回撤也才 22% 。

    资产总值 总利润 投资回报率 夏普指数 最大回撤周期 最大回撤 最大回撤资金
    318601.90 178601.90 1.28 0.69 800 22.05 46102.07

动量钟摆策略

这个策略的实现和动量策略一致,只不过在指标不同,总体思路是一样的。

资产总值 总利润 投资回报率 夏普指数 最大回撤周期 最大回撤 最大回撤资金
322237.92 182237.92 1.30 0.87 475 23.48 48961.35

总体来说,收益也相差不多,但最查看对应的回测的图片可以发现,在持续上涨过程中,交易次数过多了。

  • 总结

    策略的收益还是不错的,从 2015 年开始到现在,约 128% 的收益,与富国天惠比较一下,将时间拉到 2005 年 11 月 16 日。

    再重新跑一下。

    基金 投资回报率 夏普指数 最大回撤
    动量钟摆策略 8.07 0.72 23.48
    动量策略 4.36 0.62 22.05
    富国天惠 21.55 1.04 28.37

    恩,还是比富国天惠差很多呀。但是能看出动量钟摆要比动量熏略稳定不少,后面再优化一下两个策略。

  • 基金组合

    为了进一步提升策略的收益,组合的优化也是很重要的一点,整个组合有三个阶段。

    • 初代组合

    初代组合使用的是代表大盘股的「沪深 300 」和代表中盘股的「中证 500」再加上「创业板」作为一个基金的组合,这样实现的宽基轮动操作周期比较长,但是操作次数少。

    • 二代组合

    初代组合的三支基金代表大盘、中盘和创业,但是如果选择其中最好的前 50 的数据来进行交易的话,就是之前的「龙头股」。所以组合的三支基金改为「上证 50」、「创业 50」和「中证 500」来回测一下。

    中证 500 主要是因为没有可替代的基金,所以还是选用的「500ETF」。

    从上面的回测数据来看要好一些。

    • 三代组合

    三代组合中依旧是选择初代版本的基金组合,主要是将「创业板」替换成了「创成长 sz399296」,其中还分别测试了使用沪深 300 和上证 50 都试了一下,所以沪深 300 作为三代 1 ,上证 50 作为三代 2 。

    基金组合 投资回报率 最大回撤 夏普指数
    初代 1.67 17.43% 5.67
    二代 2.35 17.32% 3.12
    三代 1 2.54 18.95% 5.22
    三代 2 2.35 18.69% 13.85

    但是可以选择 二代、三代的两个作为回测的目标。

  • 策略优化

    数据看起来比之前的数据要漂亮很多,主要是引入了一个新的参数:「动量比率」,这个参数主要是为了解决两支标的基金的动量差别比较小的时候,频繁切换指数造成的交易损失。

    为此定义一个参数用来控制交易波动的可忽略的误差,在某个误差之内,则可以继续持有这支基金,但是因为这个比率更像是经验数据,所以会有一定的局限性。

    但是未想到更好的方法,所以暂利用这个进行更新。

    目前看四个基金组合的「动量比率」分别是:2.4、3.5、2.7、3.4,周期也都在 13 左右,可见两周是一个比较合适的周期。

    为了去除过拟化,将动量比率设定为 2.5 和 3.5 两个,周期均定为 10 。

    基金组合 投资回报率 最大回撤 夏普指数
    初代 1.48 18.13% 10.0
    二代 2.22 17.43% 6.92
    三代 1 2.01 18.13% 7.00
    三代 2 2.09 17.04% 6.46

    收益结果还可以接受。后面就是看下一下买入的机会,期待调仓了。

均值回归策略

  • 简介

    In finance, mean reversion is the assumption that a stock’s price will tend to move to the average price over time.

    译:在金融领域,均值回归假设随着时间的移动,股票的「价格」朝着它的均值移动。

    RSI 指标是一个常见的技术指标,修改 RSI 的周期来作为买卖点的信号,周期使用 2 个交易日:

    • RSI >= 90 超买信号
    • RSI <= 10 超卖信号
  • 买入信号:

    1. 以 200 日均线为基准,高于 200 为上升趋势,找做多信号;低于 200 则找做空信号。
    2. 考虑到均值回归,同时满足价格低于 10SMA 且 RSI < 10,做多。
  • 卖出信号:

    1. 价格突破 10 SMA 时可以只保留一半仓位;
  • 实现

    目前回测工具主要是利用的 Akshare 和 backtrader,这次使用的指标主要有两个:

    • SMA 均线两条:200SMA 和 10SMA
    • RSI_Safe 信号:RSI(2)
  • 回测

    使用的是单支基金中证 500 进行的回测。三年收益率只有 0.42,但是策略的胜率真的很高,达到了 80.0% 。

    但是因为收益率比较低,所以最终的夏普率只有 0.977 ,只能说还可以接受。

    使用创业板和中证 1000 进行回测。还不如中证 500 呢。

    基金 投资回报率 最大回撤 夏普指数
    中证 500 0.42 6.15% 0.977
    创业板 0.16 14.30% 0.429
    中证 1000 0.14 13.262% 0.968

双 RSI 策略

  • 介绍

    RSI 是比较常见的指标,之前看到有一个 RSI 的快慢金叉方案的策略,今天实现了一下,策略比较简单,所以最终的结果也不是特别好。

  • 交易逻辑

    买入逻辑:

    • 在 RSI 值高于震荡指数 55 时,则在快 RSI 上穿慢 RSI 时,则买入。
    • 在 RSI 值低于震荡指数 55 时,则需要 RSI 在慢 RSI 上持续 3 天以上才进行买入。

    卖出逻辑:

    • 当快 RSI 下穿慢 RSI 时,则卖出持仓。
  • 策略解析

    • 慢速 RSI 在 55 以上,单边上涨,快速 RSI 上穿慢速 RSI 可建仓
    • 慢速 RSI 在 55 以下,震荡市场,连续 N 天快速 RSI 大于慢时 RSI 可建仓
    • 慢速 RSI 在 60 以上,牛市,无需减仓
    if self.slow_rsi > self.p.normal_sign:
        if self.cross == 1.0 and position_size <= 0:
            self.order_target_percent(target=1.0)
        elif self.cross == -1.0 and position_size > 0:
            self.order_target_percent(target=0)
    if self.slow_rsi < self.p.normal_sign:
        buy_flag = True
        for i in range(self.p.upwards_day):
            if self.cross[-1 * i] < 0:
                buy_flag = False
        if buy_flag and position_size <= 0:
            self.order_target_percent(target=0.5)
        elif buy_flag and position_size > 0:
            self.order_target_percent(target=0)
    
    策略 投资回报率 最大回撤 夏普指数
    双 RSI 0.13 14.68% 0.61

钱德摆策略

  • 理论

    • Su 是今日收盘价与昨日收盘价(上涨日)差值加总。若当日下跌,则增加值为 0;
    • Sd 是今日收盘价与做日收盘价(下跌日)差值的绝对值加总。若当日上涨,则增加值为 0

    \[ CMO = \frac{S_u-S_d} {S_u+S_d} \times 100 \]

  • 指标

    钱德摆动指标在「Backtrader」 的默认指标中是没有的,但是可以在「Ta-Lib」中获取,所以在初始化时,使用下面的方式。

    bt.talib.CMO(i.close, timeperiod=self.params.period)
    
  • 钱德动量摆动策略

    实现的思路非常简单,就是计算出基金组合中所有的基金的钱德摆动值,然后取最高的一支买入,然后每天都依据收盘价计算出动量最好的一支基金,如果不再是最好的,则卖出当前的基金,买入最好的那支。

    基金组合选择的是沪深 300,中证 500 和创业板三支。

    回测最近三年的数据,起始资金为 1000000.00 元。与上次的数据做一下比较,回撤 17.09% 。

    这个策略的实现和动量策略差不多,只不过在指标不同,总体思路是一样的。

    投资回报率 夏普指数 最大回撤
    148.87% 0.94 16.11%

    总体来说,收益一般,但是比随意的交易要好很多了。

    • 减少交易次数

    与动量摆策略一样,还是应该选择减少交易次数才能获得更好的收益。

  • 总结

    策略的收益还是不错的,从 2015 年开始到现在,约 128% 的收益,与富国天惠比较一下,将时间拉到 2005 年 11 月 16 日。

    再重新跑一下。

    基金 投资回报率 夏普指数 最大回撤
    钱德摆动策略 5.03 0.45 47.18%
    动量策略 4.36 0.62 22.05%
    富国天惠 21.55 1.04 28.37%

    恩,还是比富国天惠差很多呀。

    另外夏普率也低不说,回撤还达到了 47% ,基本上等于有一半的回撤了,这个基本可以抛弃掉了。

布林强盗策略

  • 起因

    之前看到了一个非常强的策略,说是布林强盗策略,这几天一直在在研究如何实现这个策略。

    具体的策略介绍如下:

    假设周期为日线,价格超过 50 日平均线上方一个标准差作为买入信号的标准,跌破 50 日平均移动线下方一个标准差作为卖出信号的标准。

  • 策略实现

    针对策略详细做了几天的研究,主要需要实现的功能如下。

    • 入场确认

    策略有一个确认模块,交易开仓时需要多次确认,即当日收盘价必须高于 30 日前收盘价才能做多,低于 30 日前收盘价才能做空。

    • 出场确认

    积极方式出场,当建立仓位时,保护性「止损点」设置在 50 日均线(中轨)。之后持有头寸的时间每多一天,计算移动平均线的天数减一。持有头寸时间越长,周期越短。如果达到 10 则不再递减。

    • 次条件

    如果持多仓,移动平均线低于上轨发出平仓信号,如果持空仓,移动平均线高于下轨发出平仓信号

  • 总结

    入场条件:

    • 价格突破布林带上轨,收盘价大于 30 周期收盘价最高值,即做多;

    出场条件:

    • 持多仓的情况下,收盘价小于出场 MA,出家 MA 小于上轨,平仓。
    • 出场 MA 的值根据持仓周期变化。刚开仓为 50,持仓每增加一个周期,减 1,最小到 10。
  • 回测结果

    回测结果是没想到的差呀,三年的收益率是 -0.10 ,几乎是我实现的策略中最差的一个了。或许是因为使用的是单支基金,而且空仓时间比较多,交易的胜率也非常低。

  • 优化思路

    • 出场策略

    因为单支基金的表现都不特别好,但是这个出场策略是值得借鉴的,后面可以考虑把这个加入到动量策略中去。

    • 投资组合

    这个策略还可以从单支基金调整成为基金组合,但是三支基金的选择也是需要考虑,用什么来做为选择的第一指标的。

    如果使用动量来选择的话,有个问题,动量最高的基金与突破不同时出现应该如何操作。后面需要验证一下。

金肯特纳策略

  • 策略数据

    • 基础价格:(最高价+最低价+收盘价)/ 3
    • 中轨:基础价格的 N 周期指数移动平均值
    • 波动幅度:平均真实波动幅度(ATR)
    • 上轨:中轨+波动幅度
    • 下轨:中轨-波动幅度
  • 行情

    • 当价格在上轨和下轨之间运行时,我们可以认为是震荡行情。
    • 当价格突破上轨,说明更强大的买压已经出现,未来价格坑你会进一步上涨。
    • 当价格突破下轨,说明已经出现更强大的卖压,未来价格可能会进一步下跌。
  • 入场策略

    • 中轨向上,并且价格升破上轨,开多单;
    • 中轨向下,并且价格跌破下轨,开空单;
  • 出场策略

    • 当持有多单时,价格跌破中轨,平多单;
    • 当持有空单时,价格升破中轨,平空单;
  • 策略实现

    因为 A 股目前不允许做空,所以只有多单的情况,最终的实现如下:

    position_size = self.broker.getposition(self.datas[0]).size
    if self.open > self.sma[0] + self.p.dfact * self.atr[0]:
        self.order_target_percent(target=1.0)
    elif self.open < self.sma[0]:
        self.order_target_percent(target=0.0)
    
  • 回测结果

    还是使用沪深 300、中证 500 和创业板做一下回测。

    基金 投资回报率 夏普指数 最大回撤
    中证 500 -0.07 -0.22 25.76%
    沪深 300 0.19 0.55 24.32%
    创业板 0.21 0.62 11.58%

    由结果来看,比较一般,而且选择的标准越是成长性越好的基金,收益也会相应的好一些,但是还是低于动量策略的。

收益比较

将近三年中三支基金与策略投资方案按夏普比率进行排序。

基金 近三年收益 近三年最大回撤 夏普比率
周内交易策略 154.4% 17.65% 0.64
富国天惠成长混合 161005 102.96% 28.37% 1.04
动量策略 184.14% 12.71% 1.02
动量钟摆策略 2.0 188.35% 17.09% 1.21
兴全合润混合 163406 153.36% 19.98% 1.44
广发多因子混合 002943 203.57% 23.67% 1.56

Backtrader 组合生成

在生成策略的过程中,使用的是 Python 的序列方法直接生成,使用起来比较不友好,今天翻看策略时发现,其实 backtrader 提供对应的方法来生成组合的指标。

self.getdatanames()
按顺序返回所有股票的名称 list
self.getdatabyname(secu_name)
返回该股票的 data

而在生成指标序列的时候,使用字典比序列更方便,代码如下:

self.sma = {x: bt.ind.SMA(self.getdatabyname(x), period=self.p.maperiod) for x in self.getdatanames()}

Backtrader 按周重新取样

之前实现开弓策略的时候,是用日线的倍数替代的周线,重新翻看对应的方法后,发现可以使用 resample() 方法将数据转换成周数据。

data0 = bt.feeds.YahooFinanceData(datname='YHOO', fromdate=..., name='days')
cerebro.adddata(data0)
cerebro.resampledata(Data0, timeframe=bt.TimeFrame.Weeks, name='weeks')

smadays = bt.ind.SMA(self.dnames.days, period=30)  # or self.dnames['days']
smaweeks = bt.ind.SMA(self.dnames.weeks, period=10)  # or self.dnames['weeks']

做了转换之后,就需要将对应的开弓策略的买卖方案做一下修改,需要先按周数据获取指标的信息,然后再将对应的买卖转换到日数据上就可以了。

self.data_day = [i for i in self.datas if i._name != 'benchmark' and not i._name.endswith('weeks')]
self.data_weeks = [i for i in self.datas if i._name != 'benchmark' and i._name.endswith('weeks')]