量化交易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.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 = 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以及自己查找都没得到准备的答案,很多时候,还是需要查源码才能解决问题。
随着不断的实践,之前许多不清楚不明白的地方也逐渐明亮了起来,在这个过程中,也不断发现前面学习发生的错误,有错误并不可怕,更正了错误才能更好的成长。
实践是检验真理的唯一标准!