【Backtrader】统计上证指数历年收益率

795 阅读5分钟

前言

【Backtrader】怎么获取指数数据》一文已经获取到了指数数据,接下来我们统计一下历年上证指数(SSE)的增长率。这个数据会作为基准值来与各策略的效果进行对比,所以会多次用到。顺便也再熟悉一下 backtrader 代码,抽象一些工具方法。

正文

需求描述

想得到满足下列要求的列表:

  • 横轴:起始年份,从 1.1 日算起;
  • 纵轴:截止年份,到 12.31 日截止(2022 年截止至 2022.8.11);
  • 数据:增长率,公式为 增长率 = 年末收盘价 / 年初开盘价 - 1,单位「%」;

下图即为最终结果(可以拿去用了)。比如第 3 行第 2 列的 -4.34 表示:2012.1.1~2013.12.31 的上证指数增长率为 -4.34%。

image.png

注意
有的同学可能注意到了,根据 2012,2013 两年的增长率算出的 2012~2013 增长率(-5.20)并不等于 -4.34。
这是因为 2012.12.31 收盘价为 2269,2013.1.4 开盘价为 2289,有约 0.88% 的差距,补上这个差值就差不多了,所以数据还是以表中为准

获取某年的增长率

要想得到上表,我们先要用代码实现某年的增长率如何计算。关键就是要获取到年末收盘价年初开盘价。本以为很容易,结果在这里踩了好多坑,过程就不赘述了,直接把关键的坑说出来吧。

首先,年末收盘价,如果 todate 设置成 2021.12.31 的话,用 self.datas[0].close[0] 获取到的是 2021.12.30 日的收盘价,所以 todate 要写成 2022.1.1 才行。

然后,如何获取年初的开盘价呢?笔者在文档里也没查到,后来通过各种打印 self.datas 发现,可以用 self.datas[0].open[1] 的方式访问到,但是需要在 __init__ 方法中使用。虽然不知道是不是最佳方式,能用就好。

最后,在 stop 函数中计算增长率,并挂载到 self.profit 上,为什么要这样做后面会有解释。

所以大概的代码就是:

class TestStrategy(BasicStrategy):
    def __init__(self):
        self.firstbaropen = self.datas[0].open[1]

    def stop(self):
        self.profit = (self.datas[0].close[0] - self.firstbaropen) * 100/self.firstbaropen

如何批量运行

因为我们要实现 n*n 这种表,所以双层 for 循环是肯定的了。而且我们要控制的是 Data 加载时候的起始时间,并不能用 cerebro.optstrategy 的方式实现批量运行,只能是每个单元格运行一次 backtrader(if __name__ == '__main__': 部分代码)来出结果,理论上要运行 n*n/2 次。

所以要想被批量执行,需要把 if __name__ == '__main__': 中的代码提取成一个函数,让其能够被调用。结合 【Backtrader】怎么获取指数数据 说的读取网易财经数据的方法,抽了一个 utils.py 文件出来:

# utils.py

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import os.path  # To manage paths
import sys  # To find out the script name (in argv[0])

import backtrader as bt

class NetsCSVData(bt.feeds.GenericCSVData):
    '''网易财经数据结构'''
    params = (
        ('nullvalue', 0.0),
        ('dtformat', ('%Y-%m-%d')),

        ('headers', False),
        ('datetime', 0),
        ('open', 3),
        ('high', 4),
        ('low', 5),
        ('close', 6),
        ('volume', 7),
        ('time', -1),
        ('openinterest', -1)
    )

def bt_run(Strategy, data_file, fromdate, todate, strategy_params={},
           strategy_mode=0, cash=9999999999999, commission=0, stake=1, plot=False, Sizer=bt.sizers.FixedSize, DataFeed=NetsCSVData, cerebro_opt={}):
    # Create a cerebro entity
    cerebro = bt.Cerebro(**cerebro_opt)

    # Add a strategy
    if strategy_mode == 0:
        cerebro.addstrategy(Strategy, **strategy_params)
    else:
        cerebro.optstrategy(
            Strategy, **strategy_params)

    # Datas are in a subfolder of the samples. Need to find where the script is
    # because it could have been called from anywhere
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, data_file)

    # Create a Data Feed
    data = DataFeed(
        dataname=datapath,
        fromdate=fromdate,
        todate=todate,
    )
    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Set our desired cash start
    cerebro.broker.setcash(cash)

    # Add a FixedSize sizer according to the stake
    cerebro.addsizer(Sizer, stake=stake)

    # Set the commission
    cerebro.broker.setcommission(commission=commission)

    # Run over everything
    result = cerebro.run()

    # Print out the final result
    # print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # Plot the result
    if plot:
        cerebro.plot()

    return result

之前 Demo 中的代码就变为了:

if __name__ == '__main__':
    bts = bt_run(TestStrategy, 'datas/000001-20220811.csv', datetime.datetime(
        2022, 1, 1), datetime.datetime(2023, 1, 1), plot=True)

注意最后一行的 return result,这行很关键,在执行之后我们需要获取想要的数据。比如上文提到的 self.profit,必须要 return 回去才能在上述代码中用 bts[0].profit 获取到。没错,bts 是个 list,因为有可能调用的是 cerebro.optstrategy,可能有多个返回值,所以必须是 list。

这块其实踩了一些坑,比如必须这样 cerebro = bt.Cerebro(optreturn=False) 传入参数,才能让 profit 这种自定义的属性保留在 bts[0] 上,否则都会给你过滤掉。也是笔者翻了很久文档才发现的,也不知道是不是最优解法,反正能用。

生成 csv

接下来就是写入 csv 了,这属于 python 的范畴,简单查阅了文档之后差不多就有头绪了。结合之前的代码,整体代码如下:

import datetime  # For datetime objects
import csv

# Import the backtrader platform
import backtrader as bt
from utils import (BasicStrategy, bt_run)


class TestStrategy(BasicStrategy):
    params = (
        ('printlog', False),
    )

    def __init__(self):
        self.firstbaropen = self.datas[0].open[1]

    def stop(self):
        self.profit = (self.datas[0].close[0] -
                       self.firstbaropen) * 100/self.firstbaropen
        # print('%2f / %2f - 1 = %2f%%' %
        #       (self.dataclose[0], self.firstbaropen, self.profit))


if __name__ == '__main__':
    years = range(2020, 2023)

    with open('sse.csv', 'w', newline='') as csvfile:
        csv.writer(csvfile).writerow(
            ['', *years])

    for end_year in years:
        row = [end_year]

        for start_year in years:
            if start_year > end_year:
                break
            bts = bt_run(TestStrategy, 'datas/000001-20220811.csv', datetime.datetime(
                start_year, 1, 1), datetime.datetime(end_year + 1, 1, 1), cerebro_opt={"optreturn": False})
            row.append(bts[0].profit)

        with open('sse.csv', 'a+', newline='') as csvfile:
            csv.writer(csvfile).writerow(row)

结论

到此基本上 backtrader 相关的准备工作就做齐了,可以开始大干一番了。经过这番折腾,对于 backtrader 也更了解了一些。关键是生成 csv 那块,这剩了手动复制粘贴的功夫了,还是很实用的。1991~2022(8.11)的完整数据请前往笔者的 飞书文档 查看。

本文写在 【Backtrader】专治割韭菜(一)——定投靠谱吗 之后,但是如果你更想知道代码的实现,那还是推荐先阅读本文。预告一下,专治割韭菜系列还会继续,还是挺有意思的。