量化交易backtrader实践(四)_评价统计篇(5)_自定义评价

2024-09-05 01:12

本文主要是介绍量化交易backtrader实践(四)_评价统计篇(5)_自定义评价,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Analyzer应用Step-by-step

01_直接使用

直接使用是开始学习的时候会接触比较多,通过把cerebro.addStrategy()写出来,然后在.run()之后从result[0]中再取评价的数据,我们可以对这个运行的流程加深理解。

cerebro = bt.Cerebro()
# ......
cerebro.addstrategy(run_strategy)  # 添加策略
cerebro.adddata(data, name=code_)  # 添加数据# 添加评价
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='_AnnualReturn') # 年化收益率01
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') # 夏普比率04
# 其他评价...# ......
result = cerebro.run()     strat = result[0]# 获取评价数据
sout01 = result.analyzers._AnnualReturn.get_analysis()
sout04 = result.analyzers._SharpeRatio.get_analysis()
# 其他评价...

02_制作函数应用

当需要添加的评价比较多,或者需要得到的评价指标数据比较多时,制作两个函数是比较方便有效的做法。第一个函数是 add_analyzer_all,在添加评价的位置上替换掉所有评价的添加;第二个函数是analyzer_output,在.run()得到result后,对result[0]的内容进行处理,提取需要的数据并创建一个字典来保存。

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) # 计算日度收益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') # 08cerebro.addanalyzer(bt.analyzers.Transactions, _name='_Transactions')     # 09cerebro.addanalyzer(bt.analyzers.GrossLeverage, _name='_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']#......

在函数的应用过程中,我们也发现了一些问题,有的已经解决,有的还没解决。

解决的比如说如果是做参数优化,它就与常规的回测不一样,包括cerebro.run()在内,很多地方会发生变化,特别是analyzer_output内部。并且,参数优化得到的results是个list,有多少组参数参加就会有多少个result。在每一个result里,可以用.params获取参数,也可以用.analyzers获取评价结果;而评价结果在这里的顺序是与add_analyzer时一一对应的,不能错,而且似乎不能使用getbyname的方法等。这些看起来跟正常回测的评价还是不同的,因此评价输出的函数需要有2个,一个给常规回测用,另一个给参数优化使用。

import quantstats as qs
from backtrader import Analyzer, TimeFrame
def add_analyzer_all_opt(cerebro):cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='_AnnualReturn') # analyzers[0]cerebro.addanalyzer(bt.analyzers.DrawDown, _name='_DrawDown')  # analyzers[1]cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio') # analyzers[2]cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_Sharpe4',timeframe=TimeFrame.Days,riskfreerate=0,daysfactor=252,convertrate=False,factor=252) # analyzers[3]cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='pnl') # analyzers[4]def analyzer_output_opt(result):list_analyzer = []for strat in result:dic1 = {}strat1 = strat[0]params1 = strat1.params    # 取paramsstr_params = '(%2d,%2d,%2d)'%(params1.p1,params1.p2,params1.p3)# print(str_params)dic1['策略'] = params1.stra_namedic1['参数'] = str_paramsanalyzers = strat1.analyzers  # 取analyzers结果sout01 = analyzers[0].get_analysis()       for k,v in sout01.items():dic1[f'{k}年化']= v*100                 sout02 = analyzers[1].get_analysis()       dic1['回撤'] = sout02['drawdown']dic1['最大回撤'] = sout02['max']['drawdown'] sout04 = analyzers[2].get_analysis()       dic1['夏普率_default'] = sout04['sharperatio']sout042 = analyzers[3].get_analysis() dic1['夏普率_2'] = sout042['sharperatio']sout03 = analyzers[4].get_analysis()       a2 = pd.Series(sout03)recent_1y = a2[-252:].sum() *100dic1['近1年'] = recent_1y list_analyzer.append(dic1)return list_analyzer

评价指标的参数

另外一个解决的问题就是改变评价指标参数的默认值。

以夏普比率为例,之前全部都是默认值,有一个计算结果出来我们就觉得OK了,后来在使用empyrical和quantstats的过程中,发现它们计算得到的夏普率数据并不相等,某一些数值相近,另外有些数值差异比较大。夏普率的计算公式都是一样的,那么差别出在哪里?有可能在参数设置。

策略参数2023年化2024年化夏普率qs_sharp近6月近1年
经典KD交叉( 8, 2, 3)40.01097911.8447701.7700551.77243510.54820327.697481
经典KD交叉( 8, 3, 3)33.6357696.0653751.3674501.3674495.13649721.761362
经典KD交叉( 8, 4, 3)28.9504233.3149831.1806081.1247282.57326413.866847
经典KD交叉( 9, 2, 3)43.5853185.3790401.2292311.6116064.57158425.490329
经典KD交叉( 9, 3, 3)35.8520674.5358261.2258141.3803903.67702721.301610
经典KD交叉( 9, 4, 3)22.8904256.9616501.7485381.0488586.15269012.667302

 我们打开源码“\backtrader\analyzers\sharpe.py”文件,找到它的参数设置的部分

    params = (('timeframe', TimeFrame.Years),('compression', 1),('riskfreerate', 0.01),('factor', None),('convertrate', True),('annualize', False),('stddev_sample', False),# old behavior('daysfactor', None),('legacyannual', False),('fund', None),)RATEFACTORS = {TimeFrame.Days: 252,TimeFrame.Weeks: 52,TimeFrame.Months: 12,TimeFrame.Years: 1,}

 再把它的内容喂到AI里要中文解析,得到的部分解析说明如下

  • timeframe: 表示分析的时间范围,默认为年(Years)。
  • compression: 用于子日(sub-day)时间范围的压缩因子,默认为1。
  • riskfreerate: 无风险利率,默认为1%(以年度表示)。
  • convertrate: 如果为真,则将无风险利率从年度转换为月度、周度或日度利率。
  • factor: 转换因子,用于从年度无风险利率转换到选定的时间范围。
  • annualize: 如果为真,并且convertrate也为真,则最终计算出的夏普比率将以年度化形式给出。
  • stddev_sample: 如果为真,在计算标准差时会应用贝塞尔修正(Bessel's correction),即在计算平均值时分母减一。
  • daysfactor: 这是factor的旧命名方式,如果设置且时间范围为天(Days),则认为是旧代码。
  • legacyannual: 如果为真,使用AnnualReturn分析器,这仅适用于年度数据。
  • fund: 如果为None,则自动检测经纪人的基金模式;可以显式设置为真或假。

 而在quantstats中的sharp,我们看到默认是periods=252,应该是相当于上面的TimeFrame.Days,而不是默认的TimeFrame.Years

def sharpe(returns, rf=0.0, periods=252, annualize=True, smart=False):pass

 从默认的参数似乎得到一个有点矛盾的参数设定,因为convertrate默认是True,按解析它会把无风险利率转换为月、周或日度,反正不是年度;但是Rp似乎又是以年度来计算的......所以我们可以尝试着更改它的参数设置来一探究竟。

​改变评价指标的参数

一开始自己把问题想复杂了,也受到第一次问AI回答的影响,一度在错误的道路上越行越远,其实backtrader已经把它做了简化处理,直接写上即可,这里把实践过程中的2个错误示例也列了出来,其中第1个其实是问AI得到的。

# 错误示例1
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio', params=dict(riskfreerate=0.02))# 错误示例2
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio', params['riskfreerate']=0.02))# 正确用法
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='_SharpeRatio', riskfreerate=0.01)

再知道了怎么更改评价指标的参数后,我们测试了几组不同更改的夏普率值,同时也跟Quantstats计算的夏普率做了一个对比,从结果上看,backtrader的sharpe计算某些参数单独更改不起效果,例如convertrate,legacyannual等,factor更改为252后计算值会变化,另外riskfreerate越大即无风险收益越高则夏普率数值会减小。

另外,根据quantstats计算的sharpe值,与backtrader不太匹配,我们把近1年的日收益率数值列出来后发现,quantstats明显的sharpe值与近1年的日收益率正相关,而backtrader的sharpe值与2024年化的值似乎正相关,所以backtrader的似乎是用年化收益来计算的。

参数2024年化夏普率_default夏普率_rf2夏普率_252qs_sharp近1年
( 8, 2, 3)11.8447701.7700551.6990481.8410621.82976027.697481
( 8, 3, 3)6.0653751.3674501.2949091.4399921.42457221.761362
( 8, 4, 3)3.3149831.1806081.1025911.2586251.18217313.866847
( 9, 2, 3)5.3790401.2292311.1768841.2815791.66804225.490329
( 9, 3, 3)4.5358261.2258141.1619501.2896791.43776121.301610
( 9, 4, 3)6.9616501.7485381.6229791.8740971.10492112.667302
(10, 2, 3)4.2108471.1784831.1228961.2340711.46142321.82432

03_制作类应用_自定义评价

001_直接继承自评价指标

类的应用,首先我们可以继承内置评价的类,然后可以简单的参数进行预设,也可以制作自己的方法将数据直接以字典的方式输出等。我们在源码中看到有很多评价指标,这些在第1节里我们一个个都进行了实践,也大体上明白它们的用法和得到的数据以及输出。

from .annualreturn import *
from .drawdown import *
from .timereturn import *
from .sharpe import *
from .tradeanalyzer import *
from .sqn import *
from .leverage import *
from .positions import *
from .transactions import *
from .pyfolio import *
from .returns import *
from .vwr import *from .logreturnsrolling import *from .calmar import *
from .periodstats import *

我们就以夏普率这个常用评价指标为例,新建的类继承bt.analyzers.SharpeRatio,所以类的成员及方法都是直接可以使用的,例如可以把它的无风险收益设为0.03,又例如可以重写get_analysis()函数或者新建一个get_my_dict()的方法等。

# 创建一个继承自backtrader.analyzers.SharpeRatio的自定义分析器类
class CustomSharpeRatio(bt.analyzers.SharpeRatio):params = (('riskfreerate', 0.03),  # 设置无风险利率为0.03)def get_my_dict(self):output = self.get_analysis()return dict(output)# .............
cerebro = bt.Cerebro(cerebro.addanalyzer(mySharpeRatio,_name='mySharpe')result = cerebro.run()    
strat = result[0]output = strat.analyzers.mySharpe.get_analysis()  # 两种方式
# output = strat.analyzers.getbyname('mySharpe').get_analysis()
print("output",output)op = strat.analyzers.mySharpe.get_my_dict()  # 自定义方法
print(op)-----------------------------------
output OrderedDict([('sharperatio', 0.8272985845005197)])
{'sharperatio': 0.8272985845005197}

002_自定义评价

参考文档: Analyzers - Backtrader

将官方文档的内容(现在都是偷懒的直接送到AI去解析)解析后得到的知识点如下:

  • 类名backtrader.Analyzer

  • 描述:所有分析器的基类,为策略提供分析

  • 自动设置成员属性

    • self.strategy:提供对策略及其所有可访问属性的访问
    • self.datas[x]:提供对系统中数据流数组的访问
    • self.data:等同于self.datas[0]
    • self.dataX:等同于self.datas[X]
    • self.dataX_Y:等同于self.datas[X].lines[Y]
    • self.dataX_name:等同于self.datas[X].name
    • self.data_name:等同于self.datas[0].name
    • self.data_Y:等同于self.datas[0].lines[Y]
  • 方法

    • __init__:实例化和初始设置
    • start():指示操作开始,用于设置所需的事项
    • stop():指示操作结束,用于关闭所需的事项
    • prenext():策略达到最小周期前,每次prenext调用时调用
    • nextstart():当策略首次达到最小周期时调用一次
    • next():策略达到最小周期后,每次next调用时调用
    • notify_cashvalue(cash, value):每次next循环前,接收现金/价值通知
    • notify_fund(cash, value, fundvalue, shares):接收当前现金、价值、基金价值和基金股份
    • notify_order(order):每次next循环前,接收订单通知
    • notify_trade(trade):每次next循环前,接收交易通知
    • get_analysis():返回包含分析结果的字典样对象
    • create_analysis():由子类重写,用于创建保存分析的结构
    • print(*args, **kwargs):通过标准Writerfile对象打印分析结果,默认写入标准输出
    • pprint(*args, **kwargs):使用Python的pprint模组打印分析结果
    • len():返回分析器所操作策略的当前长度
  • 操作模式:开放模式,无偏好模式

  • 分析生成:可在next调用中生成分析,或在stop中结束时生成,甚至在notify_trade中生成

  • get_analysis重要性:必须重写以返回分析结果

  • 默认行为get_analysis返回由create_analysis方法创建的默认OrderedDict对象rets

 并且我们还可以直接把官方文档中的示例代码拿出来进行研究,在这里看到夏普率的评价(04)是会使用到AnnualReturn(01)这个评价的,在init里,self.anret = AnnualReturn(),最后在stop方法中,通过了self.anret进行了计算。从这里,我们是不是可以解答上面为什么backtrader计算的夏普率和quantstats计算出来的有差别了,qs计算时用到的数据是日收益率,而backtrader用的数据是年收益率。

from backtrader.analyzers import AnnualReturnclass SharpeRatio(Analyzer):params = (('timeframe', TimeFrame.Years), ('riskfreerate', 0.01),)def __init__(self):super(SharpeRatio, self).__init__()self.anret = AnnualReturn()def start(self):# Not needed ... but could be usedpassdef next(self):# Not needed ... but could be usedpassdef stop(self):retfree = [self.p.riskfreerate] * len(self.anret.rets)retavg = average(list(map(operator.sub, self.anret.rets, retfree)))retdev = standarddev(self.anret.rets)self.ratio = retavg / retdevdef get_analysis(self):return dict(sharperatio=self.ratio)

 多示例代码我们看到,它跟策略类的写法很类似,都有__init__,start, next, stop等,看起来评价指标的计算往往都是在stop里进行的,最后它会有一个get_analysis()方法得到评价指标的输出。

并且在一个评价指标类里面,是可以很轻松的调用其他评价指标类中的数据和方法,比如上面的self.anret = AnnualReturn() 。

003_自定义类添加多个内置评价

于是我们设想做一个自定义类,把需要的评价指标都加进去的那种。

先来做一个放了三个评价指标的简单实践

from backtrader.analyzers import TimeReturn, SharpeRatio, VWRclass MyAnalyzer(bt.Analyzer):def __init__(self, data):self.data = data# 创建内置Analyzer的实例self.timereturn = TimeReturn()self.sharperatio = SharpeRatio()self.vwr = VWR()def start(self):# 启动内置Analyzerself.timereturn.start()self.sharperatio.start()self.vwr.start()def stop(self):# 停止内置Analyzerself.timereturn.stop()self.sharperatio.stop()self.vwr.stop()def get_analysis(self):# 获取内置Analyzer的分析结果return {"timereturn": self.timereturn.get_analysis(),"sharperatio": self.sharperatio.get_analysis(),"vwr": self.vwr.get_analysis()}

运行后结果:

out1 {'timereturn': OrderedDict([(datetime.datetime(2023, 1, 11, 0, 0), 0.0), ......(datetime.datetime(2024, 7, 16, 0, 0), -0.002108766373780102)]), 'sharperatio': OrderedDict([('sharperatio', 0.4870776964306147)]), 'vwr': OrderedDict([('vwr', 4.415307366076662)])}

这里,在自定义评价类 MyAnalyzer中,添加了包括 timereturn, sharperatio, vwr在内的三个内置评价指标,在__init__()中创建了内置评价指标的实例,在start中手动将每个内置评价start(),在stop中又手动将每个内置评价stop(),最后在get_analysis中返回3个内置评价的get_analysis()的结果。

其实,我们去看sharperatio的源码,它也没有start方法,也没有去给AnnualReturn做start()或者stop(),所以,中间的两段def start() 和 def stop()都不需要。

00_不使用Analyzer

这里的不使用,一方面是可能初学的时候还不知道有哪些内置的评价指标,但就是着急做一些常见的评价出来,比如说胜率、盈亏比等,这些计算都很简单,因此不使用Analyzer也能做的出来;另一方面是不会使用results,特别是遇到参数优化就不知道评价分数放在哪里,还不会用。

于是,可以把评价放到了策略类的stop()里,并且根据notify_trade的信息,进行简单的评价计算。

另外,当我们实践学习了借用第三方库例如empyrical或quantstats进行评价计算,通常只需要一项数据即日收益率就可以了,这项数据计算也很简单,第3节也实践过,用pandas一个函数就搞定。

df['pct_change'] = df['close'].pct_change()

在这样的基础上,我们还可以获取基准的日收益数据,这个基准可以是大盘,沪深300,创业板指数等指数,其实也可以是当前这支股票买入不动它自身的基础日收益率,有了基准就可以计算各种其他评价例如阿尔法-贝塔值等。

本篇结束

由上,需不需要使用backtrader的Analyzer,看自己的需求。

在进行了一轮评价统计篇的实践之后,我们加深了对backtrader以及评价指标的理解,也学会了使用其他的库来计算指标以及可视化操作,我们可以创建自定义的评价类来应用内置或者借用甚至自己写计算公式来制作评价。

得到了一堆评价分数后,那我们对当前股票 vs. 策略 vs. 参数就有了一个数据分布,前面提到过或许某些股票就适合某些特殊的策略,某些股票就适合某些特殊的参数设定,所以我认为应该先找到各自的良配。

这篇关于量化交易backtrader实践(四)_评价统计篇(5)_自定义评价的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

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

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

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

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分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

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 |