strategy

Trading System Optimization

This strategy uses the global optimizer to find the best suitable parameters for technical indicators.

You can clone and edit this example there (tab Examples).


Backesting a trading system amounts to perform a simulation of the trading rules on historical data. All trading rules depend to some extent on a set of parameters. These parameters can be the lookback periods used for defining technical indicators or the hyperparameters of a complex machine learning model.

It is very important to study the parameter dependence of the key statistical indicators, for example the Sharpe ratio. A parameter choice which maximizes the value of the Sharpe ratio when the simulation is performed on the past data is a source of backtest overfitting and leads to poor performance on live data.

In this template we provide a tool for studying the parameter dependence of the statistical indicators used for assessing the quality of a trading system.

We recommend optimizing your strategy in a separate notebook because a parametric scan is a time consuming task.

Alternatively it is possible to mark the cells which perform scans using the #DEBUG# tag. When you submit your notebook, the backtesting engine which performs the evaluation on the Quantiacs server will skip these cells.

You can use the optimizer also in your local environment on your machine. Here you can use more workers and take advantage of parallelization to speed up the grid scan process.

In [1]:
%%javascript
IPython.OutputArea.prototype._should_scroll = function(lines) { return false; }
// disable widget scrolling
In [2]:
import qnt.data as qndata
import qnt.ta as qnta
import qnt.output as qnout
import qnt.stats as qns
import qnt.log as qnlog
import qnt.optimizer as qnop
import qnt.backtester as qnbt

import xarray as xr

For defining the strategy we use a single-pass implementation where all data are accessed at once. This implementation is very fast and will speed up the parametric scan.

You should make sure that your strategy is not implicitly forward looking before submission, see how to prevent forward looking.

The strategy is going long only when the rate of change in the last roc_period trading days (in this case 10) of the linear-weighted moving average over the last wma_period trading days (in this case 20) is positive.

In [3]:
def single_pass_strategy(data, wma_period=20, roc_period=10):
    wma = qnta.lwma(data.sel(field='close'), wma_period)
    sroc = qnta.roc(wma, roc_period)
    weights = xr.where(sroc > 0, 1, 0)
    weights = weights / len(data.asset) # normalize weights so that sum=1, fully invested
    with qnlog.Settings(info=False, err=False): # suppress log messages
        weights = qnout.clean(weights, data) # check for problems
    return weights

Let us first check the performance of the strategy with the chosen parameters:

In [4]:
#DEBUG#
# evaluator will remove all cells with this tag before evaluation

data = qndata.futures.load_data(min_date='2004-01-01') # indicators need warmup, so prepend data
single_pass_output = single_pass_strategy(data)
single_pass_stat = qns.calc_stat(data, single_pass_output.sel(time=slice('2006-01-01', None)))
display(single_pass_stat.to_pandas().tail())
100% (35948152 of 35948152) |############| Elapsed Time: 0:00:00 Time:  0:00:00
field equity relative_return volatility underwater max_drawdown sharpe_ratio mean_return bias instruments avg_turnover avg_holding_time
time
2024-09-05 1.173087 0.001499 0.040239 -0.079345 -0.176963 0.213221 0.008580 1.0 71.0 0.040799 26.331414
2024-09-06 1.165585 -0.006395 0.040263 -0.085233 -0.176963 0.204456 0.008232 1.0 71.0 0.040794 26.331414
2024-09-09 1.170036 0.003818 0.040268 -0.081740 -0.176963 0.209492 0.008436 1.0 71.0 0.040790 26.331414
2024-09-10 1.169447 -0.000503 0.040264 -0.082202 -0.176963 0.208795 0.008407 1.0 71.0 0.040794 26.327223
2024-09-11 1.169719 0.000232 0.040260 -0.081988 -0.176963 0.209085 0.008418 1.0 71.0 0.040792 26.412667

A parametric scan over pre-defined ranges of wma_period and roc_period can be performed with the Quantiacs optimizer function:

In [5]:
#DEBUG#
# evaluator will remove all cells with this tag before evaluation

data = qndata.futures.load_data(min_date='2004-01-01') # indicators need warmup, so prepend data

result = qnop.optimize_strategy(
    data,
    single_pass_strategy,
    qnop.full_range_args_generator(
        wma_period=range(10, 150, 5), # min, max, step
        roc_period=range(5, 100, 5)   # min, max, step
    ),
    workers=1 # you can set more workers when you run this code on your local PC to speed it up
)

qnop.build_plot(result) # interactive chart in the notebook

print("---")
print("Best iteration:")
display(result['best_iteration']) # as a reference, display the iteration with the highest Sharpe ratio
100% (532 of 532) |######################| Elapsed Time: 0:06:07 Time:  0:06:07
---
Best iteration:
{'args': {'wma_period': 15, 'roc_period': 80},
 'result': {'equity': 1.4502760831903705,
  'relative_return': -0.00035637716262848507,
  'volatility': 0.04008172355275785,
  'underwater': -0.058409898613890676,
  'max_drawdown': -0.11903276243762739,
  'sharpe_ratio': 0.5009156962294914,
  'mean_return': 0.020077564459507702,
  'bias': 1.0,
  'instruments': 71.0,
  'avg_turnover': 0.01697377171418696,
  'avg_holding_time': 74.88767353807415},
 'weight': 0.5009156962294914,
 'exception': None}

The arguments for the iteration with the highest Sharpe ratio can be later defined manually or calling result['best_iteration']['args'] for the final strategy. Note that cells with the tag #DEBUG# are disabled.

The final multi-pass call backtest for the optimized strategy is very simple, and it amounts to calling the last iteration of the single-pass implementation with the desired parameters:

In [6]:
best_args = dict(wma_period=20, roc_period=80) # highest Sharpe ratio iteration (not recommended, overfitting!)

def best_strategy(data):
    return single_pass_strategy(data, **best_args).isel(time=-1)

weights = qnbt.backtest(
    competition_type="futures",
    lookback_period=2 * 365,
    start_date='2006-01-01',
    strategy=best_strategy,
    analyze=True,
    build_plots=True
)
Run last pass...
Load data...
Run strategy...
Load data for cleanup...
Output cleaning...
fix uniq
Normalization...
Output cleaning is complete.
Write result...
Write output: /root/fractions.nc.gz
---
Run first pass...
Load data...
100% (16521772 of 16521772) |############| Elapsed Time: 0:00:00 Time:  0:00:00
Run strategy...
---
Load full data...
---
Run iterations...

100% (4881 of 4881) |####################| Elapsed Time: 0:02:14 Time:  0:02:14
Merge outputs...
Load data for cleanup and analysis...
Output cleaning...
fix uniq
ffill if the current price is None...
Check missed dates...
Ok.
Normalization...
Output cleaning is complete.
Write result...
Write output: /root/fractions.nc.gz
---
Analyze results...
Check...
Check missed dates...
Ok.
Check the sharpe ratio...
Period: 2006-01-01 - 2024-09-11
Sharpe Ratio = 0.4926044181456791
ERROR! The Sharpe Ratio is too low. 0.4926044181456791 < 1
Improve the strategy and make sure that the in-sample Sharpe Ratio more than 1.
Check correlation.
WARNING! Can't calculate correlation.
Correlation check failed.
---
Align...
Calc global stats...
---
Calc stats per asset...
Build plots...
---
Output:
asset F_AD F_AE F_AG F_AH F_AX F_BC F_BG F_BO F_BP F_C
time
2024-08-29 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-08-30 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-02 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-03 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-04 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-05 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-06 0.014085 0.014085 0.000000 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-09 0.014085 0.014085 0.014085 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-10 0.014085 0.000000 0.014085 0.0 0.014085 0.0 0.0 0.0 0.014085 0.0
2024-09-11 0.014085 0.000000 0.014085 0.0 0.000000 0.0 0.0 0.0 0.014085 0.0
Stats:
field equity relative_return volatility underwater max_drawdown sharpe_ratio mean_return bias instruments avg_turnover avg_holding_time
time
2024-08-29 1.452843 0.000270 0.040521 -0.063679 -0.121687 0.498795 0.020212 1.0 71.0 0.015434 85.058191
2024-08-30 1.452546 -0.000205 0.040517 -0.063871 -0.121687 0.498466 0.020196 1.0 71.0 0.015432 85.058191
2024-09-02 1.451920 -0.000431 0.040513 -0.064274 -0.121687 0.497830 0.020169 1.0 71.0 0.015429 85.058191
2024-09-03 1.451318 -0.000414 0.040509 -0.064662 -0.121687 0.497218 0.020142 1.0 71.0 0.015426 85.058191
2024-09-04 1.451043 -0.000190 0.040505 -0.064839 -0.121687 0.496909 0.020127 1.0 71.0 0.015428 85.021505
2024-09-05 1.451156 0.000078 0.040501 -0.064766 -0.121687 0.496963 0.020127 1.0 71.0 0.015426 85.021505
2024-09-06 1.446150 -0.003450 0.040505 -0.067992 -0.121687 0.492154 0.019935 1.0 71.0 0.015423 85.021505
2024-09-09 1.448238 0.001444 0.040502 -0.066647 -0.121687 0.494031 0.020009 1.0 71.0 0.015430 85.073206
2024-09-10 1.447185 -0.000727 0.040498 -0.067325 -0.121687 0.492994 0.019965 1.0 71.0 0.015433 85.128293
2024-09-11 1.446823 -0.000251 0.040494 -0.067559 -0.121687 0.492604 0.019948 1.0 71.0 0.015436 85.092823