目录
0,序
1,定义一个策略
2,策略运行时序
start()
prenext()
nextstart()
next()
stop()
3,操作交易
基本交易
扩展交易
4,通知回调
5,示例
0,序
众所周知,对于量化投资来说最核心的部分就是策略。而量化回测框架用来回测的也正是策略的性能。所以对于策略开发的功能支持度,开发的简易程度,是检验一个量化回测框架非常重要的指标。BackTrader对于策略开发的支持还是很完整的,这一讲从如下几个方面来介绍BackTrader的策略开发。
1,定义一个策略
BackTrader定义了一个策略的基类-Strategy,所有的策略可以从继承这个基类开始,下面展示一个基本的策略示例
class MyStrategy(bt.Strategy):
params = (('period', 30),)
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(period=self.p.period)
def next(self):
if self.sma > self.data.close:
pass
elif self.sma < self.data.close:
pass
该例中的策略继承了Strategy基类,并在__init__(self)中定义了一个移动平均值的指标,定义了一个参数用来设置这个指标。重写了next(self)函数,这个函数很关键,基本都是要重写的,下文会讲到。
2,策略运行时序
BackTrader内部运行回测时有一个心跳机制,每个data feed的最小时间间隔(不同的data feed可能会有不同的时间间隔)就是一次心跳,也可以说是一个时间点,所有时间点串在一起就是整个回测周期。
Strategy将整个周期分为了几个时间段,并对每个时间段相应地定义了成员函数,如果需要在该时间段的时间点做一些逻辑处理的话就需要重写它们。接下来分别介绍这些时间段:
start()
策略开始运行之前,即第一个bar之前
prenext()
策略准备阶段,主要取决于指标,在start之后,在所有指标能够输出之前。以上面的例子为例,移动均值指标的时长是30个bar,也就是说在前29个bar该指标是不会有输出的,此时策略无法正常运行,所以定义为准备阶段。
nextstart()
准备阶段结束后的第一个时点,也就是策略正常运行的第一个时点。
next()
策略的主要阶段,这个阶段,所有的指标都能正常输出,所有的交易也会发生在这个阶段,可以说是策略的主战场。
stop()
结束阶段,即最后一个bar的之后,可以做些收尾或重置的处理。
通过一个示例可以直观地了解这个时序。
import backtrader as bt
import inspect
def show_yourself(self):
print("当前是第{}个bar,所处阶段 {}".format(len(self), inspect.stack()[1][3]))
class MyStrategy(bt.Strategy):
params = (('period', 30),)
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(period=self.p.period)
def next(self):
show_yourself(self)
if self.sma > self.data.close:
pass
elif self.sma < self.data.close:
pass
def nextstart(self):
show_yourself(self)
pass
def prenext(self):
show_yourself(self)
pass
def start(self):
show_yourself(self)
pass
def stop(self):
show_yourself(self)
pass
cerebro = bt.Cerebro()
data = bt.feeds.GenericCSVData(
dataname='CU1811.csv',
nullvalue=0.0,
dtformat=('%Y%m%d'),
datetime=1,
open=4,
high=5,
low=6,
close=7,
volume=11,
openinterest=-1
)
cerebro.adddata(data)
#cerebro.broker.set_cash(1000000)
cerebro.addstrategy(MyStrategy, period=30)
cerebro.run()
#cerebro.plot(style='bar',iplot=False)
所用的数据是上一讲从Tushare下载的期货日线数据,定义了一个基本策略并重写了上面介绍的成员函数,这些函数都不做实际的处理,只是打印自己所在时点以及自己的函数名。输出如下
当前是第0个bar,所处阶段 start 当前是第1个bar,所处阶段 prenext
当前是第29个bar,所处阶段 prenext 当前是第30个bar,所处阶段 nextstart 当前是第31个bar,所处阶段 next
当前是第242个bar,所处阶段 next 当前是第242个bar,所处阶段 stop
省去了中间的重复部分。可以看到输出完全吻合之前的描述。
3,操作交易
BackTrader支持两个基本的交易操作买(Buy)和卖(Sell),以及由这两个基本交易扩展的一系列其他 交易。
基本交易
Buy和Sell其实就是两个相反的操作,它们的参数是一样。下面介绍主要参数
- data:由哪个data feed创建的订单。如果没有,则将使用系统中的默认data feed,self.datas [0]或self.data0(也称为self.data)
- size:交易的数量(正数)。如果没有,则将通过getsizer()检索到的sizer实例用于确定数量大小
- price:交易的价格,当交易类型是“market”或“close”,可以接受“none”由市场决定价格,当交易类型是“Limit”, “Stop”和“StopLimit”时,价格此时只是作为触发值
- plimit:仅适用于“StopLimit”交易类型。一旦触发了止损(市场价格触及price参数),以这个值设定限价订单的价格
- exectype:交易类型,“market”(默认值):市场订单将以下一个可用价格执行;在回测中,它将是下一个bar的开盘价。“Limit”:限价交易,只能以给定价格或更优的价格执行的订单。“Stop”:止损单,以价格触发并像“market”一样执行的订单。“StopLimit”:止损限价交易,以“Price”触发并以“plimit”作为限价执行的订单。
- valid:有效时间。“None”:生成的订单不会过期(也称为“有效至取消”),一直保留在市场中直到成功交易或被取消;“
” 或 “datetime.datetime
”:有效至某个特点时间;“Order.DAY” 或 “0” or “timedelta()”:日间订单,只在当天有效;数值:假定为与matplotlib编码中的日期时间相对应的值(由backtrader使用的日期时间),并将用于生成在该时间之前有效的订单(有效截止日期)datetime.date
- tradeid:这是BackTrader的内部值,用于跟踪同一资产上的重叠交易。通知订单状态更改时,此Tradeid将发送回给策略
- **kwargs:其他broker所需的参数。 BackTrader将把kwargs传递给被创建的order对象
扩展交易
- Close:平仓,清空所有的多单或空单
- order_target_size:平衡仓位至目标数量,如果现数量超过目标数量就执行卖,反之则买
- order_target_value:平衡仓位至目标价值
- order_target_percent:平衡仓位至目标价值百分比
- buy_bracket:止盈止损组合多单,会同时生成三个order,一个低价的止损卖单,一个中间价的限价买单以及一个高价的止盈限价卖单。相比基本交易的Buy和Sell,参数多了两组参数【stopprice,stopexec,stopargs】和【limitprice,limitexec,limitargs】(官方文档以及源码注释中把“limitexec”误写为“stopexec”了),分别用来配置止损订单和止盈订单。如果不想生成止损单或止盈单也可以通过设置参数stopexec=none或limitexec=none。返回的三个order顺序是[买单, 止损单, 止盈单]
- sell_bracket:止盈止损组合空单,参数跟止盈止损组合多单一样
- cancel(self, order):取消还未被处理的order
4,通知回调
BackTrader对一些状态改变的通知是以回调的方式实现的,需要重写对回调函数的实现。目前支持以下通知:
- notify_order(order):每次订单状态改变会触发回调
- notify_trade(trade):任何开仓/更新/平仓交易的通知
- notify_cashvalue(cash, value) :通知当前现金和投资组合
- notify_store(msg, *args, **kwargs):关于存储的通知
- notify_data(self, data, status, *args, **kwargs)::关于数据的通知
- notify_timer(self, timer, when, *args, **kwargs):定时器通知,定时器可以通过成员函数add_timer()添加
5,示例
例1,在前面的例子的基础上我们添加基本的买卖操作以及订单,交易和资产的通知
import backtrader as bt
import inspect
class MyStrategy(bt.Strategy):
params = (('period', 30),)
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(period=self.p.period)
def notify_order(self, order):
print("\033[31mbar序:{},订单通知 size:{},price:{},pricelimit:{},exectype{},tradeid:{},status:{}\033[0m"
.format(len(self),order.size,order.price,order.pricelimit,order.exectype,order.tradeid,order.Status[order.status]))
def notify_trade(self, trade):
print("\033[32mbar序:{},交易通知 size:{},price:{},value:{},tradeid:{},status:{}\033[0m"
.format(len(self),trade.size,trade.price,trade.value,trade.tradeid,trade.status_names[trade.status]))
def notify_cashvalue(self, cash, value):
print("\033[33mbar序:{},资产通知 cash:{},value:{}\033[0m"
.format(len(self),cash,value))
def next(self):
print("bar序:{},next sma={},close={}".format(len(self),self.sma[0],self.data.close[0]))
if self.sma > self.data.close:
print("It's time to buy!")
self.buy()
elif self.sma < self.data.close:
print("It's time to close!")
self.close()
cerebro = bt.Cerebro()
data = bt.feeds.GenericCSVData(
dataname='CU1811.csv',
nullvalue=0.0,
dtformat=('%Y%m%d'),
datetime=1,
open=4,
high=5,
low=6,
close=7,
volume=11,
openinterest=-1
)
cerebro.adddata(data)
cerebro.broker.set_cash(1000000)
cerebro.addstrategy(MyStrategy, period=30)
cerebro.run()
#cerebro.plot(style='bar',iplot=False)
该例中,策略非常简单,当移动平均值大于close值时执行买入操作,参数都是默认,当移动平均值小于close值时执行平仓操作。输出如下:
bar序:1,资产通知 cash:1000000.0,value:1000000.0 bar序:2,资产通知 cash:1000000.0,value:1000000.0 bar序:3,资产通知 cash:1000000.0,value:1000000.0
bar序:30,资产通知 cash:1000000.0,value:1000000.0 bar序:30,next sma=54738.0,close=56340.0 It's time to close! bar序:31,资产通知 cash:1000000.0,value:1000000.0 bar序:31,next sma=54823.0,close=57230.0 It's time to close!
bar序:44,next sma=55286.666666666664,close=54600.0 It's time to buy! bar序:45,订单通知 size:1,price:None,pricelimit:None,exectype0,tradeid:0,status:Submitted bar序:45,订单通知 size:1,price:None,pricelimit:None,exectype0,tradeid:0,status:Accepted bar序:45,订单通知 size:1,price:None,pricelimit:None,exectype0,tradeid:0,status:Completed bar序:45,交易通知 size:1,price:54890.0,value:54890.0,tradeid:0,status:Open bar序:45,资产通知 cash:945110.0,value:1000040.0 bar序:45,next sma=55357.666666666664,close=54930.0
bar序:62,next sma=55060.666666666664,close=53050.0 It's time to buy! bar序:63,订单通知 size:1,price:None,pricelimit:None,exectype0,tradeid:0,status:Submitted bar序:63,订单通知 size:1,price:None,pricelimit:None,exectype0,tradeid:0,status:Margin bar序:63,资产通知 cash:20430.0,value:983610.0 bar序:63,next sma=54957.333333333336,close=53510.0
bar序:66,next sma=54765.333333333336,close=55000.0 It's time to close! bar序:67,订单通知 size:-18,price:None,pricelimit:None,exectype0,tradeid:0,status:Submitted bar序:67,订单通知 size:-18,price:None,pricelimit:None,exectype0,tradeid:0,status:Accepted bar序:67,订单通知 size:-18,price:None,pricelimit:None,exectype0,tradeid:0,status:Completed bar序:67,交易通知 size:0,price:54420.555555555555,value:0.0,tradeid:0,status:Closed bar序:67,资产通知 cash:1007010.0,value:1007010.0 bar序:67,next sma=54736.333333333336,close=55040.0
这是部分输出,可以得出,1,notify_cashvalue会在每个bar被调到;2,从第3段可以看出订单的发出发生在满足条件的后面一个bar,因为采用了默认的“market”交易类型;3,在同一个bar时点内,顺序是订单通知》交易通知》资产通知》next函数;4,从第4段可以看出当现金不够是,order会进入Margin状态;5,在已经持有多单时再追加多单不会有交易通知
例2,一次只买一手太少了,我们试一次另一个控制仓位比例的函数order_target_percent,并添加一些控制
import backtrader as bt
import inspect
class MyStrategy(bt.Strategy):
params = (('period', 30),)
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(period=self.p.period)
def notify_order(self, order):
print("\033[31mbar序:{},订单通知 size:{},price:{},pricelimit:{},exectype:{},tradeid:{},status:{}\033[0m"
.format(len(self),order.size,order.price,order.pricelimit,order.ExecTypes[order.exectype],order.tradeid,order.Status[order.status]))
if not order.alive():
self.order = None
def notify_trade(self, trade):
print("\033[32mbar序:{},交易通知 size:{},price:{},value:{},tradeid:{},status:{}\033[0m"
.format(len(self),trade.size,trade.price,trade.value,trade.tradeid,trade.status_names[trade.status]))
'''def notify_cashvalue(self, cash, value):
print("\033[33mbar序:{},资产通知 cash:{},value:{}\033[0m"
.format(len(self),cash,value))'''
def start(self):
self.order = None
def next(self):
#print("bar序:{},next sma={},close={}".format(len(self),self.sma[0],self.data.close[0]))
if self.order:
return
if not self.position:
if self.sma > self.data.close:
self.order = self.order_target_percent(target=1.0, price=self.data.close[0]*0.99,exectype=bt.Order.Limit)
#self.order = self.buy(price=self.data.close[0]*0.99,exectype=bt.Order.Limit)
else:
if self.sma < self.data.close:
self.close()
cerebro = bt.Cerebro()
data = bt.feeds.GenericCSVData(
dataname='CU1811.csv',
nullvalue=0.0,
dtformat=('%Y%m%d'),
datetime=1,
open=4,
high=5,
low=6,
close=7,
volume=11,
openinterest=-1
)
cerebro.adddata(data)
cerebro.broker.set_cash(1000000)
cerebro.addstrategy(MyStrategy, period=30)
cerebro.run()
添加了一个order的变量,保存order状态,当有order被挂起时不发起新的order;添加了在“self.position”的判断,当已经开了仓不会再重复开仓;用“order_target_percent”替换了“buy”,并且满仓买入,订单类型也改为限价单;去掉了多余的打印信息。以下是输出
bar序:45,订单通知 size:18,price:54054.0,pricelimit:None,exectype:Limit,tradeid:0,status:Submitted bar序:45,订单通知 size:18,price:54054.0,pricelimit:None,exectype:Limit,tradeid:0,status:Accepted bar序:49,订单通知 size:18,price:54054.0,pricelimit:None,exectype:Limit,tradeid:0,status:Completed bar序:49,交易通知 size:18,price:54054.0,value:972972.0,tradeid:0,status:Open bar序:67,订单通知 size:-18,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Submitted bar序:67,订单通知 size:-18,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Accepted bar序:67,订单通知 size:-18,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Completed bar序:67,交易通知 size:0,price:54054.0,value:0.0,tradeid:0,status:Closed bar序:70,订单通知 size:19,price:53341.2,pricelimit:None,exectype:Limit,tradeid:0,status:Submitted bar序:70,订单通知 size:19,price:53341.2,pricelimit:None,exectype:Limit,tradeid:0,status:Accepted bar序:71,订单通知 size:19,price:53341.2,pricelimit:None,exectype:Limit,tradeid:0,status:Completed bar序:71,交易通知 size:19,price:53341.2,value:1013482.7999999999,tradeid:0,status:Open bar序:104,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Submitted bar序:104,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Accepted bar序:104,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Completed bar序:104,交易通知 size:0,price:53341.2,value:0.0,tradeid:0,status:Closed bar序:116,订单通知 size:19,price:51153.3,pricelimit:None,exectype:Limit,tradeid:0,status:Submitted bar序:116,订单通知 size:19,price:51153.3,pricelimit:None,exectype:Limit,tradeid:0,status:Accepted bar序:154,订单通知 size:19,price:51153.3,pricelimit:None,exectype:Limit,tradeid:0,status:Completed bar序:154,交易通知 size:19,price:51153.3,value:971912.7000000001,tradeid:0,status:Open bar序:181,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Submitted bar序:181,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Accepted bar序:181,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Completed bar序:181,交易通知 size:0,price:51153.3,value:0.0,tradeid:0,status:Closed bar序:182,订单通知 size:19,price:49113.9,pricelimit:None,exectype:Limit,tradeid:0,status:Submitted bar序:182,订单通知 size:19,price:49113.9,pricelimit:None,exectype:Limit,tradeid:0,status:Accepted bar序:184,订单通知 size:19,price:49113.9,pricelimit:None,exectype:Limit,tradeid:0,status:Completed bar序:184,交易通知 size:19,price:49113.9,value:933164.1,tradeid:0,status:Open bar序:206,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Submitted bar序:206,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Accepted bar序:206,订单通知 size:-19,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Completed bar序:206,交易通知 size:0,price:49113.9,value:0.0,tradeid:0,status:Closed bar序:207,订单通知 size:20,price:47935.8,pricelimit:None,exectype:Limit,tradeid:0,status:Submitted bar序:207,订单通知 size:20,price:47935.8,pricelimit:None,exectype:Limit,tradeid:0,status:Accepted bar序:207,订单通知 size:20,price:47935.8,pricelimit:None,exectype:Limit,tradeid:0,status:Completed bar序:207,交易通知 size:20,price:47935.8,value:958716.0,tradeid:0,status:Open bar序:209,订单通知 size:-20,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Submitted bar序:209,订单通知 size:-20,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Accepted bar序:209,订单通知 size:-20,price:None,pricelimit:None,exectype:Market,tradeid:0,status:Completed bar序:209,交易通知 size:0,price:47935.8,value:0.0,tradeid:0,status:Closed bar序:233,订单通知 size:20,price:49153.5,pricelimit:None,exectype:Limit,tradeid:0,status:Submitted bar序:233,订单通知 size:20,price:49153.5,pricelimit:None,exectype:Limit,tradeid:0,status:Accepted bar序:233,订单通知 size:20,price:49153.5,pricelimit:None,exectype:Limit,tradeid:0,status:Completed bar序:233,交易通知 size:20,price:49153.5,value:983070.0,tradeid:0,status:Open
可以看出订单不会马上成交,会等到设置的价格出现时才会成交,不会像上个例子一样频繁发出订单,更加合理