量化交易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 = i
    code_ = 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=1
    strat = 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') # 年化收益率 01
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown') # 回撤 02
    # 03
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') # 夏普比率 04
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='_TradeAnalyzer') # 交易分析 05
    cerebro.addanalyzer(bt.analyzers.SQN, _name='_SQN') # 交易系统性能得分 SQN 06
    # 07,08,09,10
    cerebro.addanalyzer(bt.analyzers.Returns, _name='_Returns', tann=252) # 计算252日度收益 11
    cerebro.addanalyzer(bt.analyzers.VWR, _name='_VWR')  # 可变加权回报率 VWR 12
    # 13,14
    cerebro.addanalyzer(bt.analyzers.PeriodStats, _name='_PeriodStats') # 基本数据统计 15
    
    
    # 需要通过数据记录进行计算    
    cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='pnl') # 返回收益率时序数据    # 03
    cerebro.addanalyzer(bt.analyzers.PositionsValue, _name='_PositionsValue')  # position 08
    cerebro.addanalyzer(bt.analyzers.Transactions, _name='_Transactions')  # Transactions 09
    cerebro.addanalyzer(bt.analyzers.GrossLeverage, _name='_GrossLeverage')  # GrossLeverage 07
    cerebro.addanalyzer(bt.analyzers.PyFolio, _name='_PyFolio')   # 10

def analyzer_output(result):
    dic1 = {}
    
    sout01 = result.analyzers._AnnualReturn.get_analysis()  # 年化收益率 01
    for k,v in sout01.items():
        dic1[f'{k}年化']= v*100
        
    sout02 = result.analyzers._DrawDown.get_analysis()      # 回撤 02
    dic1['回撤'] = sout02['drawdown']
    dic1['最大回撤'] = sout02['max']['drawdown']

    sout04 = result.analyzers._SharpeRatio.get_analysis()       # 夏普比率 04
    dic1['夏普率'] = sout04['sharperatio']
    
    sout06 = result.analyzers._SQN.get_analysis()     # 交易系统性能得分 SQN 06
    dic1['系统性能SQN'] = sout06['sqn']
    
    sout12 = result.analyzers._VWR.get_analysis()     # # 可变加权回报率 VWR 12
    dic1['VWR'] = sout12['vwr']
    
    sout15 = result.analyzers._PeriodStats.get_analysis()  # # 基本数据统计 15
    dic1['平均'] = sout15['average']
    dic1['标准差'] = sout15['stddev']
    dic1['最佳收益'] = sout15['best']
    dic1['最大亏损'] = sout15['worst']
    
    sout11 = result.analyzers._Returns.get_analysis()    # 计算252日度收益 11
    dic1['年化回报率'] = sout11['rnorm100']
    
    sout05 = result.analyzers._TradeAnalyzer.get_analysis()   # 交易分析 05
    dic1['关闭交易'] = 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)/cstart
    qjzf2_pct = (cend-cstart)/cstart * 100
    dic1['区间涨跌'] = qjzf2
    dic1['区间涨幅'] = 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_zero
    dic1['持仓周期数'] = cnt_position
    dic1['持仓占比'] = 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() *100
    recent_6m = a2[-126:].sum() *100
    recent_1y = a2[-252:].sum() *100
    
    dic1['近1月'] = recent_1m
    dic1['近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_name
    dic_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 = None
        
        self.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 = None
         self.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:  # 检查是否有指令等待执行
             return
      
         if 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.tradeCnt
         print("当参数为 %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,4
  p3=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') #年化收益01
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown') # 回撤02
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') #夏普比率04

    cerebro.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=1
    print(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 = params
        for 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=1
    print(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.params
        print(params1.p1)
        print(params1.p2)
        print(params1.p3)

        analyzers = strat1.analyzers
        analyzers[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):
    #........
    @classmethod
    def _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 analysis

        The keys and format of analysis results in the dictionary is
        implementation dependent.

        It is not even enforced that the result is a *dict-like object*, just
        the convention

        The 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 3
        print(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.analyzers
        ana2 = 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') # 年化收益率 01
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown') # 回撤 02
    # 03
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') # 夏普比率 04
    cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='pnl') # 返回收益率时序数据    # 03

def analyzer_output_opt(result):
    list_analyzer = []
    for strat in result:
        dic1 = {}

        strat1 = strat[0]
        params1 = strat1.params
        str_params = '(%2d,%2d,%2d)'%(params1.p1,params1.p2,params1.p3)
        # print(str_params)
        dic1['策略'] = params1.stra_name
        dic1['参数'] = str_params
        
        analyzers = strat1.analyzers

        sout01 = analyzers[0].get_analysis()       # 年化收益率 01
        for k,v in sout01.items():
            dic1[f'{k}年化']= v*100                 

        sout02 = analyzers[1].get_analysis()       # 回撤 02
        dic1['回撤'] = sout02['drawdown']
        dic1['最大回撤'] = sout02['max']['drawdown'] 

        sout04 = analyzers[2].get_analysis()       # 夏普比率 04
        dic1['夏普率'] = sout04['sharperatio']

        sout03 = analyzers[3].get_analysis()       # 返回收益率时序数据    # 03
        a2 = pd.Series(sout03)
        
        # sharp
        sharpe_ratio = qs.stats.sharpe(a2, rf=0.02)  # 
        dic1['qs_sharp'] = sharpe_ratio
        
        recent_1m = a2[-21:].sum() *100             # 近1月
        recent_3m = a2[-64:].sum() *100
        recent_6m = a2[-126:].sum() *100
        recent_1y = a2[-252:].sum() *100

        dic1['近1月'] = recent_1m
        dic1['近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以及自己查找都没得到准备的答案,很多时候,还是需要查源码才能解决问题。

随着不断的实践,之前许多不清楚不明白的地方也逐渐明亮了起来,在这个过程中,也不断发现前面学习发生的错误,有错误并不可怕,更正了错误才能更好的成长。

实践是检验真理的唯一标准!

  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值