本文主要是介绍量化交易backtrader实践(四)_评价统计篇(4)_多个回测的评价列表,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
本节目标
在第1节里,我们认识了backtrader内置评价指标,了解了每个指标的大概内容;在第2节里把内置评价指标中比较常用的指标进行了获取和输出;第3节里我们探索其他backtrader中没有的评价指标,并对pyfolio, empyrical和quantstat库进行了初步的认识,以及使用quantstat可以方便的进行评价指标的可视化实践。以上的动作,都是针对一支股票和一个特定的策略进行的,而在我们的实际中,需要对多个股票和各种不同的策略进行回测,也可能对同一策略的不同参数组合进行回测。
这就需要生成多个回测的评价列表,这一节的实践就是这个。
多回测评价列表
01_多股票x多策略的评价列表
001_大致实践的思路
- 前面我们已经有了单支股票一个策略的函数:run_main_analyser()
- 那么多个股票乘上多种策略就可以采用两重循环来进行(这里的demo采用3x2)
- 每次函数运行返回一个字典或列表或DataFrame
- 最后把这些数据放到一起以.csv的文件输出
- 然后就可以通过EXCEL来观察这些评价列表了
a_股票和策略的循环调用
首先,需要有一个自选股列表,比如拿之前的一个df_list,在demo里用2,3,6这三支股票。
代码 | 名称 |
---|---|
601168 | 西部矿业 |
300058 | 蓝色光标 |
000921 | 海信家电 |
601086 | 国芳集团 |
600794 | 保税科技 |
002516 | 旷达科技 |
300697 | 电工合金 |
159633 | 中证1000指数ETF |
515220 | 煤炭ETF |
def run_analyzer_all():sel11 = [2,3,6]straList=[myStrategys["经典_KDJ"],myStrategys['经典布林线策略']]list_analyzer = []for stra1 in straList: # 策略做大循环stra2 = eval(stra1)for isel1 in sel11: # 股票做小循环run_main_analyser4(df_list,stra2,isel1,d1,d2,list_analyzer,False,1)return list_analyzer
['000921', '海信家电'] 策略为 经典KD交叉 , 期末总资金 95522.90 盈利为 -4477.10 总共交易次数为 46 ,交易成功率为 28.3% ['601086', '国芳集团'] 策略为 经典KD交叉 , 期末总资金 132906.24 盈利为 32906.24 总共交易次数为 39 ,交易成功率为 35.9% ['300697', '电工合金'] 策略为 经典KD交叉 , 期末总资金 94248.60 盈利为 -5751.40 总共交易次数为 42 ,交易成功率为 33.3% ['000921', '海信家电'] 策略为 经典BOLL策略 , 期末总资金 101298.06 盈利为 1298.06 总共交易次数为 3 ,交易成功率为 66.7% ['601086', '国芳集团'] 策略为 经典BOLL策略 , 期末总资金 97992.30 盈利为 -2007.70 总共交易次数为 4 ,交易成功率为 75.0% ['300697', '电工合金'] 策略为 经典BOLL策略 , 期末总资金 116375.33 盈利为 16375.33 总共交易次数为 5 ,交易成功率为 60.0%
大致从盈利的情况上看,其实已经看出,有些股票适合KD策略,有的股票可能适合BOLL策略,没有最好的策略,关键看是不是跟股票匹配上了。
b_单策略cerebro与analyzer的应用
这个函数跟之前的基本没有变化,主要在 analyzer_output()时返回一个dic_analyzer,并添加到参数给的list_analyzer中。
def run_main_analyser4 (df_list,run_strategy,i, sdate1,sdate2, list_analyzer): iSel = icode_ = df_list.iloc[iSel,0]df1 = get_bt_feed_data(code_)cerebro = bt.Cerebro()data = bt.feeds.PandasData(dataname=df1, fromdate=sdate, todate=edate) add_analyzer_all(cerebro) # 加入---------analyzer ------------cerebro.addstrategy(run_strategy, log_off= logoff) cerebro.adddata(data, name=code_) result = cerebro.run() # 运行策略参数优化 maxcpus=1strat = result[0]dic_analyzer = analyzer_output(strat) # 输出-------analyzer-------------list_analyzer.append(dic_analyzer)return list_analyzer
c_添加评价函数和输出评价函数
直接把第2节最后的评价函数拿过来用
def add_analyzer_all(cerebro):cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='_AnnualReturn') # 年化收益率 01cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown') # 回撤 02# 03cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') # 夏普比率 04cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='_TradeAnalyzer') # 交易分析 05cerebro.addanalyzer(bt.analyzers.SQN, _name='_SQN') # 交易系统性能得分 SQN 06# 07,08,09,10cerebro.addanalyzer(bt.analyzers.Returns, _name='_Returns', tann=252) # 计算252日度收益 11cerebro.addanalyzer(bt.analyzers.VWR, _name='_VWR') # 可变加权回报率 VWR 12# 13,14cerebro.addanalyzer(bt.analyzers.PeriodStats, _name='_PeriodStats') # 基本数据统计 15# 需要通过数据记录进行计算 cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='pnl') # 返回收益率时序数据 # 03cerebro.addanalyzer(bt.analyzers.PositionsValue, _name='_PositionsValue') # position 08cerebro.addanalyzer(bt.analyzers.Transactions, _name='_Transactions') # Transactions 09cerebro.addanalyzer(bt.analyzers.GrossLeverage, _name='_GrossLeverage') # GrossLeverage 07cerebro.addanalyzer(bt.analyzers.PyFolio, _name='_PyFolio') # 10def analyzer_output(result):dic1 = {}sout01 = result.analyzers._AnnualReturn.get_analysis() # 年化收益率 01for k,v in sout01.items():dic1[f'{k}年化']= v*100sout02 = result.analyzers._DrawDown.get_analysis() # 回撤 02dic1['回撤'] = sout02['drawdown']dic1['最大回撤'] = sout02['max']['drawdown']sout04 = result.analyzers._SharpeRatio.get_analysis() # 夏普比率 04dic1['夏普率'] = sout04['sharperatio']sout06 = result.analyzers._SQN.get_analysis() # 交易系统性能得分 SQN 06dic1['系统性能SQN'] = sout06['sqn']sout12 = result.analyzers._VWR.get_analysis() # # 可变加权回报率 VWR 12dic1['VWR'] = sout12['vwr']sout15 = result.analyzers._PeriodStats.get_analysis() # # 基本数据统计 15dic1['平均'] = sout15['average']dic1['标准差'] = sout15['stddev']dic1['最佳收益'] = sout15['best']dic1['最大亏损'] = sout15['worst']sout11 = result.analyzers._Returns.get_analysis() # 计算252日度收益 11dic1['年化回报率'] = sout11['rnorm100']sout05 = result.analyzers._TradeAnalyzer.get_analysis() # 交易分析 05dic1['关闭交易'] = sout05['total']['closed'] dic1['连胜次数'] = sout05['streak']['won']['current']dic1['最大连胜'] = sout05['streak']['won']['longest']dic1['连负次数'] = sout05['streak']['lost']['current']dic1['最大连负'] = sout05['streak']['lost']['longest']dic1['毛利润'] = sout05['pnl']['gross']['total']dic1['净利润'] = sout05['pnl']['net']['total']dic1['总胜次数'] = sout05['won']['total']dic1['总盈利'] = sout05['won']['pnl']['total']dic1['最大盈利'] = sout05['won']['pnl']['max']dic1['总亏次数'] = sout05['lost']['total']dic1['总亏损'] = sout05['lost']['pnl']['total']dic1['最大亏损'] = sout05['lost']['pnl']['max']dic1['胜率'] = dic1['总胜次数']/dic1['关闭交易'] * 100# dic1['盈亏比'] = abs(dic1['总盈利']/dic1['总亏损'] ) # 会出现总亏损为0,除数不能为0# 盈亏时间比 盈利周期数/亏损周期数# dic1['盈亏时间比'] = sout05['len']['won']['total'] / sout05['len']['lost']['total']# 通过data.close的差值来取区间涨幅len1 = len(result.data.close)cend = result.data.close[0]cstart = result.data.close[-len1+1]qjzf2 = (cend-cstart)/cstartqjzf2_pct = (cend-cstart)/cstart * 100dic1['区间涨跌'] = qjzf2dic1['区间涨幅'] = qjzf2_pct# 持仓周期数 - 更简单用 level来统计,不需要取list第一项sout07 = result.analyzers._GrossLeverage.get_analysis()s2 = pd.Series(sout07)cnt_zero = s2.eq(0).sum()cnt_all = s2.count()cnt_position = cnt_all - cnt_zerodic1['持仓周期数'] = cnt_positiondic1['持仓占比'] = cnt_position/cnt_all * 100 sout03 = result.analyzers.pnl.get_analysis() a2 = pd.Series(sout03)recent_1m = a2[-21:].sum() *100 # 近1月recent_3m = a2[-64:].sum() *100recent_6m = a2[-126:].sum() *100recent_1y = a2[-252:].sum() *100dic1['近1月'] = recent_1mdic1['近3月'] = recent_3m dic1['近6月'] = recent_6m dic1['近1年'] = recent_1y return dic1
002_数据结构与添加区分
从analyzer_output 函数返回的是字典结构,可以用来记录每个评价指标(key)的数值(value),回到run_main_analyzer4 函数后,把字典加到列表list_analyzer 中去,每1支股票对应的1个策略都可以把评价指标dict加进去。
然后,打印列表或转成DataFrame进行显示,这个时候问题来了,面对很多行的数据,我们不知道某一行是哪支股票用了哪个策略得到的!于是,我们采用了一些做法,在run_main_analyzer4中,先把股票的代号,名称和使用的策略先添加到字典中去,这样在输出后就能区分是哪支股票应用哪个策略了。
dic_analyzer['code'] = data._name # 代号cn_name = df_list.iloc[iSel,1]dic_analyzer['名称'] = cn_name # 股票名称strategy_params = result[0].params.stra_namedic_analyzer['策略'] = strategy_params # 策略名称
代号例如“600397”在data._name里,名称我们去查自己的自选股列表可以得到,而策略可以简单的写在策略类的params里,就可以在result[0].params.stra_name去获取了。
class shortMA(BaseSt):params = (('stra_name','经典短均线'), # 设定策略名称 ('ma1',5), ) def __init__(self): self.order = Noneself.sma1 = bt.indicators.SMA(self.data.close, period=self.params.ma1)self.crs= myCross2(self.data.close, self.sma1)def next(self):pass
003_把代号名称放到最前面
得到最后的list后,进行简单处理,会发现code,名称和策略在最后面,这样在评价指标比较多的时候,就会不方便,我们需要把光标移到最后面才能看到。
这里又偷懒再次请教了AI,给出如下代码,运行后就能把code,名称和策略放到最前面了
df_ana = pd.DataFrame(list1)# 获取最后三列的名称
original_columns = df_ana.columns.tolist()
last_three_columns = original_columns[-3:]# 将最后三列添加到列名列表的开头
reordered_columns = last_three_columns + original_columns[:-3]# 使用reordered_columns重新排列DataFrame
df_ana = df_ana.reindex(columns=reordered_columns)
print(df_ana.columns)
df_ana.iloc[:,[0,1,2,3,-5,-4,-3,-2,-1]]--------------------------------
Index(['code', '名称', '策略', '2023年化', '2024年化', '回撤',
'最大回撤', '夏普率', '系统性能SQN', 'VWR', '平均', '标准差',
'最佳收益', '最大亏损', '年化回报率', '关闭交易', '连胜次数', '最大连胜',
'连负次数', '最大连负', '毛利润', '净利润', '总胜次数', '总盈利',
'最大盈利', '总亏次数', '总亏损', '胜率', '区间涨跌', '区间涨幅',
'持仓周期数', '持仓占比', '近1月', '近3月', '近6月', '近1年'],dtype='object')
004_to_csv和数据后处理
当前,我们还不太会根据评价指标来进行分析,这个不属于backtrader的实践范畴,所以直接输出成.csv格式。然后就是数据后处理,可以用excel打开来,也可以用pandas对这些数据进行分析和处理。
df_ana.to_csv('analyzer_list_01.csv',encoding='utf-8-sig')
print('to_csv OK!')
比如我们对“夏普率”进行降序排序,那我们就得到电工合金的经典BOLL策略夏普率得分最高......
名称 策略 2023年化 2024年化 最大回撤 夏普率 电工合金 经典BOLL策略 6.077330707 9.708013029 17.07450876 3.79690166 国芳集团 经典KD交叉 32.64310973 0.198374117 13.72136358 0.950585149 海信家电 经典KD交叉 -6.716765823 2.400926711 12.57285827 -0.69270148 国芳集团 经典BOLL策略 1.811742713 -3.751474224 10.89152999 -0.708175064 电工合金 经典KD交叉 -5.419165717 -0.351269565 15.71722809 -1.533266478 海信家电 经典BOLL策略 0.800965848 0.493147589 4.735670387 -2.293192627
02_参数优化的评价列表
某股票软件有“探索最佳专家系统”这种功能,它的操作是你选中一支股票后,打开这个功能,就会显示从MACD,KDJ到xxx大概十多个专家交易系统给你选择,以KDJ为例,你点击开始评测后,它会将KDJ的2个参数开始双重循环遍历,最终给出所有参数组合的评价指标,并且按你选择的(胜率最高,净利润最大......)的选项,得出成绩最好的参数组合。
参数优化其实就是这样一个功能,对于1个策略的参数进行循环回测,并且这里的参数取值范围是可以自己设置的,不是探索系统那种帮你预设好的例如KDJ的period参数从1开始测起,某些股票居然是1的净利润最高......
001_参数优化Demo
在做参数优化的评价列表之前,先完成一个参数优化的demo程序,回顾一下参数优化的注意点。
a_策略类params添加
对于参数的优化,首先要有参数,参数的值有一个范围,这个参数在策略类里面进行定义(p1,p2,p3),并且指标indicators调用时需要填写参数,而不能使用默认值。
如果对指标的参数定义不清楚,可以直接在官方文档里查看 Indicators - Reference - Backtrader
例如KD的指标(在backtrader中为Stochastic),那么它的Params就是下面这个样子,标红的是我们在股票软件里KDJ的常规参数设置(9,3,3),在backtrader中,默认是(14,3,3)
Params:
period (14)
period_dfast (3)
movav (MovingAverageSimple)
upperband (80.0)
lowerband (20.0)
safediv (False)
safezero (0.0)
period_dslow (3)
class St_KDJ_class1(BaseOptSt1):params = (('stra_name','经典KD交叉'),('p1',9), ('p2',3), ('p3',3), # KDJ的3个参数('tradeCnt',1), ('sucessCnt',0),) def __init__(self): self.order = Noneself.kd = bt.indicators.Stochastic(self.data,period=self.p.p1, # 填写参数,不用默认period_dfast=self.p.p2,period_dslow=self.p.p3)self.crs = myCross2(self.kd.percK,self.kd.percD)def next(self):if self.order: # 检查是否有指令等待执行returnif not self.position: # 没有持仓 才会进入 if self.crs.l.crsup: self.order = self.buy() # 执行买入else:if self.crs.l.crsdn: self.order = self.sell() # 执行卖出def stop(self):sucessPct = self.params.sucessCnt/self.params.tradeCntprint("当参数为 %2d,%2d,%2d 期末总资金 %.2f 盈利为 %.2f" % (self.params.p1,self.params.p2,self.p.p3,self.broker.getvalue(),self.broker.getvalue()-100000) , "总共交易次数为 %2d ,交易成功率为 %.2f" % (self.params.tradeCnt,sucessPct))
b_在.run()过程中代码变化
正常回测 | 参数优化 |
---|---|
cerebro = bt.Cerebro() | cerebro = bt.Cerebro() |
cerebro.addstrategy(run_strategy) | cerebro.optstrategy(run_strategy, p1 = range(8,15), p2=range(2,5), p3=3) |
results = cerebro.run() | results = cerebro.run(maxcpus=1) # 运行策略参数优化 maxcpus=1 |
c_简单实践
run_opt_analyser7(df_list,St_KDJ_class1,3,d1,d2,False,1)# 参数范围设置p1 = range(8,15), # 8,9,10,11,12,13,14 p2=range(2,5), # 2,3,4p3=3 # 3
参数 8, 2, 3 期末权益 156594.96 盈利 56594.96 总交易次数 50 ,成功率 0.50 参数 8, 3, 3 期末权益 141741.28 盈利 41741.28 总交易次数 40 ,成功率 0.42 参数 8, 4, 3 期末权益 133225.11 盈利 33225.11 总交易次数 36 ,成功率 0.50 参数 9, 2, 3 期末权益 151308.83 盈利 51308.83 总交易次数 48 ,成功率 0.46 参数 9, 3, 3 期末权益 142014.08 盈利 42014.08 总交易次数 38 ,成功率 0.45 参数 9, 4, 3 期末权益 131445.63 盈利 31445.63 总交易次数 36 ,成功率 0.44 参数 10, 2, 3 期末权益 146093.29 盈利 46093.29 总交易次数 50 ,成功率 0.46 参数 10, 3, 3 期末权益 123681.14 盈利 23681.14 总交易次数 40 ,成功率 0.45 参数 10, 4, 3 期末权益 137941.24 盈利 37941.24 总交易次数 33 ,成功率 0.48 参数 11, 2, 3 期末权益 142015.91 盈利 42015.91 总交易次数 50 ,成功率 0.42 参数 11, 3, 3 期末权益 131808.30 盈利 31808.30 总交易次数 37 ,成功率 0.46 参数 11, 4, 3 期末权益 139241.66 盈利 39241.66 总交易次数 33 ,成功率 0.45 参数 12, 2, 3 期末权益 150707.36 盈利 50707.36 总交易次数 45 ,成功率 0.44 参数 12, 3, 3 期末权益 137273.11 盈利 37273.11 总交易次数 37 ,成功率 0.43 参数 12, 4, 3 期末权益 144680.72 盈利 44680.72 总交易次数 32 ,成功率 0.47 参数 13, 2, 3 期末权益 133561.35 盈利 33561.35 总交易次数 49 ,成功率 0.37 参数 13, 3, 3 期末权益 130616.21 盈利 30616.21 总交易次数 38 ,成功率 0.37 参数 13, 4, 3 期末权益 146422.77 盈利 46422.77 总交易次数 33 ,成功率 0.45 参数 14, 2, 3 期末权益 139921.73 盈利 39921.73 总交易次数 51 ,成功率 0.39 参数 14, 3, 3 期末权益 132906.24 盈利 32906.24 总交易次数 39 ,成功率 0.36 参数 14, 4, 3 期末权益 141057.22 盈利 41057.22 总交易次数 32 ,成功率 0.50
002_参数优化的Analyzer
添加analyzer的函数如下
def add_analyzer_all_opt(cerebro):cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='_AnnualReturn') #年化收益01cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown') # 回撤02cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') #夏普比率04cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='pnl') # 返回收益率 03
a_直接使用原来的result报错
cerebro = bt.Cerebro()data = bt.feeds.PandasData(dataname=df1, fromdate=sdate1, todate=sdate2)
cerebro.adddata(data, name=code_) cerebro.optstrategy(run_strategy, p1 = range(8,15),p2=range(2,5),p3=3) add_analyzer_all_opt(cerebro) # 加入----------analyzer ------results= cerebro.run(maxcpus=1) strat = results[0]sout04 = strat.analyzers._SharpeRatio.get_analysis()
print(sout04)--------------------------
---> 25 sout04 = strat.analyzers._SharpeRatio.get_analysis()
AttributeError: 'list' object has no attribute 'analyzers'
b_检查results的内容
result = cerebro.run(maxcpus=1) # 运行策略参数优化 maxcpus=1print(result,len(result))strat = result[0]print(strat)---------------
[[<backtrader.cerebro.OptReturn object at 0x000001EDE5C8B4D0>],
[<backtrader.cerebro.OptReturn object at 0x000001EDE5A8AED0>],
[<backtrader.cerebro.OptReturn object at 0x000001EDE5AC8D10>],
[<backtrader.cerebro.OptReturn object at 0x000001EDE60B34D0>],
......
[<backtrader.cerebro.OptReturn object at 0x000001EDE6C83E10>],
[<backtrader.cerebro.OptReturn object at 0x000001EDE6103E10>],
[<backtrader.cerebro.OptReturn object at 0x000001EDE2D73E10>],
[<backtrader.cerebro.OptReturn object at 0x000001EDE5E57050>]] 21[<backtrader.cerebro.OptReturn object at 0x000001EDE5C8B4D0>]
通过打印result内容发现,就result是个列表,有21个元素,也就是刚才7x3的参数优化的数量,通过result[x] (x在0~20之间)就能得到其中一组参数的优化结果(OptReturn)。
c_OptReturn对象
这里还是使用问AI的方式来进行下一步,
backtrader.cerebro.OptReturn object 是什么对象,怎么查看里面的数据?
backtrader.cerebro.OptReturn
对象是在使用 Backtrader 进行策略优化时,由cerebro.optstrategy
方法返回的一个对象。这个对象包含了策略优化过程中的关键数据,如策略参数、分析器对象等。OptReturn
对象不直接包含数据,而是包含了优化过程中的统计和分析结果。要查看
OptReturn
对象中的数据,您可以通过访问该对象的属性来获取。例如,您可以通过params
属性来查看优化过程中使用的策略参数,通过analyzers
属性来获取策略的分析结果。这些属性通常是列表或字典类型,包含了优化过程中的多组数据。# 假设 cerebro 是您的 Cerebro 实例,并且已经运行了优化 opt_returns = cerebro.runopt()# 遍历优化结果,查看每个优化参数集及其对应的分析结果 for opt_return in opt_returns:print("Parameters:", opt_return.params)print("Analyzers:", opt_return.analyzers)
在上述代码中,
runopt
方法执行了优化,并返回了一个包含OptReturn
对象的列表。通过遍历这个列表,您可以查看每个优化迭代的详细信息。
我不知道它是从哪些网页上搜索得到的信息,只能说部分有用,还会有些坑在里
- 首先,参数优化不是 cerebro.runopt(),或许是某人自己封装的,前面也说到了,参数优化用到的语句是cerebro.run(maxcpus=1)
- 其实,opt_returns是参数优化返回的对象,我们刚刚看到它是一个OptReturn对象的列表,所以可以使用for in语句来遍历,但这里层次关系还是不对,所以是会报错的
错误 1 AttributeError: 'Cerebro' object has no attribute 'runopt'修正上面错误后:
38 for opt_return in opt_returns: ---> 39 print("Parameters:", opt_return.params)40 print("Analyzers:", opt_return.analyzers)AttributeError: 'list' object has no attribute 'params'
接着我们去查看了源码,然后把源码喂给了AI,然后AI的回复如下:
class OptReturn(object):def __init__(self, params, **kwargs):self.p = self.params = paramsfor k, v in kwargs.items():setattr(self, k, v)
错误信息
'list' object has no attribute 'params'
暗示您可能在一个列表上调用了.params
,而不是在OptReturn
对象上调用。这通常发生在您期望得到的是OptReturn
对象,但实际上得到的是一个包含这些对象的列表。为了解决这个问题,您需要检查导致错误的代码行,确保在尝试访问
.params
属性之前,您已经从优化结果列表中提取了OptReturn
对象。如果您在遍历优化结果列表时遇到这个错误,您应该使用索引或迭代来访问每个OptReturn
对象,然后再访问其.params
属性。
于是,我们回过来再检查一下result的类型,
result = cerebro.run(maxcpus=1) # 运行策略参数优化 maxcpus=1print(type(result))strat = result[0]print(strat,type(strat))----------------------
<class 'list'>
[<backtrader.cerebro.OptReturn object at 0x000001EDE2F66110>] <class 'list'>
由上面的结果可知,result是list类型,而result[0]仍然是list类型,这里又出现了之前我们在做持仓周期时遇到的positions的值是list,而这个list里只有一项这样的情况,因此要取到OptReturn其实需要 result[x][0]来得到。
opt_results = cerebro.run(maxcpus=1)for result in opt_results:# # 访问优化结果中的参数print(result,type(result))print(result[0],type(result[0]))---------------
[<backtrader.cerebro.OptReturn object at 0x000001EDE5FD0D10>] <class 'list'>
<backtrader.cerebro.OptReturn object at 0x000001EDE5FD0D10> <class 'backtrader.cerebro.OptReturn'>
[<backtrader.cerebro.OptReturn object at 0x000001EDE616BE10>] <class 'list'>
<backtrader.cerebro.OptReturn object at 0x000001EDE616BE10> <class 'backtrader.cerebro.OptReturn'>
.......
d_OptReturn的内容
在正确获取到OptReturn的对象后,我们就可以继续找到其内容,根据前面AI提供的代码,把层级关系搞清楚后,也可以进行print(),但是得到的都是对象,而且无法显示其内容
opt_results = cerebro.run(maxcpus=1)for result in opt_results:# # 访问优化结果中的参数# print(result,type(result))# print(result[0],type(result[0]))strat1 = result[0]print(strat1, type(strat1))print("Parameters:", strat1.params)print("Analyzers:", strat1.analyzers)---------------------
<backtrader.cerebro.OptReturn object at 0x000001EDE6A09C90>
<class 'backtrader.cerebro.OptReturn'>
Parameters: <backtrader.metabase.AutoInfoClass_LineRoot_LineMultiple_LineSeries_LineIterator_DataAccessor_StrategyBase_Strategy_BaseOptSt1_St_KDJ_class11 object at 0x000001EDE67D0810>
Analyzers: <backtrader.metabase.ItemCollection object at 0x000001EDE67B4F90>
这个时候,我们再回顾第2节补充的美化打印功能,backtrader的数据可以使用.pprint()以及.print()进行美化打印,这个可以试一下~
opt_results = cerebro.run(maxcpus=1)for result in opt_results:strat1 = result[0]params1 = strat1.paramsprint(params1.p1)print(params1.p2)print(params1.p3)analyzers = strat1.analyzersanalyzers[0].pprint()analyzers[1].pprint()analyzers[2].pprint()
-----------------
8 2 3 # params的p1,p2,p3
OrderedDict([(2023, 0.40010978535734054), (2024, 0.11844770219796641)]) # 年化收益
AutoOrderedDict([('len', 63),('drawdown', 8.33814918815141), # Drawdown('moneydown', 14244.88054622573),('max',AutoOrderedDict([('len', 99),('drawdown', 9.694768681778413),('moneydown', 16562.527088321513)]))])
OrderedDict([('sharperatio', 1.7700553868062032)]) # sharp率8 3 3
OrderedDict([(2023, 0.33635769361659196), (2024, 0.06065375497412018)])
......
e_params和analyzers的内容
到上面为止,我们已经能够看到params部分内容(如果知道有哪些的话),以及analyzers的数值。但是analyzers是AutoOrderedDict,暂时取不出来,这个就只能回顾前面所学习过的知识点以及查源码。
两个对象分别为
Parameters: <backtrader.metabase.AutoInfoClass_......>
Analyzers: <backtrader.metabase.ItemCollection object >
class AutoInfoClass(object):#........@classmethoddef _getitems(cls):return cls._getpairs().items()class ItemCollection(object):#...............def getitems(self):return zip(self._names, self._items)class Analyzer(with_metaclass(MetaAnalyzer, object)):#..............def get_analysis(self):'''Returns a *dict-like* object with the results of the analysisThe keys and format of analysis results in the dictionary isimplementation dependent.It is not even enforced that the result is a *dict-like object*, justthe conventionThe default implementation returns the default OrderedDict ``rets``created by the default ``create_analysis`` method'''return self.rets
对params和analyzers分别进行测试后,总结内容如下:
opt_results = cerebro.run(maxcpus=1)for result in opt_results:strat1 = result[0]params1 = strat1.params# print(type(params1))pa2 = params1._getitems() # odict_items([('p1', 9), ('p2', 3), ('tradeCnt', 1), # ('sucessCnt', 0), ('stra_name', '经典KD交叉'), ('p3', 3)])print(pa2)print(params1.p1,params1.p2,params1.p3) # 8 2 3print(params1.stra_name) # 经典KD交叉--------------------------
odict_items([('p1', 9), ('p2', 3), ('tradeCnt', 1),
('sucessCnt', 0), ('stra_name', '经典KD交叉'), ('p3', 3)])
8 2 3
经典KD交叉
odict_items([('p1', 9), ('p2', 3), ('tradeCnt', 1),
'sucessCnt', 0), ('stra_name', '经典KD交叉'), ('p3', 3)])
8 3 3
经典KD交叉
对于params而言
- 它是metabase.AutoInfoClass的对象,
- 可以利用类方法_getitems()获取内容,是odict_items一种字典结构
- 取值可以直接使用.p1这样的方式
opt_results = cerebro.run(maxcpus=1)for result in opt_results:analyzers = strat1.analyzersana2 = analyzers.getitems() # getitems <zip object at 0x000001EDE6763F40>print('getitems',ana2)for x in ana2:print(x)analyzers[0].pprint() sout01 = analyzers[0].get_analysis()print('get_analysis',sout01)analyzers[1].pprint()sout02 = analyzers[1].get_analysis()print('get_analysis', sout02)------------
getitems <zip object at 0x000001EDE6763F40>
('_AnnualReturn', <backtrader.analyzers.annualreturn.AnnualReturn object at 0x000001EDE68DEA90>)
('_DrawDown', <backtrader.analyzers.drawdown.DrawDown object at 0x000001EDE68DDDD0>)
('_SharpeRatio', <backtrader.analyzers.sharpe.SharpeRatio object at 0x000001EDE68DFC50>)
('pnl', <backtrader.analyzers.timereturn.TimeReturn object at 0x000001EDE2F78B10>)OrderedDict([(2023, 0.40010978535734054), (2024, 0.11844770219796641)])
get_analysis OrderedDict([(2023, 0.40010978535734054), (2024, 0.11844770219796641)])AutoOrderedDict([('len', 63),('drawdown', 8.33814918815141),('moneydown', 14244.88054622573),('max',AutoOrderedDict([('len', 99),('drawdown', 9.694768681778413),('moneydown', 16562.527088321513)]))])
get_analysis AutoOrderedDict([('len', 63),
('drawdown', 8.33814918815141), ('moneydown', 14244.88054622573),
('max', AutoOrderedDict([('len', 99),
('drawdown', 9.694768681778413), ('moneydown', 16562.527088321513)]))])getitems <zip object at 0x000001EDE6F74E00>
......
对于analyzers而言
- 它是metabase.ItemCollection object的对象
- 可以利用.getitems()来获取其内容,以当前add_analyzer为例,添加了年化,回撤,夏普和日收益率共4个评价,则获取到的内容用 for in遍历出来为('_AnnualReturn','_DrawDown','_SharpeRatio', 'pnl')
- 其中的每一项,都是 backtrader.analyzers.下面的类,例如年化收益01 - <class 'backtrader.analyzers.annualreturn.AnnualReturn'> ,前面正常回测的时候也是使用.get_analysis()来获取它的内容的
- 原来analyzer_output中 类似于 sout02 = xxx. get_analysis()之后的语句可以复用
f_参数优化加评价的初步集成
前面在参数优化Demo的时候,大部分与正常回测不一样的地方都已经修改,目前需要把添加评价和输出评价两个函数进行更新,其实添加评价函数是不用改的,但是参数优化后的result它里面的analyzer都是按添加评价时的顺序生成的,所以千万要做到一一对应、对齐。
import quantstats as qs
def add_analyzer_all_opt(cerebro):cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='_AnnualReturn') # 年化收益率 01cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown') # 回撤 02# 03cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') # 夏普比率 04cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='pnl') # 返回收益率时序数据 # 03def analyzer_output_opt(result):list_analyzer = []for strat in result:dic1 = {}strat1 = strat[0]params1 = strat1.paramsstr_params = '(%2d,%2d,%2d)'%(params1.p1,params1.p2,params1.p3)# print(str_params)dic1['策略'] = params1.stra_namedic1['参数'] = str_paramsanalyzers = strat1.analyzerssout01 = analyzers[0].get_analysis() # 年化收益率 01for k,v in sout01.items():dic1[f'{k}年化']= v*100 sout02 = analyzers[1].get_analysis() # 回撤 02dic1['回撤'] = sout02['drawdown']dic1['最大回撤'] = sout02['max']['drawdown'] sout04 = analyzers[2].get_analysis() # 夏普比率 04dic1['夏普率'] = sout04['sharperatio']sout03 = analyzers[3].get_analysis() # 返回收益率时序数据 # 03a2 = pd.Series(sout03)# sharpsharpe_ratio = qs.stats.sharpe(a2, rf=0.02) # dic1['qs_sharp'] = sharpe_ratiorecent_1m = a2[-21:].sum() *100 # 近1月recent_3m = a2[-64:].sum() *100recent_6m = a2[-126:].sum() *100recent_1y = a2[-252:].sum() *100dic1['近1月'] = recent_1mdic1['近3月'] = recent_3m dic1['近6月'] = recent_6m dic1['近1年'] = recent_1y list_analyzer.append(dic1)return list_analyzer
最后再把list_analyzer转换成DataFrame类型后,它的输出结果:
到这里,即使在参数优化里,我们也能够把策略,参数对应的评价得分获取出来,或者to_csv()后放到excel中去进行排序等,寻找对应每支股票最佳的参数配置。这里我还是觉得,不同的股票有其自己的特性,它可能适合于某种策略,或不适合某种策略;而且,对于某种策略,它可能适合某种奇怪的参数配置,这种配置对其他股票不起作用,但就是对这支股票出奇的好用。
自定义Analyzer类
原本是计划写在这一节里的,结果实践过程有点曲折,导致前面的内容也增加了这么多,自定义Analyzer类的实践内容也挺多,就放到下一节吧。
本节小结
本节通过实践,我们制作了一个相对完整的程序,把多支股票乘以多种策略进行回测的许多评价指标做成了列表,以方便后续分析和选择分数高的股票或与之相匹配的策略。
接着,考虑到参数优化时单支股票和单策略但参数配置可以多组合的情况,在参数优化demo的基础上,尝试添加评价指标并输出,这期间其实遇到几次感觉很棘手的问题,例如怎么取opt运行后的结果里的analyzer等,问AI以及自己查找都没得到准备的答案,很多时候,还是需要查源码才能解决问题。
随着不断的实践,之前许多不清楚不明白的地方也逐渐明亮了起来,在这个过程中,也不断发现前面学习发生的错误,有错误并不可怕,更正了错误才能更好的成长。
实践是检验真理的唯一标准!
这篇关于量化交易backtrader实践(四)_评价统计篇(4)_多个回测的评价列表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!