BackTrader 中文文档(一)(2)https://developer.aliyun.com/article/1489209
执行后的输出如下:
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, Price: 23.61, Cost: 23.61, Commission 0.02 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, Price: 25.70, Cost: 25.70, Commission 0.03 2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.04 2000-01-14T00:00:00, Close, 25.18 ... ... ... 2000-12-15T00:00:00, SELL CREATE, 26.93 2000-12-18T00:00:00, SELL EXECUTED, Price: 28.29, Cost: 28.29, Commission 0.03 2000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12 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, Price: 26.23, Cost: 26.23, Commission 0.03 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: 100016.98
上帝保佑女王!!! 系统仍然赚了钱。
在继续之前,让我们通过过滤“OPERATION PROFIT”行来注意一些事情:
2000-01-14T00:00:00, OPERATION PROFIT, GROSS 2.09, NET 2.04 2000-02-07T00:00:00, OPERATION PROFIT, GROSS 3.68, NET 3.63 2000-02-28T00:00:00, OPERATION PROFIT, GROSS 4.48, NET 4.42 2000-03-13T00:00:00, OPERATION PROFIT, GROSS 3.48, NET 3.41 2000-03-22T00:00:00, OPERATION PROFIT, GROSS -0.41, NET -0.49 2000-04-07T00:00:00, OPERATION PROFIT, GROSS 2.45, NET 2.37 2000-04-20T00:00:00, OPERATION PROFIT, GROSS -1.95, NET -2.02 2000-05-02T00:00:00, OPERATION PROFIT, GROSS 5.46, NET 5.39 2000-05-11T00:00:00, OPERATION PROFIT, GROSS -3.74, NET -3.81 2000-05-30T00:00:00, OPERATION PROFIT, GROSS -1.46, NET -1.53 2000-07-05T00:00:00, OPERATION PROFIT, GROSS -1.62, NET -1.69 2000-07-14T00:00:00, OPERATION PROFIT, GROSS 2.08, NET 2.01 2000-07-28T00:00:00, OPERATION PROFIT, GROSS 0.14, NET 0.07 2000-08-08T00:00:00, OPERATION PROFIT, GROSS 4.36, NET 4.29 2000-08-21T00:00:00, OPERATION PROFIT, GROSS 1.03, NET 0.95 2000-09-15T00:00:00, OPERATION PROFIT, GROSS -4.26, NET -4.34 2000-09-27T00:00:00, OPERATION PROFIT, GROSS 1.29, NET 1.22 2000-10-13T00:00:00, OPERATION PROFIT, GROSS -2.98, NET -3.04 2000-10-26T00:00:00, OPERATION PROFIT, GROSS 3.01, NET 2.95 2000-11-06T00:00:00, OPERATION PROFIT, GROSS -3.59, NET -3.65 2000-11-16T00:00:00, OPERATION PROFIT, GROSS 1.28, NET 1.23 2000-12-01T00:00:00, OPERATION PROFIT, GROSS 2.59, NET 2.54 2000-12-18T00:00:00, OPERATION PROFIT, GROSS -0.06, NET -0.12
加总“净”利润,最终数字是:
15.83
但系统在最后说了以下内容:
2000-12-29T00:00:00, SELL CREATE, 27.41 Final Portfolio Value: 100016.98
显然15.83不等于16.98。没有任何错误。15.83的“净”利润已经到手了。
不幸的是(或者幸运的是,为了更好地了解平台),在Data Feed的最后一天仍然有一个未平仓的头寸。即使已发送了一个卖出操作……但尚未执行。
经纪人计算的“最终投资组合价值”考虑了 2000-12-29 的“收盘”价格。实际执行价格将在下一个交易日设定,恰好是 2001-01-02。扩展 数据源”以考虑这一天的输出为:
2001-01-02T00:00:00, SELL EXECUTED, Price: 27.87, Cost: 27.87, Commission 0.03 2001-01-02T00:00:00, OPERATION PROFIT, GROSS 1.64, NET 1.59 2001-01-02T00:00:00, Close, 24.87 2001-01-02T00:00:00, BUY CREATE, 24.87 Final Portfolio Value: 100017.41
现在将之前的净利润添加到已完成操作的净利润中:
15.83 + 1.59 = 17.42
这样(忽略“print”语句中的四舍五入误差),策略开始时额外的投资组合超过了初始的 100000 货币单位。
自定义策略:参数
在策略中硬编码某些值并且没有机会轻松更改它们会有些不方便。参数 可以帮助解决这个问题。
参数的定义很容易,如下所示:
params = (('myparam', 27), ('exitbars', 5),)
由于这是一个标准的 Python 元组,其中包含一些元组,下面的代码可能更吸引人:
params = ( ('myparam', 27), ('exitbars', 5), )
在向 Cerebro 引擎添加策略时,允许使用任一格式化参数设置策略:
# Add a strategy cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)
注
下面的 setsizing
方法已被弃用。此内容保留在此处供查看旧源代码的人使用。源代码已更新为使用:
cerebro.addsizer(bt.sizers.FixedSize, stake=10)``` ```py Please read the section about *sizers* Using the parameters in the strategy is easy, as they are stored in a “params” attribute. If we for example want to set the stake fix, we can pass the stake parameter to the *position sizer* like this durint **init**:
`# 从参数设置 sizer stake
self.sizer.setsizing(self.params.stake)`
We could have also called *buy* and *sell* with a *stake* parameter and *self.params.stake* as the value. The logic to exit gets modified:
`# 已经在市场上……我们可能会出售
if len(self) >= (self.bar_executed + self.params.exitbars):`
With all this in mind the example evolves to look like:
`from future import (absolute_import, division, print_function,
unicode_literals)
导入 datetime # 用于日期时间对象
导入 os.path # 用于管理路径
导入 sys # 用于查找脚本名称(在 argv[0] 中)
导入 backtrader 平台
导入 backtrader as bt
创建策略
class TestStrategy(bt.Strategy):
params = ( ('exitbars', 5), ) def log(self, txt, dt=None):
‘’’ 该策略的日志记录函数’‘’
dt = dt or self.datas[0].datetime.date(0) print('%s, %s' % (dt.isoformat(), txt)) def __init__(self): # 保持对数据[0] 数据系列中的 “close” 行的引用 self.dataclose = self.datas[0].close # 跟踪未决订单、购买价格/佣金 self.order = None self.buyprice = None self.buycomm = None def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # 由经纪人提交/接受的购买/卖出订单 - 无需操作 返回 # 检查订单是否已完成 # 注意:如果资金不足,经纪人可能会拒绝订单 if order.status in [order.Completed]: if order.isbuy(): self.log( '已执行购买,价格:%.2f,成本:%.2f,佣金:%.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # 卖出 self.log('卖出执行,价格:%.2f,成本:%.2f,佣金:%.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('订单已取消/保证金/拒绝') self.order = None def notify_trade(self, trade): if not trade.isclosed: 返回 self.log('操作利润,总额:%.2f,净额:%.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # 简单记录参考系列的收盘价格 self.log('关闭, %.2f' % self.dataclose[0]) # 检查订单是否挂起 ... 如果是,则不能发送第二个订单 if self.order: 返回 # 检查我们是否在市场中 if not self.position: # 还没有...如果...我们可能会购买 if self.dataclose[0] < self.dataclose[-1]: # 当前收盘价低于上一个收盘价 if self.dataclose[-1] < self.dataclose[-2]: # 上一个收盘价低于上一个收盘价 # 购买, 购买, 购买!!! (使用默认参数) self.log('购买创建, %.2f' % self.dataclose[0]) # 跟踪已创建的订单以避免第二次下单 self.order = self.buy() else: # 已在市场中...我们可能会出售 if len(self) >= (self.bar_executed + self.params.exitbars): # 卖出, 卖出, 卖出!!! (使用所有可能的默认参数) self.log('卖出创建, %.2f' % self.dataclose[0]) # 跟踪已创建的订单以避免第二次下单 self.order = self.sell()
if name == ‘main’:
# 创建一个 cerebro 实体 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) # 根据股份添加一个固定大小的 sizer cerebro.addsizer(bt.sizers.FixedSize, stake=10) # 设置佣金 - 0.1% ... 除以 100 移除 % cerebro.broker.setcommission(commission=0.001) # 打印出起始条件 print('起始投资组合价值: %.2f' % cerebro.broker.getvalue()) # 运行全部内容 cerebro.run() # 打印出最终结果 print('最终投资组合价值: %.2f' % cerebro.broker.getvalue())`
After the execution the output is:
`起始投资组合价值: 100000.00
2000-01-03T00:00:00, 关闭, 27.85
2000-01-04T00:00:00, 关闭, 25.39
2000-01-05T00:00:00, 关闭, 24.05
2000-01-05T00:00:00, 创建购买, 24.05
2000-01-06T00:00:00, 购买已执行, 数量 10, 价格: 23.61, 成本: 236.10, 佣金 0.24
2000-01-06T00:00:00, 关闭, 22.63
…
…
…
2000-12-20T00:00:00, 创建购买, 26.88
2000-12-21T00:00:00, 购买已执行, 数量 10, 价格: 26.23, 成本: 262.30, 佣金 0.26
2000-12-21T00:00:00, 关闭, 27.82
2000-12-22T00:00:00, 关闭, 30.06
2000-12-26T00:00:00, 关闭, 29.17
2000-12-27T00:00:00, 关闭, 28.94
2000-12-28T00:00:00, 关闭, 29.29
2000-12-29T00:00:00, 关闭, 27.41
2000-12-29T00:00:00, 创建卖出, 27.41
最终投资组合价值: 100169.80`
In order to see the difference, the print outputs have also been extended to show the execution size. Having multiplied the stake by 10, the obvious has happened: the profit and loss has been multiplied by 10\. Instead of *16.98*, the surplus is now *169.80* ### Adding an indicator Having heard of *indicators*, the next thing anyone would add to the strategy is one of them. For sure they must be much better than a simple *“3 lower closes”* strategy. Inspired in one of the examples from PyAlgoTrade a strategy using a Simple Moving Average. * Buy “AtMarket” if the close is greater than the Average * If in the market, sell if the close is smaller than the Average * Only 1 active operation is allowed in the market Most of the existing code can be kept in place. Let’s add the average during **init** and keep a reference to it:
self.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod)
And of course the logic to enter and exit the market will rely on the Average values. Look in the code for the logic. Note The starting cash will be 1000 monetary units to be in line with the PyAlgoTrade example and no commission will be applied
`from future import (absolute_import, division, print_function,
unicode_literals)
import datetime # 用于日期时间对象
import os.path # 用于管理路径
import sys # 用于查找脚本名称(在 argv[0] 中)
导入 backtrader 平台
import backtrader as bt
创建一个策略
class TestStrategy(bt.Strategy):
参数 = ( ('maperiod', 15), ) def log(self, txt, dt=None):
‘’‘此策略的日志记录函数’‘’
dt = dt or self.datas[0].datetime.date(0) print('%s,%s' % (dt.isoformat(), txt)) def __init__(self): # 保留对数据[0] 数据系列中的“close”线的引用 self.dataclose = self.datas[0].close # 跟踪待定订单以及购买价格/佣金 self.order = None self.buyprice = None self.buycomm = None # 添加一个 MovingAverageSimple 指标 self.sma = bt.indicators.SimpleMovingAverage( self.datas[0], period=self.params.maperiod) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # 购买/卖出订单提交/接受给/由经纪人-无需操作 返回 # 检查订单是否已完成 # 注意:如果现金不足,经纪人可能会拒绝订单 if order.status in [order.Completed]: if order.isbuy(): self.log( '购买执行,价格:%.2f,成本:%.2f,佣金%.2f' % (order.executed.price, order.executed.value, order.executed.comm)) self.buyprice = order.executed.price self.buycomm = order.executed.comm else: # 卖出 self.log('卖出执行,价格:%.2f,成本:%.2f,佣金%.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('订单已取消/保证金/拒绝') self.order = None def notify_trade(self, trade): if not trade.isclosed: 返回 self.log('操作利润,总%.2f,净%.2f' % (trade.pnl, trade.pnlcomm)) def next(self): # 仅记录来自参考系列的收盘价 self.log('关闭,%.2f' % self.dataclose[0]) # 检查订单是否待定... 如果是,我们不能发送第二个订单 if self.order: 返回 # 检查我们是否在市场中 if not self.position: # 还没有...如果...,我们可能会买 if self.dataclose[0] > self.sma[0]: # 买,买,买!!! (具有所有可能的默认参数) self.log('购买创建,%.2f' % self.dataclose[0]) # 跟踪创建的订单,以避免第二个订单 self.order = self.buy() else: if self.dataclose[0] < self.sma[0]: # 卖,卖,卖!!! (具有所有可能的默认参数) self.log('卖出创建,%.2f' % self.dataclose[0]) # 跟踪创建的订单,以避免第二个订单 self.order = self.sell()
if name == ‘main’:
# 创建一个 cerebro 实体 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(1000.0) # 添加一个固定大小的 Sizer 根据股票份额 cerebro.addsizer(bt.sizers.FixedSize, stake=10) # 设置佣金 cerebro.broker.setcommission(commission=0.0) # 打印初始条件 print('起始组合价值: %.2f' % cerebro.broker.getvalue()) # 遍历所有操作 cerebro.run() # 打印最终结果 print('最终组合价值: %.2f' % cerebro.broker.getvalue())`
Now, before skipping to the next section **LOOK CAREFULLY** to the first date which is shown in the log: * It’ no longer *2000-01-03*, the first trading day in the year 2K. It’s 2000-01-24 … *Who has stolen my cheese?* The missing days are not missing. The platform has adapted to the new circumstances: * An indicator (SimpleMovingAverage) has been added to the Strategy. * This indicator needs X bars to produce an output: in the example: 15 * 2000-01-24 is the day in which the 15^(th) bar occurs The *backtrader* platform assumes that the Strategy has the indicator in place for a good reason, **to use it in the decision making process**. And it makes no sense to try to make decisions if the indicator is not yet ready and producing values. * *next* will be 1^(st) called when all indicators have already reached the minimum needed period to produce a value * In the example there is a single indicator, but the strategy could have any number of them. After the execution the output is:
`Starting Portfolio Value: 1000.00
2000-01-24T00:00:00, 收盘价, 25.55
2000-01-25T00:00:00, 收盘价, 26.61
2000-01-25T00:00:00, 买入信号, 26.61
2000-01-26T00:00:00, 买入执行, 数量 10, 价格: 26.76, 成本: 267.60, 手续费 0.00
2000-01-26T00:00:00, 收盘价, 25.96
2000-01-27T00:00:00, 收盘价, 24.43
2000-01-27T00:00:00, 卖出信号, 24.43
2000-01-28T00:00:00, 卖出执行, 数量 10, 价格: 24.28, 成本: 242.80, 手续费 0.00
2000-01-28T00:00:00, 操作盈利, 总额 -24.80, 净额 -24.80
2000-01-28T00:00:00, 收盘价, 22.34
2000-01-31T00:00:00, 收盘价, 23.55
2000-02-01T00:00:00, 收盘价, 25.46
2000-02-02T00:00:00, 收盘价, 25.61
2000-02-02T00:00:00, 买入信号, 25.61
2000-02-03T00:00:00, 买入执行, 数量 10, 价格: 26.11, 成本: 261.10, 手续费 0.00
…
…
…
2000-12-20T00:00:00, 卖出信号, 26.88
2000-12-21T00:00:00, 卖出执行, 数量 10, 价格: 26.23, 成本: 262.30, 手续费 0.00
2000-12-21T00:00:00, 操作盈利, 总额 -20.60, 净额 -20.60
2000-12-21T00:00:00, 收盘价, 27.82
2000-12-21T00:00:00, 买入信号, 27.82
2000-12-22T00:00:00, 买入执行, 数量 10, 价格: 28.65, 成本: 286.50, 手续费 0.00
2000-12-22T00:00:00, 收盘价, 30.06
2000-12-26T00:00:00, 收盘价, 29.17
2000-12-27T00:00:00, 收盘价, 28.94
2000-12-28T00:00:00, 收盘价, 29.29
2000-12-29T00:00:00, 收盘价, 27.41
2000-12-29T00:00:00, 卖出信号, 27.41
最终组合价值: 973.90`
In the name of the King!!! A winning system turned into a losing one … and that with no commission. It may well be that **simply** adding an *indicator* is not the universal panacea. Note The same logic and data with PyAlgoTrade yields a slightly different result (slightly off). Looking at the entire printout reveals that some operations are not exactly the same. Being the culprit again the usual suspect: *rounding*. PyAlgoTrade does not round the datafeed values when applying the divided “adjusted close” to the data feed values. The Yahoo Data Feed provided by *backtrader* rounds the values down to 2 decimals after applying the adjusted close. Upon printing the values everything seems the same, but it’s obvious that sometimes that 5^(th) place decimal plays a role. Rounding down to 2 decimals seems more realistic, because Market Exchanges do only allow a number of decimals per asset (being that 2 decimals usually for stocks) Note The Yahoo Data Feed (starting with version `1.8.11.99` allows to specify if rounding has to happen and how many decimals) ### Visual Inspection: Plotting A printout or log of the actual whereabouts of the system at each bar-instant is good but humans tend to be *visual* and therefore it seems right to offer a view of the same whereabouts as chart. Note To plot you need to have *matplotlib* installed Once again defaults for plotting are there to assist the platform user. Plotting is incredibly a 1 line operation:
BackTrader 中文文档(一)(4)https://developer.aliyun.com/article/1489213