Source code for fynance.backtest.dynamic_plot_backtest

#!/usr/bin/env python3
# coding: utf-8
# @Author: ArthurBernard
# @Email: arthur.bernard.92@gmail.com
# @Date: 2019-03-05 19:17:04
# @Last modified by: ArthurBernard
# @Last modified time: 2023-02-10 10:45:49

""" Live-updating plotting of backtest results during training.

Provides :class:`BacktestNeuralNet`, a Matplotlib figure that updates
its loss curves and cumulative-performance panel after each iteration
of a walk-forward training loop (see :class:`fynance.models.rolling._RollingBasis`).
Useful to monitor convergence and out-of-sample behavior in real time.

Main entry points
-----------------
- :class:`BacktestNeuralNet` — dynamic figure with loss and
  performance subplots refreshed via :meth:`plot_loss` and
  :meth:`plot_perf`.

"""

# Built-in packages

# External packages
from matplotlib import pyplot as plt

# Local packages
from fynance.backtest.plot_backtest import PlotBackTest

# Set plot style
plt.style.use('seaborn-v0_8')

__all__ = ['DynaPlotBackTest']


[docs] class DynaPlotBackTest(PlotBackTest): """ Dynamic plot backtest object. Subclass of :class:`PlotBackTest` configured for interactive updates: ``plt.ion()`` is enabled so that the figure refreshes on every call to :meth:`plot`. Includes default styling for train / eval / test curves and a compact legend, suitable for monitoring walk-forward training in real time alongside :class:`fynance.models.rolling._RollingBasis`. 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, label=None, color='Blues', lw=1., **kwargs) Plot performances. See Also -------- PlotBackTest, display_perf, set_text_stats """ plt.ion() test_plot_kw = dict(label='Test set', color='b', lw=2.) train_plot_kw = dict(label='Train set', color='g', lw=1.) eval_plot_kw = dict(label='Eval set', color='r', lw=1.) legend_kw = { "loc": "upper right", "ncol": 2, "fontsize": 10, "handlelength": 0.8, "columnspacing": 0.5, "frameon": True, } def __init__(self, fig=None, ax=None, size=(9, 6), **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) 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 """ super().__init__(fig, ax, size, True, **kwargs) self.ax_params = kwargs
[docs] def set_axes(self, **kwargs): """ Set axes with initial parameters. Parameters ---------- **kwargs : keyword arguments, optioanl Axes configuration, cf matplotlib documentation [1]_. By default, parameters specified in __init__ method are used. References ---------- .. [1] https://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes """ ax_params = self.ax_params.copy() ax_params.update(kwargs) self._set_axes(**ax_params)
def _set_axes(self, yscale='linear', xscale=None, ylabel='', xlabel='', title='', tick_params={}): """ Set axes parameters. """ self.ax.set_yscale(yscale) 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
[docs] def clear(self): """ Clear axes. """ self.ax.clear()
class DynaPlotAccuracy(DynaPlotBackTest): """ Plot dynamically the accuracy scores. Attributes ---------- fig : matplotlib.figure.Figure Figure to display backtest. ax : matplotlib.axes Axe(s) to display a part of backtest. ax_kwargs : dict Parameters of matplotlib axes containing title, ylabel, xlabel, yscale, xscale and ticks_params. Methods ------- plot set_axe update See Also -------- DynaPlotPerf, DynaPlotLoss """ ax_kw = { # "title": "Model Accuracy", "ylabel": "Accuracy", # "xlabel": "Epochs", "yscale": "linear", # "xscale": "linear", "tick_params": {"axis": "x", "labelsize": 10}, } test_plot_kw = { "label": "Test set", "color": "b", "lw": 1.7, "unit": 'perf', } eval_plot_kw = { "label": "Eval set", "color": "r", "lw": 1.2, "unit": "perf", } train_plot_kw = { "label": "Train set", "color": "g", "lw": 1.2, "unit": "perf", } def __init__(self, fig=None, ax=None, size=(9, 6), **kwargs): """ Initialize method. 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) kwargs : dict, optional Axes configuration, cf matplotlib documentation [1]_. Default is {'yscale': 'linear', 'xscale': 'linear', 'ylabel': 'Accuracy', 'xlabel': 'Epoch', 'title': 'Model Accuracy', 'tick_params': {'axis': 'x', 'labelsize': 10}} References ---------- .. [1] https://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes """ self.ax_kw.update(kwargs) DynaPlotBackTest.__init__(self, fig=fig, ax=ax, size=size, **self.ax_kw) def plot(self, test, eval, train=None, clear=True): """ Plot accuracy scores for test and evaluate set. Parameters ---------- test, eval, train : np.ndarray[np.float64, ndim=1] Respectively test, eval and train accuracy scores. clear : bool, optional Clear axes if True (default). """ if clear: self.clear() # Plot accuracy DynaPlotBackTest.plot(self, test, **self.test_plot_kw) if train is not None: DynaPlotBackTest.plot(self, train, **self.train_plot_kw) DynaPlotBackTest.plot(self, eval, **self.eval_plot_kw) self.set_axes() self.ax.legend(**self.legend_kw) def update(self, test, eval, train=None, clear=True): """ Update plot accuracy scores for test and evaluate set. Parameters ---------- test, eval, train : np.ndarray[np.float64, ndim=1] Respectively test, eval and train accuracy scores. """ # Plot accuracy DynaPlotBackTest.update(self, test, label=self.test_plot_kw['label']) if train is not None: DynaPlotBackTest.update(self, train, label=self.train_plot_kw['label']) DynaPlotBackTest.update(self, eval, label=self.eval_plot_kw['label']) # rescale self.ax.relim() self.ax.autoscale_view() class DynaPlotLoss(DynaPlotBackTest): """ Plot dynamically the loss scores. Attributes ---------- fig : matplotlib.figure.Figure Figure to display backtest. ax : matplotlib.axes Axe(s) to display a part of backtest. ax_kwargs : dict Parameters of matplotlib axes containing title, ylabel, xlabel, yscale, xscale and ticks_params. Methods ------- plot set_axe update See Also -------- DynaPlotPerf, DynaPlotAccuracy """ ax_kw = { # "title": "Model Loss", "ylabel": "Loss", # "xlabel": "Epochs", "yscale": "linear", # "xscale": "linear", "tick_params": {"axis": "x", "labelsize": 10}, } def __init__(self, fig=None, ax=None, size=(9, 6), **kwargs): """ Initialize method. 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) kwargs : dict, optional Axes configuration, cf matplotlib documentation [1]_. Default is {'yscale': 'log', 'xscale': 'linear', 'ylabel': 'Loss', 'xlabel': 'Epoch', 'title': 'Model Loss', 'tick_params': {'axis': 'x', 'labelsize': 10}} References ---------- .. [1] https://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes """ self.ax_kw.update(kwargs) DynaPlotBackTest.__init__(self, fig=fig, ax=ax, size=size, **self.ax_kw) def plot(self, test, eval, train=None, clear=True): """ Plot loss function values for test and evaluate set. Parameters ---------- test, eval, train : np.ndarray[np.float64, ndim=1] Respectively test, eval and train loss scores. clear : bool, optional Clear axes if True (default). """ if clear: self.clear() # Plot loss DynaPlotBackTest.plot(self, test, **self.test_plot_kw) if train is not None: DynaPlotBackTest.plot(self, train, **self.train_plot_kw) DynaPlotBackTest.plot(self, eval, **self.eval_plot_kw) self.set_axes() self.ax.legend(**self.legend_kw) def update(self, test, eval, train=None, clear=True): """ Update plot loss function values for test and evaluate set. Parameters ---------- test, eval, train : np.ndarray[np.float64, ndim=1] Respectively test, eval and train loss scores. """ # Plot loss DynaPlotBackTest.update(self, test, label=self.test_plot_kw['label']) if train is not None: DynaPlotBackTest.update(self, train, label=self.train_plot_kw['label']) DynaPlotBackTest.update(self, eval, label=self.eval_plot_kw['label']) # rescale self.ax.relim() self.ax.autoscale_view() class DynaPlotPerf(DynaPlotBackTest): """ Plot dynamically the performance values. Attributes ---------- fig : matplotlib.figure.Figure Figure to display backtest. ax : matplotlib.axes Axe(s) to display a part of backtest. ax_kwargs : dict Parameters of matplotlib axes containing title, ylabel, xlabel, yscale, xscale and ticks_params. Methods ------- plot set_axe update See Also -------- DynaPlotPerf, DynaPlotAccuracy """ ax_kw = { # "title": "Model Perf", "ylabel": "Perf.", "xlabel": "Epochs", "yscale": "log", # "xscale": "linear", "tick_params": {"axis": "x", "rotation": 30, "labelsize": 10}, } test_plot_kw = { "label": "Test set", "color": "b", "lw": 1.7, # "unit": 'perf', } eval_plot_kw = { "label": "Eval set", "color": "r", "lw": 1.2, # "unit": "perf", } under_plot_kw = { "label": "Underlying", "color": "g", "lw": 1.2, # "unit": "perf" } legend_kw = { "loc": "upper left", "ncol": 2, "fontsize": 10, "handlelength": 0.8, "columnspacing": 0.5, "frameon": True, } def __init__(self, fig=None, ax=None, size=(9, 6), **kwargs): """ Initialize method. 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) kwargs : dict, optional Axes configuration, cf matplotlib documentation [1]_. Default is {'yscale': 'log', 'xscale': 'linear', 'ylabel': 'Perf.', 'xlabel': 'Epoch', 'title': 'Model Perf', 'tick_params': {'axis': 'x', 'roation': 30, 'labelsize': 10}} References ---------- .. [1] https://matplotlib.org/api/axes_api.html#matplotlib.axes.Axes """ self.ax_kw.update(kwargs) DynaPlotBackTest.__init__(self, fig=fig, ax=ax, size=size, **self.ax_kw) self.set_axes() self.ax2 = self.ax.twinx() def plot(self, test, eval, underlying=None, index=None, clear=True): """ Plot performance results of the model for test and evaluate set. Parameters ---------- test, eval : np.ndarray[np.float64, ndim=1] Respectively test and eval performance results of the model. underlying : np.ndarray[np.float64, ndim=1] Performance results of the underlying. index : clear : bool, optional Clear axes if True (default). """ if clear: self.clear() self.ax2.clear() # Set index # if index is not None: # idx_test = index[-test.shape[0]:] # idx_eval = index[: eval.shape[0]] # else: # idx_test = idx_eval = None # Plot perf # DynaPlotBackTest.plot(self, test, x=idx_test, **self.test_plot_kw) # DynaPlotBackTest.plot(self, eval, x=idx_eval, **self.eval_plot_kw) self.h_test = self.ax.plot(test, **self.test_plot_kw) self.h_eval = self.ax2.plot(eval, **self.eval_plot_kw) # Plot perf of the underlying # if underlying is not None: # DynaPlotBackTest.plot(self, underlying, x=idx_eval, # **self.under_plot_kw) self.set_axes() # TEMPORARY SET AXES self.ax.set_ylabel('Test perf.', color='b') self.ax.tick_params(axis="y", labelcolor='b') self.ax2.set_ylabel("Eval perf", color="r") self.ax2.set_yscale("log") self.ax2.tick_params(axis="y", labelcolor="r") # self.ax.legend(**self.legend_kw) def update(self, test, eval, underlying=None, index=None): """ Update plot performance results for test and evaluate set. Parameters ---------- test, eval : np.ndarray[np.float64, ndim=1] Respectively test and eval performance results of the model. underlying : np.ndarray[np.float64, ndim=1] Performance results of the underlying. index : """ # Set index # if index is not None: # idx_test = index[-test.shape[0]:] # idx_eval = index[: eval.shape[0]] # else: # idx_test = idx_eval = None # Plot perf DynaPlotBackTest.update(self, test, label=self.test_plot_kw['label']) DynaPlotBackTest.update(self, eval, label=self.eval_plot_kw['label']) if underlying is not None: DynaPlotBackTest.update(self, underlying, label=self.under_plot_kw['label']) # rescale self.ax.relim() self.ax.autoscale_view()