天天看點

量化回測架構BackTrader【6】-開發政策0,序1,定義一個政策2,政策運作時序3,操作交易4,通知回調5,示例

目錄

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

    ” 或 “

    datetime.date

    ”:有效至某個特點時間;“Order.DAY” 或 “0” or “timedelta()”:日間訂單,隻在當天有效;數值:假定為與matplotlib編碼中的日期時間相對應的值(由backtrader使用的日期時間),并将用于生成在該時間之前有效的訂單(有效截止日期)
  • 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
           

可以看出訂單不會馬上成交,會等到設定的價格出現時才會成交,不會像上個例子一樣頻繁發出訂單,更加合理