在第一篇(上)的基礎上,本篇主要講講如何在Backtrader中進行回測、選股、優化及可視化,並給出例子中的源碼。
目錄10.使用Backtrader回測11.使用Backtrader優化策略12.使用Backtrader選股13.在Backtrader中編寫技術指標14.在Backtrader中繪圖15.使用另類數據16.其他補充的17.源碼下載10.使用Backtrader回測選擇量化研究中的Hello World策略進行介紹,即經典但不實用的雙均線策略。
雙均線策略雙均線策略,顧名思義,就是兩根均線:短期均線和長期均線。當短線均線上穿長期均線(金叉)時買入,當短期均線下穿長期均線(死叉)時賣出,這就是雙均線策略的核心思想。
在深入研究該策略前,首先專門寫一個strategies.py文件容納自己的策略。目的是將策略與主腳本分開,保證代碼結構清晰。基本上,所有與Cerebro引擎有關的腳本在整個教程中只有微小的變化,大部分變化發生在與策略相關的strategies.py文件中。
import backtrader as bt
class PrintClose(bt.Strategy):
def __init__(self):
##引用data[0]中的收盤價格數據
self.dataclose = self.datas[0].close
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt)) #Print date and close
def next(self):
#將收盤價保留兩位小數再輸出
self.log('Close: %.2f' % self.dataclose[0])
在進行測試時,需將數據分為「樣本內數據」或「樣本外數據」。程序可以針對樣本內數據進行回測,策略優化(參數調整),最終在樣本外數據上分析採用優化後的參數的策略的有效性。
對樣本內外的數據,程序設置了不同的起始日期。日期的設置採用了DateTime模塊。更新後的主腳本btmain.py如下所示:
import datetime
import backtrader as bt
from strategies import *
cerebro = bt.Cerebro()
#給原始數據設置起止時間參數,並添加給Cerebro引擎
data = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2016, 1, 1),
todate=datetime.datetime(2017, 12, 25))
#樣本外數據的參數設置如下
#fromdate=datetime.datetime(2018, 1, 1),
#todate=datetime.datetime(2019, 12, 25))
cerebro.adddata(data)
#給Cerebro引擎添加策略
cerebro.addstrategy(MAcrossover)
#默認頭寸大小
cerebro.addsizer(bt.sizers.SizerFix, stake=3)
if __name__ == '__main__':
#運行Cerebro引擎
start_portfolio_value = cerebro.broker.getvalue()
cerebro.run()
end_portfolio_value = cerebro.broker.getvalue()
pnl = end_portfolio_value - start_portfolio_value
print('Starting Portfolio Value: %.2f' % start_portfolio_value)
print('Final Portfolio Value: %.2f' % end_portfolio_value)
print('PnL: %.2f' % pnl)
從Strategies.py文件中import *,這便於調用該文件中的所有類。addsizer設置了默認頭寸大小為3股。
Cerebro.broker.getvalue()命令可獲取投資組合的當前金額。在運行Cerebro之前調用該函數獲取初始本金,在策略運行完畢後獲得投資組合的最終金額。終值扣除起始值即可得到損益。
雙均線策略的實現接下來定義Strategy的子類MACrossover類,代表雙均線策略。
class MAcrossover(bt.Strategy):
#移動平均參數
params = (('pfast',20),('pslow',50),)
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt)) # 執行策略優化時 可注釋掉此行
def __init__(self):
self.dataclose = self.datas[0].close
# Order變量包含持倉數據與狀態
self.order = None
# 初始化移動平均數據
self.slow_sma = bt.indicators.MovingAverageSimple(self.datas[0],
period=self.params.pslow)
self.fast_sma = bt.indicators.MovingAverageSimple(self.datas[0],
period=self.params.pfast)
這裡將快慢周期設置為參數pfast和pslow而不是硬編碼(固定值),以便後續對策略參數的優化。
_ init__()函數下有一些新的變量,self.order變量存儲正在執行的訂單詳細信息和訂單狀態,以便確定當前是否存在交易或是否有待處理的訂單。
基於Backtrader內置的MovingAverageSimple命令計算了兩個周期的簡單移動平均價格。
需要指出的是,在Backtrader中,為了避免look-ahead偏差,當程序發出買入或賣出信號引導程序創建訂單時,無論價格如何,該訂單要到下一個k線被調用時才會執行。同時,在回測過程中,只有在指標值計算完畢後,才會開始尋找訂單。
兩個移動平均值中較大的時間周期採用最近50個收盤價的平均值。這意味著前50個數據點的移動平均值為NaN。在擁有有效的移動平均數據之前,Backtrader不會嘗試創建訂單。
訂單相關與交易相關的所有內容都發生在notify_order()函數中。
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
#主動買賣的訂單提交或接受時 - 不觸發
return
#驗證訂單是否完成
#注意: 當現金不足時,券商可以拒絕訂單
if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
#重置訂單
self.order = None
上述代碼記錄了執行訂單的時間和價格。若訂單未成交,此部分還將提供通知。
交易邏輯Strategy類中的next()函數包含所有的交易邏輯。
這裡,首先檢查目前是否有持倉,有則不開倉。如果沒有持倉,則可以在市場中尋找開倉信號,檢查對於上一根K線SMA20移動平均線是否在SMA50移動平均線以下,但對於當前K線,SMA20移動平均線位於SMA50移動平均線以上,如果是則說明快線突破了慢線(金叉)。反之則快線跌破了慢線(死叉)。在獲得開倉信號前,程序會不斷確認是否存在開倉信號。如果持倉,則擇機平倉。這裡採用的退出策略為持倉5日。
def next(self):
# 檢測是否有未完成訂單
if self.order:
return
#驗證是否有持倉
if not self.position:
#如果沒有持倉,尋找開倉信號
#SMA快線突破SMA慢線
if self.fast_sma[0] > self.slow_sma[0] and self.fast_sma[-1] < self.slow_sma[-1]:
self.log('BUY CREATE, %.2f' % self.dataclose[0])
#繼續追蹤已經創建的訂單,避免重複開倉
self.order = self.buy()
#如果SMA快線跌破SMA慢線
elif self.fast_sma[0] < self.slow_sma[0] and self.fast_sma[-1] > self.slow_sma[-1]:
self.log('SELL CREATE, %.2f' % self.dataclose[0])
#繼續追蹤已經創建的訂單,避免重複開倉
self.order = self.sell()
else:
# 如果已有持倉,尋找平倉信號
if len(self) >= (self.bar_executed + 5):
self.log('CLOSE CREATE, %.2f' % self.dataclose[0])
self.order = self.close()
代碼運行過程中會輸出所有交易,並列印最終盈虧數據。在這種情況下,該策略獲得了79美元的利潤。
測試策略時要記住的一件事是,回測結束時有可能還存在持倉。檢查是否有未平倉交易的一種方法是確保列印投資組合值之前的倒數第二行列印了「CLOSE CREATE」。否則,未平倉交易可能會扭曲盈虧表現。
如何使用內置的交叉提示工具在雙均線策略中,next()函數實現了對均線交叉的判斷。Backtrader自帶的CrossOver()函數可以簡化這一過程,使用該功能時需在_ init_()函數中將其初始化,如下所示:
self.crossover = bt.indicators.CrossOver(self.slow_sma,self.fast_sma)
然後,程序會自動確認是否有開平倉信號發出:
if self.crossover > 0: # Fast ma crosses above slow ma
pass # 開倉信號
elif self.crossover < 0: # Fast ma crosses below slow ma
pass # 平倉信號
嘗試通過優化策略的參數(快慢線周期)來改善策略的某項指標(夏普比率)。
由於策略優化涉及大量的參數組合,並且保留所有交易記錄對策略優化意義不大,因此在log()函數中注釋掉列印語句。這裡以夏普比率來判斷策略的優劣,修改後的主程序如下:
import datetime
import backtrader as bt
from strategies import *
cerebro = bt.Cerebro(optreturn=False)
#設置數據的參數
data = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2016, 1, 1),
todate=datetime.datetime(2017, 12, 25))
#樣本外數據的設置
#fromdate=datetime.datetime(2018, 1, 1),
#todate=datetime.datetime(2019, 12, 25))
cerebro.adddata(data)
#向Cerebro引擎添加數據
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio')
cerebro.optstrategy(MAcrossover, pfast=range(5, 20), pslow=range(50, 100))
#設置頭寸參數
cerebro.addsizer(bt.sizers.SizerFix, stake=3)
if __name__ == '__main__':
optimized_runs = cerebro.run()
final_results_list = []
for run in optimized_runs:
for strategy in run:
PnL = round(strategy.broker.get_value() - 10000,2)
sharpe = strategy.analyzers.sharpe_ratio.get_analysis()
final_results_list.append([strategy.params.pfast,
strategy.params.pslow, PnL, sharpe['sharperatio']])
sort_by_sharpe = sorted(final_results_list, key=lambda x: x[3],
reverse=True)
for line in sort_by_sharpe[:5]:
print(line)
代碼的主要變化如下:
當初始化Cerebro引擎時,將optreturn參數設置為False,即只要求輸出策略的參數以及analyzer對策略運行結果的統計,來提高運行速度。
添加了一個analyzer類的對象分析不同參數組合策略的夏普比率。
移除了cerebro.addstrategy(),取而代之的是cerebro.optstrategy(),表明要對該策略進行優化,並限制了待優化參數的取值範圍。
最終,優化結果存儲在多個列表構成的列表對象optimized_runs中。遍歷該列表並將快慢線的周期數據及相應夏普比率匯總並排序,得到最終結果為
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2019, 12, 25))
對於樣本外數據,採用優化前和優化後的策略參數的盈虧分別為虧損63.42美元和170.22美元,這一結果並不令人意外,這是因為:
12.使用Backtrader選股Backtrader可在給定的時期,按用戶提出的準則選股。
這裡將依據布林帶準則選股,即選出交易價格比前20日均價低於兩個標準差的股票。
Analyzer類選股從創建Backtrader的子類Analyzer類開始,該類是選股的關鍵工具。
class Screener_SMA(bt.Analyzer):
params = (('period',20), ('devfactor',2),)
def start(self):
self.bband = {data: bt.indicators.BollingerBands(data,
period=self.params.period, devfactor=self.params.devfactor)
for data in self.datas}
def stop(self):
self.rets['over'] = list()
self.rets['under'] = list()
for data, band in self.bband.items():
node = data._name, data.close[0], round(band.lines.bot[0], 2)
if data > band.lines.bot:
self.rets['over'].append(node)
else:
self.rets['under'].append(node)
start()函數中針對多個數據Feed計算了布林帶參數,包括對應的上中下軌值。stop()函數實現了選股邏輯:遍歷所有數據Feed,根據價格與布林帶下軌的關係將股票分類。
所有的Analyzer類具有一個內置字典rets,這裡使用rets的key over和under分別存儲交易價格在布林帶下軌上方和下方的標的。
主函數中instruments列表包含了待篩選的股票池,通過循環將相應股票的CSV數據文件添加到Cerebro引擎。
import datetime
import backtrader as bt
from strategies import *
# 初始化Cerebro引擎
cerebro = bt.Cerebro()
# 建立股票池並將所有數據添加至Cerebro引擎
instruments = ['TSLA', 'AAPL', 'GE', 'GRPN']
for ticker in instruments:
data = bt.feeds.YahooFinanceCSVData(
dataname='{}.csv'.format(ticker),
fromdate=datetime.datetime(2016, 1, 1),
todate=datetime.datetime(2017, 10, 30))
cerebro.adddata(data)
# 添加基於布林帶的選股器
cerebro.addanalyzer(Screener_SMA)
if __name__ == '__main__':
# 運行Cerebro引擎
cerebro.run(runonce=False, stdstats=False, writer=True)
接下來將新創建的screener類添加到Cerebro引擎作為分析器(Analyzer),並附加一些參數來調用Cerebro.run()命令。其中 writer = True參數調用內置的輸出顯示功能。stdstats = False會刪除一些標準輸出(見後繪圖部分)。最後,runonce = False確保了數據的同步性。最終列印結果如下:
布林帶策略的選股結果
13.在Backtrader中編寫技術指標有三種方法可以在Backtrader中編寫指標。
自行編寫技術指標;
使用內置技術指標;
使用第三方庫。
下面是自定義指標的一個例子,可理解成簡化版的ATR(偷個懶)。在Backtrader中,通過使用負索引遍歷最後14個數據點,取每個周期的高點並減去低點,然後將其平均,從而計算了給定時期內,股價的平均日內波動幅度。
class MyIndicator(bt.Strategy):
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt)) #列印收盤價格和日期
def __init__(self):
#引用data[0]中的收盤價格數據
self.dataclose = self.datas[0].close
self.datahigh = self.datas[0].high
self.datalow = self.datas[0].low
def next(self):
day_range_total = 0
for i in range(-13, 1):
day_range = self.datahigh[i] - self.datalow[i]
day_range_total += day_range
M_Indicator = day_range_total / 14
self.log('Close: %.2f, M_Indicator: %.4f' % (self.dataclose[0], M_Indicator))
下圖是程序運行時輸出的技術指標值
儘管回測是一種基於數學計算的自動化過程,但人們往往希望通過可視化來了解到底發生了什麼。有時人們可能對回測過程中的具體算法或者技術指標到底傳達了什麼信息感興趣。
通過可視化繪製數據表,指標,操作,現金和投資組合價值的變化情況可以幫助人們:
更好地了解正在發生的事情;
否定/修改/創建想法;
以及人們在看到可視化結果後可能會做出的任何其他決策。
對單只股票數據進行可視化在Backtrader中繪圖非常簡單,只需要在cerebro.run()之後接上一句cerebro.plot()。
這是一個圖表示例,其中包含在示例中一直使用的TSLA數據。
CashValue Observer
在回測運行期間跟蹤現金和投資組合總價值(包括現金)的變化情況。
Observer Observer
在交易結束時顯示實際的損益,交易被定義為開倉及平倉這一對完整動作。
BuySell Observer
在價格上方繪製買賣操作點
這3個觀察者由cerebro自動添加,並由stdstats參數控制(默認值:True)。 如果需要,可通過以下命令禁用它們:
cerebro = bt.Cerebro(stdstats=False)
或者
cerebro = bt.Cerebro()
.
cerebro.run(stdstats=False)
Backtrader可以同時將多個股票輕鬆地顯示在一張圖表上。如果需要可視化兩個資產之間的相關性,這將很有用。
import datetime
import backtrader as bt
#初始化Cerebro引擎
cerebro = bt.Cerebro(stdstats=False)
#設置數據參數並添加至Cerebro引擎
data1 = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 1, 1))
cerebro.adddata(data1)
data2 = bt.feeds.YahooFinanceCSVData(
dataname='AAPL.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 1, 1))
data2.compensate(data1)
data2.plotinfo.plotmaster = data1
data2.plotinfo.sameaxis = True
cerebro.adddata(data2)
#運行Cerebro引擎
cerebro.run()
cerebro.plot()
運行結果如下圖所示:
下面的代碼給出如何給TSLA添加均線圖。
import datetime
import backtrader as bt
#簡單移動平均
class SimpleMA(bt.Strategy):
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(self.data, period=20, plotname="20 SMA")
# 初始化Cerebro引擎, 禁用數據監測
cerebro = bt.Cerebro(stdstats=False)
# 設置日期參數並添加至Cerebro引擎
data1 = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 10, 5))
cerebro.adddata(data1)
# 在圖標上添加簡單移動均線
cerebro.addstrategy(SimpleMA)
# 運行Cerebro引擎
cerebro.run()
cerebro.plot()
通過plotname可以指定技術指標對應的圖例名,運行結果如下圖所示:
這裡嘗試根據Google搜索數據來評估情緒,並根據搜索量的任何明顯變化進行交易。
首先從Google趨勢下載比特幣每周歷史搜索數據並從Yahoo Finance獲得價格數據。
由於2017年末波動很大,因此從2018年開始測試該策略。此後,搜索結果數據和價格均穩定了很長時間。Google趨勢數據與Yahoo Finance數據採用的OHLC格式不同。因此,使用Backtrader提供的通用CSV模板添加數據。代碼如下:
data2 = bt.feeds.GenericCSVData(
dataname='BTC_Gtrends.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 1, 1),
nullvalue=0.0,
dtformat=('%Y-%m-%d'),
datetime=0,
time=-1,
high=-1,
low=-1,
open=-1,
close=1,
volume=-1,
openinterest=-1,
timeframe=bt.TimeFrame.Weeks)
cerebro.adddata(data2)
對於非OHLC數據,必須定義哪些列存在,哪些不存在。對數據中不存在的列分配值-1,並為可用的列分配遞增的整數值。除此之外,其他代碼和之前沒有很大的變化。這是現在的Strategy類:
class BtcSentiment(bt.Strategy):
params = (('period', 10), ('devfactor', 1),)
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
self.btc_price = self.datas[0].close
self.google_sentiment = self.datas[1].close
self.bbands = bt.indicators.BollingerBands(self.google_sentiment, period=self.params.period, devfactor=self.params.devfactor)
self.order = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
self.order = None
def next(self):
# 檢查是否有正在執行的訂單
if self.order:
return
# 看多信號
if self.google_sentiment > self.bbands.lines.top[0]:
# 檢查是否有持倉
if not self.position:
self.log('Google Sentiment Value: %.2f' % self.google_sentiment[0])
self.log('Top band: %.2f' % self.bbands.lines.top[0])
# 沒有持倉則開倉多頭
self.log('***BUY CREATE, %.2f' % self.btc_price[0])
# 追蹤訂單避免重複開倉
self.order = self.buy()
# 看空信號
elif self.google_sentiment < self.bbands.lines.bot[0]:
# 檢查是否有持倉
if not self.position:
self.log('Google Sentiment Value: %.2f' % self.google_sentiment[0])
self.log('Bottom band: %.2f' % self.bbands.lines.bot[0])
# 沒有持倉則開倉空頭
self.log('***SELL CREATE, %.2f' % self.btc_price[0])
# 追蹤訂單避免重複開倉
self.order = self.sell()
# 中性信號,對既有倉位平倉
else:
if self.position:
# 如果有倉位,則平倉
self.log('Google Sentiment Value: %.2f' % self.google_sentiment[0])
self.log('Bottom band: %.2f' % self.bbands.lines.bot[0])
self.log('Top band: %.2f' % self.bbands.lines.top[0])
self.log('CLOSE CREATE, %.2f' % self.btc_price[0])
self.order = self.close()
這裡再次使用布林帶策略,當搜索數量超過10日布林帶上軌時開倉多頭,少於10日布林帶下軌時開倉空頭。當搜索量介於上下軌之間時,若存在倉位則平倉,否則不採取任何行動。
運行回測後的結果如下:
Backtrader包含了許多功能,能夠滿足一般用戶的絕大多數需求。
Backtrader有潛質成為商業解決方案,十分感激原作者將其保持開源。
在閱讀完這兩篇連載後,相信大家可在Backtrader中進行策略初探了。但在回測時,還需要注意一下幾點:
依據不同類型的風險管理,實際的回測結果可能會有很大不同。量化的目標是在可接受的風險水平下優化策略獲取最大的收益,而不是嘗試以承擔巨大風險為代價來最大化利潤。
最後,策略開發的重點應該是找到一個良好的基礎策略,然後採用優化進行微小的調整。有時量化研究者陷入了完全相反的怪圈,即選擇一個不太好的策略,試圖通過數值優化使結果變得好看,這是難以盈利的。
17.源碼下載請發送 回測神器backtrader 到公眾號獲取本教程涉及源碼。
此公眾號的建設與維護仍處於剛剛起步,因此排版和布局暫時都比較亂
最近仍在構思中,請各位看官見諒
微信公眾號:Trading4Freedom
關注可了解更多的量化交易知識與工具
喜歡大屏閱讀的請關注知乎:
https://www.zhihu.com/people/yu-tian-37-37/
[如果你覺得本文對你有幫助,歡迎點擊在看與轉發]