#!/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()