#!/usr/bin/env python3
# coding: utf-8
# @Author: ArthurBernard
# @Email: arthur.bernard.92@gmail.com
# @Date: 2019-03-05 13:50:16
# @Last modified by: ArthurBernard
# @Last modified time: 2020-07-31 19:42:35
""" Static plotting of backtest results.
Matplotlib- and Seaborn-based helpers to render strategy performance
curves, drawdowns and rolling statistics from a return series. Suited
to post-run analysis and reporting; see
:mod:`fynance.backtest.dynamic_plot_backtest` for live-training plots.
Main entry points
-----------------
- :class:`PlotBackTest` — figure object that draws cumulative
performance, underlying series and configurable annotations.
"""
# Built-in packages
# External packages
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
# Local packages
# Set plot style
plt.style.use('seaborn-v0_8')
__all__ = ['PlotBackTest']
[docs]
class PlotBackTest:
""" Plot backtest object.
Lightweight wrapper around a Matplotlib ``Figure`` / ``Axes``
pair used to render the cumulative-performance curve of one or
several strategies. Reuse an existing figure by passing ``fig``
and ``ax``, or let the constructor create one. Axis labels,
scales, ticks and title are configured once at construction via
``**kwargs``; subsequent calls to :meth:`plot` only add data.
For live updates during walk-forward training, use
:class:`fynance.backtest.dynamic_plot_backtest.BacktestNeuralNet`,
which builds on the same primitives but refreshes the canvas on
each iteration.
Attributes
----------
fig : matplotlib.figure.Figure
Figure to display backtest.
ax : matplotlib.axes
Axe(s) to display a part of backtest.
Methods
-------
plot(y, x=None, names=None, col='Blues', lw=1., **kwargs)
Plot performances.
See Also
--------
DynaPlotBackTest, display_perf, set_text_stats
"""
def __init__(self, fig=None, ax=None, size=(9, 6), dynamic=False,
**kwargs):
""" Initialize method.
Sets size of training and predicting period, inital value to backtest,
a target filter and training parameters.
Parameters
----------
fig : matplotlib.figure.Figure, optional
Figure to display backtest.
ax : matplotlib.axes, optional
Axe(s) to display a part of backtest.
size : tuple, optional
Size of figure, default is (9, 6)
dynamic : bool, optional
If True set on interactive plot.
kwargs : dict, optional
Axes configuration, cf matplotlib documentation [1]_. Default is
{'yscale': 'linear', 'xscale': 'linear', 'ylabel': '',
'xlabel': '', 'title': '', 'tick_params': {}}
References
----------
.. [1] https://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes
"""
# Set Figure
self._set_figure(fig, ax, size, dynamic=dynamic)
# Set axes
self._set_axes(**kwargs)
def _set_figure(self, fig, ax, size, dynamic=False):
""" Set figure, axes and parameters for dynamic plot. """
# Set figure and axes
if fig is None and ax is None:
self.fig, self.ax = plt.subplots(1, 1, size)
else:
self.fig, self.ax = fig, ax
if dynamic:
plt.ion()
return self
[docs]
def plot(self, y, x=None, names=None, col='Blues', lw=1., unit='raw',
**kwargs):
""" Plot performances.
Parameters
----------
y : np.ndarray[np.float64, ndim=2], with shape (`T`, `N`)
Returns or indexes.
x : np.ndarray[ndim=2], with shape (`T`, 1), optional
x-axis, can be series of int or dates or string.
names : str, optional
Names y lines for legend.
col : str, optional
Color of palette, cf seaborn documentation [2]_.
Default is 'Blues'.
lw : float, optional
Line width of lines.
kwargs : dict, optional
Parameters for `ax.legend` method, cf matplotlib
documentation [3]_.
Returns
-------
pbt : PlotBackTest
Self object.
References
----------
.. [2] https://seaborn.pydata.org/api.html
.. [3] https://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes
"""
# Set data
if len(y.shape) == 1:
T, N = y.size, 1
else:
T, N = y.shape
if x is None:
x = np.arange(T)
col = sns.color_palette(col, N)
# Set graphs
h = self.ax.plot(x, y, LineWidth=lw)
# Set name lines
if names is None:
names = 'Model'
names = [r'${}_{}$'.format(names, i) for i in range(N)]
# Set color and label lines
if len(y.shape) == 1:
h[0].set_color(col[0])
h[0].set_label(self._set_name(names[0][1:-3], y, unit=unit))
else:
for i in range(N):
h[i].set_color(col[i])
h[i].set_label(self._set_name(names[i], y[:, i], unit=unit))
# display
self.ax.legend(**kwargs)
# self.f.canvas.draw()
return self
def _set_name(self, name, y, unit='raw'):
if unit.lower() == 'raw':
return '{}: {:.2f}'.format(name, y[-1])
elif unit.lower() == 'perf':
return '{}: {:.0%}'.format(name, y[-1] / y[0] - 1)
else:
raise ValueError
def _set_axes(self, yscale='linear', xscale='linear', ylabel='',
xlabel='', title='', tick_params={}):
""" Set axes parameters. """
self.ax.clear()
self.ax.set_yscale(yscale)
self.ax.set_xscale(xscale)
self.ax.set_ylabel(ylabel)
self.ax.set_xlabel(xlabel, x=0.9)
self.ax.set_title(title)
self.ax.tick_params(**tick_params)
return self