BackTrader 中文文件(二十八)

绝不原创的飞龙發表於2024-04-15

原文:www.backtrader.com/

訂單管理和執行

原文:www.backtrader.com/blog/posts/2015-08-08-order-creation-execution/order-creation-execution/

回測,因此 backtrader,如果不能模擬訂單,將不完整。為此,平臺提供了以下功能。

對於訂單管理有 3 個原語:

  • 購買

  • 賣出

  • 取消

注意

update原語顯然是一種邏輯,但常識告訴我們,這種方法主要由使用判斷性交易方法的手動操作員使用。

對於訂單執行邏輯,以下執行型別:

  • 市價

  • 關閉

  • 限價

  • 停止

  • StopLimit

訂單管理

主要目標是易於使用,因此進行訂單管理的最直接(和簡單)的方法是從策略本身開始。

buyself原語具有以下簽名作為Strategy方法:

  • def buy(self, data=None, size=None, price=None, plimit=None, exectype=None, valid=None):

  • def buy(self, data=None, size=None, price=None, exectype=None, valid=None)

    • data -> 資料來源引用,用於購買的資產

    如果傳遞None,則策略的主要資料將用作目標

    • size -> 確定要應用的股份的 int/long

    如果傳遞None,則策略中可用的Sizer將被用於自動確定股份。預設的Sizer使用固定狀態1

    • price -> 對於Market將被忽略並且可以留空,但對於其他訂單型別必須是浮點數。如果留空,則將使用當前收盤價

    • plimit -> 在StopLimit訂單中的限價,其中price將被用作觸發價格

    如果留空,則price將被用作限價(觸發和限價相同)

    • exectype -> 訂單執行型別之一。如果傳遞None,則將假定為Market

    執行型別在Order中被列舉。例如:Order.Limit

    • valid -> 從 date2num(或資料來源)獲取的浮點值或 datetime.datetime Python 物件

    注意Market訂單將在不考慮valid引數的情況下執行

    返回值:一個Order例項

  • def sell(self, data=None, size=None, price=None, exectype=None, valid=None)

因為取消訂單隻需要buyself返回的order引用,所以經紀人的原語可以使用(見下文)

一些例子:

# buy the main date, with sizer default stake, Market order
order = self.buy()

# Market order - valid will be "IGNORED"
order = self.buy(valid=datetime.datetime.now() + datetime.timedelta(days=3))

# Market order - price will be IGNORED
order = self.buy(price=self.data.close[0] * 1.02)

# Market order - manual stake
order = self.buy(size=25)

# Limit order - want to set the price and can set a validity
order = self.buy(exectype=Order.Limit,
                 price=self.data.close[0] * 1.02,
                 valid=datetime.datetime.now() + datetime.timedelta(days=3)))

# StopLimit order - want to set the price, price limit
order = self.buy(exectype=Order.StopLimit,
                 price=self.data.close[0] * 1.02,
                 plimit=self.data.close[0] * 1.07)

# Canceling an existing order
self.broker.cancel(order)

注意

所有訂單型別都可以透過建立Order例項(或其子類之一),然後透過以下方式傳遞給經紀人:

order = self.broker.submit(order)

注意

broker本身有buysell原語,但對於預設引數要求更嚴格。

訂單執行邏輯

broker使用 2 個主要準則(假設?)進行訂單執行。

  • 當前資料已經發生,不能用於執行訂單。

    如果策略中的邏輯如下:

     if self.data.close > self.sma:  # where sma is a Simple Moving Average
         self.buy()
    
    The expectation CANNOT be that the order will be executed with the
    ``close`` price which is being examined in the logic BECAUSE it has already
    happened.
    
    The order CAN BE 1st EXECUTED withing the bounds of the next set of
    Open/High/Low/Close price points (and the conditions set forth herein by
    the order)` 
    
  • 交易量不起作用

    實際上,在真實交易中,如果交易員選擇非流動性資產,或者精確地擊中了價格條的極值(高/低),訂單將被取消。

    但是擊中高/低點是個罕見的事件(如果你這麼做...你就不需要backtrader了),並且所選擇的資產將有足夠的流動性來吸收任何常規交易的訂單

市場

執行:

下一個一組 Open/High/Low/Close 價格(通常稱為bar)的開盤價

理由:

如果邏輯在時間點 X 執行併發出了Market訂單,則接下來會發生的價格點是即將到來的open價格

注意

這個訂單總是執行,忽略任何用於建立它的pricevalid引數

關閉

執行:

當下一個價格條實際上關閉時,使用下一個價格條的close價格

理由:

大多數回測資料來源已經包含了已關閉的價格條,訂單將立即執行,使用下一個價格條的close價格。每日資料來源是最常見的例子。

但是系統可以被喂入“tick”價格,實際的條(按時間/日期)正在不斷地隨著新的 ticks 更新,而實際上並沒有移動到下一個條(因為時間和/或日期沒有改變)

只有當時間或日期改變時,條才會實際上被關閉,訂單才會執行

限價

執行:

訂單建立時設定的price,如果data觸及它,則從下一個價格條開始。

如果設定了valid並且到達了時間點,訂單將被取消

價格匹配:

backtrader試圖為Limit訂單提供最真實的執行價格

使用 4 個價格點(Open/High/Low/Close),可以部分推斷請求的price是否可以改善。

對於Buy訂單

- Case 1:

  If the `open` price of the bar is below the limit price the order
  executes immediately with the `open` price. The order has been swept
  during the opening phase of the session

- Case 2:

  If the `open` price has not penetrated below the limit price but the
  `low` price is below the limit price, then the limit price has been
  seen during the session and the order can be executed

對於Sell訂單,邏輯顯然是相反的。

止損

執行:

訂單建立時設定的觸發器price,如果data觸及它,則從下一個價格條開始。

如果設定了valid並且到達了時間點,訂單將被取消

價格匹配:

backtrader試圖為Stop訂單提供最真實的觸發價格

使用 4 個價格點(Open/High/Low/Close),可以部分推斷請求的price是否可以改善。

對於\Stoporders whichBuy`

- Case 1:

  If the `open` price of the bar is above the stop price the order is
  executed immediately with the `open` price.

  Intended to stop a loss if the price is moving upwards against an
  existing short position

- Case 2:

  If the `open` price has not penetrated above the stop price but the
  `high` price is above the stop price, then the stop price has been
  seen during the session and the order can be executed

對於Sell訂單,邏輯顯然是相反的。

止損限價

執行:

觸發器price設定了訂單的啟動,從下一個價格條開始。

價格匹配:

  • 觸發器:使用Stop匹配邏輯(但僅觸發並將訂單轉換為Limit訂單)

  • 限制:使用Limit價格匹配邏輯

一些樣本

像往常一樣,圖片(帶程式碼)價值數百萬長的解釋。請注意,程式碼片段集中在訂單建立部分。完整程式碼在底部。

一個價格高於/低於簡單移動平均線策略將用於生成買入/賣出訊號

訊號在圖表底部可見:使用交叉指標的CrossOver

僅保留對生成的“買入”訂單的引用,以允許系統中最多隻有一個同時訂單。

執行型別:市價

請在圖表中檢視訂單如何在生成訊號後一根棒棒後以開盤價執行。

 if self.p.exectype == 'Market':
                self.buy(exectype=bt.Order.Market)  # default if not given

                self.log('BUY CREATE, exectype Market, price %.2f' %
                         self.data.close[0])

輸出圖表。

影像

命令列和輸出:

$ ./order-execution-samples.py --exectype Market
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Market, price 3641.42
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-27T23:59:59+00:00, BUY EXECUTED, Price: 3643.35, Cost: 3643.35, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
...
...
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Market, price 4052.89
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-12T23:59:59+00:00, BUY EXECUTED, Price: 4052.55, Cost: 4052.55, Comm 0.00

執行型別:收盤

注意

Issue#11之後,建立了一個開發分支,更新了圖表和輸出。錯誤的收盤價格被使用。

現在訂單也是在訊號後一根棒棒後執行,但是使用的是收盤價。

 elif self.p.exectype == 'Close':
                self.buy(exectype=bt.Order.Close)

                self.log('BUY CREATE, exectype Close, price %.2f' %
                         self.data.close[0])

輸出圖表。

影像

命令列和輸出:

$ ./order-execution-samples.py --exectype Close
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Close, price 3641.42
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-27T23:59:59+00:00, BUY EXECUTED, Price: 3685.48, Cost: 3685.48, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
...
...
2006-11-06T23:59:59+00:00, BUY CREATE, exectype Close, price 4045.22
2006-11-06T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-07T23:59:59+00:00, BUY EXECUTED, Price: 4072.86, Cost: 4072.86, Comm 0.00
2006-11-24T23:59:59+00:00, SELL CREATE, 4048.16
2006-11-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-27T23:59:59+00:00, SELL EXECUTED, Price: 4045.05, Cost: 4045.05, Comm 0.00
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Close, price 4052.89
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-12T23:59:59+00:00, BUY EXECUTED, Price: 4059.74, Cost: 4059.74, Comm 0.00

執行型別:限價

在幾行之前計算有效性以防已作為引數傳遞。

 if self.p.valid:
                valid = self.data.datetime.date(0) + \
                        datetime.timedelta(days=self.p.valid)
            else:
                valid = None

設定了訊號生成價格(訊號條的收盤價)下跌 1%的限價。請注意,這阻止了上面許多訂單的執行。

 elif self.p.exectype == 'Limit':
                price = self.data.close * (1.0 - self.p.perc1 / 100.0)

                self.buy(exectype=bt.Order.Limit, price=price, valid=valid)

                if self.p.valid:
                    txt = 'BUY CREATE, exectype Limit, price %.2f, valid: %s'
                    self.log(txt % (price, valid.strftime('%Y-%m-%d')))
                else:
                    txt = 'BUY CREATE, exectype Limit, price %.2f'
                    self.log(txt % price)

輸出圖表。

影像

僅釋出了 4 個訂單。嘗試捕捉小幅度下跌的價格完全改變了輸出。

命令列和輸出:

$ ./order-execution-samples.py --exectype Limit --perc1 1
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Limit, price 3605.01
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-05-18T23:59:59+00:00, BUY EXECUTED, Price: 3605.01, Cost: 3605.01, Comm 0.00
2006-06-05T23:59:59+00:00, SELL CREATE, 3604.33
2006-06-05T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-06T23:59:59+00:00, SELL EXECUTED, Price: 3598.58, Cost: 3598.58, Comm 0.00
2006-06-21T23:59:59+00:00, BUY CREATE, exectype Limit, price 3491.57
2006-06-21T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-28T23:59:59+00:00, BUY EXECUTED, Price: 3491.57, Cost: 3491.57, Comm 0.00
2006-07-13T23:59:59+00:00, SELL CREATE, 3562.56
2006-07-13T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-07-14T23:59:59+00:00, SELL EXECUTED, Price: 3545.92, Cost: 3545.92, Comm 0.00
2006-07-24T23:59:59+00:00, BUY CREATE, exectype Limit, price 3596.60
2006-07-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED

執行型別:限價與有效期

為了不會永遠等待一個限價訂單,該訂單僅在 4(日曆)天內有效,該訂單可能只有在價格對“買入”訂單不利時才執行。

輸出圖表。

影像

更多訂單已生成,但除了一個“買入”訂單過期外,其他所有訂單都過期了,進一步限制了運算元量。

命令列和輸出:

$ ./order-execution-samples.py --exectype Limit --perc1 1 --valid 4
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Limit, price 3605.01, valid: 2006-01-30
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-30T23:59:59+00:00, BUY EXPIRED
2006-03-10T23:59:59+00:00, BUY CREATE, exectype Limit, price 3760.48, valid: 2006-03-14
2006-03-10T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-14T23:59:59+00:00, BUY EXPIRED
2006-03-30T23:59:59+00:00, BUY CREATE, exectype Limit, price 3835.86, valid: 2006-04-03
2006-03-30T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-04-03T23:59:59+00:00, BUY EXPIRED
2006-04-20T23:59:59+00:00, BUY CREATE, exectype Limit, price 3821.40, valid: 2006-04-24
2006-04-20T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-04-24T23:59:59+00:00, BUY EXPIRED
2006-05-04T23:59:59+00:00, BUY CREATE, exectype Limit, price 3804.65, valid: 2006-05-08
2006-05-04T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-05-08T23:59:59+00:00, BUY EXPIRED
2006-06-01T23:59:59+00:00, BUY CREATE, exectype Limit, price 3611.85, valid: 2006-06-05
2006-06-01T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-05T23:59:59+00:00, BUY EXPIRED
2006-06-21T23:59:59+00:00, BUY CREATE, exectype Limit, price 3491.57, valid: 2006-06-25
2006-06-21T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-06-26T23:59:59+00:00, BUY EXPIRED
2006-07-24T23:59:59+00:00, BUY CREATE, exectype Limit, price 3596.60, valid: 2006-07-28
2006-07-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-07-28T23:59:59+00:00, BUY EXPIRED
2006-09-12T23:59:59+00:00, BUY CREATE, exectype Limit, price 3751.07, valid: 2006-09-16
2006-09-12T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-09-18T23:59:59+00:00, BUY EXPIRED
2006-09-20T23:59:59+00:00, BUY CREATE, exectype Limit, price 3802.90, valid: 2006-09-24
2006-09-20T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-09-22T23:59:59+00:00, BUY EXECUTED, Price: 3802.90, Cost: 3802.90, Comm 0.00
2006-11-02T23:59:59+00:00, SELL CREATE, 3974.62
2006-11-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-03T23:59:59+00:00, SELL EXECUTED, Price: 3979.73, Cost: 3979.73, Comm 0.00
2006-11-06T23:59:59+00:00, BUY CREATE, exectype Limit, price 4004.77, valid: 2006-11-10
2006-11-06T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-10T23:59:59+00:00, BUY EXPIRED
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Limit, price 4012.36, valid: 2006-12-15
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-15T23:59:59+00:00, BUY EXPIRED

執行型別:止損

設定了訊號價格上漲 1%的止損價。這意味著策略僅在生成訊號並價格繼續上漲時購買,這可以被解釋為強勢訊號。

這完全改變了執行情況。

 elif self.p.exectype == 'Stop':
                price = self.data.close * (1.0 + self.p.perc1 / 100.0)

                self.buy(exectype=bt.Order.Stop, price=price, valid=valid)

                if self.p.valid:
                    txt = 'BUY CREATE, exectype Stop, price %.2f, valid: %s'
                    self.log(txt % (price, valid.strftime('%Y-%m-%d')))
                else:
                    txt = 'BUY CREATE, exectype Stop, price %.2f'
                    self.log(txt % price)

輸出圖表。

影像

命令列和輸出:

$ ./order-execution-samples.py --exectype Stop --perc1 1
2006-01-26T23:59:59+00:00, BUY CREATE, exectype Stop, price 3677.83
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-01-27T23:59:59+00:00, BUY EXECUTED, Price: 3677.83, Cost: 3677.83, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
2006-03-10T23:59:59+00:00, BUY CREATE, exectype Stop, price 3836.44
2006-03-10T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-15T23:59:59+00:00, BUY EXECUTED, Price: 3836.44, Cost: 3836.44, Comm 0.00
2006-03-28T23:59:59+00:00, SELL CREATE, 3811.45
2006-03-28T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-29T23:59:59+00:00, SELL EXECUTED, Price: 3811.85, Cost: 3811.85, Comm 0.00
2006-03-30T23:59:59+00:00, BUY CREATE, exectype Stop, price 3913.36
2006-03-30T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-09-29T23:59:59+00:00, BUY EXECUTED, Price: 3913.36, Cost: 3913.36, Comm 0.00
2006-11-02T23:59:59+00:00, SELL CREATE, 3974.62
2006-11-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-03T23:59:59+00:00, SELL EXECUTED, Price: 3979.73, Cost: 3979.73, Comm 0.00
2006-11-06T23:59:59+00:00, BUY CREATE, exectype Stop, price 4085.67
2006-11-06T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-13T23:59:59+00:00, BUY EXECUTED, Price: 4085.67, Cost: 4085.67, Comm 0.00
2006-11-24T23:59:59+00:00, SELL CREATE, 4048.16
2006-11-24T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-11-27T23:59:59+00:00, SELL EXECUTED, Price: 4045.05, Cost: 4045.05, Comm 0.00
2006-12-11T23:59:59+00:00, BUY CREATE, exectype Stop, price 4093.42
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-13T23:59:59+00:00, BUY EXECUTED, Price: 4093.42, Cost: 4093.42, Comm 0.00

執行型別:止損限價

設定了訊號價格上漲 1%的止損價。但限價設定在訊號(收盤)價格上漲 0.5%,這可以解釋為:等待力量展現,但不要買入高峰。等待下跌。

有效期限制為 20(日曆)天

 elif self.p.exectype == 'StopLimit':
                price = self.data.close * (1.0 + self.p.perc1 / 100.0)

                plimit = self.data.close * (1.0 + self.p.perc2 / 100.0)

                self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid,
                         plimit=plimit)

                if self.p.valid:
                    txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
                           ' valid: %s, pricelimit: %.2f')
                    self.log(txt % (price, valid.strftime('%Y-%m-%d'), plimit))
                else:
                    txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
                           ' pricelimit: %.2f')
                    self.log(txt % (price, plimit))

輸出圖表。

影像

命令列和輸出:

$ ./order-execution-samples.py --exectype StopLimit --perc1 1 --perc2 0.5 --valid 20
2006-01-26T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 3677.83, valid: 2006-02-15, pricelimit: 3659.63
2006-01-26T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-02-03T23:59:59+00:00, BUY EXECUTED, Price: 3659.63, Cost: 3659.63, Comm 0.00
2006-03-02T23:59:59+00:00, SELL CREATE, 3763.73
2006-03-02T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-03T23:59:59+00:00, SELL EXECUTED, Price: 3763.95, Cost: 3763.95, Comm 0.00
2006-03-10T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 3836.44, valid: 2006-03-30, pricelimit: 3817.45
2006-03-10T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-21T23:59:59+00:00, BUY EXECUTED, Price: 3817.45, Cost: 3817.45, Comm 0.00
2006-03-28T23:59:59+00:00, SELL CREATE, 3811.45
2006-03-28T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-03-29T23:59:59+00:00, SELL EXECUTED, Price: 3811.85, Cost: 3811.85, Comm 0.00
2006-03-30T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 3913.36, valid: 2006-04-19, pricelimit: 3893.98
2006-03-30T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-04-19T23:59:59+00:00, BUY EXPIRED
...
...
2006-12-11T23:59:59+00:00, BUY CREATE, exectype StopLimit, price 4093.42, valid: 2006-12-31, pricelimit: 4073.15
2006-12-11T23:59:59+00:00, ORDER ACCEPTED/SUBMITTED
2006-12-22T23:59:59+00:00, BUY EXECUTED, Price: 4073.15, Cost: 4073.15, Comm 0.00

測試指令碼執行

在命令列的help中詳細說明:

$ ./order-execution-samples.py --help
usage: order-execution-samples.py [-h] [--infile INFILE]
                                  [--csvformat {bt,visualchart,sierrachart,yahoo,yahoo_unreversed}]
                                  [--fromdate FROMDATE] [--todate TODATE]
                                  [--plot] [--plotstyle {bar,line,candle}]
                                  [--numfigs NUMFIGS] [--smaperiod SMAPERIOD]
                                  [--exectype EXECTYPE] [--valid VALID]
                                  [--perc1 PERC1] [--perc2 PERC2]

Showcase for Order Execution Types

optional arguments:
  -h, --help            show this help message and exit
  --infile INFILE, -i INFILE
                        File to be read in
  --csvformat {bt,visualchart,sierrachart,yahoo,yahoo_unreversed},
  -c {bt,visualchart,sierrachart,yahoo,yahoo_unreversed}
                        CSV Format
  --fromdate FROMDATE, -f FROMDATE
                        Starting date in YYYY-MM-DD format
  --todate TODATE, -t TODATE
                        Ending date in YYYY-MM-DD format
  --plot, -p            Plot the read data
  --plotstyle {bar,line,candle}, -ps {bar,line,candle}
                        Plot the read data
  --numfigs NUMFIGS, -n NUMFIGS
                        Plot using n figures
  --smaperiod SMAPERIOD, -s SMAPERIOD
                      Simple Moving Average Period
  --exectype EXECTYPE, -e EXECTYPE
                        Execution Type: Market (default), Close, Limit,
                        Stop, StopLimit
  --valid VALID, -v VALID
                        Validity for Limit sample: default 0 days
  --perc1 PERC1, -p1 PERC1
                        % distance from close price at order creation time for
                        the limit/trigger price in Limit/Stop orders
  --perc2 PERC2, -p2 PERC2
                        % distance from close price at order creation time for
                        the limit price in StopLimit orders

完整程式碼

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import datetime
import os.path
import time
import sys

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

class OrderExecutionStrategy(bt.Strategy):
    params = (
        ('smaperiod', 15),
        ('exectype', 'Market'),
        ('perc1', 3),
        ('perc2', 1),
        ('valid', 4),
    )

    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
            self.log('ORDER ACCEPTED/SUBMITTED', dt=order.created.dt)
            self.order = order
            return

        if order.status in [order.Expired]:
            self.log('BUY EXPIRED')

        elif 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))

            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

        # Sentinel to None: new orders allowed
        self.order = None

    def __init__(self):
        # SimpleMovingAverage on main data
        # Equivalent to -> sma = btind.SMA(self.data, period=self.p.smaperiod)
        sma = btind.SMA(period=self.p.smaperiod)

        # CrossOver (1: up, -1: down) close / sma
        self.buysell = btind.CrossOver(self.data.close, sma, plot=True)

        # Sentinel to None: new ordersa allowed
        self.order = None

    def next(self):
        if self.order:
            # An order is pending ... nothing can be done
            return

        # Check if we are in the market
        if self.position:
            # In the maerket - check if it's the time to sell
            if self.buysell < 0:
                self.log('SELL CREATE, %.2f' % self.data.close[0])
                self.sell()

        elif self.buysell > 0:
            if self.p.valid:
                valid = self.data.datetime.date(0) + \
                        datetime.timedelta(days=self.p.valid)
            else:
                valid = None

            # Not in the market and signal to buy
            if self.p.exectype == 'Market':
                self.buy(exectype=bt.Order.Market)  # default if not given

                self.log('BUY CREATE, exectype Market, price %.2f' %
                         self.data.close[0])

            elif self.p.exectype == 'Close':
                self.buy(exectype=bt.Order.Close)

                self.log('BUY CREATE, exectype Close, price %.2f' %
                         self.data.close[0])

            elif self.p.exectype == 'Limit':
                price = self.data.close * (1.0 - self.p.perc1 / 100.0)

                self.buy(exectype=bt.Order.Limit, price=price, valid=valid)

                if self.p.valid:
                    txt = 'BUY CREATE, exectype Limit, price %.2f, valid: %s'
                    self.log(txt % (price, valid.strftime('%Y-%m-%d')))
                else:
                    txt = 'BUY CREATE, exectype Limit, price %.2f'
                    self.log(txt % price)

            elif self.p.exectype == 'Stop':
                price = self.data.close * (1.0 + self.p.perc1 / 100.0)

                self.buy(exectype=bt.Order.Stop, price=price, valid=valid)

                if self.p.valid:
                    txt = 'BUY CREATE, exectype Stop, price %.2f, valid: %s'
                    self.log(txt % (price, valid.strftime('%Y-%m-%d')))
                else:
                    txt = 'BUY CREATE, exectype Stop, price %.2f'
                    self.log(txt % price)

            elif self.p.exectype == 'StopLimit':
                price = self.data.close * (1.0 + self.p.perc1 / 100.0)

                plimit = self.data.close * (1.0 + self.p.perc2 / 100.0)

                self.buy(exectype=bt.Order.StopLimit, price=price, valid=valid,
                         plimit=plimit)

                if self.p.valid:
                    txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
                           ' valid: %s, pricelimit: %.2f')
                    self.log(txt % (price, valid.strftime('%Y-%m-%d'), plimit))
                else:
                    txt = ('BUY CREATE, exectype StopLimit, price %.2f,'
                           ' pricelimit: %.2f')
                    self.log(txt % (price, plimit))

def runstrat():
    args = parse_args()

    cerebro = bt.Cerebro()

    data = getdata(args)
    cerebro.adddata(data)

    cerebro.addstrategy(
        OrderExecutionStrategy,
        exectype=args.exectype,
        perc1=args.perc1,
        perc2=args.perc2,
        valid=args.valid,
        smaperiod=args.smaperiod
    )
    cerebro.run()

    if args.plot:
        cerebro.plot(numfigs=args.numfigs, style=args.plotstyle)

def getdata(args):

    dataformat = dict(
        bt=btfeeds.BacktraderCSVData,
        visualchart=btfeeds.VChartCSVData,
        sierrachart=btfeeds.SierraChartCSVData,
        yahoo=btfeeds.YahooFinanceCSVData,
        yahoo_unreversed=btfeeds.YahooFinanceCSVData
    )

    dfkwargs = dict()
    if args.csvformat == 'yahoo_unreversed':
        dfkwargs['reverse'] = True

    if args.fromdate:
        fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
        dfkwargs['fromdate'] = fromdate

    if args.todate:
        fromdate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
        dfkwargs['todate'] = todate

    dfkwargs['dataname'] = args.infile

    dfcls = dataformat[args.csvformat]

    return dfcls(**dfkwargs)

def parse_args():
    parser = argparse.ArgumentParser(
        description='Showcase for Order Execution Types')

    parser.add_argument('--infile', '-i', required=False,
                        default='../datas/2006-day-001.txt',
                        help='File to be read in')

    parser.add_argument('--csvformat', '-c', required=False, default='bt',
                        choices=['bt', 'visualchart', 'sierrachart',
                                 'yahoo', 'yahoo_unreversed'],
                        help='CSV Format')

    parser.add_argument('--fromdate', '-f', required=False, default=None,
                        help='Starting date in YYYY-MM-DD format')

    parser.add_argument('--todate', '-t', required=False, default=None,
                        help='Ending date in YYYY-MM-DD format')

    parser.add_argument('--plot', '-p', action='store_false', required=False,
                        help='Plot the read data')

    parser.add_argument('--plotstyle', '-ps', required=False, default='bar',
                        choices=['bar', 'line', 'candle'],
                        help='Plot the read data')

    parser.add_argument('--numfigs', '-n', required=False, default=1,
                        help='Plot using n figures')

    parser.add_argument('--smaperiod', '-s', required=False, default=15,
                        help='Simple Moving Average Period')

    parser.add_argument('--exectype', '-e', required=False, default='Market',
                        help=('Execution Type: Market (default), Close, Limit,'
                              ' Stop, StopLimit'))

    parser.add_argument('--valid', '-v', required=False, default=0, type=int,
                        help='Validity for Limit sample: default 0 days')

    parser.add_argument('--perc1', '-p1', required=False, default=0.0,
                        type=float,
                        help=('%% distance from close price at order creation'
                              ' time for the limit/trigger price in Limit/Stop'
                              ' orders'))

    parser.add_argument('--perc2', '-p2', required=False, default=0.0,
                        type=float,
                        help=('%% distance from close price at order creation'
                              ' time for the limit price in StopLimit orders'))

    return parser.parse_args()

if __name__ == '__main__':
    runstrat()

擴充套件資料來源

原文:www.backtrader.com/blog/posts/2015-08-07-extending-a-datafeed/extending-a-datafeed/

GitHub 上的問題實際上正在推動完成文件部分或幫助我瞭解 backtrader 是否具有我最初設想的易用性和靈活性,並沿途做出的決定。

在這種情況下是問題#9

最終問題似乎歸結為:

  • 終端使用者是否可以輕鬆擴充套件現有機制,以新增額外資訊,以行的形式傳遞到其他現有價格資訊點,如openhigh等?

就我理解的問題,答案是:是的

發帖人似乎有這些要求(來自問題#6):

  • 一個被解析為 CSV 格式的資料來源

  • 使用GenericCSVData來載入資訊

    這種通用的 CSV 支援是為了響應這個問題#6而開發的

  • 一個額外欄位,顯然包含需要傳遞到解析的 CSV 資料中的 P/E 資訊

讓我們在 CSV 資料來源開發和通用 CSV 資料來源示例帖子的基礎上構建。

步驟:

  • 假設 P/E 資訊被設定在被解析的 CSV 資料中

  • 使用GenericCSVData作為基類

  • 將現有線(open/high/low/close/volumen/openinterest)擴充套件為pe

  • 新增一個引數,讓呼叫者確定 P/E 資訊的列位置

結果:

from backtrader.feeds import GenericCSVData

class GenericCSV_PE(GenericCSVData):

    # Add a 'pe' line to the inherited ones from the base class
    lines = ('pe',)

    # openinterest in GenericCSVData has index 7 ... add 1
    # add the parameter to the parameters inherited from the base class
    params = (('pe', 8),)

工作完成了...

稍後,在策略中使用這個資料來源時:

import backtrader as bt

....

class MyStrategy(bt.Strategy):

    ...

    def next(self):

        if self.data.close > 2000 and self.data.pe < 12:
            # TORA TORA TORA --- Get off this market
            self.sell(stake=1000000, price=0.01, exectype=Order.Limit)
    ...

繪製額外的 P/E 線

顯然,在資料來源中沒有對該額外線的自動繪圖支援。

最好的選擇是對該線進行簡單移動平均,並在單獨的軸上繪製它:

import backtrader as bt
import backtrader.indicators as btind

....

class MyStrategy(bt.Strategy):

    def __init__(self):

        # The indicator autoregisters and will plot even if no obvious
        # reference is kept to it in the class
        btind.SMA(self.data.pe, period=1, subplot=False)

    ...

    def next(self):

        if self.data.close > 2000 and self.data.pe < 12:
            # TORA TORA TORA --- Get off this market
            self.sell(stake=1000000, price=0.01, exectype=Order.Limit)
    ...

CSV 資料來源開發

原文:www.backtrader.com/blog/posts/2015-08-06-csv-data-feed-development/csv-data-feed-development/

backtrader 已經提供了一個通用的 CSV 資料來源和一些特定的 CSV 資料來源。總結如下:

  • GenericCSVData

  • VisualChartCSVData

  • YahooFinanceData(用於線上下載)

  • YahooFinanceCSVData(用於已下載的資料)

  • BacktraderCSVData(內部…用於測試目的,但可以使用)

但即使如此,終端使用者可能希望為特定的 CSV 資料來源開發支援。

通常的格言是:“說起來容易做起來難”。實際上,結構設計得很簡單。

步驟:

  • 繼承自backtrader.CSVDataBase

  • 如果需要,定義任何params

  • start方法中進行任何初始化

  • stop方法中進行任何清理

  • 定義一個_loadline方法,其中實際工作發生

    此方法接收一個引數:linetokens。

    正如名稱所示,這包含了在根據separator引數(從基類繼承)分割當前行後的令牌。

    如果在完成其工作後有新資料... 填充相應的行並返回True

    如果沒有可用的資料,因此解析已經結束:返回False

    如果在幕後讀取檔案行的程式碼發現沒有更多可解析的行,則可能甚至不需要返回False

已經考慮到的事項:

  • 開啟檔案(或接收類似檔案的物件)

  • 如果存在,則跳過標題行

  • 讀取行

  • 對行進行標記

  • 預載入支援(一次性將整個資料來源載入到記憶體中)

通常,一個例子勝過千言萬語的要求描述。讓我們使用從BacktraderCSVData定義的簡化版本的內部定義的 CSV 解析程式碼。這個不需要初始化或清理(例如,可以稍後開啟套接字並關閉)。

注意

backtrader資料來源包含常見的行業標準資料來源,這些是需要填充的。即:

  • 日期時間

  • 開啟

  • 關閉

  • 成交量

  • 持倉量

如果您的策略/演算法或簡單的資料查閱只需要,例如收盤價,您可以將其他內容保持不變(每次迭代都會在終端使用者程式碼有機會執行任何操作之前自動使用 float('NaN')值填充它們。

在本例中僅支援每日格式:

import itertools
...
import backtrader import bt

class MyCSVData(bt.CSVDataBase):

    def start(self):
        # Nothing to do for this data feed type
        pass

    def stop(self):
        # Nothing to do for this data feed type
        pass

    def _loadline(self, linetokens):
        i = itertools.count(0)

        dttxt = linetokens[next(i)]
        # Format is YYYY-MM-DD
        y = int(dttxt[0:4])
        m = int(dttxt[5:7])
        d = int(dttxt[8:10])

        dt = datetime.datetime(y, m, d)
        dtnum = date2num(dt)

        self.lines.datetime[0] = dtnum
        self.lines.open[0] = float(linetokens[next(i)])
        self.lines.high[0] = float(linetokens[next(i)])
        self.lines.low[0] = float(linetokens[next(i)])
        self.lines.close[0] = float(linetokens[next(i)])
        self.lines.volume[0] = float(linetokens[next(i)])
        self.lines.openinterest[0] = float(linetokens[next(i)])

        return True

程式碼期望所有欄位都就位,並且可轉換為浮點數,除了日期時間之外,它具有固定的 YYYY-MM-DD 格式,並且可以在不使用datetime.datetime.strptime的情況下解析。

只需新增幾行程式碼即可滿足更復雜的需求,以處理空值,日期格式解析。GenericCSVData就是這樣做的。

買方注意事項

使用GenericCSVData現有的資料來源和繼承可以實現很多支援格式的功能。

讓我們為Sierra Chart的每日格式新增支援(該格式始終以 CSV 格式儲存)。

定義(透過檢視一個‘.dly’資料檔案):

  • 欄位:日期、開盤價、最高價、最低價、收盤價、成交量、持倉量

    行業標準和已由 GenericCSVData 支援的那些檔案,按相同順序(這也是行業標準)

  • 分隔符:,

  • 日期格式:YYYY/MM/DD

針對這些檔案的解析器:

class SierraChartCSVData(backtrader.feeds.GenericCSVData):

    params = (('dtformat', '%Y/%m/%d'),)

params 的定義只是重新定義基類中的一個現有引數。在這種情況下,只需更改日期的格式化字串。

哎呀……Sierra Chart 的解析器完成了。

這裡是 GenericCSVData 的引數定義作為提醒:

class GenericCSVData(feed.CSVDataBase):
    params = (
        ('nullvalue', float('NaN')),
        ('dtformat', '%Y-%m-%d %H:%M:%S'),
        ('tmformat', '%H:%M:%S'),

        ('datetime', 0),
        ('time', -1),
        ('open', 1),
        ('high', 2),
        ('low', 3),
        ('close', 4),
        ('volume', 5),
        ('openinterest', 6),
    )

通用 CSV 資料來源

原文:www.backtrader.com/blog/posts/2015-08-04-generic-csv-datafeed/generic-csv-datafeed/

一個問題導致實現了GenericCSVData,可用於解析不同的 CSV 格式。

GitHub 上的問題,Issue #6清楚地顯示需要有能夠處理任何傳入 CSV 資料來源的東西。

引數宣告中包含關鍵資訊:

class GenericCSVData(feed.CSVDataBase):
    params = (
        ('nullvalue', float('NaN')),
        ('dtformat', '%Y-%m-%d %H:%M:%S'),
        ('tmformat', '%H:%M:%S'),

        ('datetime', 0),
        ('time', -1),
        ('open', 1),
        ('high', 2),
        ('low', 3),
        ('close', 4),
        ('volume', 5),
        ('openinterest', 6),
    )

因為該類繼承自 CSVDataBase,一些標準引數可用:

  • fromdate(接受日期時間物件以限制起始日期)

  • todate(接受日期時間物件)以限制結束日期)

  • headers(預設值:True,指示 CSV 資料是否有標題行)

  • separator(預設值:“,”,分隔欄位的字元)

  • dataname(包含 CSV 資料的檔名或類似檔案的物件)

其他一些引數如namecompressiontimeframe僅供參考,除非您計劃執行重新取樣。

當然更重要的是,新定義引數的含義:

  • datetime(預設值:0)列包含日期(或日期時間)欄位

  • time(預設值:-1)列包含時間欄位,如果與日期時間欄位分開(-1 表示不存在)

  • open(預設值:1),high(預設值:2),low(預設值:3),close(預設值:4),volume(預設值:5),openinterest(預設值:6)

    包含相應欄位的列的索引

    如果傳遞負值(例如:-1),表示 CSV 資料中不存在該欄位

  • nullvalue(預設值:float('NaN'))

    如果應該存在的值缺失(CSV 欄位為空),將使用的值

  • dtformat(預設值:%Y-%m-%d %H:%M:%S)

    用於解析日期時間 CSV 欄位的格式

  • tmformat(預設值:%H:%M:%S)

    用於解析時間 CSV 欄位的格式(如果“存在”)(“時間”CSV 欄位的預設值是不存在)

這可能足以涵蓋許多不同的 CSV 格式和值的缺失。

涵蓋以下要求的示例用法:

  • 限制輸入至 2000 年

  • HLOC 順序而不是 OHLC

  • 缺失值將被替換為零(0.0)

  • 提供每日 K 線資料,日期時間格式為 YYYY-MM-DD

  • 沒有openinterest列存在

程式碼:

import datetime
import backtrader as bt
import backtrader.feeds as btfeed

...
...

data = btfeed.GenericCSVData(
    dataname='mydata.csv',

    fromdate=datetime.datetime(2000, 1, 1),
    todate=datetime.datetime(2000, 12, 31),

    nullvalue=0.0,

    dtformat=('%Y-%m-%d'),

    datetime=0,
    high=1,
    low=2,
    open=3,
    close=4,
    volume=5,
    openinterest=-1
)

稍微修改的要求:

  • 限制輸入至 2000 年

  • HLOC 順序而不是 OHLC

  • 缺失值將被替換為零(0.0)

  • 提供分鐘級 K 線資料,具有單獨的日期和時間列

    • 日期格式為 YYYY-MM-DD

    • 時間格式為 HH.MM.SS

  • 沒有openinterest列存在

程式碼:

import datetime
import backtrader as bt
import backtrader.feeds as btfeed

...
...

data = btfeed.GenericCSVData(
    dataname='mydata.csv',

    fromdate=datetime.datetime(2000, 1, 1),
    todate=datetime.datetime(2000, 12, 31),

    nullvalue=0.0,

    dtformat=('%Y-%m-%d'),
    tmformat=('%H.%M.%S'),

    datetime=0,
    time=1,
    high=2,
    low=3,
    open=4,
    close=5,
    volume=6,
    openinterest=-1
)

改進佣金:股票與期貨

原文:www.backtrader.com/blog/posts/2015-07-31-commission-schemes-updated/commission-schemes-updated/

釋出 backtrader 使用示例讓我對一些缺失的東西有了瞭解。首先:

  • 多核最佳化

  • 佣金:股票與期貨

後者告訴了我:

  • 經紀人在利潤和損失的計算方面做得很對,向呼叫策略提供了正確的訂單通知

  • 策略無法訪問operations(又稱trades),這是訂單已經開倉並關閉頭寸的結果(後者顯示出盈虧數字)

  • 繪製的Operation盈虧數字由一個Observer收集,並且無法訪問實際的commission scheme,因此對於類似於期貨的操作和類似於股票的操作,將顯示相同的盈虧數字。

顯然,需要進行一些小的內部重組才能實現:

  • 向策略傳送Operation通知

  • Operations顯示正確的盈虧數字

broker已經擁有了所有需要的資訊,並且已經將大部分資訊填入了被通知到strategyorder中,而broker需要做出的唯一決定是是否將額外的資訊位放入訂單中,或者它可以自己計算operations

由於策略已經獲得了orders,並且將operations保留在列表中似乎很自然,broker只是在訂單部分/完全關閉頭寸時新增實際的盈虧,將計算責任留給了strategy

這進一步簡化了Operations Observer的實際角色,即觀察新關閉的Operation並記錄下來。這是它一直應該有的角色。

下面的程式碼已經被重新設計,不再計算盈虧數字,而只是注意到notify_operation中通知的數字。

現在圖表反映出了真實的盈虧數字(cashvalue已經是真實的)

舊的期貨記錄:

2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30

新的期貨記錄:

2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30
2006-05-02, BUY CREATE, 3862.24

舊的股票記錄:

2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84

新的股票記錄:

2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84
2006-05-02, BUY CREATE, 3862.24

圖表(僅新的圖表)。現在可以清楚地看到futures-like操作和stock-like操作之間的差異,不僅在cashvalue的變化中。

期貨佣金

image

股票佣金

image

程式碼

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

futures_like = True

if futures_like:
    commission, margin, mult = 2.0, 2000.0, 10.0
else:
    commission, margin, mult = 0.005, None, 1

class SMACrossOver(bt.Strategy):
    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify(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 enougth cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            if order.isbuy():
                self.log(
                    'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                    (order.executed.price,
                     order.executed.value,
                     order.executed.comm))
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

    def notify_trade(self, trade):
        if trade.isclosed:
            self.log('TRADE PROFIT, GROSS %.2f, NET %.2f' %
                     (trade.pnl, trade.pnlcomm))

    def __init__(self):
        sma = btind.SMA(self.data)
        # > 0 crossing up / < 0 crossing down
        self.buysell_sig = btind.CrossOver(self.data, sma)

    def next(self):
        if self.buysell_sig > 0:
            self.log('BUY CREATE, %.2f' % self.data.close[0])
            self.buy()  # keep order ref to avoid 2nd orders

        elif self.position and self.buysell_sig < 0:
            self.log('SELL CREATE, %.2f' % self.data.close[0])
            self.sell()

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    cerebro.addstrategy(SMACrossOver)

    # Create a Data Feed
    datapath = ('../../datas/2006-day-001.txt')
    data = bt.feeds.BacktraderCSVData(dataname=datapath)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # set commission scheme -- CHANGE HERE TO PLAY
    cerebro.broker.setcommission(
        commission=commission, margin=margin, mult=mult)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot()

佣金:股票 vs 期貨

原文:www.backtrader.com/blog/posts/2015-07-26-commission-schemes/commission-schemes/

backtrader 的誕生是出於必要性。我自己...希望有一種感覺,我可以控制自己的回測平臺並嘗試新的想法。但是在這樣做並從一開始就完全開源化之後,很明顯它必須有一種方式來滿足其他人的需求和願望。

作為交易者未來,我本可以選擇編寫基於點數的計算和每輪固定價格的佣金,但那將是一個錯誤。

注意

2015 年 7 月 31 日

跟進帖子,附帶新新增的操作/交易通知,修復交易 P&L 圖表的繪製,並避免像下面示例中那樣的手動計算。

改善佣金:股票 vs 期貨

相反,backtrader提供了使用常規%大小/價格基礎方案和固定價格/點方案的可能性。選擇權在你手上。

不可知論

在繼續之前,讓我們記住`backtrader`試圖保持對資料表示的不可知。可以將不同的佣金方案應用於相同的資料集。

讓我們看看如何做到這一點。

## 使用經紀人快捷方式

這樣可以使終端使用者遠離`CommissionInfo`物件,因為可以透過單個函式呼叫建立/設定佣金方案。在常規的`cerebro`建立/設定過程中,只需將呼叫新增到`broker`成員變數上即可。以下呼叫在使用*InteractiveBrokers*時為**Eurostoxx50**期貨設定了一種常規佣金方案:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)

由於大多數使用者通常只測試單個工具,因此這就是問題的全部。如果您已經為資料來源指定了name,因為圖表上同時考慮了多個工具,因此此呼叫可以略微擴充套件為如下所示:

cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0,
name='Eurostoxxx50')

在這種情況下,這種即時佣金方案將僅應用於名稱與Eurostoxx50匹配的工具。

設定佣金引數的含義

  • commission(預設值:0.0)

    每個操作的貨幣單位以絕對值或百分比形式的成本。

    在上面的示例中,每個buy合約需要 2.0 歐元,每個sell合約也是如此。

    這裡的重要問題是何時使用絕對值或百分比值。

    • 如果margin評估為False(例如為 False、0 或 None),則將認為commission表示price乘以size操作值的百分比。

    • 如果margin是其他內容,則認為操作發生在類似期貨的工具上,並且commission是每個size合約的固定價格

  • margin(預設值:None)

    使用期貨等工具時需要的保證金。如上所述

    • 如果設定了no margin,則commission將被理解為以百分比表示,並應用於buysell操作的price * size元件

    • 如果設定了margin,則commission將被理解為與buysell操作的size分量相乘的固定值

  • mult(預設:1.0)

    對於類似期貨的工具,這決定了要應用於利潤和損失計算的乘數。

    這就是期貨同時具有吸引力和風險的原因。

  • name(預設:無)

    限制佣金方案的應用於與name匹配的工具。

    這可以在建立資料來源時設定。

    如果不設定,方案將應用於系統中存在的任何資料。

現在有兩個例子:股票 vs 期貨

來自上述的期貨示例:

cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)

股票的一個例子:

cerebro.broker.setcommission(commission=0.005)  # 0.5% of the operation value

建立永久佣金方案

更持久的佣金方案可以透過直接使用CommissionInfo類來建立。使用者可以選擇將此定義放在某處:

from bt import CommissionInfo

commEurostoxx50 = CommissionInfo(commission=2.0, margin=2000.0, mult=10.0)

然後在另一個 Python 模組中應用它與addcommissioninfo

from mycomm import commEurostoxx50

...

cerebro.broker.addcomissioninfo(commEuroStoxx50, name='Eurostoxxx50')

CommissionInfo是一個物件,它使用與backtrader環境中的其他物件一樣的params宣告。因此,上述內容也可以表示為:

from bt import CommissionInfo

class CommEurostoxx50(CommissionInfo):
    params = dict(commission=2.0, margin=2000.0, mult=10.0)

後來:

from mycomm import CommEurostoxx50

...

cerebro.broker.addcomissioninfoCommEuroStoxx50(), name='Eurostoxxx50')

現在是與 SMA 交叉的“真實”比較

使用 SimpleMovingAverage 交叉作為入場/出場訊號,將使用類似期貨的佣金方案對同一資料集進行測試,然後再使用類似股票的方案。

注意

期貨頭寸不僅可以在每次發生時賦予進入/退出行為,還可以在每次發生時賦予反轉行為。但是,此示例是關於比較佣金方案的。

程式碼(請參閱底部獲取完整策略)是相同的,可以在定義策略之前選擇方案。

futures_like = True

if futures_like:
    commission, margin, mult = 2.0, 2000.0, 10.0
else:
    commission, margin, mult = 0.005, None, 1

只需將futures_like設定為 false 即可使用類似股票的方案執行。

已新增一些記錄程式碼以評估不同佣金方案的影響。讓我們只關注前兩個操作。

對於期貨:

2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30

對於股票:

2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84

第一次操作具有以下價格:

  • 買入(執行)-> 3754.13 / 賣出(執行)-> 3786.93

    • 期貨利潤和損失(含佣金):324.0

    • 股票利潤和損失(含佣金):-4.91

    嘿!! 佣金完全吞噬了股票操作的任何利潤,但對期貨操作只是造成了小小的凹痕。

第二次操作:

  • 買入(執行)-> 3863.57 / 賣出(執行)-> 3389.24

    • 期貨利潤和損失(含佣金):-247.30

    • 股票利潤和損失(含佣金):-62.84

    這次負面操作對於期貨的咬度明顯更大

但:

  • 期貨累計淨利潤和損失:324.00 + (-247.30) = 76.70

  • 股票累計淨利潤和損失:(-4.91) + (-62.84) = -67.75

累計效果可以在下面的圖表中看到,在完整年份結束時,期貨產生了更大的利潤,但也遭受了更大的回撤(深入水中更深)

但重要的是:無論是期貨還是股票都可以進行回測。

期貨佣金

影像

股票佣金

影像

程式碼

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind

futures_like = True

if futures_like:
    commission, margin, mult = 2.0, 2000.0, 10.0
else:
    commission, margin, mult = 0.005, None, 1

class SMACrossOver(bt.Strategy):
    def log(self, txt, dt=None):
        ''' Logging function fot this strategy'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify(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 enougth cash
        if order.status in [order.Completed, order.Canceled, order.Margin]:
            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
                self.opsize = order.executed.size
            else:  # Sell
                self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
                         (order.executed.price,
                          order.executed.value,
                          order.executed.comm))

                gross_pnl = (order.executed.price - self.buyprice) * \
                    self.opsize

                if margin:
                    gross_pnl *= mult

                net_pnl = gross_pnl - self.buycomm - order.executed.comm
                self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
                         (gross_pnl, net_pnl))

    def __init__(self):
        sma = btind.SMA(self.data)
        # > 0 crossing up / < 0 crossing down
        self.buysell_sig = btind.CrossOver(self.data, sma)

    def next(self):
        if self.buysell_sig > 0:
            self.log('BUY CREATE, %.2f' % self.data.close[0])
            self.buy()  # keep order ref to avoid 2nd orders

        elif self.position and self.buysell_sig < 0:
            self.log('SELL CREATE, %.2f' % self.data.close[0])
            self.sell()

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    cerebro.addstrategy(SMACrossOver)

    # Create a Data Feed
    datapath = ('../datas/2006-day-001.txt')
    data = bt.feeds.BacktraderCSVData(dataname=datapath)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # set commission scheme -- CHANGE HERE TO PLAY
    cerebro.broker.setcommission(
        commission=commission, margin=margin, mult=mult)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot()

多核最佳化

www.backtrader.com/blog/posts/2015-07-23-multicore-optimization/multicore-optimization/

利用所有可用核心是我對 backtrader 有的想法,但從未實現。支援自然操作,移除陣列表示法,包含新的指標等等。

實際上,我並不是最佳化的忠實粉絲,因此對於為此利用所有核心也不是忠實粉絲。在我看來,一個好主意值得百萬次最佳化。

注意

初始的多核支援已經存在,並且對於眾所周知的一組測試用例有效。鑑於pickle所展示的行為,預計還需要進行一些其他調整,以確保在進行多核最佳化時可以在程序之間傳遞所有指標和函式。

注意

針對多核的一些額外校正已經發布為 1.0.10.88 版本,以使更多的“不可序列化”項變得可序列化。到目前為止,指標測試沒有出現任何問題。

但是 BigMikeTrading 論壇上有人詢問這個平臺相比其他平臺有什麼優勢,我提到了一些功能,包括PyAlgoTrade,例如,已經有了(甚至是多機器的)。

這需要做一點小而正確的推動。根據過去的經驗以及因為網際網路上充滿了參考資料,我已經知道:多執行緒即使是最簡單的(無論 GIL 律師們可能說什麼),在 Python 中也是行不通的,無論版本如何。在 Python 中,多執行緒是假的,因為你有多個執行緒,但沒有程式碼的並行執行。在 Python 中使用多執行緒可能會建立抽象,並用 IO 繫結的執行緒分開程式碼路徑的執行,但這確實是一個致命問題。

那麼我只剩下一個選擇:模組multiprocessing或類似的模組。

展望光明的未來,我決定選擇現代版本:concurrent.futures(後來證明是一個錯誤的選擇)。即使這意味著為 Python 2.6/2.7 支援新增外部依賴。

歷史:

  • Python 的一些動態特性與在程序之間傳送資料不相容

  • 序列化一些像類不在模組級別定義、lambda 表示式、對例項方法的引用以及沒有唯一名稱的動態類(即使類本身是唯一的)時,所涉及的模組(pickle)會出錯。

我把這些東西散落在程式碼中。然後我發現了dill和 pathos 多程序的兄弟姐妹pypi.python.org/pypi/multiprocess。顯然它們可以解決序列化問題,但是新增更多的外部依賴……不行不行。

回到起點,看看那些不可序列化的項是否可以被序列化,即使pickle模組產生了一些錯誤,這將使一些舊的 GCC 開發人員非常高興。

它完成了嗎……還是沒有?

  • 將不可選的專案改造為可選專案

  • 用 Python 2.7.9 進行測試,並像風一樣輕鬆地執行……我的機器的 8 個核心順暢且令人耳目一新

  • 使用 Python 3.4.3 進行測試,8 個核心開始運作,但在進行一些最佳化後,每個後續策略的執行時間會越來越長……直到不堪忍受為止。

    顯然,將結果(完整的執行策略)反向 pickling 到主程序中觸及了一些與記憶體分配相關的限制(我的機器有大量空閒 RAM……足夠多以進行幾小時的並行最佳化)

閱讀了一些額外的內容後,我考慮簡化我的情景:

  • 使用 concurrent.futures 看起來更具未來性

  • 但標準的 multiprocessing 模組已經具備了 backtrader 所需的功能

聞起來好像有點過度,一些行被迅速改寫成:

  • 測試用 Python 2.7 執行正常(甚至比以前更快)

  • 測試用 Python 3.4 同樣快速執行

進行清理,執行完整的一系列測試並執行推送,釋出 1.0.9.88。沒有新的指標……只是多核最佳化的普通舊方式

讀完這些……是時候寫一個關於如何控制最佳化以使用多個核心的清爽指令碼了

  • 好訊息……不需要做任何事情……它在使用者不介入的情況下完成了

當使用者希望最佳化 strategy 時,Strategy 子類將被新增到 Cerebro 例項中,如下所示:

cerebro.optstrategy(StrategyClass, *args, **kwargs)

與向 Cerebro 傳遞策略的常規方式相反:

cerebro.addstrategy(StrategyClass, *args, **kwargs)

這一直都是這樣,沒有改變。背景是:

  • Cerebro 需要了解是否要最佳化策略,以正確處理可能已經是常規策略的策略的引數

現在……透過 optstrategy 傳遞給 cerebro策略將獲得使用機器上所有可用核心的額外好處。

當然,如果終端使用者希望對使用的核心進行精細控制……是可能的。建立 Cerebro 的標準方式:

cerebro = bt.Cerebro() # runonce 為 True,preload 為 True,且 “new” maxcpus 為 None

maxcpus(此版本中的新引數)是控制鍵:

  • maxcpus = None -> 使用所有可用的 CPU

  • maxcpus = 1 -> 不要執行多核

  • maxcpues = 2 … -> 使用指定數量的核心

這是一種選擇退出策略,因為多核已經存在。

在擁有 16 GBytes RAM 的 4 核(每核 2 個執行緒 - 總共 8 個邏輯處理器)機器上進行比較,執行 Windows 8.1 和 Python 64 位 2.7.9

  • 使用 1 個核心執行:326 秒

  • 使用 8 個核心執行:127 秒

不同的測試執行顯示,平均比例約為 2.75:1。

不幸的是,程序的建立/銷燬和物件的反覆 pickling 帶來了潛在的好處,但加速效果仍然顯著。

影像顯示了正在使用的 8 個核心。

image

程式碼如下。只需將maxcpus引數的1更改為限制測試為 1 個核心。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import time

from six.moves import xrange

import backtrader as bt
import backtrader.indicators as btind
import backtrader.feeds as btfeeds

class OptimizeStrategy(bt.Strategy):
    params = (('smaperiod', 15),
              ('macdperiod1', 12),
              ('macdperiod2', 26),
              ('macdperiod3', 9),
              )

    def __init__(self):
        # Add indicators to add load

        btind.SMA(period=self.p.smaperiod)
        btind.MACD(period_me1=self.p.macdperiod1,
                   period_me2=self.p.macdperiod2,
                   period_signal=self.p.macdperiod3)

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro(maxcpus=None)

    # Add a strategy
    cerebro.optstrategy(
        OptimizeStrategy,
        smaperiod=xrange(5, 40),
        macdperiod1=xrange(12, 20),
        macdperiod2=xrange(26, 30),
        macdperiod3=xrange(9, 15),
    )

    # Create a Data Feed
    datapath = ('../datas/2006-day-001.txt')
    data = bt.feeds.BacktraderCSVData(dataname=datapath)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # clock the start of the process
    tstart = time.clock()

    # Run over everything
    stratruns = cerebro.run()

    # clock the end of the process
    tend = time.clock()

    print('==================================================')
    for stratrun in stratruns:
        print('**************************************************')
        for strat in stratrun:
            print('--------------------------------------------------')
            print(strat.p._getkwargs())
    print('==================================================')

    # print out the result
    print('Time used:', str(tend - tstart))

擴充套件一個指標

原文:www.backtrader.com/blog/posts/2015-07-20-extending-an-indicator/extending-an-indicator/

在物件導向程式設計中,當然也包括 Python 本身,對現有類的擴充套件可以透過兩種方式實現。

  • 繼承(或子類化)

  • 組合(或嵌入)

在開發一個指標時,指標Trix只需幾行程式碼就可以開發完成。ChartSchool - Trix 參考文獻中有一個帶有訊號線的Trix,顯示了與 MACD 的相似之處。

讓我們使用已經開發的Trix“組合”MyTrixSignal

class MyTrixSignalComposed(bt.Indicator):

    lines = ('trix', 'signal')
    params = (('period', 15), ('sigperiod', 9))

    def __init__(self):
        self.lines.trix = MyTrix(self.data, period=self.p.period)
        self.lines.signal = btind.EMA(self.lines.trix, period=self.p.sigperiod)

在定義中有一些必須重複的內容,比如trix線的名稱和用於計算的period。定義了一個新的signal線和相應的sigperiod引數。

這個兩行的結果很好。

現在讓我們來看看繼承,但首先回顧一下Trix的樣子:

class MyTrix(bt.Indicator):

    lines = ('trix',)
    params = (('period', 15),)

    def __init__(self):
        ema1 = btind.EMA(self.data, period=self.p.period)
        ema2 = btind.EMA(ema1, period=self.p.period)
        ema3 = btind.EMA(ema2, period=self.p.period)

        self.lines.trix = 100.0 * (ema3 - ema3(-1)) / ema3(-1)

使用Trix作為基類,這是TrixSignal的外觀

class MyTrixSignalInherited(MyTrix):

    lines = ('signal',)
    params = (('sigperiod', 9),)

    def __init__(self):
        super(MyTrixSignalInherited, self).__init__()
        self.lines.signal = btind.EMA(self.lines.trix, period=self.p.sigperiod)

繼承的指標最終也是一個兩行程式碼,但是:

  • 不需要重新定義trix

  • 不需要重新定義period引數

兩者都是從基類Trix繼承而來。trix線的計算是在基類__init__方法中完成的:

  • super(MyTrixSignalInherited, self).init()

組合繼承的選擇是一個經典問題。這個例子並不是為了澄清哪種更好,而更多是為了展示:

注意

即使存在linesparams的元定義,它們也繼承自基類的元定義

最後是程式碼和圖表,展示兩個版本的執行情況。

  1. 第一個展示了繼承版本
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.feeds as btfeeds

from mytrix import MyTrixSignalInherited

class NoStrategy(bt.Strategy):
    params = (('trixperiod', 15),
              ('analyzer', False),)

    def __init__(self):
        MyTrixSignalInherited(self.data, period=self.p.trixperiod)

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    cerebro.addstrategy(NoStrategy, trixperiod=15)

    # Create a Data Feed
    datapath = ('../datas/2006-day-001.txt')
    data = bt.feeds.BacktraderCSVData(dataname=datapath)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot()

image

  1. 第一個展示了組合版本
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.feeds as btfeeds

from mytrix import MyTrixSignalComposed

class NoStrategy(bt.Strategy):
    params = (('trixperiod', 15),
              ('analyzer', False),)

    def __init__(self):
        MyTrixSignalComposed(self.data, period=self.p.trixperiod)

if __name__ == '__main__':
    # Create a cerebro entity
    cerebro = bt.Cerebro()

    # Add a strategy
    cerebro.addstrategy(NoStrategy, trixperiod=15)

    # Create a Data Feed
    datapath = ('../datas/2006-day-001.txt')
    data = bt.feeds.BacktraderCSVData(dataname=datapath)

    # Add the Data Feed to Cerebro
    cerebro.adddata(data)

    # Run over everything
    cerebro.run()

    # Plot the result
    cerebro.plot()

image

相關文章