量化交易backtrader实践(二)_基础加强篇(4)_交易设置与自定义绘图

news2024/11/15 10:52:30

这一节我们来深入实践交易的设置以及自定义绘图。

01_交易设置

参考文档:

  • Backtrader系列教程④:交易篇(上)_backtrader撤单操作-CSDN博客
  • 量化投资之工具篇一:Backtrader从入门到精通(8)-交易系统代码详解-CSDN博客

001_股票软件的交易评测系统

炒股的人电脑上通常都会有几个PC版的股票软件,这些软件现在也在与时俱进做了量化交易的功能,可以在软件上进行量化模拟回测(免费)以及量化交易(收费)。我们暂且不谈这些新增的量化交易的部分,先看一下经典的交易评测系统的构成以及进行评测前有哪些设置以及与backtrader所对应的功能:

A_建仓规则

包含评测时间段的选择,对应到backtrader里就是feeds.PandasData()中的fromdate和todate,这里可以看到它们很细心的设置了向前多取100个数据用于计算,这主要是对于均线比如60日需要到第60天才会有计算数据的小问题,所以我们的交易策略里是不是也需要加入这个功能?

另外默认评测结束时强制平仓计入收益,这也对应了backtrader里如果是isopen的状态不会计算利润的问题,这个在上一节我们是添加了判断语句做成了浮动盈亏和浮动交易成功率,显示出来会更清楚些,毕竟强制平仓并没有遵守策略。

建仓规则里的评测公式,就对应backtrader的指标判断了,而分配资金对应setCash(),开仓使用多少资金或固定数量对应sizer中的固定数量和固定百分比。

股票软件中对于连续信号不再买卖,这是因为它的平仓规则相对简单,也没有一次买入必须对应一次卖出的要求,这个在backtrader中是没有这个问题的。

B_交易方式

首先是手续费,可以看到手续费的设置也是比较简单的示意做法,并没有与实际相符合,我们先忽略关于期货的部分,股票交易通过是双向收取佣金,卖出时还要收取印花税,另外还有一个不免5的规则即手续费最低5元。另外ETF是近来出现的,它不需要收取印花税,这里没有这个选择,我们在backtrader里也会回测ETF,所以需要添加的。

介入价位这里就很有意思了,可以有很多的选择例如本周期收盘价(信号触发当天收盘价),本周期中价,本周期开盘价,最高价,最低价;次周期(第二日)收盘价,开盘价...... 默认都是本周期收盘价。这个在backtrader里就有比较科学的分辨,它是这么定义的,正常的情况下,今天收盘后才能确认信号触发,所以成交是第二天的开盘价。

滑点设置,这个在backtrader中对应Slippage滑点设置。

C_平仓规则

可以看到,股票软件中对于平仓的规则非常的简陋,采用的是固定止盈,固定止损和固定周期平仓这三种,并没有使用到指标的卖出信号,在这一点上,backtrader的回测就相对要高级和严谨的多了,这也是为什么我们没在股票软件里评测系统而选择用backtrader的一个重要因素。

其他设置中,计算相对收益率选择了默认的沪深300,这里可以选择任何一个指数或股票做基准,目的是计算类似于阿尔法-贝塔的评价指标,它们需要有一个基准才能进行计算。

002_backtrader的交易设置

看完了股票软件中的交易评测系统的设置,再回过头来学习和实践backtrader里的交易设置,就会发现很多问题迎刃而解了。

A_持仓与交易规模

这里有个单词是position,金融行业翻译过来叫做"头寸",在百度里查了一下,据说是跟袁大头(银元)有关系,十个银元叠起来的高度刚好是一寸,大家就用"头寸"这个词来形容持有的资产。我还是喜欢称它为"持仓",感觉更容易明白是什么。

假设在回测中设定了setCash(100000),那么开始的时账户里就有10万的现金,这个跟position没关系,只有进行了买入操作才会有position的产生。注意一点,为什么我们到现在的程序中next()函数必须有一句 if not self.position的判断?

    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()  # 执行卖出

这是因为期货以及其他国家的股票是可以卖空的,没有持仓这个条件限制,那么先触发了sell的条件就会进行卖空操作了,其实在下跌趋势过程中卖空是很赚钱的,但是由于规则限制,我们只能先买入,这是对应现实的规矩,那么在backtrader中回测就要按着实际的情况来。

position有两个重要的信息:size,规模;price,价格。

我们看到在股票软件建仓规则里分2种交易数量的控制,一种是固定数量,另一种是按百分比(全仓100%,部分可以设置比如50%)。在backtrader里,这两种规模都是可以设置的。

于是,我们把run_main_plot_01中的addsizer替换掉,重命名为run_main_plot_02

    cerebro.addsizer(bt.sizers.SizerFix,stake=2000)  # 设置固定2000股
run_main_plot_02(mystrategy5,df_stock_list[1], myStockList[1])

----------------------
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 89040.00, Comm :89.04,size :2000.00
2022-06-06, 卖出, Price: 46.42, Comm 92.84
2022-06-06, OPERATION PROFIT, 毛利润 3800.00, 净利润 3618.12
2022-08-02, 买入, No.002179, Price: 47.08, Cost: 94160.00, Comm :94.16,size :2000.00
2022-09-15, 卖出, Price: 50.28, Comm 100.56
2022-09-15, OPERATION PROFIT, 毛利润 6400.00, 净利润 6205.28
.......

设置成固定数量操作非常简单,但并不符合实际需求,因为市场上的股票价格区间差异很大,有的1块钱1股,有的要几百块,这样股票与股票之间就不能进行有效的对比了。因此,我们更倾向于使用资金百分比的设置,这样各股票持仓就差不多(size * price)。

前面我们一直使用的就是资金的50%规模买入,使用的语句如下,

cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%

-------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 50000.00, Comm :50.00,size :1123.09
2022-06-06, 卖出, Price: 46.42, Comm 52.13
2022-06-06, OPERATION PROFIT, 毛利润 2133.87, 净利润 2031.74
2022-08-02, 买入, No.002179, Price: 47.08, Cost: 51299.17, Comm :51.30,size :1089.62
2022-09-15, 卖出, Price: 50.28, Comm 54.79
.............

在这种情况下,100000的资金的时候,cost=50000,size=1123.09,当卖出有了利润后,继续以50%的资金买入,cost = 51299,size=1089,size降低因为price从44.52增加到了47.08。

这种模式的sizer是可以用的,但远远不够准确,因为A股的最小买入成交量是100股,所以我们还不能这么简单的使用PercentSizer,需要自定义一个更符合实际的。

# 自定义percent_sizer
class MyPercentSizer2(bt.Sizer):  
    '''This sizer return percents of fixed cash 
       My sizer will clip min size to 100  

    '''
    params = (
        ('percents', 20),
        ('retint', False),  # return an int size or rather the float value
    )                           
        

    def __init__(self):
        pass

    def _getsizing(self, comminfo, cash, data, isbuy):   # 重写这个函数
        position = self.broker.getposition(data)
        if not position:
#             size = cash / data.close[0] * (self.params.percents / 100)  # (资金 * 20%)/ 收盘价
            size_raw = 100000 / data.close[0] * (self.params.percents / 100)
            size = round(size_raw /100.0, 0) * 100    # 取最小100整数
        else:
            size = position.size

        if self.p.retint:
            size = int(size)

        return size

而在run_main_plot中,需要更改代码如下:

#     cerebro.addsizer(bt.sizers.PercentSizer, percents=50) 
    cerebro.addsizer(MyPercentSizer2,percents=50)

再次运行回测输出如下,每次买入的是接近资金50%的100股的整数倍,与A股的规则就同步了。

run_main_plot_03(mystrategy5,df_stock_list[1], myStockList[1])

----------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 48972.00, Comm :48.97,size :1100.00
2022-06-06, 卖出, Price: 46.42, Comm 51.06
2022-06-06, OPERATION PROFIT, 毛利润 2090.00, 净利润 1989.97
2022-08-02, 买入, No.002179, Price: 47.08, Cost: 51788.00, Comm :51.79,size :1100.00
2022-09-15, 卖出, Price: 50.28, Comm 55.31
2022-09-15, OPERATION PROFIT, 毛利润 3520.00, 净利润 3412.90
....................

B_交易手续费与滑点

在实际交易中,由于市场波动、网络延迟等原因,指定的交易价格与实际成交价格会存在差别,出现滑点

滑点是指交易的预期价格与交易实际执行价格之间的差异。滑点可能随时发生,在使用市价单时市场波动较大的时期最为普遍。当执行大订单但所选价格没有足够的交易量来维持当前的买卖差价时也会发生这种情况。这个对于个人交易者作用实在不大,咱们那点钱,还不至于能影响市场,滑点可能性比较小。

为了让回测结果更真实,在交易前可以通过 brokers 设置滑点,滑点的类型有 2 种:

  • 百分比滑点
  • 固定滑点

不论哪种设置方式,都是起到相同的作用: 即买的 “更贵”,卖的 “更便宜” 。

cerebro = bt.Cerebro() 
#......
cerebro.broker.set_slippage_perc(perc=0.01)  # 设置1%的滑点

我们用同一个数据分别有滑点和没滑点的回测对应买入和卖出位置进行对比

不使用滑点设置 1%的滑点

组合期初资金: 100000.00

------------- next 的第19次循环 --------------

当前持仓量 0 当前持仓成本 0.0

2023-03-24, 已买入, 价格: 10.70, 费用: 10700.00

------------- next 的第27次循环 --------------

当前持仓量 1000 当前持仓成本 10.7

2023-04-06, 已卖出, 价格: 10.30, 费用: 9630.00

组合期初资金: 100000.00

------------- next 的第19次循环 --------------

当前持仓量 0 当前持仓成本 0.0

2023-03-24, 已买入, 价格: 10.81, 费用: 10807.00

# 原价为10.7 取1%为 0.107

# 即 原价*(1.01) = 10.7*(1.01) = 10.81

------------- next 的第27次循环 --------------

当前持仓量 1000 当前持仓成本 10.807

2023-04-06, 已卖出, 价格: 10.20, 费用: 9726.30,

# 原价10.30 所以 10.30*(0.99) = 10.20

手续费是在进行金融交易或使用某些服务时,由服务提供者收取的费用。手续费的计算标准通常基于多种因素,包括交易金额、交易类型、支付方式、商户类型、支付平台以及交易性质等。

在前面的实践中,我们一直使用的是一个简单设置的百分比费用:


cerebro.broker.setcommission(commission=0.001)  # 设置交易佣金

这就是设定了一个双向收取的千分之一的手续费。但这个实在太简单了,不能与实际的交易相符。

交易时是否考虑交易费用对回测的结果影响很大,所以在回测是通常会设置交易税费,不同标的的费用收取规则也各不相同:

  • 股票:目前 A 股的交易费用分为 2 部分:佣金和印花税,其中佣金双边征收,不同证券公司收取的佣金各不相同,一般在 0.02%-0.03%(万二) 左右,单笔佣金不少于 5 元;印花税只在卖出时收取,税率当前为 0.05%(万五)。
  • 期货:期货交易费用包括交易所收取手续费和期货公司收取佣金 2 部分,交易所手续费较为固定,不同期货公司佣金不一致,而且不同期货品种的收取方式不相同,有的按照固定费用收取,有的按成交金额的固定百分比收取:合约现价*合约乘数*手续费费率。
  • ETF:ETF即交易所交易基金(Exchange Traded Fund),是一种特殊类型的开放式基金。中国大陆的税务政策规定,在二级市场买卖ETF时不征收印花税. 这是ETF相较于其他投资产品的一个优势。

考虑到目前自己的情况,我们暂时只做A股的回测(股票和ETF),所以根据交易费用的设置,把佣金和印花税按实际情况计算出来。这个只要做一次就行,后面的所有策略都不会去改变它。最麻烦的可能就是在回测时要判断是股票还是ETF。

我们不在commission的部分啰嗦,想深入了解详细信息的可以去看参考的文档或者源码,这里就直接把税费管理类做出来

### 税费管理类
class StockCommission(bt.CommInfoBase):
    params = (
            ( 'stocklike', True), # 指定为股票模式
            ( 'commtype', bt.CommInfoBase.COMM_PERC), # 使用百分比费用模式
            ( 'percabs', True), # commission 不以 % 为单位
            ( 'isETF',False),    # 是否是ETF
            ( 'stamp_duty', 0.0005),) # 印花税
 
    # 自定义费用计算公式
 
    def _getcommission(self, size, price, pseudoexec):
        comm1 = max(abs(size) * price * self.p.commission, 5)
        if size > 0: # 买入时,只考虑佣金
            return comm1
        elif size < 0: # 卖出时,同时考虑佣金和印花税
            if self.p.isETF == False:          # 股票要计算印花税
                return comm1 + abs(size) *price *self.p.stamp_duty
            else:
                return comm1                   # ETF不计算印花税
        else:
            return 0

然后在run_main_plot函数中,把原来的语句替换掉,这里用参数简单判断是否是股票列表中的来确定ETF还是股票,后续再做细节的调整

def run_main_plot_05(strategy1, df_data0,name1):
    cerebro = bt.Cerebro()                           # 创建Cerebro引擎
    # 将数据源设置为PandasData,并加载数据
    data = bt.feeds.PandasData(dataname=df_data0,fromdate=datetime(2022, 1, 15),todate=datetime(2024,9,11))
    cerebro.adddata(data,name=name1)       # 将数据添加到Cerebro


    cerebro.addstrategy(strategy1)                  # 添加策略

    cerebro.broker.setcash(100000.0)                # 设置初始资金

    # cerebro.broker.setcommission(commission=0.001)  # 取消初始设置交易佣金
    
    if name1 in myStockList:
        comm_etf = False
    mycomm = StockCommission(commission=0.0002,isETF=comm_etf)  # 佣金万分之二
    cerebro.broker.addcommissioninfo(mycomm)     # 佣金设置
    
    cerebro.addsizer(MyPercentSizer2,percents=50)

    print(f'Starting Portfolio Value: {cerebro.broker.getvalue():.2f}')  # 打印起始组合价值

    cerebro.run()  # 运行分析

    print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}')    # 打印最后组合价值
    cerebro.plot(style='candle')    # 绘制结果图




run_main_plot_04(mystrategy5,df_stock_list[1], myStockList[1])  # 默认千一佣金

---------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 48972.00, Comm :48.97,size :1100.00
2022-06-06, 卖出, Price: 45.96, Comm 50.55
2022-06-06, OPERATION PROFIT, 毛利润 1579.38, 净利润 1479.86
........

run_main_plot_05(mystrategy5,df_stock_list[1], myStockList[1])  # 新的手续费

---------------------
Starting Portfolio Value: 100000.00
2022-03-22, 买入, No.002179, Price: 44.52, Cost: 48972.00, Comm :9.79,size :1100.00
2022-06-06, 卖出, Price: 46.42, Comm 35.74
2022-06-06, OPERATION PROFIT, 毛利润 2090.00, 净利润 2044.46

C_交易时机管理

交易时机管理就对应于股票软件的交易价位选择。对于交易订单生成和执行时间,Backtrader 默认是 :  “当日收盘后下单,次日以开盘价成交”,这种模式在回测过程中能有效避免使用未来数据。

为了应对一些特殊交易场景,Backtrader 还提供了一些 cheating 式的交易时机模式:

  • Cheat-On-Open 是“当日下单,当日以开盘价成交”模式, 在该模式下,Strategy 中的交易逻辑不再写在 next 方法里, 而是写在特定的 next_open、nextstart_open 、prenext_open 函数中
  • Cheat-On-Close 是“当日下单,当日以收盘价成交”模式, 在该模式下,Strategy 中的交易逻辑仍写在 next 中

实际情况下,只有收盘当时这根K线走完了才能指标触发(策略如果是分钟线相同),这个时候已经无法买入了,真实的情况就应该是第二根时间一出来就买入。

联想一下很多短视频里大V们讲指标的时候总是故意避开这个交易时机的问题,有时甚至把指标向日K线对齐的时候还偏了那么一点,造成指标提前的假象或者让你感觉你能在涨起来的开盘价(甚至是最低价)买入,在计算盈亏的时候也是从这根红柱的实体底部开始计算买入而在高位下跌的顶部计算卖出,这就会造成这个指标能赚很多的假象。 而事实上永远不可能。

在股票软件中从来不会讨论这个,但实际上这个非常的重要,很多的时候指标触发买入是因为当天的K线涨得足够高,计算下来指标就触发了,但是等你看到它涨起来了再买进去,它第二天又下跌或者就是一直横盘了。如果你选择了cheat的模式,那评测得到的收益可能是高的,所以在这种实际问题上,使用backtrader进行回测就可以更接近实际情况,数据真实有效。

这也是为什么在绘制的图形上,crossover的信号已经触发了,但向上或向下的箭头会在下一个交易日才出现,特别是A股现阶段容易走抵消K线,就是那种昨天大涨,今天大跌恰好抵消回到前天收盘的位置,那么真实情况下,指标触发了才买入,接着第二天买进就大跌,第三天卖出又涨了进入恶性循环圈,散户往往就是这么亏了大钱。

我们必须要知道以当天的开盘或收盘成交都属于cheat模式。很多讲指标的都是使用了这个模式。不过对于日线交易来说,我们会比较多的在2:30之后感觉K线没有悬念时就买入,近似可以认为买在收盘价,我个人觉得是可以应用这种情况的。

设置Cheat只需要一句代码,若是cheat-on-open则需要在next_open里写策略,这显然很麻烦。综上,我们还是设置一下cheat-on-close。

cerebro.broker.set_coc(True) # coc即Cheat On Close

# 原模式, 当天下单以第二天开盘价成交

------------- next 的第189次循环 --------------

卖单日期 2023-12-08 收盘价 12.49 明日开盘价 12.0

2023-12-11, 已卖出, 价格: 12.00, 费用: 10590.00, 佣金 17.00      

#以第二天开盘价成交

------------- next 的第236次循环 --------------

买单日期 2024-02-22 收盘价 8.83 明日开盘价 8.85

2024-02-23, 已买入, 价格: 8.85, 费用: 8850.00, 佣金 5.00

组合期末资金: 101316.36

# Cheat-On-Close模式, 第二天成交,但成交价是当天的收盘价

------------- next 的第189次循环 --------------

卖单日期 2023-12-08 收盘价 12.49 明日开盘价 12.0           #以收盘价成交

2023-12-11, 已卖出, 价格: 12.49, 费用: 10640.00, 佣金 17.49

------------- next 的第236次循环 --------------

买单日期 2024-02-22 收盘价 8.83 明日开盘价 8.85

2024-02-23, 已买入, 价格: 8.83, 费用: 8830.00, 佣金 5.00

组合期末总资产: 101595.93

从上面的对比输出结果可以看到,使用了Cheat-On-Close模式后,成交价是当天的,但成交日期仍然是第二天的。

D_订单类型

关于订单类型,直接问AI得到:

以下是Backtrader中一些常见订单类型的代码示例和解释:

  1. 市价单 (Market Order): 市价单以当前市场价格立即执行。在Backtrader中,如果没有指定exectype,那么默认就是市价单。市价单的价格通常由下一个Bar的开盘价决定。

    # 市价单,以默认的持仓大小购买
    order = self.buy()
  2. 限价单 (Limit Order): 限价单只有在市场价格达到或更好于指定的price时才会执行。限价单可以设置有效期valid

    # 限价单,设置价格和有效期
    order = self.buy( exectype=Order.Limit, 
                      price=self.data.close[0] * 1.02, 
                      valid=datetime.datetime.now() + datetime.timedelta(days=3))
  3. 收盘价单 (Close Order): 收盘价单会在下一个Bar实际收盘时以收盘价执行。

    # 收盘价单
    order = self.buy(exectype=Order.Close)
  4. 止损单 (Stop Order): 止损单在市场价格达到或超过指定的price时触发,并以市价单的方式执行。这通常用于在价格达到某个不利水平时退出市场。

    # 止损单,设置触发价格和有效期
    order = self.buy(exectype=Order.Stop, 
                     price=self.data.close[0] * 1.02, 
                     valid=datetime.datetime.now() + datetime.timedelta(days=3))
  5. 止损限价单 (Stop Limit Order): 止损限价单在市场价格达到或超过指定的price时触发,然后以plimit价格或更好的价格执行。这种订单结合了止损单和限价单的特点。

    # 止损限价单,设置触发价格和限价
    order = self.buy(exectype=Order.StopLimit, 
                     price=self.data.close[0] * 1.02, 
                     plimit=self.data.close[0] * 1.07)
  6. 取消订单 (Cancel Order): 取消订单用于撤销之前提交的订单。

    # 取消已存在的订单
    self.cancel(order)

直接看订单类型可能是有点懵的,这个东西要么在期货交易模拟盘中交易一下,要么在股票软件APP中用一下“条件单”这个功能,经过这两个实践以后再回来看订单类型就迎刃而解了。

看一下止损单,当资产触及设定的止损价格时触发。这里重要的是学到在任何情况下要设置止损,股票交易总是有亏有盈,但是不设止损就交易是非常危险的行为,错了就要认。

条件单最基本是的限价单和价格条件单,前者同上面order中的限价单,只设定1个值(都有时间有效范围),后者与限价止损单类似,先触发再以某价格成交。有的软件的条件单功能比较多,比如日涨跌幅买卖,反弹买,回落卖,定时买卖,还有的自带止盈止损设置,设定好了严格执行也是非常不错的选择。其实有些软件的条件单已经有很不错的量化或自动交易的功能在里面了,比如说网格交易。

003_交易设置与交易系统

由上面的股票软件的交易评测系统和backtrader的交易设置,我们在实践过程中发现了一些有趣的知识点。如果不自己在backtrader里面实践一番,我们就很可能跟股票软件的评测系统一样,忽略了某些其实很关键的因素。

比如说佣金的问题,不免5情况下,你的真实收益会被折扣很多,再比如期货交易模拟盘里是不设佣金的,而实际上手续费高的吓死人,特别是日内交易,有时候连赚的钱连手续费都不够......

比如说介入价位,讲指标的总是在说"你看指标触发买点了,这个时候你买进去,那这一波的涨幅你就全都拿下了",其实的情况是指标触发时可能已经涨完了。再比如金叉的交叉位置,是昨天和今天的连线,所以显示在图上是在今天之前的,给人的错觉就是指标先触发了,但其实还是今天大涨了以后,才会得到计算出的短周期上穿长周期,才会在昨天和今天之间产生一个金叉。

再比如说滑点,从滑点的概念,我们需要学习到的主要是交易过程中的最高/低价格和此价格的成交量的关系,很多时候,你已经成功设定到了当天的最低价买入,订单也submitted了,但是盯着它看了半天,它就是没有成交。实际的交易中,我们常遇到价格对了,但成交不了,以前我们总是拍大腿认为自己运气不好,其实还是交易系统认知不到位。

backtrader是个很不错的工具,通过它我们可以一步一步认识到交易上的问题、难点和关键点,逐渐建立起自己的交易系统,逐渐完善自己的交易系统。至少实践到这里,我们听别人讲指标时能有一个客观的评判,不再那么单纯的只要是坑就掉进去了。

02_绘图增强

参考文档:

  • Backtrader系列教程⑦:可视化篇(重构)_backtrader benchmark-CSDN博客
  • 《BackTrader量化交易图解》第8章:plot 绘制金融图_backtrader plot-CSDN博客
  • Backtrader 文档学习- Plotting_backtrader plot-CSDN博客

这里提一下,参考文档第1个里代码区的显示不知道出了什么问题,我是用BS4把DIV中有文本的给抓出来,重新调整(去空行,去空格)以后才看的。

001_观测器observers

A_观测器模块与绘图

observers 是 Backtrader 的“观测器模块”,主要用于统计回测信息,并在 plot( ) 的帮助下实现信息的可视化展示。

Observers功能可视化
Broker记录了经纪商 broker 中各时间点的可用资金和总资产同时展示 cash 和 values 曲线
BuySell记录了回测过程中的买入和卖出信号会在价格曲线上标注买卖点
Trades记录了回测过程中每次交易的盈亏会绘制盈亏点
TimeReturn记录了回测过程中的收益率(每日)绘制 TimeReturn 收益曲线
DrawDown过程的回撤情况绘制回撤曲线
Benchmark记录了业绩基准的收益序列,业绩基准的数据必须事先通过 adddata同时绘制策略本身的收益序列和业绩基准的收益曲线

其中,Broker、Trades、BuySell 3个观测器,默认是自动添加给 cerebro 的。我们可以关闭它们再重新添加,否则就会在这3个观测器的基础上添加。

简单实践几种情况下的代码与图形如下

# 左上图
    cerebro = bt.Cerebro()                  # 创建Cerebro引擎,默认3个观测器


# 右上图
    cerebro = bt.Cerebro(stdstats=False)    # 创建Cerebro引擎,去掉默认观测器


# 左下图
    cerebro = bt.Cerebro()                           # 创建Cerebro引擎
    
    cerebro.addobserver(bt.observers.TimeReturn)
    cerebro.addobserver(bt.observers.DrawDown)

# 右下图
    cerebro = bt.Cerebro(stdstats=False)    # 创建Cerebro引擎,去掉默认观测器

    cerebro.addobserver(bt.observers.TimeReturn)
    cerebro.addobserver(bt.observers.DrawDown)
    

由上面结果可知,默认观测器是buy/sell, Broker,Trades这三个,如果默认Cerebro()则绘制的图形是顶部带有2个小图Broker和Trades,且主图里会显示buy/sell图标的;如果去掉默认(stdstats=False)则只显示主图和附图。

前面也谈到过我们对于国际的绿涨红跌非常不习惯,以及Trades显示的蓝和红圆不够直观,我们会把它们替换掉,这就需要先去掉默认的观测器的基础上再添加。

B_观测器与数据

从代码cerebro.addobserver(bt.observers.Broker)可以跳转到源码\observers\__init__.py

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

# The modules below should/must define __all__ with the Indicator objects
# of prepend an "_" (underscore) to private classes/variables

from .broker import *
from .buysell import *
from .trades import *
from .drawdown import *
from .timereturn import *
from .benchmark import *

from .logreturns import *

再任意点开一个观测器,以Broker为例

class Broker(Observer):
    '''This observer keeps track of the current cash amount and portfolio value in
    the broker (including the cash)

    Params: None
    '''
    _stclock = True

    params = (
        ('fund', None),
    )

    alias = ('CashValue',)
    lines = ('cash', 'value')

    plotinfo = dict(plot=True, subplot=True)

    def start(self):
        if self.p.fund is None:
            self._fundmode = self._owner.broker.fundmode
        else:
            self._fundmode = self.p.fund

        if self._fundmode:
            self.plotlines.cash._plotskip = True
            self.plotlines.value._name = 'FundValue'

    def next(self):
        if not self._fundmode:
            self.lines.value[0] = value = self._owner.broker.getvalue()
            self.lines.cash[0] = self._owner.broker.getcash()
        else:
            self.lines.value[0] = self._owner.broker.fundvalue

observers  中记录了各种回测数据,可以将其看作是一个支持可视化展示的数据存储器,所以 observers 属于 lines 对象。由上面源码可知,如果不是基金模式(当前我们在做股票回测,默认不是基金模式 fundmode = False),则observers中的next赋值的数据就是 broker.getvalue和broker.getcash的值,而Broker这个观测器有2条lines,分别为cash和value.

如果想在 Strategy 中读取 observers 中的数据,就会用到 line 的相关操作,observers 的数据通过 self.stats 对象 来连接。

# 请先确保有Broker观测器

#......
    def next(self):
        if not self.position:
            if self.crsup > 0:            
                print('buy Broker getvalue', self.broker.getvalue())
                print('observers getvalue', self.stats.broker.value[0])
                self.buy() 
        else:
            if self.crsdn > 0:
                print('sell Broker getvalue', self.broker.getvalue())
                print('observers getvalue',  self.stats.broker.value[0])
                self.sell()



-----------------------------
Starting Portfolio Value: 100000.00
buy Broker getvalue 100000.0
observers getvalue 100000.0
2022-10-12, 买入, No.002179, Price: 46.18, Cost: 50798.00, Comm :10.16,size :1100.00

sell Broker getvalue 97074.8404
observers getvalue 96414.8404
2023-06-30, 卖出, Price: 43.75, Comm 33.69
2023-06-30, OPERATION PROFIT, 毛利润 -2673.00, 净利润 -2716.85

......

从输出结果上看,使用self.broker和self.stats.broker都能得到值,另外buy之前的两者是相同的,但sell之前的value的值却不相等,看一些说明observers 是在所有指标被计算完之后、在执行 Strategy 的 next 方法之后才运行并统计数据的,所以某些情况下或许也有手续费的关系它们就不相等。

话说回来,在运行策略的时候,我们所需要的数据都是已经计算得到的,而observers的数据只是需要在图上绘制出来才额外准备的数据列表,所以除非绘图我们没有必要去取observers里的值,而有些时候如果有额外的绘图需求,我们需要自定义observers。

C_自定义observers

首先,内置的observers的确很少,除去buysell是用在主图上的标记,trades是显示交易的日期和盈亏外,其他的基本上都可以说是评价指标,比如收益率,回撤,基准收益等。除了这些评价指标外,还有许多其他的评价指标,比如说夏普率,MAR率,波动率,阿尔法-贝塔等,这些的确可以通过自定义observers添加到图中进行显示。

但是,我们也看到backtrader的图形的处理并不是非常的棒,而且很多指标需要用其他类型的图(比如热力图,直方图等)而不是散点图来显示,最重要的,已经有其他的库做好了评价指标的计算以及各种图形的可视化操作,我们可以直接把关键数据交给它们即可。

参考第四篇量化交易backtrader实践(四)_评价统计篇(3)_更多评价与可视化-CSDN博客。

所以,我们偷懒了,自定义observers就不做实践了。我们就拿着内置的Broker的代码分析一下:

class Broker(Observer):  # 继承自Observer

    _stclock = True      # 开启时钟属性

    params = (           # 只有一个参数 fund (基金模式)
        ('fund', None),
    )

    alias = ('CashValue',)     # 设置别名
    lines = ('cash', 'value')      # 重点,有2条lines,分别对应现金和资产值

    plotinfo = dict(plot=True, subplot=True)    # 绘图信息

    def start(self):
        pass                                    # 基金模式设置内容,不用看

    def next(self):               # 在next中给lines赋值
        if not self._fundmode:
            self.lines.value[0] = value = self._owner.broker.getvalue()
            self.lines.cash[0] = self._owner.broker.getcash()

其中主要的就是定义lines的内容,以及在next里进行赋值,params一般用于判断模式和辅助计算。

D_更换图标与颜色

我们对于国际的绿涨红跌的颜色系统已经烦它很久了,这里来改掉一部分,后面在plot()里再解决剩下的部分。

打开trades.py的源码,很容易看到绘图中plotlines = XXX里面有marker='o', color = 'blue'这样的设置,即画蓝色圆和红色圆。继续看下去,在MetaDataTrades类里面,有markers的列表和colors的列表,也就是最起码我们可以选取列表中的图标和颜色。

class Trades(Observer):

    _stclock = True

    lines = ('pnlplus', 'pnlminus')

    params = dict(pnlcomm=True)

    plotinfo = dict(plot=True, subplot=True,
                    plotname='Trades - Net Profit/Loss',
                    plotymargin=0.10,
                    plothlines=[0.0])

    plotlines = dict(
        pnlplus=dict(_name='Positive',
                     ls='', marker='o', color='blue',
                     markersize=8.0, fillstyle='full'),
        pnlminus=dict(_name='Negative',
                      ls='', marker='o', color='red',
                      markersize=8.0, fillstyle='full')
    )

    def __init__(self):
    .......


class MetaDataTrades(Observer.__class__):
    def donew(cls, *args, **kwargs):
       #.......

        # Generate plotlines info
        markers = ['o', 'v', '^', '<', '>', '1', '2', '3', '4', '8', 's', 'p',
                   '*', 'h', 'H', '+', 'x', 'D', 'd']

        colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', 'b', 'g', 'r', 'c', 'm',
                  'y', 'k', 'b', 'g', 'r', 'c', 'm']

        basedict = dict(ls='', markersize=8.0, fillstyle='full')

        plines = dict()
        for lname, marker, color in zip(lnames, markers, colors):
            plines[lname] = d = basedict.copy()
            d.update(marker=marker, color=color)

        plotlines = cls.plotlines._derive(
            uuid.uuid4().hex, plines, [], recurse=True)
        _obj.plotlines = plotlines()

        return _obj, args, kwargs  # return the instantiated object and args

于是我们用最省力的方法,直接继承它,然后改写它的plotlines

class my_Trades(bt.observers.Trades):
    plotlines = dict( pnlplus=dict(_name= 'Win', ls='',
                                   marker= '^', color= '#ed665d',
                               markersize= 8.0, fillstyle= 'full'),
                     pnlminus=dict(_name= 'Lose',ls='',
                                   marker= 'v', color= '#729ece',
                                   markersize= 8.0, fillstyle= 'full'))


--------

    cerebro.addobserver(my_Trades)

同理,直接继承buysell的类,然后改写plotlines

class my_BuySell(bt.observers.BuySell):  # 这个是继承BuySell的
    params = ((
                'barplot', True), (
                'bardist', 0.02))
    plotlines = dict( buy=dict(marker=r'$\Uparrow$', 
                                markersize=10.0, color='#d62728' ),
    sell=dict(marker=r'$\Downarrow$', 
                                markersize=10.0, color='#2ca02c'))  # 改为箭头



--------

    cerebro.addobserver(my_BuySell)

输出图形,可以看到Trades和buy/sell的图标和颜色都换掉了。图标还可以换成打勾打叉的不同型式,这里就不一一列举了。

E_基准收益benchmark

在__init__.py中,我们前面没有实践的还有logreturns和benchmark这两个,其中logreturns从名字上看就是对数收益,其使用方式与TimeReturn是一样的,只有benchmark这个与它们都不一样。

Benchmark” 在金融和投资领域通常被称为“基准”。它是一个参考点或标准,用于衡量投资的表现。基准通常是一个市场指数,如上证指数,沪深300指数,标普500指数、纳斯达克指数等,或者是某个特定的资产,如一只股票或债券。通过将投资的表现与基准进行比较,投资者可以评估投资策略的效果,是否跑赢了市场平均水平,或者是否需要调整策略。

因此,Benchmark一般是至少有2个data的数据,不然怎么能进行比较,代码上注意把data和bench_data千万要分开分清,包括adddata()时以及addobserver()时。这里补充一点,由于是做策略回测,TimeReturn得到的策略的执行结果,在策略中有时会空仓(空仓与股票本身的日收益就没有关系了),因此是可以在仅有一个data的情况下来比较使用/不使用策略的收益。

    cerebro = bt.Cerebro(stdstats=False)        # 关闭默认observers
    cerebro.addobserver(bt.observers.Broker)
    cerebro.addobserver(my_Trades)
    cerebro.addobserver(my_BuySell)

    data = bt.feeds.PandasData(dataname=df_data0,fromdate=datetime(2022, 1, 15))
    cerebro.adddata(data,name=name1)       # 将数据添加到Cerebro

    bench_data = bt.feeds.PandasData(dataname=df_index_list[0],fromdate=datetime(2022, 1, 15)
    cerebro.adddata(bench_data,name='bench')  # 额外添加bench_data(上证指数)

    cerebro.addstrategy(strategy1)                  # 添加策略

    cerebro.addobserver(bt.observers.Benchmark, data=bench_data )  # 添加bench_data到Observers

    #........ 
    cerebro.run()  # 运行分析

    cerebro.plot(style='candle')    # 绘制结果图

由上面代码绘制图形如下,从上往下数第三栏里就是benchmark的图线。

从这张图上,benchmark显示实在是没办法看,即使有办法把它的高度增大一些也看不清,所以一般情况下我们不会在backtrader中来使用benchmark的observers,在后续的评价篇里我们会去实践关于基准指标的数据,完全不需要通过observers或者Analyzers来计算,而是直接用pandasData计算出基准日收益率,然后放到第三方库里去计算例如阿尔法-贝塔系数等,也直接在第三方库里去显示各种不同的图形。

另外,如果只是想可视化对比基准收益的话,股票软件会更加的清晰。例如我们在主图上右键"叠加品种"--"叠加指定品种"--"沪深300"--按百分比显示,另外在副图上用公式显示(1.这支股票的日收益率,2.这支股票的累积日收益率, 3.沪深300的日收益率, 4. 沪深300的累积日收益率),则可以看到这支股票与基准之间的区别。

上面主图的灰色K线就是叠加的沪深300指数,而副图红线是这支股票的日收益,绿线就是沪深300的日收益,我们再把它们分别进行累加,就得到了累加日收益率,这支股票的累加用紫线表示,沪深300的用蓝线表示。从这张图可以看到,这支股票与基准的走势差别不是很大,也从侧面说明了这支股票基本上是跟随沪深300的趋势的。

往往从图上(视觉发现)就会冒出一个又一个的想法甚至闪光点,也等待着我们去验证它们,比如

  • 它们跟阿尔法-贝塔的关系?
  • 把副图数据直接导出给backtrader使用?或是在backtrader的策略中加入类似的指标?
  • 可不可以用股票的累加收益与某个基准的累加收益金叉死叉进行选股?
  • 可不可以给它再加一条平均线,做金叉死叉?
  • ......

这里先不继承深入了,与股票软件看来还有深度结合应用的方向,这里先简单记录一下它的代码

RET1:(C-REF(C,1))/REF(C,1),COLORRED,LINETHICK3;
ACCRET:SUM(RET1,252), LINETHICK3;

RBASE:("399300$C"-REF("399300$C",1))/REF("399300$C",1);
ACCBASE:SUM(RBASE,252),COLORBLUE;

002_绘图plot

A_绘图参数

plot() 中的参数主要用于系统性的配置图形,具体参数如下所示:

     plot(plotter=None, # 包含各种绘图属性的对象或类,如果为None,默认取 PlotScheme 类,如下所示
          numfigs=1, # 是否将图形拆分成多幅图展示,如果时间区间比较长,建议分多幅展示
          iplot=True, # 在 Jupyter Notebook 上绘图时是否自动 plot inline
          **kwargs) # 对应 PlotScheme 中的各个参数
    
     # PlotScheme 中的参数如下所示
     class PlotScheme(object):
         def __init__(self):
             # to have a tight packing on the chart wether only the x axis or also
             # the y axis have (see matplotlib)
             self.ytight = False
             # y-margin (top/bottom) for the subcharts. This will not overrule the
             # option plotinfo.plotymargin
             self.yadjust = 0.0
             # Each new line is in z-order below the previous one. change it False
             # to have lines paint above the previous line
             self.zdown = True
             # Rotation of the date labes on the x axis
             self.tickrotation =15
             # How many "subparts" takes a major chart (datas) in the overall chart
             # This is proportional to the total number of subcharts
             self.rowsmajor = 
             # How many "subparts" takes a minor chart (indicators/observers) in the
             # overall chart. This is proportional to the total number of subcharts
             # Together with rowsmajor, this defines a proportion ratio betwen data
             # charts and indicators/observers charts
             self.rowsminor = 
             # Distance in between subcharts
             self.plotdist = 0.0
             # Have a grid in the background of all charts
             self.grid = True
            
     # Default plotstyle for the OHLC bars which (line -> line on close)
             # Other options:'bar' and 'candle'
             self.style ='line'
             # Default color for the 'line on close' plot
             self.loc ='black'
            
             # Default colorfor a bullish bar/candle (0.75 -> intensity of gray)
             self.barup ='0.75'
             # Default color for a bearish bar/candle
             self.bardown = 'red'
             # Level of transparency to apply to bars/cancles (NOT USED)
             self.bartrans = 1.0
             # Wether the candlesticks have to be filled or be transparent
             self.barupfill = True
             self.bardownfill = True
             # Wether the candlesticks have to be filled or be transparent
             self.fillalpha = 0.20
             # Wether to plot volume or not. Note:if the data in question has no
             # volume values, volume plotting will be skipped even if this is True
             self.volume = True
             # Wether to overlay the volume on the data or use a separate subchart
             self.voloverlay = True
             # Scaling of the volume to the data when plotting as overlay
             self.volscaling = 0.33
             # Pushing overlay volume up for better visibiliy. Experimentation
             # needed if the volume and data overlap too much
             self.volpushup = 0.00
             # Default colour for the volume of a bullish day
             self.volup = '#aaaaaa'  #  0.66 of gray
             # Default colour for the volume of a bearish day
             self.voldown =  '#cc6073'  # ( 204,  96, 115)
             # Transparency to apply to the volume when overlaying
             self.voltrans =  0.50
             # Transparency for text labels (NOT USED CURRENTLY)
             self.subtxttrans = 0.66
             # Default font text size for labels on the chart
             self.subtxtsize = 
             # Transparency for the legend (NOT USED CURRENTLY)
             self.legendtrans = 0.25
             # Wether indicators have a leged displaey in their charts
             self.legendind = True
             # Location of the legend for indicators (see matplotlib)
             self.legendindloc =  'upper left'
             # Plot the last value of a line after the Object name
             self.linevalues = True
             # Plot a tag at the end of each line with the last value
             self.valuetags = True
             # Default color for horizontal lines (see plotinfo.plothlines)
             self.hlinescolor = '0.66'  # shade of gray
             # Default style for horizontal lines
             self.hlinesstyle = '--'
             # Default width for horizontal lines
             self.hlineswidth = 1.0
             # Default color scheme: Tableau 10
             self.lcolors = tableau10
             # strftime Format string for the display of ticks on the x axis
             self.fmt_x_ticks = None
             # strftime Format  string for the display of data points values
             self.fmt_x_data = None

如果想要系统性修改图形样式,可以重新定义 PlotScheme 类,然后修改里面用到的参数;也可以直接在plot() 中修改。

B_图中的绿涨红跌更改

前面已经提到很多次了,国际上使用的是绿涨红跌,所以K线和volume柱都是这样,现在我们就可以进行更改了,从上面的plot()参数中找到这几个参数的默认值,可以看到K线涨是灰色,跌是红色,成交量涨也是灰色,跌是近红色:

# Default colorfor a bullish bar/candle (0.75 -> intensity of gray)
self.barup ='0.75'
# Default color for a bearish bar/candle
self.bardown = 'red'


# Default colour for the volume of a bullish day
self.volup = '#aaaaaa'  #  0.66 of gray
# Default colour for the volume of a bearish day
self.voldown =  '#cc6073'  # ( 204,  96, 115)

这里直接修改这4个参数的值即可,再次运行回测,图中的K线和成交量颜色就变成我们熟悉的红涨绿跌了。

cerebro.plot(
             style='candel', # 设置主图行情数据的样式为蜡烛图

             barup = '#ff9896', 
             bardown= '#98df8a', # 设置蜡烛图上涨和下跌的颜色
             volup= '#ff9896', 
             voldown= '#98df8a', # 设置成交量在行情上涨和下跌情况下的颜色
            )

C_局部绘图与是否绘图

先看一下上面的绘制图形,简单把它分成三类

  1. 观察器图形- observers - Broker , Trades, 以及主图里的buy/sell
  2. 主图 - K线,股价相关指标,成交量,以及observers控制的buy/sell
  3. 附图 - 通常是非股价相关指标产生的

由上,大量的图线是与指标相关联的,比如上面的布林线,金叉/死叉标志位等,这些图线的控制通过在策略类的init()里进行。

我们先进到源码中看一下不同的指标的设置,以下列这些指标为例,

# moving averages (so envelope and oscillators can be auto-generated)
from .sma import *
from .ema import *

from .bollinger import *
from .cci import *
from .crossover import *

from .lrsi import *
from .macd import *

sma,ema等都从MovingAverageBase来,需要到backtrader.indicators.MovingAverageBase去查看,其他直接可以跳转过去,将它们的plotinfo列出来:

class MovingAverageBase(with_metaclass(MetaMovAvBase, Indicator)):
    params = (('period', 30),)
    plotinfo = dict(subplot=False)           # 主图绘制



class BollingerBands(Indicator):
   
    lines = ('mid', 'top', 'bot',)
    plotinfo = dict(subplot=False)           # 主图绘制


class MACD(Indicator):
    
    lines = ('macd', 'signal',)
    plotinfo = dict(plothlines=[0.0])        # 没有subplot=False

class LaguerreRSI(PeriodN):
  
    lines = ('lrsi',)

    plotinfo = dict(                         # 没有subplot=False
        plotymargin=0.15,
        plotyticks=[0.0, 0.2, 0.5, 0.8, 1.0]
    )

class _CrossBase(Indicator):
   
    lines = ('cross',)
    plotinfo = dict(plotymargin=0.05, plotyhlines=[0.0, 1.0])  # 没有subplot=False

显然,要主图绘制的需要写上subplot=False这句,否则就绘制到副图去了。

我们可以在策略类里改变它们的是否绘制及主图绘制。将默认与更改的分别执行,图形如下:

    def __init__(self):
        self.order = None
        
        # 默认主图
#         sma1 = bt.indicators.SMA(self.data, period=60)
        sma1 = bt.indicators.SMA(self.data, period=60, subplot=True) # 画在副图上
#         BOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2)
        BOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2,subplot=True) # 画在副图上
        bot = BOLL1.l.bot
        top = BOLL1.l.top
        
        # 默认副图
#         CCI1 = bt.indicators.CCI(self.data)
        CCI1 = bt.indicators.CCI(self.data, subplot=False) # 画到主图上
#         RSI1 = bt.indicators.RSI(self.data) 
        RSI1 = bt.indicators.RSI(self.data, subplot= False) # 画到主图上
#         self.crsup = bt.indicators.CrossUp(self.data.close,bot)
        self.crsup = bt.indicators.CrossUp(self.data.close,bot, plot=False) # 不画图
#         self.crsdn = bt.indicators.CrossDown(top,self.data.close)
        self.crsdn = bt.indicators.CrossDown(top,self.data.close, plot=False) # 不画图

D_plot属性调整更多方式

刚刚是直接在indicators的语句里全用参数plot=和subplot=进行的控制,现在换一些方式来,第1种是先出indicators,再改绘图属性的方式,这样产生的结果与上面的一模一样。

        # 默认主图
        sma1 = bt.indicators.SMA(self.data, period=60)
        sma1.plotinfo.subplot=True

        BOLL1 = bt.indicators.BBands(self.data,period =self.p.p1, devfactor=self.p.p2)
        BOLL1.plotinfo.subplot=True

        bot = BOLL1.l.bot
        top = BOLL1.l.top
        
        # 默认副图
        CCI1 = bt.indicators.CCI(self.data)
        CCI1.plotinfo.subplot=False
        
        RSI1 = bt.indicators.RSI(self.data) 
        RSI1.plotinfo.subplot=False
        self.crsup = bt.indicators.CrossUp(self.data.close,bot)
        self.crsup.plotinfo.plot=False # 不画图
        self.crsdn = bt.indicators.CrossDown(top,self.data.close)
        self.crsdn.plotinfo.plot=False # 不画图

另外,我们还可以用类继承再重写的方式,把plot或subplot的默认属性改掉,这样使用起来更方便。例如下面代码就是通过重写CrossUp中的plotinfo,从而实现不在图上显示这个指标的功能。

class myCrsup(bt.indicators.CrossUp):
    plotinfo = dict(plotymargin=0.05, plotyhlines=[0.0, 1.0],plot=False)

#............


#    self.crsup = bt.indicators.CrossUp(self.data.close,bot)
#    self.crsup.plotinfo.plot=False # 不画图

    self.crsup = myCrsup(self.data.close,bot) # 与上面2句功能相同

E_强制绘图的方法

从上面的实践看到,indicators出来的默认能绘制图线的,不管是主图还是副图,有时候我们不需要它显示一些指标,则用上面C和D的方式都可以让它们不显示。

反过来,有些数据我们想在图上画出来,需要怎么做?

使用bt.LinePlotterIndicator()方法

这个主要应用于一些需要对指标进行简单计算的数据,例如就是在30天平均线的数值上加个2(每次股价超过平均线2块就卖出盈利),这就需要在sma30的线上+2再画一条线

    def __init__(self):
        self.order = None
        
        # 默认主图
        sma1 = bt.indicators.SMA(self.data, period=30)  # 默认绘图
        sma_up = sma1 + 2                               # 计算默认不绘图
        
        print(sma1)              # sma1的类型
        print(sma_up)            # sma_up的类型

----------------
<backtrader.indicators.sma.SMA object at 0x00000146D36E1810>
<backtrader.linebuffer.LinesOperation object at 0x00000146D3925210>

从输出结果看,indicators出来的就是indicators类型,而进行计算后变成了LineOperation类型,这里就需要用LinePlotterIndicator的方法,在主图上强制绘图(subplot=False)

bt.LinePlotterIndicator(sma_up, name='up',subplot=False)

加上这句后,就可以在主图上把计算的线绘制出来了。下图上部分和下部分就分别是没有以及添加了LinePlotterIndicator语句的效果:

将数据制作成indicators

经过计算后得到的类型是lineoperation,这个类型可以使用LinePlotterIndicator强制绘图,再加上subplot=False属性则能画在主图上,但是也看到了图上的标签是LinePlotterIndicator xx.xx,如果我们需要更有个性化的显示,以及让data里面的lines数据也能绘制图线(LineBuffer是不能用LinePlotterIndicator绘制的),就需要制作自定义indicator类。

自定义新指标类格式如下

class MyInd(bt.Indicator):
    lines = (xxx,xxx, ) # 最后一个 “,” 别省略
    params = ((xxx, n),) # 最后一个 “,” 别省略
    
    def __init__(self):
        '''可选'''
        pass
    
    def next(self):
        '''可选'''
        pass
    
    def once(self):
        '''可选'''
        pass 
    
    plotinfo = dict(...)
    plotlines = dict(...)

我们根据这个先简单写上面这个根据SMA1加上一个数的自定义类

class myAdd(bt.Indicator):
    lines=('out',)          
    params=(('p1',2.0),)              # 加上的数

    plotinfo = dict(subplot=False)   # 画在主图上
    plotlines = dict(out=dict(ls='--',color='g')) # 画虚线,绿色

    def __init__(self):
        self.l.out = self.data + self.p.p1   # 直接相加


----------------
class mystrategy11(SmaCross_s4):
    #......
    def __init__(self):
        self.order = None
        
        # 默认主图
        sma1 = bt.indicators.SMA(self.data, period=30)
        sma_up = myAdd(sma1,p1=2.0)    # 使用自定义指标类
        
#         bt.LinePlotterIndicator(sma_up, name='up',subplot=False)  # 这句就不需要了

运行后输出图形如下,可以看到把sma1+2.0的绿色虚线显示出来了,并且标签的名字变成了myAdd(),这样就更有辨识度了。

使用这种方法,还可以把我们前面说的从股票软件里导出的某些指标的值(甚至是加密指标)制作成LineBuffer然后通过自定义indicator类转换成指标,也就能方便的在backtrader中使用股票软件的指标了,连公式移植都不用,大大节省工作量,提高了工作效率。

小结

本节对交易的设置和绘图的一些关键点进行了实践,通过试验对backtrader以及自己的交易系统都有了一定的提升。

backtrader实践第二篇之基础加强篇到这里就结束了,在这一篇里我们以第一篇得到的数据进行进一步的实践,拓展了对数据Datas,Data以及lines的认识,加深了对策略类的应用包括一次回测能否添加2个策略,能否将一个策略用到所有的数据上等,对交易系统的评测准备和交易设置也有了更深层次的了解,并且也可以比较自由的进行绘图的操作了。

在这一篇里,我们在实践过程中逐渐更改和添加了一些类和函数代码,把这些代码放到一个.py文件中,我们就拥有了一个算是比较全面的backtrader回测框架了,这些代码后面都不要改,但每次都要用到,后续要开始对指标和策略进行研究、分析,有了这个框架就能把精力放在关键的东西上,而不要耗费在这琐碎的、重复的没有效率的东西上了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2156135.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记软件开发者画图(UML),使用WPS应用制图

目录 前言 一、什么是UML 二、使用什么画图工具 三、示例 ​四、IntelliJ IDEA 2021快速生成UML图 前言 做软件开发的从写第一个示例程序到最后写项目程序避不开的需要设计画图&#xff0c;所以今天我们就来梳理一下‌UML&#xff08;统一建模语言&#xff09;图形需要画…

《深度学习》卷积神经网络CNN 原理及其流程解析

目录 一、CNN图像原理 1、了解图像的原理 2、举例 二、CNN图像识别 1、画面不变性 2、主要表现 1&#xff09;平移不变性 2&#xff09;尺度不变性 3&#xff09;旋转不变性 3、传统神经网络识别 1&#xff09;数据预处理 2&#xff09;特征提取 3&#xff09;搭建神经网…

C++基础(8.继承_多态)

目录 继承&#xff1a; 继承的概念&#xff1a; 继承的定义&#xff1a; 基类和派生类对象赋值转换 &#xff1a; 继承中的作用域&#xff1a; 派生类的默认成员函数&#xff1a; 例题&#xff1a; ​编辑​编辑​编辑 继承与友元&#xff1a; 继承与静态成员&#xff1…

图片马赛克处理(Java)

1.需求 给图片的指定区域打码给整张图片打码马赛克方格取色支持中心点取色和随机取色马赛克支持灰度处理 2.源码 package com.visy.utils;import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOE…

Rabbitmq消息队列,安装,使用,三种工作模式

产品 消息队列技术介绍 消息队列概述 消息队列中间件是分布式系统中重要的组件&#xff0c;主要解决应用耦合、异步消息、流量削锋等问题。实现高性能、高可用、可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 目前在生产环境&#xff0c;使用较多的消息队列有…

基于51单片机的汽车倒车防撞报警器系统

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 本课题基于微控制器控制器&#xff0c; 设计一款汽车倒车防撞报警器系统。 要求&#xff1a; 要求&#xff1a;1.配有距离&#xff0c; 用于把车和障碍物之间的距离信号送入控制器。 2.配有报警系…

2024AI做PPT软件如何重塑演示文稿的创作

现在AI技术的发展已经可以帮我们写作、绘画&#xff0c;最近我发现了不少ai做ppt的工具&#xff01;不体验不知道&#xff0c;原来合理使用AI工具可以有效的帮我们进行一些办公文件的编写&#xff0c;提高了不少工作效率。如果你也有这方面的需求就接着往下看吧。 1.笔灵AIPPT…

内网穿透out了?黑群晖+IPv6+NAS公网助手的访问体验

科技宅最带折腾黑群晖&#xff0c;这不&#xff0c;尝试一下ipv6动态域名解析&#xff0c;远程访问群晖NAS的方法千千万&#xff0c;这个方法我早就想到了&#xff0c;今天终于体验了一把&#xff0c;把经验分享一下&#xff1a; 目录 黑群晖的魅力 IPv6的加入&#xff1a;无…

Python办公自动化教程(003):PDF的加密

【1】代码 from PyPDF2 import PdfReader, PdfWriter# 读取PDF文件 pdf_reader PdfReader(./file/Python教程_1.pdf) pdf_writer PdfWriter()# 对第1页进行加密 page pdf_reader.pages[0]pdf_writer.add_page(page) # 设置密码 pdf_writer.encrypt(3535)with open(./file/P…

上位机图像处理和嵌入式模块部署(linux小系统开发)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 和若干年前相比较&#xff0c;现在嵌入式linux开发要简单得多。稍微贵一点的有树莓派&#xff0c;国产的有各种水果派&#xff0c;基本上都可以按照…

苍穹外卖学习日志 -----20天项目从零到完结-----含软件下载,环境配置,框架学习,代码编写,报错处理,测试联调,每日总结,心路历程等等......

年份 2024 基础&#xff1a;Javase Javaweb 已完结 2024 8.25---9.14 20天 Day-01 8.25 今天开始学习已经晚了&#xff0c;网盘下载了一下文件&#xff0c;做了一些开始项目的准备工作。 本来其实打算用notepad来写学习日志的&#xff0c;但是那个传…

如何给bat文件替换好看的图标

最近遇到软件运行在Windows系统&#xff0c;通过bat文件启动&#xff0c;但是bat文件的图标不好看&#xff0c;而且作为启动快捷方式放桌面看上去跟其他软件不搭调&#xff0c;于是得给bat文件换个软件图标。 软件ico图标 Windows系统下使用.ico文件作为软件图标。另外favicon…

go libreoffice word 转pdf

一、main.go 关键代码 完整代码 package mainimport ("fmt""github.com/jmoiron/sqlx""github.com/tealeg/xlsx""log""os/exec""path/filepath" ) import _ "github.com/go-sql-driver/mysql"import &q…

Python练习宝典:Day 1 - 选择题 - 基础知识

目录 一、踏上Python之旅二、Python语言基础三、流程控制语句四、序列的应用 一、踏上Python之旅 1.想要输出 I Love Python,应该使用()函数。 A.printf() B.print() C.println() D.Print()2.Python安装成功的标志是在控制台(终端)输入python/python3后,命令提示符变为: A.&…

数业智能心大陆:职场倦怠的新解法

什么是职业倦怠&#xff1f; 在职场中&#xff0c;职业倦怠的表现形式丰富多样。从数业智能心大陆 AI 心理咨询平台的数据来看&#xff0c;职业倦怠呈现出多种状态。教师可能对教学不再满怀热情&#xff0c;精心备课也成为过去式&#xff1b;情绪上容易烦躁、易怒&#xff0c;在…

【d47_2】【Java】【力扣】1791.找出星型图的中心节点

思路 直接判断 edges[0][0] edges[0][1] edges[1][0] edges[1][1] 谁重复了 例如&#xff1a; [ [1,2] [2,3] ....],那么中心节点一定是2 代码 class Solution {public int findCenter(int[][] edges) {for (int i0;i<1;i){if (edges[1][0]edges[0][i]) {return edg…

车载软件调试工具系列---Trace32简介UI界面简介

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

java项目之常规应急物资管理系统(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的常规应急物资管理系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息。 项目简介&#xff1a; 基于SpringBootVue的…

TikTok流量不佳:是网络环境选择不当还是其他原因?

TikTok&#xff0c;作为全球短视频社交平台的佼佼者&#xff0c;每天都有海量的内容被上传和分享。然而&#xff0c;很多用户和内容创作者发现&#xff0c;他们的TikTok视频流量并不理想。这引发了一个问题&#xff1a;TikTok流量不佳&#xff0c;是因为网络环境选择不当&#…

S3C2440定时器

ee一、构造 二、设置相关位 1、MPLLCON寄存器&#xff08;配置MPLL寄存器&#xff0c;进行倍频&#xff09; 根据下列表格的想要输出的频率进行选择&#xff0c;选择完毕之后&#xff0c;对该寄存器进行设置 2、时钟分频控制&#xff08;CLKDIVN&#xff09;寄存器 根据不…