1. backtrader介绍
1.1 基本情况
参考文档
- backtrader官方文档(英文):https://www.backtrader.com/docu/
- backtrader中文文档(非官方):http://backtrader.com.cn/docu/#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 some-package
"""加一个镜像,下载的快点"""
- 我这里使用的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),)
就去看中文文档吧。。。基本概念了解清楚就差不多可以直接上手代码了。
额外知识
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 - 在市场实时报价中的卖价达到或低于挂单价位时建立短仓(卖出)。该挂单价位应低于下单时的市场报价。