文章目录
基本概念
接下来将会通过一系列例子来介绍本系统。但是在那之前需要先解释两个基本概念:
- Lines
Data Feeds,指标(indicator),策略(Strategies)都有lines.
line由一系列连续的点组成。以资本市场为例,一个Data Feed每天有以下几种点:
Open,High,Low,Close,Volume,OpenInterest(未平仓合约)。
其中Open的时间序列就构成一个line。也就是说一个DataFeed通常由6个lines。如果考虑到还有时间数据,就有7个lines。
- 下标为0的Index(索引)
访问一个line中的数据时,当前时间的数据通过下标0访问。前一个时间的数据使用下标-1访问。在Python中line是可迭代(iterable)的,因此下标-1就是数组中的上一个对象。
在策略中,使用下标0表示line中当前bar(当天、当前分钟等)的数据。
例如:在策略中初始化时定义了移动平均指标:
self.sma = SimpleMovingAverage(.....)
最简单的访问当前移动平均值的方式是:
av = self.sma[0]
例子:从入门到精通
基本设置
运行以下程序
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.run())
- 输出结果
虽然从这个例子本身看不出太多干货,但是有以下几点值得注意:
- cerebro引擎在后台创建了一个broker(经纪人)实例(译注:可以把broker看做开户的证券公司,所有交易通过它进行,当然还要收交易费)
- 这个broker实例有一些初始现金
broker实例是对用户全周期的持续记录(译注:也就是证券公司有你的账户所有资料)。如果用户没有创建broker,平台会创建一个默认的。
通常broker会用10000元作为启动资金。
设置启动资金
在金融世界里,穷人才会只有1万启动资金。我们修改一下启动资金,然后重新运行。
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
任务完成。让我们继续。
添加数据DataFeed
手中有钱心中不慌,但是,我们的目的是通过自动化的策略,对某类资产进行交易,最终让自己的资金成倍增加。而资产在平台中是一个Data Feed。
但是,现在还没有DataFeed,不好玩。这个程序又要增加内容了,接下来增加一个DataFeed。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # 日期时间类型对象
import os.path # 管理路径
import sys # 用于找到当前程序名字
# 导入backtrader平台
import backtrader as bt
if __name__ == '__main__':
# 创建cerebro实例
cerebro = bt.Cerebro()
# 在这个例子中,数据文件在子目录下。由于程序可能在任何地方运行,所以需要找到数据文件位置。
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# 创建Data Feed
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())
最后输出为:
Starting Portfolio Value: 1000000.00
Final Portfolio Value: 1000000.00
代码长度肉眼可见地增加了,因为我们加入了以下内容:
- 确定当前程序位置以找到数据文件
- 使用datetime对象过滤我们需要的时间段的数据
另外,我们还把DataFeed添加到了cerebro引擎。
最后,输出没有什么变化(有变化才有鬼)。
注意
雅虎财经返回的数据是时间降序排列的,这不符合常规,因此在数据文件中已经将其按时间升序排列了。
第一个策略Strategy
现在账户broker中有现金了,也有资产数据DataFeed了,前方就是风险投资了。
让我们加入一个策略,同时每天(或每个时间点bar)输出收盘价close。
数据序列DataSeries是基于DataFeeds的类。DataSeries对象通过别名访问每天的OHLC(Open High Low Close)数据。这有助于简化逻辑代码。
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # 日期时间类型对象
import os.path # 管理路径
import sys # 用于找到当前程序名字
# 导入backtrader平台
import backtrader as bt
# 注意这是新增代码
# 创建策略
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):
# 保持对datas[0]数据序列(DataSeries)中收盘价close的line的引用。其他函数可以直接用self.dataclose以简化代码。
self.dataclose = self.datas[0].close
#每个bar执行函数
def next(self):
# 通过初始化中定义的变量输出当前收盘价。注意下标0代表当前bar(如当天)收盘价。
self.log('Close, %.2f' % self.dataclose[0])
if __name__ == '__main__':
# 创建cerebro实例
cerebro = bt.Cerebro()
# 在这个例子中,数据文件在子目录下。由于程序可能在任何地方运行,所以需要找到数据文件位置。
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# 创建Data Feed
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())
最后的输出结果
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函数时,策略strategy已经得到数据列表了(指self.datas[0])。这是个标准的python列表(list),可以通过插入数据的顺序访问这个数据列表。列表中的第一个数据self.data[0]是交易操作的默认数据(译注:同时也是默认标的),同时作为系统时钟使所有策略元素保持同步
。
self.dataclose = self.datas[0].close
这行代码保持对收盘价line的引用。后面的程序就只需要使用该引用变量就能访问收盘价数据(译注:在next方法中,每个bar的close都在变,dataclose[0]永远指向当前bar的收盘价)。
- 策略Strategy类中的next函数在每个bar被调用。在next函数中会进行一些计算,如各个指标的计算。这些指标会调用之前的一些bar的数据。
向策略添加逻辑
接下来我们实现一点疯狂的想法:如果价格连续下降三天就买买买!(下面只放出修改了的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()
输出
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
下了多个买入单以后,资产价值降低了。
还有两个重要的事情没搞清:
- 发出了下单指令,但是不知道是否执行。
- 不清楚交易执行的时间和价格。
下一个例子将会通过监控订单状态实现这些功能。
有人会问到底买了什么标的,买了多少,订单怎么执行的。这些问题的解释如下:
- 如果没有指定其他标的,将会交易self.datas[0](主数据)对应的标的。
- 后台通过固定的头寸大小(position sizer)指定交易数量,默认为1.后面会演示如何修改。
- 交易在市场中进行,broker会使用下一个bar的开盘价执行交易,因为这是当前数据后的第一个bar。
- 现在交易还没有设置交易费,稍后会演示如何修改。
别只买,还要卖
知道了怎么入场(买入),接下来该介绍怎么离场(卖出),同时还要知道策略是否持有标的。
- Strategy对象提供了position属性作为默认DataFeed
- buy和sell方法返回已创建交易单(还未执行)
- 交易单状态改变将会通过notify方法通知Strategy。
下面例子的离场策略也很简单:买入5个bar后卖出。
注意此处没有指定卖出时间是5天还是5分钟,虽然我们知道数据源中一个bar是一天,但是策略对此无所谓,只关心bar的数量。
简单起见,只有空仓时下买入单。
注意:
next方法没有bar index这个数据,似乎无法知道买入后过去了5个bar。通过以下方法可以解决:对一个对象使用len可以得到它的line长度(开始时间到现在的bar的长度)。在上一次操作时将其保存在变量中,与当前的len比较就可以知道过去了几个bar。
def __init__(self):
# Keep a reference to the "close" line in the data[0] dataseries
self.dataclose = self.datas[0].close
# 跟踪当前订单
self.order = None
def notify_order(self, order):
#针对订单的通知
if order.status in [order.Submitted, order.Accepted]:
# 订单状态是submited(提交),accepted(接受)状态(没有completed),不执行操作。
return
# 检查订单状态是否完成。
# 注意:如果现金不足,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)
# 记录当前line的len
self.bar_executed = len(self)
# 订单状态是取消、margin?、拒绝
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# 取消对交易单的跟踪。
self.order = None
def next(self):
# Simply log the closing price of the series from the reference
self.log('Close, %.2f' % self.dataclose[0])
# 检查是否有未完成的交易单,有则不挂新单。
if self.order:
return
# 检查是否在场中(有头寸),有则不买入
if not self.position:
# 买入逻辑
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])
# 跟踪当前交易单,避免重复下单。
self.order = self.buy()
else:
# 有头寸,考虑卖出
if len(self) >= (self.bar_executed + 5):
# 当前据买入日期过去5bar,执行卖出
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# 跟踪卖单
self.order = self.sell()
执行结果:
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
broker说:给我点钱
broker(经纪人)需要交易费。
我们加上一点合理的交易费,买卖双向收取0.1%。
只需要一行代码:
# 0.1% 转为小数就是0.001
cerebro.broker.setcommission(commission=0.001)
同时,作为经验丰富的交易者,我们还想知道在买卖循环中,有没有交易费分别取得的收益或损失。
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()
输出如下:
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
看来还是能继续赚钱。
在继续之前来看看这个数据:
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,但是系统最后输出的总资产是100016.98。很明显15.83不是16.98.但是系统也没提示出错,为什么呢?
因为策略还有一个持仓,虽然最后一天下了卖单,但是卖单没有执行。此时资产价值按最后一天的现金+持仓量*收盘价计算,但是持仓不算入净利润(因为还没有实现)。
定制策略:参数Parameters
在程序中写死某些数值是不明智的,以后不方便修改。此时使用Parameter很有用。
参数定义很简单:
class TestStrategy(bt.Strategy):
params = (
('myparam', 27),
('exitbars', 5),
)
参数是由多个元组tuples组成的元组。
在内部定义了parameter后,从外部可以传入参数:
cerebro.addstrategy(TestStrategy, myparam=20, exitbars=7)
(另一个例子是旧版本程序的setsizing,但是现在没用了所以就不放这里了)
顺便说一下,修改固定的交易量(股票数量)的方法:
# stake就是每次交易的数量
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
修改的部分代码如下:
class TestStrategy(bt.Strategy):
params = (
('exitbars', 5),
)
...
def next(self):
...
if not self.position:
...
else:
if len(self) >= (self.bar_executed + self.params.exitbars):
# SELL, SELL, SELL!!! (with all possible default parameters)
self.log('SELL CREATE, %.2f' % self.dataclose[0])
...
if __name__ == '__main__':
...
# Set our desired cash start
cerebro.broker.setcash(100000.0)
# 此处指定每次交易的固定交易量
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# Set the commission - 0.1% ... divide by 100 to remove the %
cerebro.broker.setcommission(commission=0.001)
添加指标
知道指标indicator的人都想将一个指标加入策略中,肯定比“下跌3天就买入”这样的策略好。
下面使用PyAlgoTrade的一个简单移动平均策略:
- 收盘价高于均值买入
- 如果有持仓,收盘价低于均值卖出
- 同一时间只能有一个活动操作。
上面的大多数代码都还能用。在init函数中加入移动平均线指标并保持引用。
self.sma = bt.indicators.MovingAverageSimple(self.datas[0], period=self.params.maperiod)
当然同时入场和离场规则都会改为依赖均值。看看策略逻辑代码:
。。。
class TestStrategy(bt.Strategy):
params = (
('maperiod', 15),
)
。。。
def __init__(self):
。。。
# 添加简单移动平均指标
self.sma = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.maperiod)
。。。
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:
# 比较当前收盘价与移动平均线(译注:不用担心未来函数,当天的比较结果要下一交易日执行)
if self.dataclose[0] > self.sma[0]:
# BUY, BUY, BUY!!! (with all possible 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:
if self.dataclose[0] < self.sma[0]:
# 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()
。。。
先别着急进入下面部分,看看输出结果日志
开始时间不再是2000-1-3,而是2000-1-24了。中间的日子去哪儿了?
别急,中间的日子没有掉,因为平台调整了条件:
- 现在加入了移动平均线SimpleMovingAverage这个指标。
- 这个指标需要前X个bar的数据。在这个例子中x=15。
- 因此策略将从15个bar后即2000-1-24开始。
backtrader平台认为策略中加入的指标肯定是有充分理由的,能辅助决策过程。因此必须保证指标准备好需要的数据。
- next函数会在所有指标准备好最低要求的数据后才会调用。
- 这个例子只有一个简单的指标,真正的策略可能有很多指标。
可视化:Plotting
将系统中每个bar的情况打印或记录日志当然很好,但是人们更愿意看到视觉展示。因此提供数据的图形展示应该更好。
要做图形展示需要安装matplotlib
系统提供了默认的绘图展示,因此要进行图形展示可以只要短短的1行:
cerebro.plot()
只要这个代码在cerebro.run()之后调用。
为了展示系统的自动绘图能力和一些简单定制方法,需要做以下事情:
- 添加一条指数移动平均线,默认将会与数据一起绘制。
- 添加一条加权移动平均线,并定制在一个单独的plot窗口中(但是不会让人感觉到)。
- 添加随机指标,不修改默认值
- 添加MACD指标,不修改默认值。
- 添加RSI指标,不修改默认值。
- 将简单移动平均线添加到RSI线上,不修改默认值。
- 添加ATR指标。修改默认值为不显示。
在init函数中加入以下代码:
bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
bt.indicators.WeightedMovingAverage(self.datas[0], period=25).subplot = True
bt.indicators.StochasticSlow(self.datas[0])
bt.indicators.MACDHisto(self.datas[0])
rsi = bt.indicators.RSI(self.datas[0])
bt.indicators.SmoothedMovingAverage(rsi, period=10)
bt.indicators.ATR(self.datas[0]).plot = False
注意
虽然这些指标没有显式加入策略的成员变量,但是他们实际上在策略中自动注册,并且会影响next函数的最小周期,同时作为plot的一部分。
完整程序就不发了,只是在init函数中加入以上代码。
调优
很多交易书籍都说不同的市场不同的股票(或者大宗商品等等)有不同的节奏,因此没有一个参数可以适应所有的情况。
在可视化之前的例子中,策略使用了指标的默认周期值15。这就是策略的参数。在调优过程中,可以调整参数的值,看哪个参数更适合这个市场。
注意
有很多文章讨论优化的优缺点,而最终的建议都是一个方向:不要过度优化。如果交易思路不对,优化只会生成一个只在过去数据回测中有效的参数。
下面的代码修改为优化移动平均线策略的周期参数。为了输出简洁,去掉了所有买卖交易的输出。
下面是完整的例子:
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime
import os.path
import sys
# Import the backtrader platform
import backtrader as bt
# 创建策略类
class TestStrategy(bt.Strategy):
params = (
('maperiod', 15),
('printlog', False),
)
def log(self, txt, dt=None, doprint=False):
'''策略的输出日志函数。注意此处判断参数printlog如果为false就不输出'''
if self.params.printlog or doprint:
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
# 跟踪交易单及价格、费率
self.order = None
self.buyprice = None
self.buycomm = None
# 添加移动平均线c
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]:
# 交易未完成不执行操作
return
# 交易是否完成
# 注意,如果现金不够不能买入
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):
# 日志输出当前收盘价
self.log('Close, %.2f' % self.dataclose[0])
# 检查是否有未完成交易单
if self.order:
return
# 检查是否持有头寸
if not self.position:
# 买入逻辑
if self.dataclose[0] > self.sma[0]:
# 买入
self.log('BUY CREATE, %.2f' % self.dataclose[0])
# 记录当前交易单避免重复买入
self.order = self.buy()
else:
if self.dataclose[0] < self.sma[0]:
# 卖出
self.log('SELL CREATE, %.2f' % self.dataclose[0])
# 记录当前交易避免重复卖出
self.order = self.sell()
#单个回测结束调用本方法打印结果。
def stop(self):
self.log('(MA Period %2d) Ending Value %.2f' %
(self.params.maperiod, self.broker.getvalue()), doprint=True)
if __name__ == '__main__':
# 创建cerebro引擎
cerebro = bt.Cerebro()
# 添加策略。注意使用cerebro.optsrategy,其中的参数maperiod为一个范围
strats = cerebro.optstrategy(
TestStrategy,
maperiod=range(10, 31))
# 定义数据位置
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, '../../datas/orcl-1995-2014.txt')
# 创建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)
# 添加数据
cerebro.adddata(data)
# 设置启动资金
cerebro.broker.setcash(1000.0)
# 设置固定交易量
cerebro.addsizer(bt.sizers.FixedSize, stake=10)
# 设置交易费率
cerebro.broker.setcommission(commission=0.0)
# 运行
cerebro.run(maxcpus=1)
注意这次cerebro类不是调用addstrategy方法,而是optstrategy方法。传递的参数也不是一个值而是一个范围。
单个回测进行时,添加一个策略钩子(hooks),当数据遍历完,回测结束时,调用stop方法,用以输出最终资产净值。系统会对传递参数范围内每个值执行一次策略。
输出如下:
2000-12-29, (MA Period 10) Ending Value 880.30
2000-12-29, (MA Period 11) Ending Value 880.00
2000-12-29, (MA Period 12) Ending Value 830.30
2000-12-29, (MA Period 13) Ending Value 893.90
2000-12-29, (MA Period 14) Ending Value 896.90
2000-12-29, (MA Period 15) Ending Value 973.90
2000-12-29, (MA Period 16) Ending Value 959.40
2000-12-29, (MA Period 17) Ending Value 949.80
2000-12-29, (MA Period 18) Ending Value 1011.90
2000-12-29, (MA Period 19) Ending Value 1041.90
2000-12-29, (MA Period 20) Ending Value 1078.00
2000-12-29, (MA Period 21) Ending Value 1058.80
2000-12-29, (MA Period 22) Ending Value 1061.50
2000-12-29, (MA Period 23) Ending Value 1023.00
2000-12-29, (MA Period 24) Ending Value 1020.10
2000-12-29, (MA Period 25) Ending Value 1013.30
2000-12-29, (MA Period 26) Ending Value 998.30
2000-12-29, (MA Period 27) Ending Value 982.20
2000-12-29, (MA Period 28) Ending Value 975.70
2000-12-29, (MA Period 29) Ending Value 983.30
2000-12-29, (MA Period 30) Ending Value 979.80
结果是:周期18日以下结果是亏损,18-26日可以赚钱,26日以上又亏损。最好的参数是20日,盈利7.8%
总结
以上实例展示了怎么从最简单的程序扩展到可以可视化输出和优化参数的能完整工作的交易系统。
要提高交易胜率,还可以做很多其他尝试:
- 自定义指标。创建自定义指标和可视化展示指标值都很容易。
- 交易量。资金管理是成功的关键。
- 其他类型的尝试,如限制交易,止损等。
- 其他
要充分使用以上工具,文档在其他主题中提供了更深入的资料。