BackTrader 中文文档(一)(1)https://developer.aliyun.com/article/1489208
Python 2.x/3.x 兼容
开发工作在 Python 2.7 下进行,有时也在 3.4 下进行。本地同时运行两个版本的测试。
在 Travis 下,使用连续集成检查与 3.2 / 3.3 / 3.5 以及 pypy/pyp3 的兼容性
从 pypi 安装
例如使用 pip:
pip install backtrader
使用相同语法也可以应用 easy_install
从 pypi 安装(包括 matplotlib)
若需要绘图功能,请使用此选项:
pip install backtrader[plotting]
这会引入 matplotlib,它将进一步引入其他依赖项。
你可能更喜欢(或只能使用…)easy_install
从源码安装
首先从 github 网站下载一个发布版或最新的压缩包:
解压后运行以下命令:
python setup.py install
从源码在你的项目中运行
从 github 网站下载一个发布版或最新的压缩包:
然后将 backtrader 包目录复制到你自己的项目中。例如,在类 Unix 操作系统下:
tar xzf backtrader.tgz cd backtrader cp -r backtrader project_directory
请记住,你随后需要手动安装 matplotlib
以进行绘图。
快速开始
注意
快速入门指南中使用的数据文件会不时更新,这意味着调整后的收盘价
会变化,以及收盘价
(以及其他组件)。这意味着实际输出可能与撰写文档时的情况不同。
使用平台
让我们通过一系列的例子运行一遍(从几乎空白到完全成熟的策略),但在粗略解释backtrader的两个基本概念之前。
- 线数据源、指标和策略都有线。一条线是一系列的点,当它们连接在一起时形成这条线。当谈到市场时,一个数据源通常每天有以下一组点:
- 开盘价、最高价、最低价、收盘价、成交量、持仓量
- 一系列“开盘价”随时间的变化是一条线。因此,一个数据源通常有 6 条线。
如果我们还考虑“DateTime”(这是单个点的实际参考),我们可以计算出 7 条线。 - 第 0 个指数方法
当访问线中的值时,当前值通过索引访问:0
通过*-1访问“最后”输出值。这符合 Python 对可迭代对象的惯例(一条线可以被迭代,因此是可迭代的),其中索引-1*用于访问可迭代/数组的“最后”项。
在我们的情况下,访问的是最后的输出值。
因此,作为* -1 之后的索引 0 *,它用于访问当前行。
考虑到这一点,如果我们想象一个在初始化过程中创建的简单移动平均策略:
self.sma = SimpleMovingAverage(.....)
访问当前移动平均线的最简单和最简单的方法:
av = self.sma[0]
无需知道已处理了多少个条/分钟/天/月,因为“0”唯一标识当前时刻。
按照 Python 的传统,通过*-1*来访问“最后”输出值:
previous_value = self.sma[-1]
当然,早期的输出值可以用-2、-3 等来访问
从 0 到 100:样本
基本设置
让我们开始吧。
from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt if __name__ == '__main__': cerebro = bt.Cerebro() print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) cerebro.run() print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后输出为:
Starting Portfolio Value: 10000.00 Final Portfolio Value: 10000.00
在这个例子中:
- backtrader 被导入
- Cerebro 引擎被实例化
- 创建的cerebro实例被告知运行(循环遍历数据)
- 结果输出并打印出来
虽然看起来不起眼,但让我们明确指出一些事情:
- Cerebro 引擎在后台创建了一个broker实例
- 实例已经有一些现金可以开始了
在幕后经纪人实例化是该平台的一个固定特征,以简化用户的生活。如果用户未设置经纪人,则会放置一个默认的经纪人。
10000 个货币单位是一些经纪人开始使用的常见值。
设置现金
在金融世界中,确实只有“失败者”才从 10k 开始。让我们改变现金并再次运行示例。
from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt if __name__ == '__main__': cerebro = bt.Cerebro() cerebro.broker.setcash(100000.0) print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) cerebro.run() print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后输出为:
Starting Portfolio Value: 1000000.00 Final Portfolio Value: 1000000.00
任务完成。让我们转向风云变幻的水域。
添加一个 Data Feed
拥有现金很有趣,但所有这一切背后的目的是让一个自动化策略通过对我们视为 Data Feed 的资产进行操作而无需动手指就能增加现金。
因此… 没有 Data Feed -> 没趣。让我们给这个不断增长的示例添加一个。
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) # Import the backtrader platform import backtrader as bt if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # 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, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values after this date todate=datetime.datetime(2000, 12, 31), reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后的输出为:
Starting Portfolio Value: 1000000.00 Final Portfolio Value: 1000000.00
模板代码的数量略有增加,因为我们添加了:
- 找出我们示例脚本所在的位置,以便能够定位示例 Data Feed 文件
- 有 datetime 对象用于过滤我们将要操作的 Data Feed 中的数据。
除此之外,Data Feed 被创建并添加到 cerebro 中。
输出没有变化,如果有变化将是个奇迹。
注意
Yahoo Online 以日期降序发送 CSV 数据,这不是标准约定。reversed=True 参数考虑到 CSV 文件中的数据已经被 反转,并具有标准预期的日期升序。
我们的第一个策略
现金在 broker 中,而 Data Feed 在那里。看起来,危险的生意就在拐角处。
让我们将一个策略引入到等式中,并打印每天(每个 bar)的“Close”价格。
DataSeries(Data Feeds 中的基础类)对象具有访问已知 OHLC(开盘价 最高价 最低价 收盘价)日常值的别名。这应该能够简化我们的打印逻辑的创建。
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) # Import the backtrader platform import backtrader as bt # Create a Stratey class TestStrategy(bt.Strategy): def log(self, txt, dt=None): ''' Logging function for this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries 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__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # 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, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后的输出为:
Starting Portfolio Value: 100000.00 2000-01-03T00:00:00, Close, 27.85 2000-01-04T00:00:00, Close, 25.39 2000-01-05T00:00:00, Close, 24.05 ... ... ... 2000-12-26T00:00:00, Close, 29.17 2000-12-27T00:00:00, Close, 28.94 2000-12-28T00:00:00, Close, 29.29 2000-12-29T00:00:00, Close, 27.41 Final Portfolio Value: 100000.00
有人说股票市场是危险的生意,但似乎并不是这样。
让我们解释一些魔法:
- 在调用 init 后,策略已经拥有了平台上存在的数据列表。
这是一个标准的 Python list,可以按插入顺序访问数据。
列表中的第一个数据 self.datas[0] 是默认用于交易操作的数据,并且用于保持所有策略元素同步(它是系统时钟)。 - self.dataclose = self.datas[0].close 保持对 close 线 的引用。稍后只需要一级间接引用就能访问 close 值。
- 策略的 next 方法将在系统时钟的每个 bar 上调用(self.datas[0])。直到其他因素开始起作用,如 指标,它们需要一些 bar 才能开始产生输出。稍后会详细介绍。
在策略中添加一些逻辑
让我们通过查看一些图表来尝试一些疯狂的想法。
- 如果价格连续下跌 3 个交易会话… 买买买!!!
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) # Import the backtrader platform import backtrader as bt # Create a Stratey class TestStrategy(bt.Strategy): def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries 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 self.dataclose[0] < self.dataclose[-1]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with all possible default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) self.buy() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # 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, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后的输出为:
Starting Portfolio Value: 100000.00 2000-01-03, Close, 27.85 2000-01-04, Close, 25.39 2000-01-05, Close, 24.05 2000-01-05, BUY CREATE, 24.05 2000-01-06, Close, 22.63 2000-01-06, BUY CREATE, 22.63 2000-01-07, Close, 24.37 ... ... ... 2000-12-20, BUY CREATE, 26.88 2000-12-21, Close, 27.82 2000-12-22, Close, 30.06 2000-12-26, Close, 29.17 2000-12-27, Close, 28.94 2000-12-27, BUY CREATE, 28.94 2000-12-28, Close, 29.29 2000-12-29, Close, 27.41 Final Portfolio Value: 99725.08
发出了多个“BUY”创建订单,我们的投资组合价值减少了。显然有几个重要的事情缺失了。
- 订单已创建,但不清楚是否已执行,何时执行以及以什么价格执行。
下一个示例将在此基础上建立,通过监听订单状态通知。
好奇的读者可能会问买了多少股票,购买了什么资产以及订单是如何执行的。在可能的情况下(在这种情况下是可能的),平台会填补这些空白:
- self.datas[0](主数据,也称为系统时钟)是目标资产,如果没有指定其他资产
- 股份是由position sizer在后台提供的,它使用固定的股份,“1”是默认值。稍后将进行修改。
- 订单是“市价”执行的。经纪人(在前面的示例中显示)使用下一根 bar 的开盘价执行此操作,因为那是当前检查的 bar 之后的第一个 tick。
- 到目前为止,订单已经执行了,没有任何佣金(稍后会详细介绍)
不仅买……还卖
在了解如何进入市场(做多)之后,需要一个“退出概念”,并且还要了解策略是否处于市场中。
- 幸运的是,Strategy 对象为默认的data feed提供了对position属性的访问权限
- buy和sell方法返回创建的(尚未执行)订单
- 订单状态的更改将通过notify方法通知策略
*“退出概念”*将是一个简单的概念:
- 在过了 5 个 bar(第 6 个 bar)之后退出,无论好坏都要退出
请注意,没有暗示“时间”或“时间框架”:bar 的数量。bar 可以表示 1 分钟、1 小时、1 天、1 周或任何其他时间段。
尽管我们知道数据源是每日的,但策略不对此做任何假设。
此外,为了简化:
- 只有在市场中还没有持仓时才允许买入订单
注意
next方法没有传递“bar index”,因此似乎不清楚如何理解 5 个 bar 是否已经过去,但这已经以 Pythonic 的方式进行了建模:在对象上调用len,它将告诉您它的lines长度。只需记录(保存在变量中)操作发生的长度,然后查看当前长度是否相差 5 个 bar。
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) # Import the backtrader platform import backtrader as bt # Create a Stratey class TestStrategy(bt.Strategy): def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.dataclose = self.datas[0].close # To keep track of pending orders self.order = None def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash 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) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') # Write down: no pending order self.order = None def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one if self.order: return # Check if we are in the market if not self.position: # Not yet ... we MIGHT BUY if ... if self.dataclose[0] < self.dataclose[-1]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: # Already in the market ... we might sell if len(self) >= (self.bar_executed + 5): # SELL, SELL, SELL!!! (with all possible default parameters) self.log('SELL CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.sell() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # 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, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
执行后的输出如下:
Starting Portfolio Value: 100000.00 2000-01-03T00:00:00, Close, 27.85 2000-01-04T00:00:00, Close, 25.39 2000-01-05T00:00:00, Close, 24.05 2000-01-05T00:00:00, BUY CREATE, 24.05 2000-01-06T00:00:00, BUY EXECUTED, 23.61 2000-01-06T00:00:00, Close, 22.63 2000-01-07T00:00:00, Close, 24.37 2000-01-10T00:00:00, Close, 27.29 2000-01-11T00:00:00, Close, 26.49 2000-01-12T00:00:00, Close, 24.90 2000-01-13T00:00:00, Close, 24.77 2000-01-13T00:00:00, SELL CREATE, 24.77 2000-01-14T00:00:00, SELL EXECUTED, 25.70 2000-01-14T00:00:00, Close, 25.18 ... ... ... 2000-12-15T00:00:00, SELL CREATE, 26.93 2000-12-18T00:00:00, SELL EXECUTED, 28.29 2000-12-18T00:00:00, Close, 30.18 2000-12-19T00:00:00, Close, 28.88 2000-12-20T00:00:00, Close, 26.88 2000-12-20T00:00:00, BUY CREATE, 26.88 2000-12-21T00:00:00, BUY EXECUTED, 26.23 2000-12-21T00:00:00, Close, 27.82 2000-12-22T00:00:00, Close, 30.06 2000-12-26T00:00:00, Close, 29.17 2000-12-27T00:00:00, Close, 28.94 2000-12-28T00:00:00, Close, 29.29 2000-12-29T00:00:00, Close, 27.41 2000-12-29T00:00:00, SELL CREATE, 27.41 Final Portfolio Value: 100018.53
烈焰般的船舱!!! 系统赚了钱……一定有问题。
经纪人说:给我看看钱!
钱被称为“佣金”。
让我们为每次操作(买入和卖出……是的,经纪人很贪婪……)添加合理的*0.1%*佣金率。
一条线足矣:
# 0.1% ... divide by 100 to remove the % cerebro.broker.setcommission(commission=0.001)
由于对该平台有经验,我们想要在买入/卖出周期之后看到利润或损失,有无佣金都行。
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime # For datetime objects import os.path # To manage paths import sys # To find out the script name (in argv[0]) # Import the backtrader platform import backtrader as bt # Create a Stratey class TestStrategy(bt.Strategy): def log(self, txt, dt=None): ''' Logging function fot this strategy''' dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # Keep a reference to the "close" line in the data[0] dataseries self.dataclose = self.datas[0].close # To keep track of pending orders and buy price/commission self.order = None self.buyprice = None self.buycomm = None def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: if order.isbuy(): self.log( 'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # Sell self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' % (order.executed.price, order.executed.value, 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): if not trade.isclosed: return self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # Simply log the closing price of the series from the reference self.log('Close, %.2f' % self.dataclose[0]) # Check if an order is pending ... if yes, we cannot send a 2nd one if self.order: return # Check if we are in the market if not self.position: # Not yet ... we MIGHT BUY if ... if self.dataclose[0] < self.dataclose[-1]: # current close less than previous close if self.dataclose[-1] < self.dataclose[-2]: # previous close less than the previous close # BUY, BUY, BUY!!! (with default parameters) self.log('BUY CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.buy() else: # Already in the market ... we might sell if len(self) >= (self.bar_executed + 5): # SELL, SELL, SELL!!! (with all possible default parameters) self.log('SELL CREATE, %.2f' % self.dataclose[0]) # Keep track of the created order to avoid a 2nd order self.order = self.sell() if __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Add a strategy cerebro.addstrategy(TestStrategy) # 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, '../../datas/orcl-1995-2014.txt') # Create a Data Feed data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime.datetime(2000, 1, 1), # Do not pass values before this date todate=datetime.datetime(2000, 12, 31), # Do not pass values after this date reverse=False) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(100000.0) # Set the commission - 0.1% ... divide by 100 to remove the % cerebro.broker.setcommission(commission=0.001) # Print out the starting conditions print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue()) # Run over everything cerebro.run() # Print out the final result print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
BackTrader 中文文档(一)(3)https://developer.aliyun.com/article/1489211