绘图#

尽管*回测*是基于数学计算的自动化过程,但通常情况下,人们还是希望能够实际可视化正在发生的情况。无论是使用已经进行过回测运行的现有算法,还是查看数据(内置或自定义)与指标的实际交付情况。

由于一切都有人类的背后支持,绘制数据源、指标、操作、现金和组合价值的图表可以帮助人类更好地理解正在发生的事情,并且能够根据视觉信息来丢弃、修改、创建想法,以及任何查看图表的人对图表所做的任何操作。

这就是为什么 backtrader 使用 matplotlib 提供的功能提供了内置的绘图功能。

如何绘图?#

可以通过调用一个方法来绘制任何回测运行:

cerebro.plot()

当然,这通常是最后发出的命令,就像在这个使用 backtrader 源代码中的一个示例数据的简单代码中一样。

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

import backtrader as bt


class St(bt.Strategy):
    def __init__(self):
        self.sma = bt.indicators.SimpleMovingAverage(self.data)


data = bt.feeds.BacktraderCSVData(dataname='../../datas/2005-2006-day-001.txt')

cerebro = bt.Cerebro()
cerebro.adddata(data)
cerebro.addstrategy(St)
cerebro.run()
cerebro.plot()

这将生成以下图表。.. 缩略图:: 01-sample-code.png

该图表包含了 3个观察者 ,在这种情况下,由于缺乏任何交易,它们大多毫无意义:

  • 一个名称为 CashValue 的观察者,它跟踪回测运行期间的 现金 和总投资组合 价值 (包括现金)

  • 一个被称为 Trade 的观察者,它在一次交易结束时显示实际的 利润和损失

    一次交易的定义是打开一个头寸并将头寸交回到 0 (直接交回或从多头到空头或从空头到多头)

  • 一个名为 BuySell 的观察者,在价格图上显示 买入*和 卖出*操作的位置

3个观察者 是由 cerebro 自动添加的,并且可以通过 stdstats 参数进行控制(默认值为 True )。如果希望禁用它们,请按照以下步骤操作:

cerebro = bt.Cerebro(stdstats=False)

或者以后在*运行*时如下方式操作:

cerebro = bt.Cerebro() … cerebro.run(stdstats=False)绘图元素


尽管在前面的介绍中已经提到了 观察者(Observers) ,但它们并不是唯一要绘制的元素。以下三个元素将被绘制:

  • 使用 adddatareplaydataresampledata数据源 添加到 Cerebro

  • 在策略级别声明 指标(Indicators) (或使用 addindicator 将其添加到 cerebro,这纯粹用于实验目的,并将指标添加到一个虚拟策略中)

  • 使用 addobserver观察者(Observers) 添加到 cerebro

    观察者是与 策略(strategy) 同步运行的 线(lines) 对象,可以访问整个系统,以便跟踪 CashValue 等内容

绘图选项#

指标(Indicators)观察者(Observers) 有几个选项来控制它们在图表上的绘制方式。有 3 个主要的选项组:

  • 影响整个对象绘制行为的选项

  • 影响单独线条绘制行为的选项

  • 影响系统范围绘制选项的选项

对象范围的绘图选项 ============================这些由 IndicatorsObservers 数据集控制:

plotinfo = dict(plot=True,

subplot=True, plotname=’’, plotskip=False, plotabove=False, plotlinelabels=False, plotlinevalues=True, plotvaluetags=True, plotymargin=0.0, plotyhlines=[], plotyticks=[], plothlines=[], plotforce=False, plotmaster=None, plotylimited=True,

)

虽然在类定义时 plotinfo 被显示为一个 dict ,但 backtrader 的元类机制将其转换为一个可以被继承甚至可以多重继承的对象。这意味着:

  • 如果子类更改了 subplot=True 这样的参数值为 subplot=False ,那么继承整个层次结构的子类将会将后者作为 subplot 的默认值

给这些参数赋值的有两种方法。让我们先看看第一种方法的 SimpleMovingAverage 实例化:

sma = bt.indicators.SimpleMovingAverage(self.data, period=15, plotname=’mysma’)

从示例中可以推断出,任何未被 SimpleMovingAverage 构造函数使用的 **kwargs 参数(如果可能)将被解析为 plotinfo 的值。 SimpleMovingAverage 只定义了一个参数,即 period 。这意味着 plotname 将与 plotinfo 中同名的参数进行匹配。

第二种方法:

sma = bt.indicators.SimpleMovingAverage(self.data, period=15) sma.plotinfo.plotname = ‘mysma’

SimpleMovingAverage 实例化期间,可以访问 plotinfo 对象,并且还可以使用标准的 Python 点符号来访问其中的参数。这种方式更简单,也可能更清晰。选项的含义#

  • plot : 是否需要绘制对象

  • subplot : 是否在数据上方或独立的子图中绘制。 移动平均线 是一个在数据上绘制的示例。 随机指标 (Stochastic)相对强弱指标 (RSI) 是在不同刻度上绘制的示例。

  • plotname : 在图表上使用的名称,而不是使用 class 名称。例如,在上面的示例中,可以使用 mysma 而不是 SimpleMovingAverage

  • plotskip ( 已弃用 ): plot 的一个旧别名

  • plotabove : 是否在数据之上绘制。否则在数据之下绘制。只有在 subplot=True 的情况下才会发生作用。

  • plotlinelabels : 是否在图表上的图例中绘制各条线的名称。当 subplot=False 时有效。

    示例: 布林带 (Bollinger Bands) 有 3 条线,但是指标是在数据之上绘制的。在图例上只显示一个名称,比如 BollingerBands ,而不是显示 3 条线的名称( midtopbot )会更合理。

    此选项的一个使用场景是为了 BuySell 观察者,其中显示两条线的名称和其标记: BuySell ,以便清楚地向最终用户解释这两者的含义。

  • plotlinevalues : 控制指标和观察者图表中各条线的图例是否包含最后绘制的值。可以使用每条线的 _plotvalue 单独进行控制。- plotvaluetags :控制是否在线的右侧绘制一个带有最后一个值的值标签。可以使用每行的 _plotvaluetag 单独控制。

  • plotymargin :在图表的每个子图的顶部和底部添加的边距。它是一个基于百分比的值。例如:0.05 -> 5%。

  • plothlines :一个包含需要绘制 水平*线的值(在刻度内)的 可迭代 对象。例如,这对于经典的指标如 `RSI` ,通常在70和30处绘制线条很有帮助,表示 超买 和*超卖 区域。

  • plotyticks :一个包含需要在刻度上特定放置的值(在刻度内)的 可迭代 对象。例如,可以强制刻度上有一个50来标识刻度的中点。尽管这似乎很明显,但是指标使用自动缩放机制,如果指标的刻度在30-95之间的区间上经常移动,那么50可能不会明显地位于中心。

  • plotyhlines :一个包含需要在刻度内绘制 水平*线的值(在刻度内)的 可迭代*对象。这可以接管 plothlines 和`plotyticks`的功能。如果上述没有定义,则水平线和刻度的位置完全由此值控制。如果上述任何一个被定义了,它们的值将优先于该选项中存在的值。

  • plotforce :有时候,由于匹配数据源到指标的复杂过程,自定义指标可能无法绘制。这是一种最后的手段来强制绘图。

    如果其他方法都失败了,请使用它。

  • plotmaster指标/* 观察者 有一个主master,即它所工作的 数据*。在某些情况下,希望使用不同的master绘制它可能是需要的。

    一个用例是 PivotPoint 指标,它是基于 月度 数据计算的,但是适用于 每日 数据。只有在*每日*数据上绘制它才有意义,因为那是该指标有意义的地方。

  • plotylimited :目前仅适用于数据源。如果为 True (默认值),数据绘图中的其他线不会改变比例尺。例如:布林带(上下)可能远离数据源的实际最小/最大值。如果设置为 plotlimited=True ,这些带子会保持在图表之外,因为数据控制着比例尺。如果设置为 False ,这些带子将影响y轴的比例尺,并在图表上可见。

    一个用例是 PivotPoint 指标,它是基于 月度 数据计算的,但是适用于 每日 数据。只有在*每日*数据上绘制它才有意义,因为那是该指标有意义的地方。

特定行的绘图选项#

指标/* 观察者 有*行 ,并且可以使用 plotlines 对象来影响如何绘制这些*行*。 plotlines 中指定的大多数选项都是直接传递给 matplotlib 绘图时使用的。因此,文档依赖于已完成的示例。

重要提示 :选项是基于每一行指定的。一些选项由 backtrader 直接控制。这些选项都以下划线( _ )开头:

  • _plotskip布尔值 ),如果设置为 True ,则表示跳过对特定线条的绘图

  • _plotvalue布尔值 ),用于控制此线条的图例是否包含最后绘制的值(默认为 True

  • _plotvaluetag布尔值 ),用于控制是否绘制带有最后一个值的右侧标签(默认为 True

  • _name字符串 ),可以更改特定线条的图名

  • _samecolor布尔值 ),这会强制下一条线条与上一条线条具有相同的颜色,避免 matplotlib 默认的颜色循环机制

  • _method字符串 ),用于选择 matplotlib 将用来绘制该元素的绘图方法。如果未指定,则选择最基本的 plot 方法。

    以下是来自 MACDHisto 的示例。这里将 histo 线条绘制为一个 bar ,这是业界事实标准。下面的定义可以在 MACDHisto 的定义中找到:

    lines = (‘histo’,) plotlines = dict(histo=dict(_method=’bar’, alpha=0.50, width=1.0))

    alphawidthmatplotlib 的选项 - _fill_gt / _fill_lt

    允许在给定的线和以下之间进行填充:

    • 另一条线

    • 数值

    参数是一个包含2个元素的可迭代对象,其中:

    • 第一个参数是一个 字符串 (参考线的名称)或一个数值

    • 填充将在自身数值和线或数值的数值之间进行

    • 第二个参数要么是:

      • 包含颜色名称(与 matplotlib 兼容)或十六进制表示的字符串 (请参阅 matplotlib 的示例)- 一个可迭代对象,其中第一个元素是颜色的字符串/十六进制值,第二个元素是指定 alpha 透明度的数值(默认值为 0.20 ,在绘图方案中使用 fillalpha 控制)

示例:

# 当 myline 在 other_line 上方时,使用红色进行填充
plotlines = dict(
    myline=dict(_fill_gt('other_line', 'red'))
)

# 当 myline 在 50 之上时,使用红色进行填充
plotlines = dict(
    myline=dict(_fill_gt(50, 'red'))
)

# 当 myline 在 other_line 上方时,使用红色进行填充并带有 50% 的透明度(1.0 表示“无透明度”)

plotlines = dict(
    myline=dict(_fill_gt('other_line', ('red', 0.50)))
)
  • 使用名称 _X,其中 ` X` 代表从零开始的索引中的一个数字。这意味着选项适用于第 X

OscillatorMixIn 中的一个用例:

plotlines = dict(_0=dict(_name='osc'))正如其名,这是一个 *mixin* 类,用于在多重继承方案(特别是在右侧)中使用。 *mixin* 对于其他指标的第一行(索引基于零)的实际名称没有任何了解,该指标将成为多重继承的混合部分。

这就是选项指定为: _0 的原因。在继承发生后,结果类的第一行的名称将在plot中为“osc”。

BuySell 观察者的例子如下:

plotlines = dict(

buy=dict(marker=’^’, markersize=8.0, color=’lime’, fillstyle=’full’), sell=dict(marker=’v’, markersize=8.0, color=’red’, fillstyle=’full’)

)

buysell 行有直接传递给 matplotlib 的选项,来定义 markermarkersize *、*colorfillstyle 。所有这些选项都在 matplotlib 中定义。

Trades 观察者的例子如下:

… lines = (‘pnlplus’, ‘pnlminus’) …

plotlines = dict(
pnlplus=dict(_name=’Positive’,

marker=’o’, color=’blue’, markersize=8.0, fillstyle=’full’),

pnlminus=dict(_name=’Negative’,

marker=’o’, color=’red’, markersize=8.0, fillstyle=’full’)

)

在这里,行的名称已经通过使用 _namepnlplus 重新定义为 Positive 。其余的选项是为了 matplotlibDrawDown 观察器:

lines = ('drawdown', 'maxdrawdown',)

...

plotlines = dict(maxdrawdown=dict(_plotskip='True',))

这里定义了两条线,让最终用户不仅可以访问当前 drawdown 的值,还可以访问其最大值( maxdrawdown )。但是由于 _plotskip=True ,后者不会被绘制。

BollingerBands 指标:

plotlines = dict(
    mid=dict(ls='--'),
    top=dict(_samecolor=True),
    bot=dict(_samecolor=True),
)

这里 mid 线将使用*虚线*样式, topbot 线将与 mid 线具有相同的颜色。

Stochastic (在 _StochasticBase 中定义并继承):

lines = ('percK', 'percD',)
...
plotlines = dict(percD=dict(_name='%D', ls='--'),
                 percK=dict(_name='%K'))控制绘图的方法

在处理 指示器 和*观察者*时,可以使用以下方法进一步控制绘图:

  • _plotlabel(self)

    它应该返回一个列表,用于在 指示器 或*观察者*名称后的括号中放置标签。

    来自 RSI 指示器的一个示例:

    def _plotlabel(self):

    plabels = [self.p.period] plabels += [self.p.movav] * self.p.notdefault(‘movav’) return plabels

    如上例所示,该方法返回:

    • 一个 int ,用于指示配置为 RSI 的周期,以及是否已更改默认移动平均线的特定类别

      这两者都将在后台转换为字符串。对于*类别*来说,将努力只打印类别的名称,而不是完整的 module.name 组合。 - _plotinit(self)

    在绘图开始时被调用,用于进行某些特定指示器所需的初始化工作。再次以 RSI 为例:

    def _plotinit(self):

    self.plotinfo.plotyhlines = [self.p.upperband, self.p.lowerband]

    在这里,代码将一个值赋给 plotyhlines ,以便在特定 y 值上绘制水平线( hlines 部分)。

    这里使用了参数 upperbandlowerband 的值,这些值是不能预先知道的,因为参数可能由最终用户更改。

系统级绘图选项#

首先是cerebro中 plot 的*签名*:

def plot(self, plotter=None, numfigs=1, iplot=True, **kwargs):

这意味着:

  • plotter :一个包含作为属性的对象/类,用于控制全局绘图选项的选项如果传入 None ,将实例化一个默认的 PlotScheme 对象(见下文)

  • numfigs :一个图表需要被分成多少个独立的图形

有时候一个图表包含太多的柱状图,如果放在一个图形中会很难阅读,因此需要将其分成多个部分。

  • iplot :如果在Jupyter Notebook中运行,则自动将图表嵌入到文档中

  • **kwargs :这些参数将用于更改 plotter 的属性值或者创建默认的 PlotScheme 对象(如果没有传入 plotter )。

该对象包含控制系统级绘图的所有选项。选项在代码中有文档说明:

class PlotScheme(object):
def __init__(self):

# 控制图表的紧凑度,一个图表可以只有x轴或者同时有x轴和y轴(参考matplotlib) self.ytight = False

# 每个子图的上下边距。这不会覆盖plotinfo.plotymargin选项 self.yadjust = 0.0 # 每条新线按照z顺序位于前一条线的下面。将其设置为False可以使线条位于前一条线的上方 self.zdown = True # x轴上日期标签的旋转角度 self.tickrotation = 15

# 主要图表(数据)在整个图表中占据的“子部分”数量。 # 这与总的子图数量成正比 self.rowsmajor = 5# 次要图表(指标/观察器)在整体图表中占据多少个“子部分”。这与子图表的总数成比例。

# 与rowsmajor一起,定义了数据图表和指标/观察器图表之间的比例比 self.rowsminor = 1

# 子图表之间的距离 self.plotdist = 0.0

# 所有图表背景上是否有网格 self.grid = True

# OHLC蜡烛图的默认绘图样式(线条 - > 收盘线) # 其他选项:’bar’和’candle’ self.style = ‘line’

# “线条on收盘”绘图的默认颜色 self.loc = ‘black’ # 一个看涨的蜡烛/蜡烛图的默认颜色(0.75 -> 灰色的强度) self.barup = ‘0.75’ # 一个看跌的蜡烛/蜡烛图的默认颜色 self.bardown = ‘red’ # 应用于蜡烛/蜡烛图的透明度级别(未使用) self.bartrans = 1.0

# 蜡烛/蜡烛图是否要填充或透明 self.barupfill = True self.bardownfill = True

# 蜡烛/蜡烛图的填充透明度 self.fillalpha = 0.20

# 是否绘制成交量。注意:如果所讨论的数据没有交易量值,即使为True,也将跳过绘制交易量 self.volume = True

# 是否将交易量叠加在数据上或使用单独的子图表 self.voloverlay = True # 绘制叠加时将交易量缩放到数据的比例 self.volscaling = 0.33 # 为了更好的可见性将叠加的交易量向上推。如果交易量和数据重叠太多,需要进一步实验 self.volpushup = 0.00

# 看涨日交易量的默认颜色 self.volup = ‘#aaaaaa’ # 灰色的0.66 # 看跌日交易量的默认颜色 self.voldown = ‘#cc6073’ # (204, 96, 115) # 叠加时应用于交易量的透明度 self.voltrans = 0.50# 文本标签的透明度(当前未使用) self.subtxttrans = 0.66 # 图表标签的默认字体大小 self.subtxtsize = 9

# 图例的透明度(当前未使用) self.legendtrans = 0.25 # 图表中是否显示指标的图例 self.legendind = True # 指标图例的位置(参见 matplotlib) self.legendindloc = ‘左上角’

# 在对象名称后绘制线的最后一个值 self.linevalues = True

# 在每条线的末尾绘制一个标签,显示最后一个值 self.valuetags = True

# 水平线的默认颜色(参见 plotinfo.plothlines) self.hlinescolor = ‘0.66’ # 灰色的色调 # 水平线的默认样式 self.hlinesstyle = ‘–’ # 水平线的默认宽度 self.hlineswidth = 1.0

# 默认颜色方案:Tableau 10 self.lcolors = tableau10

# x 轴刻度显示的 strftime 格式字符串 self.fmt_x_ticks = None

# 数据点值显示的 strftime 格式字符串 self.fmt_x_data = None

PlotScheme 类定义了一个在子类中可以重写的方法,返回下一个要使用的颜色:def color(self, idx)

其中 idx 是正在绘制的个别子图上的当前索引。例如,“MACD”绘制3条线,因此 idx 变量只有以下值:“0”、“1”和“2”。下一个图表(可能是另一个指标)将从“0”开始计数。

backtrader 中使用的默认颜色方案使用了(如上所示) Tableau 10 调色板 ,索引被修改为:

tab10_index = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9]

通过覆盖 color 方法或将 lcolors 变量传递给 plot (或在 PlotScheme 的子类中)可以完全更改颜色。

源代码还包含了 Tableau 10 LightTableau 20 颜色调色板的定义。