原文:
www.backtrader.com/
訂單管理和執行
原文:
www.backtrader.com/blog/posts/2015-08-08-order-creation-execution/order-creation-execution/
回測,因此 backtrader,如果不能模擬訂單,將不完整。為此,平臺提供了以下功能。
對於訂單管理有 3 個原語:
-
購買
-
賣出
-
取消
注意
update
原語顯然是一種邏輯,但常識告訴我們,這種方法主要由使用判斷性交易方法的手動操作員使用。
對於訂單執行邏輯,以下執行型別:
-
市價
-
關閉
-
限價
-
停止
-
StopLimit
訂單管理
主要目標是易於使用,因此進行訂單管理的最直接(和簡單)的方法是從策略本身開始。
buy
和self
原語具有以下簽名作為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)
因為取消訂單隻需要buy
或self
返回的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
本身有buy
和sell
原語,但對於預設引數要求更嚴格。
訂單執行邏輯
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
價格
注意
這個訂單總是執行,忽略任何用於建立它的price
和valid
引數
關閉
執行:
當下一個價格條實際上關閉時,使用下一個價格條的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 which
Buy`
- 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。
最終問題似乎歸結為:
- 終端使用者是否可以輕鬆擴充套件現有機制,以新增額外資訊,以行的形式傳遞到其他現有價格資訊點,如
open
,high
等?
就我理解的問題,答案是:是的
發帖人似乎有這些要求(來自問題#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 資料的檔名或類似檔案的物件)
其他一些引數如name
,compression
和timeframe
僅供參考,除非您計劃執行重新取樣。
當然更重要的是,新定義引數的含義:
-
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
已經擁有了所有需要的資訊,並且已經將大部分資訊填入了被通知到strategy
的order
中,而broker
需要做出的唯一決定是是否將額外的資訊位放入訂單中,或者它可以自己計算operations
。
由於策略已經獲得了orders
,並且將operations
保留在列表中似乎很自然,broker
只是在訂單部分/完全關閉頭寸時新增實際的盈虧,將計算責任留給了strategy
。
這進一步簡化了Operations Observer
的實際角色,即觀察新關閉的Operation
並記錄下來。這是它一直應該有的角色。
下面的程式碼已經被重新設計,不再計算盈虧數字,而只是注意到notify_operation
中通知的數字。
現在圖表反映出了真實的盈虧數字(cash
和value
已經是真實的)
舊的期貨記錄:
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
操作之間的差異,不僅在cash
和value
的變化中。
期貨佣金
股票佣金
程式碼
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
將被理解為以百分比表示,並應用於buy
或sell
操作的price * size
元件 -
如果設定了
margin
,則commission
將被理解為與buy
或sell
操作的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 個核心。
程式碼如下。只需將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()
組合與繼承的選擇是一個經典問題。這個例子並不是為了澄清哪種更好,而更多是為了展示:
注意
即使存在lines和params的元定義,它們也繼承自基類的元定義
最後是程式碼和圖表,展示兩個版本的執行情況。
- 第一個展示了繼承版本
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()
- 第一個展示了組合版本
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()