Parameter Heatmap

This tutorial will show how to optimize strategies with multiple parameters and how to examine and reason about optimization results. It is assumed you're already familiar with basic backtesting.py usage.

First, let's again import our helper moving average function. In practice, one should use functions from an indicator library, such as TA-Lib or Tulipy.

Our strategy will be a similar moving average cross-over strategy to the one in Quick Start User Guide, but we will use four moving averages in total: two moving averages whose relationship determines a general trend (we only trade long when the shorter MA is above the longer one, and vice versa), and two moving averages whose cross-over with daily close prices determine the signal to enter or exit the position.

It's not a robust strategy, but we can optimize it.

Grid search is an exhaustive search through a set of specified sets of values of hyperparameters. One evaluates the performance for each set of parameters and finally selects the combination that performs best.

Let's optimize our strategy on Google stock data using randomized grid search over the parameter space, evaluating at most (approximately) 200 randomly chosen combinations:

Notice return_heatmap=True parameter passed to Backtest.optimize(). It makes the function return a heatmap series along with the usual stats of the best run. heatmap is a pandas Series indexed with a MultiIndex, a cartesian product of all permissible (tried) parameter values. The series values are from the maximize= argument we provided.

This heatmap contains the results of all the runs, making it very easy to obtain parameter combinations for e.g. three best runs:

But we use vision to make judgements on larger data sets much faster. Let's plot the whole heatmap by projecting it on two chosen dimensions. Say we're mostly interested in how parameters n1 and n2, on average, affect the outcome.

Let's plot this table using the excellent Seaborn package:

We see that, on average, we obtain the highest result using trend-determining parameters n1=40 and n2=60, and it's not like other nearby combinations work similarly well — in our particular strategy, this combination really stands out.

Since our strategy contains several parameters, we might be interested in other relationships between their values. We can use backtesting.lib.plot_heatmaps() function to plot interactive heatmaps of all parameter combinations simultaneously.

Model-based optimization

Above, we used randomized grid search optimization method. Any kind of grid search, however, might be computationally expensive for large data sets. In the follwing example, we will use scikit-optimize package to guide our optimization better informed using forests of decision trees. The hyperparameter model is sequentially improved by evaluating the expensive function (the backtest) at the next best point, thereby hopefully converging to a set of optimal parameters with as few evaluations as possible.

So, with method="skopt":

Notice how the optimization runs somewhat slower even though max_tries= is the same. But that's due to the sequential nature of the algorithm and should actually perform rather comparably even in cases of much larger parameter spaces where grid search would effectively blow up, but likely (hopefully) reaching a better local optimum than a randomized search would. A note of warning, again, to take steps to avoid overfitting insofar as possible.

Understanding the impact of each parameter on the computed objective function is easy in two dimensions, but as the number of dimensions grows, partial dependency plots are increasingly useful. Plotting tools from scikit-optimize take care of many of the more mundane things needed to make good and informative plots of the parameter space:

Learn more by exploring further examples or find more framework options in the full API reference.