目錄
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
可以看出訂單不會馬上成交,會等到設定的價格出現時才會成交,不會像上個例子一樣頻繁發出訂單,更加合理