Backtrader回测可视化重构解析

今天的《可视化篇》先会介绍与可视化相关的观测器模块 observers ,然后介绍 Backtrader 自带的绘图函数 plot() ,在介绍的过程中会指出如何修改图形的样式;最后直接基于回测返回的收益序列 TimeReturn,结合pyfolio和matplotlib工具,自定义了一个可视化图形。


获取完整代码+数据,见文末链接

 

observers 观测器

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

图片

最常用的观测器

下面是对最常用的观测器的介绍,其他观测器可以参考Backtrader 官方文档 ~ Observers – Reference:

  • backtrader.observers.Broker:记录了经纪商 broker 中各时间点的可用资金和总资产;可视化时,会同时展示 cash 和 values 曲线;如果想各自单独展示 cash 和 values,可以分别调用 backtrader.observers.Cash 和 backtrader.observers.Value;
  • backtrader.observers.BuySell:记录了回测过程中的买入和卖出信号;可视化时,会在价格曲线上标注买卖点;
  • backtrader.observers.Trades:记录了回测过程中每次交易的盈亏(从买入建仓到卖出清仓算一次交易);可视化时,会绘制盈亏点;
  • backtrader.observers.TimeReturn:记录了回测过程中的收益序列;可视化时,会绘制 TimeReturn 收益曲线;
  • backtrader.observers.DrawDown:记录了回测过程的回撤序列;可视化时,绘制回撤曲线;
  • backtrader.observers.Benchmark:记录了业绩基准的收益序列,业绩基准的数据必须事先通过 adddata、resampledata、replaydata 等数据添加函数添加进大脑中 cerebro;可视化时,会同时绘制策略本身的收益序列(即:backtrader.observers.TimeReturn 绘制的收益曲线)和业绩基准的收益曲线。

如何添加 observers

observers 观测器是通过 addobserver() 添加给大脑 cerebro 的:addobserver(obscls, *args, **kwargs),其中,参数 obscls 对应 observers 类下的观测器、*args, **kwargs 对应观测器支持设置的参数,具体如下所示:

import backtrader as bt
...
cerebro = bt.Cerebro(stdstats=False)
cerebro.addobserver(bt.observers.Broker)
cerebro.addobserver(bt.observers.Trades)
cerebro.addobserver(bt.observers.BuySell)
cerebro.addobserver(bt.observers.DrawDown)
cerebro.addobserver(bt.observers.TimeReturn)
# 添加业绩基准时,需要事先将业绩基准的数据添加给 cerebro
banchdata = bt.feeds.PandasData(dataname=data, fromdate=st_date, todate=ed_date)
cerebro.adddata(banchdata, name='xxxx')
cerebro.addobserver(bt.observers.Benchmark, data=banchdata)

对于 Broker、Trades、BuySell 3个观测器,默认是自动添加给 cerebro 的,可以在实例化大脑时,通过 stdstats 来控制:bt.Cerebro(stdstats=False) 表示可视化时,不展示 Broker、Trades、BuySell 观测器;反之,自动展示;默认情况下是自动展示。

如何读取 observers 中的数据

observers  中记录了各种回测数据,可以将其看作是一个支持可视化展示的数据存储器,所以 observers 属于 lines 对象。如果想在 Strategy 中读取 observers 中的数据,就会用到 line 的相关操作,具体可以参考《Backtrader 数据篇》的内容,observers 的数据通过 self.stats 对象 来连接:

class MyStrategy(bt.Strategy):
    def next(self):
        # 当前时点的前一天的可用现金
        self.stats.broker.cash[0]
        # 当前时点的前一天的总资产
        self.stats.broker.value[0]
        # 获取当前时刻前一天的收益
        self.stats.timereturn.line[0]
        # observers 取得[0]值,对应的 next 中 self.data.datetime[-1] 这一天的值

observers 是在所有指标被计算完之后、在执行 Strategy 的 next 方法之后才运行并统计数据的,所以读取的最新数据 [0] 相对与 next 的当前时刻是晚一天的。比如 2019-04-08 的总资产为 99653.196672,2019-04-09 的总资产为 99599.008652,2019-04-09 这一天的收益为 -0.0005437,如果在 next 通过 self.stats.timereturn.line[0] 提取,取值为 -0.0005437 时,对应的 next 的当前时间是  2019-04-10。

如果想要将 observers  中的数据保存到本地,可以通过 writer  写入本地文件,如下面的读写到本地 CSV 文件:

import csv

class TestStrategy(bt.Strategy):
    ...
    def start(self):
        self.mystats = csv.writer(open("mystats.csv", "w"))
        self.mystats.writerow(['datetime',
                               'drawdown', 'maxdrawdown',
                               'timereturn',
                               'value', 'cash'])
    def next(self): 
        self.mystats.writerow([self.data.datetime.date(-1).strftime('%Y-%m-%d'),
                               '%.4f' % self.stats.drawdown.drawdown[0],
                               '%.4f' % self.stats.drawdown.maxdrawdown[0],
                               '%.4f' % self.stats.timereturn.line[0],
                               '%.4f' % self.stats.broker.value[0],
                               '%.4f' % self.stats.broker.cash[0]])
    def stop(self):  
        self.mystats.writerow([self.data.datetime.date(0).strftime('%Y-%m-%d'),
                               '%.4f' % self.stats.drawdown.drawdown[0],
                               '%.4f' % self.stats.drawdown.maxdrawdown[0],
                               '%.4f' % self.stats.broker.value[0],
                               '%.4f' % self.stats.broker.cash[0]])
        
    # 当运行到最后一根 bar 后, next 中记录的是上一根 bar 的收益
    # stop 是在 next 运行完后才运行的,此时 observers 已经计算完 最后一根 bar 的收益了
    # 所以可以在 stop 中获取最后一根 bar 的收益

自定义 observers 

和之前各种自定义一致,自定义 observers 同样是在继承父类  bt.observer.Observer 的基础上,自定义新的的observers。下面是 Backtrader 官网提供的例子,用于统计已成功创建的订单的价格和到期订单的价格:

class OrderObserver(bt.observer.Observer):
    lines = ('created', 'expired',)

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

    plotlines = dict(
        created=dict(marker='*', markersize=8.0, color='lime', fillstyle='full'),
        expired=dict(marker='s', markersize=8.0, color='red', fillstyle='full')
    )

    def next(self):
        for order in self._owner._orderspending:
            if order.data is not self.data:
                continue

            if not order.isbuy():
                continue

            # Only interested in "buy" orders, because the sell orders
            # in the strategy are Market orders and will be immediately
            # executed

            if order.status in [bt.Order.Accepted, bt.Order.Submitted]:
                self.lines.created[0] = order.created.price

            elif order.status in [bt.Order.Expired]:
                self.lines.expired[0] = order.created.price
  • observers 本身是 Lines 对象,所以构建逻辑与自定义 Indicator 类似,将要统计的数据指定为相应的 line,然后随着回测的进行依次存入数据;
  • 作为 Lines 对象的 Observers 和 Indicator ,类内部都有 plotinfo = dict(…)、plotlines = dict(…) 属性,用于回测结束后通过 cerebro.plot() 方法进行可视化展示;
  • 有时候如果想修改 Backtrader 已有观测器的相关属性,可以直接继承该观测器,然后设置属性取值进行修改。如下面在原始 bt.observers.BuySell 的基础上,修改买卖点的样式。
class my_BuySell(bt.observers.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'))
    # 将 三角形改为箭头
    
# 突然感受到了继承的强大!

plot() 图形绘制

cerebro.plot() 写在 cerebro.run() 后面,用于回测的可视化。总的来说,cerebro.plot() 支持回测如下 3 大内容:

  • Data Feeds:即在回测开始前,通过 adddata、replaydata、resampledata 等方法导入大脑的原始数据;
  • Indicators :即回测时构建的各类指标,比如在 strategy 中构建的指标、通过 addindicator 添加的;
  • Observers :即上文介绍的观测器对象;
  • 在绘制图形时,默认是将 Data Feeds 绘制在主图上;Indicators 有的与 Data Feeds 一起绘制在主图上,比如均线,有的以子图形式绘制;Observers 通常绘制在子图上。

plot() 中的参数

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 = 5

        # 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 = 1

        # 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 color for 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 = 9

        # 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() 中修改:

# 通过参数形式来设置
cerebro.plot(iplot=False,
             style='candel', # 设置主图行情数据的样式为蜡烛图
             lcolors=colors , # 重新设置主题颜色
             plotdist=0.1, # 设置图形之间的间距
             barup = '#ff9896', bardown='#98df8a', # 设置蜡烛图上涨和下跌的颜色
             volup='#ff9896', voldown='#98df8a', # 设置成交量在行情上涨和下跌情况下的颜色
             ....)

关于主题颜色,Backtrader 提供了Tableau 10 、Tableau 10 Light、Tableau 20 3种主题色,默认是以 Tableau 10 为主题色。但是看源代码,不知道如何修改 lcolors,源码 scheme.py 文件中的 tableau10 只一个变量,直接赋值给 self.lcolors = tableau10,如果在我们在自己的的 notebook上运行 lcolors=tableau10 会报错,提示 tableau10 变量不存在。所以,如果想修改主题色,需要重新定义 tableau10 变量:

# 定义主题色变量:直接从源码 scheme.py 中复制的
tableau20 = [
    'steelblue', # 0
    'lightsteelblue', # 1
    'darkorange', # 2
    'peachpuff', # 3
    'green', # 4
    'lightgreen', # 5
    'crimson', # 6
    'lightcoral', # 7
    'mediumpurple', # 8
    'thistle', # 9
    'saddlebrown', # 10
    'rosybrown', # 11
    'orchid', # 12
    'lightpink', # 13
    'gray', # 14
    'lightgray', # 15
    'olive', # 16
    'palegoldenrod', # 17
    'mediumturquoise', # 18
    'paleturquoise', # 19
]

tableau10 = [
    'blue', # 'steelblue', # 0
    'darkorange', # 1
    'green', # 2
    'crimson', # 3
    'mediumpurple', # 4
    'saddlebrown', # 5
    'orchid', # 6
    'gray', # 7
    'olive', # 8
    'mediumturquoise', # 9
]

tableau10_light = [
    'lightsteelblue', # 0
    'peachpuff', # 1
    'lightgreen', # 2
    'lightcoral', # 3
    'thistle', # 4
    'rosybrown', # 5
    'lightpink', # 6
    'lightgray', # 7
    'palegoldenrod', # 8
    'paleturquoise', # 9
]

# 选一个主题色做修改
cerebro.plot(lcolors=tableau10)


# 当然也可以选自己喜欢的主题色
mycolors = ['#729ece', '#ff9e4a', '#67bf5c',
          '#ed665d', '#ad8bc9', '#a8786e',
          '#ed97ca', '#a2a2a2', '#cdcc5d', '#6dccda']

cerebro.plot(lcolors=mycolors)

大家可以发现,从源码中复制的主题色,后面都注释了索引号,而 Backtrader 在绘制图形时,选择颜色的顺序依次是这样的:

  • tab10_index = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9];
  • tab10_index 中的序号对应的是 主题色 的索引号;
  • 每一幅图,依次取 tab10_index 中的序号对应的颜色来绘制,比如 MACD 有 3 条 line,line0 的颜色为 tab10_index[0] = 3,也就是 lcolors=tableau10 中的索引号为 3 对应的颜色 ‘crimson’;line1 的颜色为 tab10_index[1] = 0,也就是 lcolors=tableau10 中的索引号为 0 对应的颜色 ‘blue’;
  • 所以在设置颜色时,需要与 tab10_index  中的序号结合起来看。

局部绘图参数设置

对于 Indicators  和 Observers 的可视化设置,通过类内部的 plotinfo = dict(…)、plotlines = dict(…) 属性来控制,其中 plotinfo 主要对图形整体布局进行设置,plotlines 主要对具体 line 的样式进行设置:

plotinfo = dict(plot=True, # 是否绘制
                subplot=True, # 是否绘制成子图
                plotname='', # 图形名称
                plotabove=False, # 子图是否绘制在主图的上方
                plotlinelabels=False, # 主图上曲线的名称
                plotlinevalues=True, # 是否展示曲线最后一个时间点上的取值
                plotvaluetags=True, # 是否以卡片的形式在曲线末尾展示最后一个时间点上的取值
                plotymargin=0.0, # 用于设置子图 y 轴的边界
                plothlines=[a,b,...], # 用于绘制取值为 a,b,... 的水平线
                plotyticks=[], # 用于绘制取值为 a,b,... y轴刻度
                plotyhlines=[a,b,...], # 优先级高于plothlines、plotyticks,是这两者的结合
                plotforce=False, # 是否强制绘图
                plotmaster=None, # 用于指定主图绘制的主数据
                plotylimited=True,
                # 用于设置主图的 y 轴边界,
                # 如果True,边界只由主数据 data feeds决定,无法完整显示超出界限的辅助指标;
                # 如果False, 边界由主数据 data feeds和指标共同决定,能确保所有数据都能完整展示
           )

# 修改交易观测器的样式
class my_Trades(bt.observers.Trades):
    plotlines = dict(
    pnlplus=dict(_name='Positive',
                 marker='^', color='#ed665d',
                 markersize=8.0, fillstyle='full'),
    pnlminus=dict(_name='Negative',
                  marker='v', color='#729ece',
                  markersize=8.0, fillstyle='full'))
    
# 修改买卖点样式
class my_BuySell(bt.observers.BuySell):
    params = (('barplot', True), ('bardist', 0.02)) # bardist 控制买卖点与行情线之间的距离
    plotlines = dict(
    buy=dict(marker=r'$\Uparrow$', markersize=10.0, color='#d62728' ),
    sell=dict(marker=r'$\Downarrow$', markersize=10.0, color='#2ca02c'))

部分修改效果

一般主图的样式通过 plot() 中的参数来设置;Indicators  和 Observers 的样式通过继承原始类,然后通过修改plotinfo 和 plotlines 属性来设置;部分修改效果如下所示:

蜡烛图样式:

plt.style.use('seaborn') # 使用 seaborn 主题
plt.rcParams['figure.figsize'] = 20, 10  # 全局修改图大小

# 修改 Trades 观测器的样式
class my_Trades(bt.observers.Trades):
    plotlines = dict(
    pnlplus=dict(_name='Positive',
                 marker='^', color='#ed665d',
                 markersize=8.0, fillstyle='full'),
    pnlminus=dict(_name='Negative',
                  marker='v', color='#729ece',
                  markersize=8.0, fillstyle='full'))

# 修改 BuySell 的样式
class my_BuySell(bt.observers.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'))
    
    
# 简单均线策略
class TestStrategy(bt.Strategy):
    ......
    
# 绘制图形
cerebro1 = bt.Cerebro(stdstats=False)
......
# 添加观测器
cerebro1.addobserver(bt.observers.DrawDown)
cerebro1.addobserver(bt.observers.Benchmark, data=datafeed1)
cerebro1.addobserver(bt.observers.Broker)
cerebro1.addobserver(my_Trades)
cerebro1.addobserver(my_BuySell)
#先运行策略
rasult = cerebro1.run()
#策略运行完后,绘制图形
colors = ['#729ece', '#ff9e4a', '#67bf5c', '#ed665d', '#ad8bc9', '#a8786e', '#ed97ca', '#a2a2a2', '#cdcc5d', '#6dccda']
tab10_index = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9]
cerebro1.plot(iplot=False,
              style='line', # 绘制线型价格走势,可改为 'candel' 样式
              lcolors=colors,
              plotdist=0.1,
              bartrans=0.2,
              volup='#ff9896',
              voldown='#98df8a',
              loc='#5f5a41',
              grid=False) # 删除水平网格

图片

蜡烛图样式:

绘制蜡烛图时,蜡烛之间会比较拥挤,可以通过设置 numfigs=2,分 2 部分绘制。下图只展示了后半部分的内容。

图片

基于收益序列进行可视化

Backtrader 自带的绘图工具方便好用,不过平时在汇报策略回测结果时,可能更关注的是策略的累计收益曲线和业绩评价指标等结果,而这些回测统计信息只需基于回测返回的 TimeReturn 收益序列做简单计算即可得到。下面是基于 Backtrader 回测返回的分析器 TimeReturn、pyfolio、matplotlib 得到的可视化图形:

.....
# 回测时需要添加 TimeReturn 分析器
cerebro1.addanalyzer(bt.analyzers.TimeReturn, _name='_TimeReturn')
result = cerebro1.run()

# 提取收益序列
pnl = pd.Series(result[0].analyzers._TimeReturn.get_analysis())
# 计算累计收益
cumulative = (pnl + 1).cumprod()
# 计算回撤序列
max_return = cumulative.cummax()
drawdown = (cumulative - max_return) / max_return
# 计算收益评价指标
import pyfolio as pf
# 按年统计收益指标
perf_stats_year = (pnl).groupby(pnl.index.to_period('y')).apply(lambda data: pf.timeseries.perf_stats(data)).unstack()
# 统计所有时间段的收益指标
perf_stats_all = pf.timeseries.perf_stats((pnl)).to_frame(name='all')
perf_stats = pd.concat([perf_stats_year, perf_stats_all.T], axis=0)
perf_stats_ = round(perf_stats,4).reset_index()


# 绘制图形
import matplotlib.pyplot as plt
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号
import matplotlib.ticker as ticker # 导入设置坐标轴的模块
plt.style.use('seaborn') # plt.style.use('dark_background')

fig, (ax0, ax1) = plt.subplots(2,1, gridspec_kw = {'height_ratios':[1.5, 4]}, figsize=(20,8))
cols_names = ['date', 'Annual\nreturn', 'Cumulative\nreturns', 'Annual\nvolatility',
       'Sharpe\nratio', 'Calmar\nratio', 'Stability', 'Max\ndrawdown',
       'Omega\nratio', 'Sortino\nratio', 'Skew', 'Kurtosis', 'Tail\nratio',
       'Daily value\nat risk']

# 绘制表格
ax0.set_axis_off() # 除去坐标轴
table = ax0.table(cellText = perf_stats_.values,
                bbox=(0,0,1,1), # 设置表格位置, (x0, y0, width, height)
                rowLoc = 'right', # 行标题居中
                cellLoc='right' ,
                colLabels = cols_names, # 设置列标题
                colLoc = 'right', # 列标题居中
                edges = 'open' # 不显示表格边框
                )
table.set_fontsize(13)

# 绘制累计收益曲线
ax2 = ax1.twinx()
ax1.yaxis.set_ticks_position('right') # 将回撤曲线的 y 轴移至右侧
ax2.yaxis.set_ticks_position('left') # 将累计收益曲线的 y 轴移至左侧
# 绘制回撤曲线
drawdown.plot.area(ax=ax1, label='drawdown (right)', rot=0, alpha=0.3, fontsize=13, grid=False)
# 绘制累计收益曲线
(cumulative).plot(ax=ax2, color='#F1C40F' , lw=3.0, label='cumret (left)', rot=0, fontsize=13, grid=False)
# 不然 x 轴留有空白
ax2.set_xbound(lower=cumulative.index.min(), upper=cumulative.index.max())
# 主轴定位器:每 5 个月显示一个日期:根据具体天数来做排版
ax2.xaxis.set_major_locator(ticker.MultipleLocator(100))
# 同时绘制双轴的图例
h1,l1 = ax1.get_legend_handles_labels()
h2,l2 = ax2.get_legend_handles_labels()
plt.legend(h1+h2,l1+l2, fontsize=12, loc='upper left', ncol=1)

fig.tight_layout() # 规整排版
plt.show()

‘seaborn’ 主题下的绘制效果:

图片

‘dark_background’ 主题下的绘制效果:

图片

总结

关于回测结果的可视化,不同的需求对应不同的可视化内容。Backtrader 回测框架本身就对原始数据集 Data Feeds、回测指标 Indicators 、回测结果观测器 Observers 提供了对用户非常友好的绘图接口。对于一些额外的数据,也可以结合 Backtrader 分析器 Analyzers 返回的指标,选用自己熟悉的 Python 绘图工具 Matplotlib、Seaborn、Plotly 等来进行可视化展示。

赞(0)
未经允许不得转载:大象juǎn » Backtrader回测可视化重构解析