当前位置: > 热文

Backtrader量化平台教程

时间:2022-04-22 04:58:23 热文 我要投稿

我们知道,有时候我们要测试的可不仅仅是一个标的,但是之前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。别的,其实实现起来还是比较方便的,但是性能有待提高,等后续需要继续启动这个项目的时候,笔者继续努力吧。譬如如何修改一个查找的逻辑和数据类型的使用。

最新推荐