当前位置:首页 > 综合 > 正文

Backtrader量化平台教程

我们知道,有时候我们要测试的可不仅仅是一个标的,但是之前backtrader的教程中,我们都是针对的是一个标的的回测。如果接触过优矿、ricequant这样的平台的同学,可能觉得backtrader不适合做这样的portfolio层面的回测。确实,似乎backtrader整个官方教程里面,没有任何讲到这种全市场、组合的回测demo,但是backtrader其实也是可以胜任这样的任务的。

前段时间,笔者就做了这样的一个事情,让backtrader能够完成我们想要的组合层面的回测。

1.最终的效果

和一般的portfolio层面的回测平台一样,我们希望,最后我们实现一个策略只要进行一些设置就可以了。笔者利用backtrader封装了一个函数,实现了几乎和优矿一样的功能。

使用的时候,笔者的函数只需要如下的设置:

[python]
  1. start_date="2017-04-01"
  2. end_date="2017-06-20"
  3. trading_csv_name='trading_data_two_year.csv'
  4. portfolio_csv_name='port_two_year.csv'
  5. benchmark_csv_name=None
然后我们就可以回测了。笔者把回测的类封装了起来,只要调用笔者的回测类就可以了。
[python]
  1. begin=datetime.datetime.now()
  2. result_dict=bt_backtest.backtrader_backtest(start_date=start_date,end_date=end_date,trading_csv_name=trading_csv_name,\
  3. portfolio_csv_name=portfolio_csv_name,bechmark_csv_name=benchmark_csv_name)
然后,回测结束后输出评价指标。
[python]
  1. end=datetime.datetime.now()
  2. print"timeelapse:",(end-begin)
  3. print'StartPortfolioValue:%.2f'%result_dict['start_cash']
  4. print'FinalPortfolioValue:%.2f'%result_dict['final_value']
  5. print'TotalReturn:',result_dict['total_return']
  6. print'SharpeRatio:',result_dict['sharpe_ratio']#*2#todothereshouldbeconsider!
  7. print'MaxDrowdown:',result_dict['max_drowdown']*2
  8. print'MaxDrowdownMoney:',result_dict['max_drowdown_money']
  9. print"TradeInformation",result_dict['trade_info']
  10. result=pd.read_csv('result.csv',index_col=0)
  11. result.plot()
  12. plt.show()
  13. position_info=pd.read_csv('position_info.csv',index_col=0)
接下来说一下我们要输入的这些csv文件吧。
[python]
  1. trading_csv_name='trading_data_two_year.csv'
  2. portfolio_csv_name='port_two_year.csv'
  3. benchmark_csv_name=None
第一个是交易行情的数据,数据格式如下:

tradingdate,ticker,_open,_high,_low,_close,_volume

20070104,000001,14.65,15.32,13.83,14.11,69207082.0

20070104,000002,15.7,16.56,15.28,15.48,75119519.0

20070104,000004,4.12,4.12,3.99,4.06,1262915.0

20070104,000005,2.51,2.53,2.46,2.47,14123749.0

20070104,000006,13.5,14.07,13.39,13.7,15026054.0

20070104,000007,2.44,2.44,2.35,2.36,3014956.0

20070104,000008,4.16,4.24,4.15,4.16,818282.0

20070104,000009,0.0,0.0,0.0,4.23,0.0

20070104,000010,0.0,0.0,0.0,5.37,0.0

20070104,000011,5.78,5.85,5.42,5.45,4292318.0

20070104,000012,11.15,11.3,10.66,10.95,6934903.0

所以,这一部分的数据会相当的大,一天就会有3000条左右的记录,毕竟,A股的股票数目就是这样。如果是用的更小级别的数据,那么数据量必然更大了。

[python]
  1. portfolio_csv_name='port_two_year.csv'

这是我们调仓日的目标仓位,只需要sigdate,secucode,weight三个字段就行。

benchmark_csv_name自然就是benchmark的daily return数据文件了。这里可以不设置。

2.回测函数

接下来就是核心的回测的函数了。

[python]
  1. #-*-coding:utf-8-*-
  2. from__future__import(absolute_import,division,print_function,
  3. unicode_literals)
  4. importdatetime
  5. importbacktraderasbt
  6. frombacktraderimportOrder
  7. importpandasaspd
  8. importmatplotlib.pyplotasplt
  9. #CreateaStratey
  10. classAlphaPortfolioStrategy(bt.Strategy):
  11. deflog(self,txt,dt=None):
  12. dt=dtorself.datas[0].datetime.date(0)
  13. print('%s,%s'%(dt.isoformat(),txt))
  14. def__init__(self,dataId_to_secId_dict,secId_to_dataId_dict,adj_df,end_date,backtesting_length=None,benchmark=None,result_csv_name='result'):
  15. #1.gettargetportfolioweight
  16. self.adj_df=adj_df
  17. self.backtesting_length=backtesting_length
  18. self.end_date=end_date
  19. #2.backtraderdata_idandsecIdtransferdiction
  20. self.dataId_to_secId_dict=dataId_to_secId_dict
  21. self.secId_to_dataId_dict=secId_to_dataId_dict
  22. self.benchmark=benchmark
  23. #3.storetheuntradabledayduetotheupanddownfloor,emptyitinanewadjustmentday
  24. self.order_line_dict={}
  25. self.pre_position_data_id=list()
  26. self.value_for_plot={}
  27. #4.keeptrackofpendingordersandbuyprice/commission
  28. self.order=None
  29. self.buyprice=None
  30. self.buycomm=None
  31. self.value=10000000.0
  32. self.cash=10000000.0
  33. self.positions_info=open('position_info.csv','wb')
  34. self.positions_info.write('date,bt_id,sec_code,size,lastprice\n')
  35. defstart(self):
  36. print("theworldcallme!")
  37. defnotify_fund(self,cash,value,fundvalue,shares):
  38. #updatethemarketvalueeveryday
  39. #print("actualvalue:",value)
  40. self.value=value-10000000.0#wegivethebrokermore10millionforthepurposeofilliquidity
  41. #self.value=value#wegivethebrokermore10millionforthepurposeofilliquidity
  42. self.value_for_plot[self.datetime.datetime()]=self.value/10000000.0
  43. self.cash=cash
  44. #print("cash:",cash)
  45. defnotify_order(self,order):
  46. iforder.statusin[order.Submitted,order.Accepted]:
  47. #Buy/Sellordersubmitted/acceptedto/bybroker-Nothingtodo
  48. return
  49. #Checkifanorderhasbeencompleted
  50. #Attention:brokercouldrejectorderifnotenougthcash
  51. iforder.statusin[order.Completed]:
  52. iforder.isbuy():
  53. #order.
  54. self.log(
  55. 'BUYEXECUTED,Price:%.2f,Cost:%.2f,Comm%.2f'%
  56. (order.executed.price,
  57. order.executed.value,
  58. order.executed.comm))
  59. self.buyprice=order.executed.price
  60. self.buycomm=order.executed.comm
  61. else:#Sell
  62. self.log('SELLEXECUTED,Price:%.2f,Cost:%.2f,Comm%.2f'%
  63. (order.executed.price,
  64. order.executed.value,
  65. order.executed.comm))
  66. self.bar_executed=len(self)
  67. eliforder.statusin[order.Canceled,order.Rejected]:
  68. self.log('OrderCanceled/Rejected')
  69. eliforder.status==order.Margin:
  70. self.log('OrderMargin')
  71. self.order=None
  72. defnotify_trade(self,trade):
  73. ifnottrade.isclosed:
  74. return
  75. self.log('OPERATIONPROFIT,GROSS%.2f,NET%.2f'%
  76. (trade.pnl,trade.pnlcomm))
  77. defnext(self):
  78. #0.Checkifanorderispending...ifyes,wecannotsenda2ndone
  79. #ifself.order:
  80. #return
  81. bar_time=self.datetime.datetime()
  82. bar_time_str=bar_time.strftime('%Y-%m-%d')
  83. trading_date=bar_time+datetime.timedelta(days=1)
  84. #bar_time_str=(self.datetime.datetime()+datetime.timedelta(1)).strftime('%Y-%m-%d')
  85. print(bar_time_str,self.value)
  86. print("barday===:",bar_time_str,"===============")
  87. for(k,v)inself.dataId_to_secId_dict.items():
  88. size=self.positions[self.datas[k]].size
  89. price=self.datas[k].close[-1]
  90. self.positions_info.write('%s,%s,%s,%s,%s'%(bar_time_str,k,v,size,price))
  91. self.positions_info.write('\n')
  92. #1.nomattertheadjustmentday,up/downfloorblockedordershoulddealwitheachbar
  93. for(k,v)inself.order_line_dict.items():
  94. bar=self.datas[k]
  95. buyable=Falseifbar.low[1]/bar.close[0]>1.0950elseTrue
  96. sellable=Falseifbar.high[1]/bar.close[0]<0.910elseTrue
  97. #buyable=Falseif(bar.open[1]/bar.close[0]>1.0950)and(bar.close[1]/bar.close[0]>1.0950)elseTrue
  98. #sellable=Falseif(bar.open[1]/bar.close[0]<0.910and(bar.close[1]/bar.close[0]<0.91))elseTrue
  99. ifv>0:
  100. ifbuyable:
  101. delself.order_line_dict[k]
  102. self.log('%sBUYCREATE,%.2f,vlois%s'%(self.dataId_to_secId_dict[k],bar.open[1],v))
  103. self.order=self.buy(data=bar,size=v,exectype=Order.Market)
  104. else:
  105. print("############:")
  106. print("unbuyable:",self.dataId_to_secId_dict[k])
  107. elifv<0:
  108. ifsellable:
  109. delself.order_line_dict[k]
  110. self.log('%sSELLCREATE,%.2f,volis%s'%(self.dataId_to_secId_dict[k],bar.open[1],v))
  111. self.order=self.sell(data=bar,size=abs(v),exectype=Order.Market)
  112. else:
  113. print("############:")
  114. print("unsellable:",self.dataId_to_secId_dict[k])
  115. #2.ensuretheadjustmentday
  116. #2.1getthecurrentbartime
  117. adj_sig=self.adj_df[self.adj_df['sigdate']==trading_date.strftime('%Y-%m-%d')][['secucode','hl_weight']]
  118. #2.2checktheadjustmentday
  119. iflen(adj_sig)==0orbar_time_str==self.end_date:
  120. return
  121. #3.adjusttheportfolio
  122. #3.1settwodictstostorethebuyorderandsellorderspearately
  123. buy_dict={}
  124. sell_dict={}
  125. self.order_line_dict={}
  126. current_position_data_id=list()
  127. #3.2iteratetheportfolioinstrumentsanddivideintobuygroupandsellgroup
  128. forindexinadj_sig.index:
  129. #getcurrentinstrumentcodeandtransfertothedata_id
  130. sec_id=adj_sig.loc[index]['secucode']
  131. data_id=self.secId_to_dataId_dict[sec_id]
  132. ifself.backtesting_lengthanddata_id>=self.backtesting_length:
  133. continue
  134. bar=self.datas[data_id]
  135. current_position_data_id.append(data_id)
  136. #getthetargetweightvalue
  137. target_weight=adj_sig.loc[index]['hl_weight']
  138. #calculatethecurrentweightvalue
  139. current_position=self.positions[self.datas[data_id]]
  140. current_mv=current_position.size*bar.close[0]
  141. current_weight=current_mv/float(self.value)
  142. diff_weight=(target_weight-current_weight)
  143. ifbar.open[1]==0:
  144. continue
  145. diff_volume=int(diff_weight*self.value/bar.open[1]/100)*100
  146. print("theweightdifference",diff_weight)
  147. ifdiff_volume>0:
  148. buy_dict[data_id]=diff_volume
  149. elifdiff_volume<0:
  150. sell_dict[data_id]=diff_volume
  151. #3.3makeorderwork
  152. for(k,v)insell_dict.items():
  153. bar=self.datas[k]
  154. #sellable=Falseif(bar.high[1]==bar.low[1])and(bar.open[1]/bar.close[0]<0.920)elseTrue
  155. sellable=Falseif(bar.open[1]/bar.close[0]<0.920)elseTrue
  156. #sellable=Falseif(bar.open[1]/bar.close[0]<0.910and(bar.close[1]/bar.close[0]<0.91))elseTrue
  157. ifsellable:
  158. self.log('%sSELLCREATE,%.2f,volis%s'%(self.dataId_to_secId_dict[k],bar.open[1],v))
  159. self.order=self.sell(data=bar,size=abs(v),exectype=Order.Market)
  160. else:
  161. print("############:")
  162. print("unsellable:",self.dataId_to_secId_dict[k])
  163. self.order_line_dict[k]=v
  164. for(k,v)inbuy_dict.items():
  165. bar=self.datas[k]
  166. #buyable=Falseif(bar.low[1]==bar.high[1])and(bar.open[1]/bar.close[0])>1.0950elseTrue
  167. buyable=Falseif(bar.open[1]/bar.close[0])>1.0950elseTrue
  168. #buyable=Falseif(bar.open[1]/bar.close[0]>1.0950)and(bar.close[1]/bar.close[0]>1.0950)elseTrue
  169. ifbuyable:
  170. self.log('%sBUYCREATE,%.2f,vlois%s'%(self.dataId_to_secId_dict[k],bar.open[1],v))
  171. self.order=self.buy(data=bar,size=v,exectype=Order.Market)
  172. else:
  173. print("############:")
  174. print("unbuyable:",self.dataId_to_secId_dict[k])
  175. self.order_line_dict[k]=v
  176. #3.4closeposition,whenthedata_idisnotincurrentportfolioandinlastportfolio,weclosetheposition
  177. ifself.pre_position_data_id:
  178. clost_data_id=[difordiinself.pre_position_data_idifdinotincurrent_position_data_id]
  179. fordiinclost_data_id:
  180. bar=self.datas[di]
  181. print('CLOSEPOSITION:',self.dataId_to_secId_dict[di])
  182. self.order=self.close(data=bar)
  183. #3.5updatethepositiondataidfornextchecking
  184. self.pre_position_data_id=current_position_data_id
  185. defstop(self):
  186. #plotthenetvalueandthebenchmarkcurves
  187. plot_df=pd.concat([pd.Series(self.value_for_plot,).to_frame(),self.benchmark],axis=1,join='inner')
  188. plot_df.to_csv('result.csv')
  189. self.positions_info.close()
  190. print("death")
  191. defbacktrader_backtest(start_date,end_date,trading_csv_name,portfolio_csv_name,bechmark_csv_name):
  192. #1.backtestparameterssetting:starttime,endtime,theassetsnumber(backtest_length)andthebenckmarkdataandnewacerebro
  193. start_date,end_date=datetime.datetime.strptime(start_date,"%Y-%m-%d"),datetime.datetime.strptime(end_date,"%Y-%m-%d")
  194. backtest_length=None
  195. #benchmarkseries
  196. ifbechmark_csv_name:
  197. benchmark=pd.read_csv(bechmark_csv_name,date_parser=True,dtype={'date':str})
  198. benchmark['date']=benchmark['date'].apply(lambdax:datetime.datetime.strptime(x,"%Y-%m-%d"))
  199. benchmark=benchmark.set_index('date')
  200. else:
  201. benchmark=None
  202. #result_df=pd.DataFrame()
  203. cerebro=bt.Cerebro()
  204. #2.getrequiredtradingdata
  205. #2.1gettradingdata(totaltradingdata)
  206. trading_data_df=pd.read_csv(trading_csv_name,dtype={'sigdate':str,'secucode':str})
  207. trading_data_df.rename(columns={'sigdate':'tradingdate','secucode':'ticker'},inplace=True)
  208. transer=lambdax:datetime.datetime.strptime(x,"%Y-%m-%d")
  209. trading_data_df['tradingdate']=trading_data_df['tradingdate'].apply(transer)
  210. trading_data_df=trading_data_df[(trading_data_df['tradingdate']>start_date)&(trading_data_df['tradingdate']
  211. trading_data_df['openinterest']=0
  212. trading_data_df=trading_data_df.set_index('tradingdate')
  213. #2.2gettargetportfoliodataforthetargetassetsfilter
  214. adj_df=pd.read_csv(portfolio_csv_name,dtype={'secucode':str})
  215. adj_df=adj_df[['sigdate','secucode','hl_weight']]
  216. parser1=lambdax:datetime.datetime.strptime(x,"%Y/%m/%d")
  217. parser2=lambdax:x.strftime("%Y-%m-%d")
  218. adj_df['sigdate']=adj_df['sigdate'].apply(parser1)
  219. adj_df=adj_df[(adj_df['sigdate']>start_date)&(adj_df['sigdate']
  220. adj_df['sigdate']=adj_df['sigdate'].apply(parser2)
  221. #2.3generatetwodictiontotransbetweensecIdandbacktraderid
  222. sec_id_list=adj_df['secucode'].drop_duplicates().tolist()
  223. data_id_list=[iforiinrange(len(sec_id_list))]
  224. dataId_to_secId_dict=dict(zip(data_id_list,sec_id_list))
  225. secId_to_dataId_dict=dict(zip(sec_id_list,data_id_list))
  226. print("totalstocks'number",len(sec_id_list),'.','datafeeding......')
  227. #2.4feedrequireddatafeedandaddthemtothecerebro
  228. forindex,sec_idinenumerate(sec_id_list[0:backtest_length]):
  229. sec_df=trading_data_df[trading_data_df['ticker']==sec_id]
  230. #sec_df=sec_df.set_index('tradingdate')
  231. sec_raw_start=sec_df.index[0]
  232. ifsec_raw_start!=start_date.strftime("%Y-%m-%d"):
  233. na_fill_value=sec_df.head(1)['open'].values[0]
  234. df_temp=pd.DataFrame(index=pd.date_range(start=start_date,end=sec_raw_start,freq='D')[:-1],
  235. columns=['open','high','low','close','volume','openinterest']
  236. ).fillna(na_fill_value)
  237. frames=[df_temp,sec_df]
  238. sec_df=pd.concat(frames)
  239. data_feed=bt.feeds.PandasData(dataname=sec_df,
  240. fromdate=start_date,
  241. todate=end_date
  242. )
  243. cerebro.adddata(data_feed,name=sec_id)
  244. print('datafeedfinish!')
  245. #3.cereberoconfig
  246. cerebro.addstrategy(AlphaPortfolioStrategy,
  247. dataId_to_secId_dict,secId_to_dataId_dict,adj_df,end_date.strftime("%Y-%m-%d"),backtest_length,benchmark)
  248. cerebro.broker.setcash(20000000.0)
  249. #cerebro.broker.setcash(10000000.0)
  250. cerebro.broker.setcommission(commission=0.0008)
  251. cerebro.broker.set_slippage_fixed(0.02)
  252. cerebro.addanalyzer(bt.analyzers.Returns,_)
  253. cerebro.addanalyzer(bt.analyzers.SharpeRatio,_name='SharpeRatio',riskfreerate=0.00,stddev_sample=True,annualize=True)
  254. cerebro.addanalyzer(bt.analyzers.AnnualReturn,_name='AnnualReturn')
  255. cerebro.addanalyzer(bt.analyzers.DrawDown,_name='DW')
  256. cerebro.addanalyzer(bt.analyzers.TradeAnalyzer,_name='TradeAnalyzer')
  257. ###informationprint
  258. start_cash=(cerebro.broker.getvalue()-10000000.0)
  259. #start_cash=(cerebro.broker.getvalue())
  260. #print('StartingPortfolioValue:%.2f'%(cerebro.broker.getvalue()-10000000.0))
  261. print("startcerebro.run()")
  262. results=cerebro.run()#runthecerebro
  263. #4.showtheresult
  264. strat=results[0]
  265. final_value=(cerebro.broker.getvalue()-10000000.0)
  266. total_return=((cerebro.broker.getvalue()-10000000.0)/10000000.0-1)
  267. #final_value=(cerebro.broker.getvalue())
  268. #total_return=((cerebro.broker.getvalue())/10000000.0-1)
  269. sharpe_ratio=strat.analyzers.SharpeRatio.get_analysis()['sharperatio']
  270. max_drowdown=strat.analyzers.DW.get_analysis()['max']['drawdown']
  271. max_drowdown_money=strat.analyzers.DW.get_analysis()['max']['moneydown']
  272. trade_info=strat.analyzers.TradeAnalyzer.get_analysis()
  273. return_dict={'start_cash':start_cash,'final_value':final_value,'total_return':total_return,\
  274. 'sharpe_ratio':sharpe_ratio,'max_drowdown':max_drowdown,'max_drowdown_money':max_drowdown_money,\
  275. 'trade_info':trade_info}
  276. returnreturn_dict
基本上,代码的注释很全面了,基本实现了涨跌停不能买入,进入排队等待,当日开盘价买入,滑点设置等等这些功能。Backtrader有一点,可能是为了加快速度,特别不方便,就是datafeed不能按照名字来实现查找,而是用index来寻找,所以我们需要建立一个全局的dict,能够实现data id到股票代码以及反过来的一一对应的dict。别的,其实实现起来还是比较方便的,但是性能有待提高,等后续需要继续启动这个项目的时候,笔者继续努力吧。譬如如何修改一个查找的逻辑和数据类型的使用。

你可能想看:

有话要说...

取消
扫码支持 支付码