量化交易backtrader实践(四)_评价统计篇(4)_多个回测的评价列表

2024-09-01 16:28

本文主要是介绍量化交易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.0773307079.70801302917.074508763.79690166
国芳集团经典KD交叉32.643109730.19837411713.721363580.950585149
海信家电经典KD交叉-6.7167658232.40092671112.57285827-0.69270148
国芳集团经典BOLL策略1.811742713-3.75147422410.89152999-0.708175064
电工合金经典KD交叉-5.419165717-0.35126956515.71722809-1.533266478
海信家电经典BOLL策略0.8009658480.4931475894.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)_多个回测的评价列表的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1127457

相关文章

基于MySQL Binlog的Elasticsearch数据同步实践

一、为什么要做 随着马蜂窝的逐渐发展,我们的业务数据越来越多,单纯使用 MySQL 已经不能满足我们的数据查询需求,例如对于商品、订单等数据的多维度检索。 使用 Elasticsearch 存储业务数据可以很好的解决我们业务中的搜索需求。而数据进行异构存储后,随之而来的就是数据同步的问题。 二、现有方法及问题 对于数据同步,我们目前的解决方案是建立数据中间表。把需要检索的业务数据,统一放到一张M

hdu1496(用hash思想统计数目)

作为一个刚学hash的孩子,感觉这道题目很不错,灵活的运用的数组的下标。 解题步骤:如果用常规方法解,那么时间复杂度为O(n^4),肯定会超时,然后参考了网上的解题方法,将等式分成两个部分,a*x1^2+b*x2^2和c*x3^2+d*x4^2, 各自作为数组的下标,如果两部分相加为0,则满足等式; 代码如下: #include<iostream>#include<algorithm

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

flume系列之:查看flume系统日志、查看统计flume日志类型、查看flume日志

遍历指定目录下多个文件查找指定内容 服务器系统日志会记录flume相关日志 cat /var/log/messages |grep -i oom 查找系统日志中关于flume的指定日志 import osdef search_string_in_files(directory, search_string):count = 0

hdu4267区间统计

题意:给一些数,有两种操作,一种是在[a,b] 区间内,对(i - a)% k == 0 的加value,另一种操作是询问某个位置的值。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import

hdu4417区间统计

给你一个数列{An},然后有m次查询,每次查询一段区间 [l,r] <= h 的值的个数。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamRead

hdu3333区间统计

题目大意:求一个区间内不重复数字的和,例如1 1 1 3,区间[1,4]的和为4。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;

实例:如何统计当前主机的连接状态和连接数

统计当前主机的连接状态和连接数 在 Linux 中,可使用 ss 命令来查看主机的网络连接状态。以下是统计当前主机连接状态和连接主机数量的具体操作。 1. 统计当前主机的连接状态 使用 ss 命令结合 grep、cut、sort 和 uniq 命令来统计当前主机的 TCP 连接状态。 ss -nta | grep -v '^State' | cut -d " " -f 1 | sort |

Spring+MyBatis+jeasyui 功能树列表

java代码@EnablePaging@RequestMapping(value = "/queryFunctionList.html")@ResponseBodypublic Map<String, Object> queryFunctionList() {String parentId = "";List<FunctionDisplay> tables = query(parent