我们知道,有时候我们要测试的可不仅仅是一个标的,但是之前backtrader的教程中,我们都是针对的是一个标的的回测。如果接触过优矿、ricequant这样的平台的同学,可能觉得backtrader不适合做这样的portfolio层面的回测。确实,似乎backtrader整个官方教程里面,没有任何讲到这种全市场、组合的回测demo,但是backtrader其实也是可以胜任这样的任务的。
前段时间,笔者就做了这样的一个事情,让backtrader能够完成我们想要的组合层面的回测。
1.最终的效果
和一般的portfolio层面的回测平台一样,我们希望,最后我们实现一个策略只要进行一些设置就可以了。笔者利用backtrader封装了一个函数,实现了几乎和优矿一样的功能。
使用的时候,笔者的函数只需要如下的设置:
[python]
start_date="2017-04-01"
end_date="2017-06-20"
trading_csv_name="trading_data_two_year.csv"
portfolio_csv_name="port_two_year.csv"
benchmark_csv_name=None
然后我们就可以回测了。笔者把回测的类封装了起来,只要调用笔者的回测类就可以了。
[python]
begin=datetime.datetime.now()
result_dict=bt_backtest.backtrader_backtest(start_date=start_date,end_date=end_date,trading_csv_name=trading_csv_name,
portfolio_csv_name=portfolio_csv_name,bechmark_csv_name=benchmark_csv_name)
然后,回测结束后输出评价指标。
[python]
end=datetime.datetime.now()
print"timeelapse:",(end-begin)
print"StartPortfolioValue:%.2f"%result_dict["start_cash"]
print"FinalPortfolioValue:%.2f"%result_dict["final_value"]
print"TotalReturn:",result_dict["total_return"]
print"SharpeRatio:",result_dict["sharpe_ratio"]#*2#todothereshouldbeconsider!
print"MaxDrowdown:",result_dict["max_drowdown"]*2
print"MaxDrowdownMoney:",result_dict["max_drowdown_money"]
print"TradeInformation",result_dict["trade_info"]
result=pd.read_csv("result.csv",index_col=0)
result.plot()
plt.show()
position_info=pd.read_csv("position_info.csv",index_col=0)
接下来说一下我们要输入的这些csv文件吧。
[python]
trading_csv_name="trading_data_two_year.csv"
portfolio_csv_name="port_two_year.csv"
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]
portfolio_csv_name="port_two_year.csv"
这是我们调仓日的目标仓位,只需要sigdate,secucode,weight三个字段就行。
benchmark_csv_name自然就是benchmark的daily return数据文件了。这里可以不设置。
2.回测函数
接下来就是核心的回测的函数了。
[python]
#-*-coding:utf-8-*-
from__future__import(absolute_import,division,print_function,
unicode_literals)
importdatetime
importbacktraderasbt
frombacktraderimportOrder
importpandasaspd
importmatplotlib.pyplotasplt
#CreateaStratey
classAlphaPortfolioStrategy(bt.Strategy):
deflog(self,txt,dt=None):
dt=dtorself.datas[0].datetime.date(0)
print("%s,%s"%(dt.isoformat(),txt))
def__init__(self,dataId_to_secId_dict,secId_to_dataId_dict,adj_df,end_date,backtesting_length=None,benchmark=None,result_csv_name="result"):
#1.gettargetportfolioweight
self.adj_df=adj_df
self.backtesting_length=backtesting_length
self.end_date=end_date
#2.backtraderdata_idandsecIdtransferdiction
self.dataId_to_secId_dict=dataId_to_secId_dict
self.secId_to_dataId_dict=secId_to_dataId_dict
self.benchmark=benchmark
#3.storetheuntradabledayduetotheupanddownfloor,emptyitinanewadjustmentday
self.order_line_dict={}
self.pre_position_data_id=list()
self.value_for_plot={}
#4.keeptrackofpendingordersandbuyprice/commission
self.order=None
self.buyprice=None
self.buycomm=None
self.value=10000000.0
self.cash=10000000.0
self.positions_info=open("position_info.csv","wb")
self.positions_info.write("date,bt_id,sec_code,size,lastpricen")
defstart(self):
print("theworldcallme!")
defnotify_fund(self,cash,value,fundvalue,shares):
#updatethemarketvalueeveryday
#print("actualvalue:",value)
self.value=value-10000000.0#wegivethebrokermore10millionforthepurposeofilliquidity
#self.value=value#wegivethebrokermore10millionforthepurposeofilliquidity
self.value_for_plot[self.datetime.datetime()]=self.value/10000000.0
self.cash=cash
#print("cash:",cash)
defnotify_order(self,order):
iforder.statusin[order.Submitted,order.Accepted]:
#Buy/Sellordersubmitted/acceptedto/bybroker-Nothingtodo
return
#Checkifanorderhasbeencompleted
#Attention:brokercouldrejectorderifnotenougthcash
iforder.statusin[order.Completed]:
iforder.isbuy():
#order.
self.log(
"BUYEXECUTED,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
else:#Sell
self.log("SELLEXECUTED,Price:%.2f,Cost:%.2f,Comm%.2f"%
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed=len(self)
eliforder.statusin[order.Canceled,order.Rejected]:
self.log("OrderCanceled/Rejected")
eliforder.status==order.Margin:
self.log("OrderMargin")
self.order=None
defnotify_trade(self,trade):
ifnottrade.isclosed:
return
self.log("OPERATIONPROFIT,GROSS%.2f,NET%.2f"%
(trade.pnl,trade.pnlcomm))
defnext(self):
#0.Checkifanorderispending...ifyes,wecannotsenda2ndone
#ifself.order:
#return
bar_time=self.datetime.datetime()
bar_time_str=bar_time.strftime("%Y-%m-%d")
trading_date=bar_time+datetime.timedelta(days=1)
#bar_time_str=(self.datetime.datetime()+datetime.timedelta(1)).strftime("%Y-%m-%d")
print(bar_time_str,self.value)
print("barday===:",bar_time_str,"===============")
for(k,v)inself.dataId_to_secId_dict.items():
size=self.positions[self.datas[k]].size
price=self.datas[k].close[-1]
self.positions_info.write("%s,%s,%s,%s,%s"%(bar_time_str,k,v,size,price))
self.positions_info.write("n")
#1.nomattertheadjustmentday,up/downfloorblockedordershoulddealwitheachbar
for(k,v)inself.order_line_dict.items():
bar=self.datas[k]
buyable=Falseifbar.low[1]/bar.close[0]>1.0950elseTrue
sellable=Falseifbar.high[1]/bar.close[0]<0.910elseTrue
#buyable=Falseif(bar.open[1]/bar.close[0]>1.0950)and(bar.close[1]/bar.close[0]>1.0950)elseTrue
#sellable=Falseif(bar.open[1]/bar.close[0]<0.910and(bar.close[1]/bar.close[0]<0.91))elseTrue
ifv>0:
ifbuyable:
delself.order_line_dict[k]
self.log("%sBUYCREATE,%.2f,vlois%s"%(self.dataId_to_secId_dict[k],bar.open[1],v))
self.order=self.buy(data=bar,size=v,exectype=Order.Market)
else:
print("############:")
print("unbuyable:",self.dataId_to_secId_dict[k])
elifv<0:
ifsellable:
delself.order_line_dict[k]
self.log("%sSELLCREATE,%.2f,volis%s"%(self.dataId_to_secId_dict[k],bar.open[1],v))
self.order=self.sell(data=bar,size=abs(v),exectype=Order.Market)
else:
print("############:")
print("unsellable:",self.dataId_to_secId_dict[k])
#2.ensuretheadjustmentday
#2.1getthecurrentbartime
adj_sig=self.adj_df[self.adj_df["sigdate"]==trading_date.strftime("%Y-%m-%d")][["secucode","hl_weight"]]
#2.2checktheadjustmentday
iflen(adj_sig)==0orbar_time_str==self.end_date:
return
#3.adjusttheportfolio
#3.1settwodictstostorethebuyorderandsellorderspearately
buy_dict={}
sell_dict={}
self.order_line_dict={}
current_position_data_id=list()
#3.2iteratetheportfolioinstrumentsanddivideintobuygroupandsellgroup
forindexinadj_sig.index:
#getcurrentinstrumentcodeandtransfertothedata_id
sec_id=adj_sig.loc[index]["secucode"]
data_id=self.secId_to_dataId_dict[sec_id]
ifself.backtesting_lengthanddata_id>=self.backtesting_length:
continue
bar=self.datas[data_id]
current_position_data_id.append(data_id)
#getthetargetweightvalue
target_weight=adj_sig.loc[index]["hl_weight"]
#calculatethecurrentweightvalue
current_position=self.positions[self.datas[data_id]]
current_mv=current_position.size*bar.close[0]
current_weight=current_mv/float(self.value)
diff_weight=(target_weight-current_weight)
ifbar.open[1]==0:
continue
diff_volume=int(diff_weight*self.value/bar.open[1]/100)*100
print("theweightdifference",diff_weight)
ifdiff_volume>0:
buy_dict[data_id]=diff_volume
elifdiff_volume<0:
sell_dict[data_id]=diff_volume
#3.3makeorderwork
for(k,v)insell_dict.items():
bar=self.datas[k]
#sellable=Falseif(bar.high[1]==bar.low[1])and(bar.open[1]/bar.close[0]<0.920)elseTrue
sellable=Falseif(bar.open[1]/bar.close[0]<0.920)elseTrue
#sellable=Falseif(bar.open[1]/bar.close[0]<0.910and(bar.close[1]/bar.close[0]<0.91))elseTrue
ifsellable:
self.log("%sSELLCREATE,%.2f,volis%s"%(self.dataId_to_secId_dict[k],bar.open[1],v))
self.order=self.sell(data=bar,size=abs(v),exectype=Order.Market)
else:
print("############:")
print("unsellable:",self.dataId_to_secId_dict[k])
self.order_line_dict[k]=v
for(k,v)inbuy_dict.items():
bar=self.datas[k]
#buyable=Falseif(bar.low[1]==bar.high[1])and(bar.open[1]/bar.close[0])>1.0950elseTrue
buyable=Falseif(bar.open[1]/bar.close[0])>1.0950elseTrue
#buyable=Falseif(bar.open[1]/bar.close[0]>1.0950)and(bar.close[1]/bar.close[0]>1.0950)elseTrue
ifbuyable:
self.log("%sBUYCREATE,%.2f,vlois%s"%(self.dataId_to_secId_dict[k],bar.open[1],v))
self.order=self.buy(data=bar,size=v,exectype=Order.Market)
else:
print("############:")
print("unbuyable:",self.dataId_to_secId_dict[k])
self.order_line_dict[k]=v
#3.4closeposition,whenthedata_idisnotincurrentportfolioandinlastportfolio,weclosetheposition
ifself.pre_position_data_id:
clost_data_id=[difordiinself.pre_position_data_idifdinotincurrent_position_data_id]
fordiinclost_data_id:
bar=self.datas[di]
print("CLOSEPOSITION:",self.dataId_to_secId_dict[di])
self.order=self.close(data=bar)
#3.5updatethepositiondataidfornextchecking
self.pre_position_data_id=current_position_data_id
defstop(self):
#plotthenetvalueandthebenchmarkcurves
plot_df=pd.concat([pd.Series(self.value_for_plot,).to_frame(),self.benchmark],axis=1,join="inner")
plot_df.to_csv("result.csv")
self.positions_info.close()
print("death")
defbacktrader_backtest(start_date,end_date,trading_csv_name,portfolio_csv_name,bechmark_csv_name):
#1.backtestparameterssetting:starttime,endtime,theassetsnumber(backtest_length)andthebenckmarkdataandnewacerebro
start_date,end_date=datetime.datetime.strptime(start_date,"%Y-%m-%d"),datetime.datetime.strptime(end_date,"%Y-%m-%d")
backtest_length=None
#benchmarkseries
ifbechmark_csv_name:
benchmark=pd.read_csv(bechmark_csv_name,date_parser=True,dtype={"date":str})
benchmark["date"]=benchmark["date"].apply(lambdax:datetime.datetime.strptime(x,"%Y-%m-%d"))
benchmark=benchmark.set_index("date")
else:
benchmark=None
#result_df=pd.DataFrame()
cerebro=bt.Cerebro()
#2.getrequiredtradingdata
#2.1gettradingdata(totaltradingdata)
trading_data_df=pd.read_csv(trading_csv_name,dtype={"sigdate":str,"secucode":str})
trading_data_df.rename(columns={"sigdate":"tradingdate","secucode":"ticker"},inplace=True)
transer=lambdax:datetime.datetime.strptime(x,"%Y-%m-%d")
trading_data_df["tradingdate"]=trading_data_df["tradingdate"].apply(transer)
trading_data_df=trading_data_df[(trading_data_df["tradingdate"]>start_date)&(trading_data_df["tradingdate"]<end_date)]
trading_data_df["openinterest"]=0
trading_data_df=trading_data_df.set_index("tradingdate")
#2.2gettargetportfoliodataforthetargetassetsfilter
adj_df=pd.read_csv(portfolio_csv_name,dtype={"secucode":str})
adj_df=adj_df[["sigdate","secucode","hl_weight"]]
parser1=lambdax:datetime.datetime.strptime(x,"%Y/%m/%d")
parser2=lambdax:x.strftime("%Y-%m-%d")
adj_df["sigdate"]=adj_df["sigdate"].apply(parser1)
adj_df=adj_df[(adj_df["sigdate"]>start_date)&(adj_df["sigdate"]<end_date)]
adj_df["sigdate"]=adj_df["sigdate"].apply(parser2)
#2.3generatetwodictiontotransbetweensecIdandbacktraderid
sec_id_list=adj_df["secucode"].drop_duplicates().tolist()
data_id_list=[iforiinrange(len(sec_id_list))]
dataId_to_secId_dict=dict(zip(data_id_list,sec_id_list))
secId_to_dataId_dict=dict(zip(sec_id_list,data_id_list))
print("totalstocks"number",len(sec_id_list),".","datafeeding......")
#2.4feedrequireddatafeedandaddthemtothecerebro
forindex,sec_idinenumerate(sec_id_list[0:backtest_length]):
sec_df=trading_data_df[trading_data_df["ticker"]==sec_id]
#sec_df=sec_df.set_index("tradingdate")
sec_raw_start=sec_df.index[0]
ifsec_raw_start!=start_date.strftime("%Y-%m-%d"):
na_fill_value=sec_df.head(1)["open"].values[0]
df_temp=pd.DataFrame(index=pd.date_range(start=start_date,end=sec_raw_start,freq="D")[:-1],
columns=["open","high","low","close","volume","openinterest"]
).fillna(na_fill_value)
frames=[df_temp,sec_df]
sec_df=pd.concat(frames)
data_feed=bt.feeds.PandasData(dataname=sec_df,
fromdate=start_date,
todate=end_date
)
cerebro.adddata(data_feed,name=sec_id)
print("datafeedfinish!")
#3.cereberoconfig
cerebro.addstrategy(AlphaPortfolioStrategy,
dataId_to_secId_dict,secId_to_dataId_dict,adj_df,end_date.strftime("%Y-%m-%d"),backtest_length,benchmark)
cerebro.broker.setcash(20000000.0)
#cerebro.broker.setcash(10000000.0)
cerebro.broker.setcommission(commission=0.0008)
cerebro.broker.set_slippage_fixed(0.02)
cerebro.addanalyzer(bt.analyzers.Returns,_)
cerebro.addanalyzer(bt.analyzers.SharpeRatio,_name="SharpeRatio",riskfreerate=0.00,stddev_sample=True,annualize=True)
cerebro.addanalyzer(bt.analyzers.AnnualReturn,_name="AnnualReturn")
cerebro.addanalyzer(bt.analyzers.DrawDown,_name="DW")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer,_name="TradeAnalyzer")
###informationprint
start_cash=(cerebro.broker.getvalue()-10000000.0)
#start_cash=(cerebro.broker.getvalue())
#print("StartingPortfolioValue:%.2f"%(cerebro.broker.getvalue()-10000000.0))
print("startcerebro.run()")
results=cerebro.run()#runthecerebro
#4.showtheresult
strat=results[0]
final_value=(cerebro.broker.getvalue()-10000000.0)
total_return=((cerebro.broker.getvalue()-10000000.0)/10000000.0-1)
#final_value=(cerebro.broker.getvalue())
#total_return=((cerebro.broker.getvalue())/10000000.0-1)
sharpe_ratio=strat.analyzers.SharpeRatio.get_analysis()["sharperatio"]
max_drowdown=strat.analyzers.DW.get_analysis()["max"]["drawdown"]
max_drowdown_money=strat.analyzers.DW.get_analysis()["max"]["moneydown"]
trade_info=strat.analyzers.TradeAnalyzer.get_analysis()
return_dict={"start_cash":start_cash,"final_value":final_value,"total_return":total_return,
"sharpe_ratio":sharpe_ratio,"max_drowdown":max_drowdown,"max_drowdown_money":max_drowdown_money,
"trade_info":trade_info}
returnreturn_dict
基本上,代码的注释很全面了,基本实现了涨跌停不能买入,进入排队等待,当日开盘价买入,滑点设置等等这些功能。Backtrader有一点,可能是为了加快速度,特别不方便,就是datafeed不能按照名字来实现查找,而是用index来寻找,所以我们需要建立一个全局的dict,能够实现data id到股票代码以及反过来的一一对应的dict。别的,其实实现起来还是比较方便的,但是性能有待提高,等后续需要继续启动这个项目的时候,笔者继续努力吧。譬如如何修改一个查找的逻辑和数据类型的使用。