Backtrader快速入门——1. QuickStart

1. backtrader介绍

1.1 基本情况

参考文档

backtrader框架介绍

官方定义:
backtrader是一个用于回测和交易的功能丰富的框架,可以让你专注于写可用的交易策略,指标和分析器,而不会花费时间在构建基础架构上

1.2 安装

安装的话还是直接去github上面看最新的吧。(截止2021.1.29我写这个博客的时候,github主页最新一次更新是在2020年6月,7个月之前。。。)

  • 支持python3.2及以上
  • backtrader这个库自成体系,只有绘图功能才会有外部依赖
  • pip安装:
"""不需要画东西 """
pip install backtrader  

"""需要画东西 """
pip install backtrader[plotting] -i https://pypi.tuna.tsinghua.edu.cn/simple 
"""加一个镜像,下载的快点"""
  • 我这里使用的python3.7的环境
    在这里插入图片描述
  • 默认安装的是1.9.76.123版本,关于版本号的解释:
    • X: 主版本号。 应该保持稳定,除非发生大的变化,比如用numpy进行大改
    • Y: 次要版本号。只有当添加(或禁止)了一个完整的新功能或者兼容的API版本才会发生改变
    • Z: 修订版本号。一般当文档更新,小的改变以及修复bug时才会改变
    • I: 平台已内置的指标数量

1.3 用到的一些金融词语

portfolio:投资组合,有价证券


2. QuickStart

主要就是参考两个文档,加了一些个人理解的修正。
(建议直接看英文,看不懂再去看中文翻译文档)

注意:由于这个QuickStart教程所使用的的数据文件会一直更新,所以最后得出的类似收盘价这些内容都会有所改变,所以文档中展示的一些结果可能和你实际操作之后得到的不同。

2.1 使用这个平台/框架

让我们从零开始来跑一些完整的例子,在此之前,先来了解两个基本概念。

折线(Line)

  • 交易数据(Data Feeds)、技术指标(Indicators)和策略(Strategies)都是折线(Line)。 折线(Line)是由一系列的点组成的。
  • 通常交易数据(Data Feeds)包含以下几个组成部分: 开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close)、成交量(Volume)、持仓量(OpenInterest)等。 比如:所有的开盘价(Open)按时间组成一条折线(Line),那么一组交易数据(Data Feeds)就应该包含了6条折线(Line)。
  • 再加上时间(DateTime)一共有7条折线(Line)。时间,一般用作一组交易数据的主键。

索引从0开始

  • 当访问一条折线(Line)的数据时,会默认从下标为0的位置开始,最后一个数据通过下标-1来获取。这样的设计和Python的迭代器是一致的,所以折线(Line)是可以迭代遍历的。
  • 例如:创建一个简单移动平均值的策略(均值策略): self.sma = SimpleMovingAverage(.....) 访问此移动平均线的当前值的最简单方法: av = self.sma[0]所以在回测过程中,无需知道已经处理了多少条/分钟/天/月,“0”一直指向当前值。
  • 按照Python遍历数组的方式,用下标-1来访问最后一个值: previous_value = self.sma[-1]。同理:-2、-3下标也是可以照常使用。

2.2 从0到100 ,样本

1. 基础设置

import backtrader as bt 
'''首先引入了backtrader这个包'''
cerebro = bt.Cerebro()
'''实例化Cerebro (大脑)引擎'''
# Cerebro引擎在后台创建了broker(经纪人)实例,系统默认每个broker的初始资金量为10000
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

2. 设置现金(cash)

很少有人会拿着1w块钱去炒股,所以要改改这个初始设置(可以接着上面的代码继续跑)

cerebro.broker.setcash(100000.0)
"""设置cash用broker.setcash这个函数"""
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

3. 添加现有数据

import datetime 
import os.path 
import sys  
import backtrader as bt
cerebro = bt.Cerebro()

"""设定一个根目录(数据文件的上级目录)"""
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))

"""获取数据文件的完整路径"""
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')

"""
创建数据交易集
注意: Yahoo的价格数据有点非主流,它是以时间倒序排列的。
datetime.datetime()中的reversed=True参数是将顺序反转一次,
这样就得到了我们想要的正序数据。
"""
data = bt.feeds.YahooFinanceCSVData(
    dataname=datapath,
    fromdate=datetime.datetime(2000, 1, 1), """数据起始日期"""
    todate=datetime.datetime(2000, 12, 31),"""数据截止日期"""
    reverse=False)
    
"""把数据加入Cerebro中"""
cerebro.adddata(data)

cerebro.broker.setcash(100000.0)
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

4. 第一个策略

现在有了交易数据,也有了本金(经纪人管理本金,类似RL中的agent,负责执行算法动作的代理),可以开始进入股市/证券市场去搏一搏,单车变摩托了。
可以考虑用代码实现一个策略,然后应用这个策略,来看看每天的收盘价。

DataSeries(交易数据的基类)对象,可以直接访问到 OHLC (开盘价、最高价、最低价、收盘价)等数据。这使我们打印数据很方便。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  
import os.path  
import sys  
import backtrader as bt

class TestStrategy(bt.Strategy):
"""创建策略(继承bt.Strategy)"""
    def log(self, txt, dt=None):
        ''' 这个策略的日志函数(输出信息)'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))
    def __init__(self):
    """保存一个原数据中close的副本(便于比较)"""
        self.dataclose = self.datas[0].close
    def next(self):
        # Simply log the closing price of the series from the reference
        self.log('Close, %.2f' % self.dataclose[0])
if __name__ == '__main__':
    cerebro = bt.Cerebro()
    """添加策略 虽然那个策略就是打印信息,剩下啥都没干"""
    cerebro.addstrategy(TestStrategy)
    """创建数据"""
    modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
    datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
    data = bt.feeds.YahooFinanceCSVData(
        dataname=datapath,
        fromdate=datetime.datetime(2000, 1, 1),
        todate=datetime.datetime(2000, 12, 31),
        reverse=False)

  """向cerebro中添加数据"""
    cerebro.adddata(data)
    cerebro.broker.setcash(100000.0) # 设置初始资金
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    # 打印一开始的情况
    cerebro.run()# 运行一下(运行时的cerebro以及添加过策略了)
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
    # 打印最后的结果

这里的这个策略只是个示例,这个TestStrategy并没有进行任何交易,只是打印出日期和对应的收盘价,所以可以看到最初和最后打印出的资金没有发生变化。

这里只是给出一个示例,cerebro设置初始资金,添加数据之后,再添加策略,就可以run得到一个结果。

额外说明:

框架在调用init时,该策略已经具有一个数据列表datas,这是标准的Python列表,可以按插入顺序访问数据。

这个datas应该是cerebro添加adddata添加的那个数据,也就是策略会作用于那个data上,策略和数据通过cerebro连接在一起。

列表中的第一个数据self.datas[0]用于交易操作,并且策略中的所有元素都是由框架的系统时钟进行同步的。

由于只需访问收盘价数据,于是使用 self.dataclose = self.datas[0].close将第一条价格数据的收盘价赋值给新变量。

系统时钟当经过一个K线柱的时候,策略的next()方法就会被调用一次。这一过程将一直循环,直到其他指标信号出现为止。此时,便会输出最终结果。关于这些,后继内容会讲到。

5. 给策略加点逻辑

上面的第一个策略只是打印了一些信息,没什么用,这里可以考虑加入一些逻辑,比如:如果价格连续三个交易日下跌,就买(高抛低吸中的低吸)

只需要修改上面策略类中的next()函数即可

def next(self):
	self.log('Close, %.2f' % self.dataclose[0])	
	if self.dataclose[0] < self.dataclose[-1]:
	    if self.dataclose[-1] < self.dataclose[-2]:
	    """如果连续三天跌,就买"""
	        self.log('BUY CREATE, %.2f' % self.dataclose[0])
	        self.buy()
     """其实两个if可以写成一个"""
     if (self.dataclose[0] < self.dataclose[-1]) and (self.dataclose[-1] < self.dataclose[-2]) :
  • 执行多个买入动作后,余额发生了一些变化。关于买入多少,买的什么,订单如何执行的?这些都是由backtrader框架自动帮我们完成的, 如果没有指定的话,self.datas[0]就是目标数据。 交易数量默认值等于1,后面例子我们会修改此参数。
  • 订单被以”市价”成交了。 Broker(经纪人,之前提到过)使用了下一个交易日的开盘价,因为是broker在当前的交日易收盘后天提交的订单,下一个交易日开盘价是他接触到的第一个价格。 这里没有为订单设置佣金费(手续费),后边会加上。

6. 不仅有买入 还有卖出

在知道如何买入(做多)之后,需要知道如何卖出,并且还需要了解该策略是否在市场中。 Strategy类有一个变量position保存当前持有的资产数量(可以理解为金融术语中的头寸),buy()sell()会返回被创建的订单(尚未执行的), 订单状态的更改将通过notify方法通知给策略Strategy

卖出逻辑也很简单: 5个K线柱后(第6个K线柱)不管涨跌都卖。 请注意,这里没有指定具体时间,而是指定的柱的数量。一个柱可能代表1分钟、1小时、1天、1星期等等,这取决于你价格数据文件里一条数据代表的周期。 虽然我们知道每个柱代表一天,但策略并不知道。(一般都是一天)

因为买入的时候,用的是交易日,所以这里应翻译为:5个交易日(第6个交易日)不管涨跌都卖。

只在空仓的时候才进行买入操作(空仓表示不在市场里,没有可以交易的证券)

注意: 没有将柱的下标传给next()方法,怎么知道已经经过了5个柱了呢? 这里用了Python的len()方法获取它Line数据的长度。 交易发生时记下它的长度,后边比较大小,看是否经过了5个柱。

代码部分还是只用更新策略类,其他的不变(设置数据feed和本金不用变)

class TestStrategy(bt.Strategy):

    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def __init__(self):
        """从后面的使用不难看出,这个order也是一个类,有status这个属性"""
        self.dataclose = self.datas[0].close
        self.order = None # 追踪挂单(挂单 创建但是还没有执行的)

    def notify_order(self, order):
        """订单状态的更改将通过notify方法通知给策略Strategy"""
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            return

        """检查一个订单order是否完成completed,当cash本金不够时,broker经纪人可以拒绝订单"""
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log('BUY EXECUTED, %.2f' % order.executed.price)
            elif order.issell():
                self.log('SELL EXECUTED, %.2f' % order.executed.price)

            self.bar_executed = len(self)# 衡量已经执行过几个k线柱/ticks(系统时钟周期)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
        """执行完之后,将order重新标记为none,标明没有待执行订单"""
        self.order = None

    def next(self):
        self.log('Close, %.2f' % self.dataclose[0]) # 打印出原始的收盘价
        
        # 检查订单是否待执行,如果待执行,不能发送第二个订单
        if self.order:
            return

        # Check if we are in the market
        # 如果仓位为空(有本金 但是还没有买证券,没有进行市场进行交易的权利,只能买)
        if not self.position:
            if self.dataclose[0] < self.dataclose[-1]:
                    if self.dataclose[-1] < self.dataclose[-2]:      
                        # 连续三天收盘价都在下降,低吸(按照默认的参数买入)
                        self.log('BUY CREATE, %.2f' % self.dataclose[0])
                        # 追踪创建的订单避免第二个出现
                        self.order = self.buy()
        else:
            # 已经在市场中(有可以交易的证券了)才可以卖
            if len(self) >= (self.bar_executed + 5):
                # 经过5个k线柱之后,不管涨跌都卖
                self.log('SELL CREATE, %.2f' % self.dataclose[0])
                # 继续追踪避免产生第二个订单
                self.order = self.sell()

这里的position指的是仓位,是否持有股票/证券进场(进场资格)

7. 经纪人的手续费

给代理人/经纪人的手续费一般称之为佣金commission。设置一个常见的比率0.1%(不管买还是卖都要付,代理人就是挺贪心的),一行代码就可以搞定

cerebro.broker.setcommission(commission=0.001)

其余代码参考中文文档里的(注释也很详细)backtrader中文文档(非官方)中的4.9 经纪人说,手续费呢?

8.自定义策略:技术指标参数

Parameters(指标参数)

在实战中,一般不将参数写死到策略中。Parameters(参数)就是用来处理这个的。 参数的定义像这样:

# 参数的定义像这样: 
params = (('myparam', 27), ('exitbars', 5),)

就去看中文文档吧。。。基本概念了解清楚就差不多可以直接上手代码了。

9.关于可视化

搜索过程中发现网上推荐的还有另一个专门用来画图的库,
利用Backtrader进行期权回测之五:用backtrader_plotting查看回测结果
专门画图的库看起来是要更好一些。。。

示例中使用的是backtrader内置的绘图方式,cerebro.plot(),这里需要强调一下,使用jupyter notebook展示出的图如下:
在这里插入图片描述
这个类似插件显示图的其实是matplotlib在jupyter notebook上的一种显示方式,参考:在 jupyter notebook 中使用 matplotlib 绘图的注意事项
使用的是%matplotlib notebook这个显示方式。(自己在代码中手动指定inline无效,应该是plot函数中进一步限定了显示方式)
这个图很奇怪,第二个和第三个子图是一样的,暂时不清楚这些图标的意义,需要进一步查看(可能需要去看英文文档中的plot部分)

此外,无法保存,搜索得知,还可以使用

import matplotlib
%matplotlib qt5

qt5的方式去进行保存(需要提前安装Qt)。
参考CSDN博客:使用 matplotlib 画图的保存方法有两种

进一步搜索,自己查看谷歌浏览器的console输出台信息:
在这里插入图片描述
与搜索到的问题:matplotlib + jupter notbook: %matplotlib notebook failed to download figures. #9117表现一致,可知大致原因是因为:谷歌浏览器不允许以图片数据作为一个新网页的打开内容,大概是这样。可以考虑换个浏览器(Edge不行,其它还没试过)或者使用代码方式去保存图像。

关于这个函数,查看函数文档可知:

cerebro.plot(
    plotter=None,
    numfigs=1,
    iplot=True,
    start=None,
    end=None,
    width=16,
    height=9,
    dpi=300,
    tight=True,
    use=None,
    **kwargs,
)
Docstring:
Plots the strategies inside cerebro

If ``plotter`` is None a default ``Plot`` instance is created and
``kwargs`` are passed to it during instantiation.

``numfigs`` split the plot in the indicated number of charts reducing
chart density if wished

``iplot``: if ``True`` and running in a ``notebook`` the charts will be
displayed inline
# 这个参数控制 notebook中图标是否inline显示
``use``: set it to the name of the desired matplotlib backend. It will
take precedence over ``iplot``

``start``: An index to the datetime line array of the strategy or a
``datetime.date``, ``datetime.datetime`` instance indicating the start
of the plot

``end``: An index to the datetime line array of the strategy or a
``datetime.date``, ``datetime.datetime`` instance indicating the end
of the plot

``width``: in inches of the saved figure

``height``: in inches of the saved figure

``dpi``: quality in dots per inches of the saved figure

``tight``: only save actual content and not the frame of the figure

10. 参数优化

这里的代码部分要参考英文,中文文档的代码有两个地方有误。主要是main函数的部分有错误:

cerebro = bt.Cerebro()
"""这里不再使用addstrategy来条件策略了"""
strats = cerebro.optstrategy(TestStrategy,maperiod=range(10, 31))
cerebro.adddata(data)
cerebro.broker.setcash(100000.0) 
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
cerebro.broker.setcommission(commission=0.001) 
"""这里加了一个参数maxcpus=1"""
cerebro.run(maxcpus=1)

注意:在jupyter notebook中使用多线程(maxcpus=1)会报错,所以运行这个代码的时候,如果不想限制线程数量,最好使用类似Pycharm之类的IDE(在Pycharm中去掉这个maxcpus=1也不会报错,但是jupyter notebook会直接卡死)

参考CSDN博客:
Python Jupyter notebook 运行 multiprocessing 跑不了的问题
问题存在解决方案,但是这可能就是jupyter notebook的设计缺陷吧,了解就好了

3. 错误解决

backtrader和matplotlib版本不匹配

报错:

ImportError: cannot import name 'warnings' from 'matplotlib.dates'
(C:\software\anaconda\envs\rl37\lib\site-packages\matplotlib\dates.py)

参考Stack Overflow回答:
ImportError Cannot import name ‘warnings’ from 'matplotlib.dates

大意就是backtrader和matplotlib版本不匹配造成的,需要将matplotlib降级。

解决1
降级matplotlib:

pip uninstall matplotlib
pip install matplotlib==3.2.2

解决2
直接修改backtrader引用warning的代码
在这里插入图片描述
可以在完整的报错信息中找到这个引用文件locator.py的位置:
在这里插入图片描述
在这里插入图片描述
就是这个位置,删除就好了。

之后紧接着出现了另一个错误,由于我用的是jupyter lab,所以报错Javascript Error: IPython is not defined。如果使用jupyter notebook就不会报错,参考Stack Overflow解决:Javascript Error: IPython is not defined in JupyterLab

4. 额外知识

T+0

参考百度百科视频blob(是一种交易制度):
https://baike.baidu.com/058bdf84-dd05-4e1c-8aa1-eaef8053ed7e

头寸

  • 头寸(position)是一个金融术语,指的是个人或实体持有或拥有的特定商品、证券、货币等的数量
  • 头寸(position)是一个金融术语,指的是个人或实体持有或拥有的特定商品、证券、货币等的数量

但是在股票市场,似乎用来代表仓位更合适,position你可以理解成“开仓位置”或者“持仓位置”
参考知乎回答:
关于《股票作手回忆录》中position,应该怎么理解的问题,部位?头寸?仓位?

关于K线

这里插一部分,关于k线的知识。
在这里插入图片描述
这是去东方财富网随便截的一张图,可以看到有一些框框,参考知乎专栏文章:股票基础知识—K线图基础知识(一)

  • K线是一条柱状的线条,由影线和实体组成。

    • 影线在实体上方的部分叫上影线,下方的部分叫下影线。
    • 实体分阳线和阴线。
    • 其中影线表明当天交易的最高和最低价,而实体表明当天的开盘价和收盘价。
      在这里插入图片描述
      在这里插入图片描述
      阳线,上面的是收盘价=最高价,下面是开盘价=最低价,这时候上影线和下影线都没有。
  • 阴阳代表股价的趋势

    • 在K线图中,阳线表示股价将继续上涨;阴线表示股价继续下跌。
    • 股价涨跌本质是由股票买卖的供求关系决定,也可以说是由买卖双方力量决定
    • 一般来说股价运行具有惯性。特别是大阴线与大阳线,在放量的情况下具有非常大的后市指示作用。
      在这里插入图片描述
      确实具有一些惯性,会有一段时间的红,一段时间的绿。
  • 实体大小代表股价运行内在动力

    • 实体越大,上涨或下跌的趋势越明显,反之趋势越不明显。
    • 所以在大部分趋势确认的行情中,实体都比较大;震荡市中,实体都比较小。
    • 由于趋势具有惯性,惯性的结束可以从K线的变化中看出端倪。
    • 当连续大阴或大阳以后出现小阴小阳,这说明趋势可能会暂缓,并且进入调整状态。如果在震荡行情中,连续的小阴小阳以后出现大阳大阴,这说明震荡趋势肯能结束,趋势行情将展开。
  • 影线长短,影线代表股价反转的信号,特别是在重要的支撑位与压力位附近。它构成下一阶段股价继续前进的阻力,无论K线是阳还是阴,向某个方向的影线越长,越不利于股价超整个方向变动。即长上影线越长,越不利于股价上涨,股价调整后下行的可能性越大;下影线越长,越不利于股价下跌,股价调整后上涨可能性大。(仅仅是概率大小的区别,在行情比较稳定的情况下,趋势的惯性仍然会占据主导地位。)
    在这里插入图片描述

  • K线类型

    • 大盘K线图与个股K线图(大盘指的就是沪市的“上证综合指数”深市的“深证成份股指数”。它能科学地反应整个股票市场的行情,如股票的整体涨跌或股票价格走势等。)

      大盘K线图与个股K线图就K线本身来说是一致的,判断方法也是一致的,只是表达含义的对象不同罢了。
      如果大盘K线收阳,而个股K线的阳线小于大盘K线,甚至还收阴线;则说明个股弱于大盘(即弱于市场平均水平)。
      如果大盘K线收阴,而个股K线的阴线小于大盘K线,甚至收阳线;则说明个股强于大盘(即强于市场平均水平)。

      例如下面这两个,当然也存在个股和大盘不一致的情况 在这里插入图片描述
      在这里插入图片描述

  • 月K线,周K线,日K线,60分钟K线

    根据K线的计算周期,可以把K线分为年,月,周,日,60分钟。。。。。。K线。一般月,周K线适合作为中长线投资者看盘工具。
    日K线对该股短期的历史行情记录最为准确和详尽,是适合短线投资者的看盘工具。由于股票市场缺乏T+0机制,所以60分钟以下的K线图都不太具备太大的参考价值。(超级短线除外)

挂单

参考
福步外贸论坛(FOB Business Forum) » 外贸英语 » pending order什么意思啊?

挂单(Pending order)- 用户下达给经纪商的、在市场报价达到某个水平才能执行的订单。 待执行订单。
有四种挂单形式:
Buy Limit - 在市场实时报价中的买价达到或低于挂单价位时建立长仓(买进)。该挂单价位应低于下单时的市场报价;
Buy Stop - 在市场实时报价中的买价达到或高于挂单价位时建立长仓(买进)。该挂单价位应高于下单时的市场报价;
Sell Limit - 在市场实时报价中的卖价达到或高于挂单价位时建立短仓(卖出)。该挂单价位应高于下单时的市场报价;
Sell Stop - 在市场实时报价中的卖价达到或低于挂单价位时建立短仓(卖出)。该挂单价位应低于下单时的市场报价。

猜你喜欢

转载自blog.csdn.net/Castlehe/article/details/113378863