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

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

解决的比如说如果是做参数优化,它就与常规的回测不一样,包括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    # 取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  # 取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() *100
        dic1['近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 AnnualReturn

class 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 used
        pass

    def next(self):
        # Not needed ... but could be used
        pass

    def 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 / retdev

    def 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, VWR

class MyAnalyzer(bt.Analyzer):
    def __init__(self, data):
        self.data = data
        # 创建内置Analyzer的实例
        self.timereturn = TimeReturn()
        self.sharperatio = SharpeRatio()
        self.vwr = VWR()

    def start(self):
        # 启动内置Analyzer
        self.timereturn.start()
        self.sharperatio.start()
        self.vwr.start()

    def stop(self):
        # 停止内置Analyzer
        self.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. 参数就有了一个数据分布,前面提到过或许某些股票就适合某些特殊的策略,某些股票就适合某些特殊的参数设定,所以我认为应该先找到各自的良配。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值