There is a way to use the optimizer with a (stateful) mulit pass algo, but depending on the total number of changed parameters it can take a very long time. However, if it runs on a local computer with many workers this can still be useful.
We could run the backtester with the multi pass algo to get all the weights for the test period and pass these weights to the optimizer.
There's just one problem with this: you can't pass changed parameters to the strategy using the backtester.
In order to solve this I created a nested function where the outer function takes the changed parameters from the optimizer. The inner function is the actual multi pass strategy and doesn't define the params but just uses the ones from the outer function. Still within the outer function we run the backtester with one set of params, get the weights it returns and return them to the optimizer.
The time it takes to run the optimization would roughly be
(time for 1 multi pass backtest) x (total number of parameter changes) / (number of workers that are able to run)
So if one multi pass takes 1 minute, you want to optimize 10 parameter changes and can run 5 workers it would take about 2 minutes.
Here's an example based on the one above with 2 parameter changes and 2 workers:
import qnt.data as qndata
import qnt.ta as qnta
import qnt.optimizer as qnop
import qnt.backtester as qnbt
import xarray as xr
def load_data(period):
"""Loads the BTC Futures data for the BTC Futures contest"""
return qndata.cryptofutures.load_data(tail=period, dims=("time", "field", "asset"))
def multi_pass_strategy(data, ma_slow_param=50, ma_fast_param=10):
"""The outer function gets called by the optimizer with changed params, the inner function gets passed
to the backtester."""
def strategy(data, state):
# The state isn't used in this example, this is just to show that it can be used while optimizing.
if state is None:
state = 0
state += 1
close = data.sel(field="close")
ma_slow = qnta.lwma(close, ma_slow_param).isel(time=-1)
ma_fast = qnta.lwma(close, ma_fast_param).isel(time=-1)
weights = xr.zeros_like(close.isel(time=-1))
weights[:] = 1 if ma_fast > ma_slow else -1
return weights, state
"""The backtester returns all weights for the test period which will then be returned to the optimizer"""
weights, state = qnbt.backtest(
strategy=strategy,
competition_type="cryptofutures",
load_data=load_data,
lookback_period=700,
start_date='2014-01-01',
build_plots=False,
)
return weights
data = qndata.cryptofutures.load_data(min_date='2014-01-01')
result = qnop.optimize_strategy(
data,
multi_pass_strategy,
qnop.full_range_args_generator(
ma_slow_param=range(50, 60, 5), # min, max, step
# ma_fast_param=range(5, 100, 5) # min, max, step
),
workers=2 # you can set more workers on your PC
)
print("---")
print("Best iteration:")
print(result['best_iteration'])
qnop.build_plot(result)
There might be more efficient ways to do this, so if anyone has one feel free to post it here.