Skip to content
All library documents

Optimizing Parameters in Stateful Trading Strategies

Article Strategy library · Author: Quantiacs

Summary

This Quantiacs example shows how to tune parameters in a stateful strategy, which processes data day by day and carries prior positions forward. The sample combines moving averages, RSI, candle size, and rate of change to form signals, then applies ATR-based take profit and stop rules and a maximum holding period. Its signal weights and position state are maintained across time steps.

The notebook contrasts exhaustive parameter search with random sampling and recommends testing a subset of combinations, tuning parameters in groups, and examining how results change across settings. It illustrates ranking trials by Sharpe ratio and plotting parameter values against that score before running a final backtest with selected settings. These are workflow examples, not reported evidence that the strategy is profitable: no optimization results or performance statistics are provided. The accompanying guidance warns that searching many parameters can overfit historical data and suggests using fewer parameters, wider parameter spacing, and judgment about how the strategy behaves across the tested range. The example uses Nasdaq 100 stock data and a one-year lookback, so its setup may not transfer directly to other markets or periods.

Key ideas

  • Stateful strategies carry prior positions or other state forward as new data arrives.
  • The example builds signals from trend, momentum, and candle measures, then applies ATR exits and a holding limit.
  • Random parameter sampling can make optimization less computationally demanding than exhaustive grid search.
  • Sharpe ratio plots can help inspect how candidate parameters relate to backtest scores.
  • Searching many settings risks overfitting, and the notebook provides no results establishing live performance.

Tags

Full text
# strategy-stateful_strategy_optimization


# strategy-stateful_strategy_optimization









## Source (MIT)

```python
# ## Stateful Strategy Optimization
# 
# This template demonstrates a method for finding optimal values for each parameter in your stateful/multi-pass strategy. These strategies use day-by-day processing and rely on previous day's input to make decisions.
# If you are not familiar with stateful strategies, you can take a look at [Stateful Long-Short with Exits](https://quantiacs.com/referee/template/17156239/html).
# For single-pass submissions, please take a look at [Trading System Optimization](https://www.quantiacs.com/referee/template/588497/html).
# 
# When you begin writing your trading strategies, the simplest method is to employ technical analysis. However, relying solely on pure technical analysis is often insufficient. You need to adjust the parameters of technical indicators to get an idea of how they affect the Sharpe Ratio and other statistics.
# 
# There is a significant risk of creating an overfitted strategy when using such optimization techniques. Overfitting can be mitigated by using less parameters, bigger ranges between them and examining the results to see how system behaves with the changes. This example illustrates how to implement this approach effectively, reducing the impact of overfitting and enhancing the robustness of your trading strategies.

import xarray as xr
import numpy as np
import pandas as pd
import plotly.express as px

import qnt.backtester as qnbt
import qnt.data as qndata          
import qnt.ta as qnta              
import qnt.optimizer as qno      
import qnt.state as qnstate
import qnt.exits as qnte

# ## Strategy Function
# 
# Here you can define your strategy and enter desired parameters as the arguments.

def strategy(data, state, p1=7.5, p2 = 3, p3 = 18, p4 = 6, p5 = 0.5, p6 = -17):
    
    #Technical indicators
    close = data.sel(field='close')
    open_ = data.sel(field='open')
    atr14 = qnta.atr(data.sel(field='high'), data.sel(field='low'), data.sel(field='close'), 14)
    last_atr = atr14.isel(time=-1)
    atr_perc = xr.where(atr14/close > 0.01, atr14/close, 0.01)
    sma40  = qnta.sma(close, 40)
    sma70  = qnta.sma(close, 70)
    sma200  = qnta.sma(close, 200)
    rsi120 = qnta.rsi(close, 120)
    candle = close - open_
    candlesma100 = qnta.sma(abs(candle), 100)
    roc1day = qnta.roc(close, 1)
    
    if state is None:
        state = {
            "weights": xr.zeros_like(close),
            "open_price": xr.full_like(data.isel(time=-1).asset, np.nan, dtype=int),
            "holding_time": xr.zeros_like(data.isel(time=-1).asset, dtype=int),
            }
    weights_prev = state['weights']
    
    #To reuse the template, define your trading signals here ---------------------
    long_signal = xr.where(sma40 > sma200, xr.where(sma40.shift(time=1) < sma200.shift(time=1), p4, 0), 0) 
    long_signal_2 = xr.where(candle > candlesma100.shift(time=1) * 4.5, p5, 0)
    short_signal = xr.where(rsi120 > 65 , p6, 0)
    exit1 = xr.where(close < sma70, 0, 1)
    exit2 = xr.where(roc1day < -5, 0, 1)
    entry_signal = short_signal + (long_signal + long_signal_2) * exit1 * exit2
    entry_signal = entry_signal/atr_perc
    # ----------------------------------------------------------------------------
    
    #Keeping track of the previous position
    weights_prev, entry_signal = xr.align(weights_prev, entry_signal, join='right')
    weights = xr.where(entry_signal == 0, weights_prev.shift(time=1), entry_signal)
    weights = weights.fillna(0)
    
    #Define additional exit parameters here----------------------------------
    open_price = qnte.update_open_price(data, weights, state) #Update open prices on position change to use with exits
    signal_tp = qnte.take_profit_long_atr(data, weights, open_price, last_atr, atr_amount = p1) #Exit long positions if current close is bigger than entry price + 7*ATR
    signal_sl = qnte.stop_loss_long_atr(data, weights, open_price, last_atr, atr_amount = p2) #Exit long positions if current close is lower than entry price - 3*ATR
    signal_dc = qnte.max_hold_short(weights, state, max_period = p3) #Exit short positions after 10 periods (depending on the data - days, hours etc)
    weights = weights * signal_tp * signal_sl * signal_dc
    #------------------------------------------------------------------------
    
    state['weights'] = weights
    return weights, state

# All optimization code cells in this notebook begin with the comment `#DEBUG#`. This ensures that the Quantiacs backtester will ignore these cells during submission, preventing execution and simplifying the submission process without requiring you to delete or comment out optimizers.

#DEBUG#
data = qndata.stocks.load_ndx_data(min_date="2004-01-01") #Load the data for selected competition type

# ### Optimization Guidelines for Quantiacs Backtester
# 
# The Quantiacs library offers two functions for parameter optimization:
# 
# - **`full_range_args_generator`**: This function exhaustively explores all possible parameter combinations you define.
# 
# - **`random_range_args_generator`**: This function performs a fixed number of random iterations.
# 
# To save time, we recommend using `random_range_args_generator`. Running a defined number of iterations (usually 1-5% of the total grid) often provides sufficient insight into the overall results of the parametric analysis.
# 
# In the example strategy, the 3 iterations defined in the function will take approximately 20 minutes to complete. You can adjust the number of iterations based on your needs—either reducing it for quicker results or increasing it for more thorough analysis, especially if you can let the notebook run for extended periods, such as overnight. Furthermore, it is not necessary to put all of the parameters into the optimizer at once. You can tune them separately to get a better idea of what gives you good results for each one.

#DEBUG#
results = qno.optimize_strategy(
    data, 
    strategy, 
    qno.random_range_args_generator(iterations = 3, 
                                    p3=range(5, 25, 2),
                                    p4=np.arange(3, 6.5, 0.5), 
                                    p5=np.arange(0.5, 2, 0.5), 
                                    p6=range(-10, -21, -5)),
    lookback_period = 365, #Lookback period, default is 365 days
    workers=1 # you can set more workers on your local PC to speed up
)

# Optimizer will create a dictionary with all the results which you can then sort by Sharpe Ratio - see below. This can help you pick the best combination and also see how different parameters influence the score.
# 
# When you want to use the selected parameters in your strategy, simply replace the arguments in the strategy function with the optimizer output

#DEBUG#
all_points = [(elem['weight'], elem['args']) for elem in results['iterations']]
best_points = sorted(all_points, reverse=True)[:10]

best_points

# ### Visualization and Analysis of Optimization Results
# 
# You can find 2 visualization functions below. The first one helps identify how different parameters affect the Sharpe Ratio, facilitating the selection of optimal parameters for the strategy. The second one displays all parameters on a 3D plot, displaying the overall distribution and behavior of the results. You can use these to change (narrow down or expand) parameter ranges, exclude certain parameters and generally get a better grip on where to look for potential improvements. When you are done, simply run the backtester with optimized parameters.

#DEBUG#
sharpe_ratio = [item[0] for item in all_points]
parameters = [item[1] for item in all_points]

param_df = pd.DataFrame(parameters)
param_df['Sharpe Ratio'] = sharpe_ratio
param_columns = param_df.columns[:-1]  

labels = {col: col for col in param_columns}
labels['Sharpe Ratio'] = 'Sharpe Ratio'

fig = px.scatter(
    param_df,
    x=param_columns[0],
    y='Sharpe Ratio',
    color='Sharpe Ratio',
    title='Parameter influence on Sharpe Ratio',
    labels=labels,
    color_continuous_scale='temps_r',  
    range_color=[0, 1]  
)

fig.update_traces(marker={'size': 15})

dropdown_buttons = [
    {
        'args': [{'x': [param_df[col]], 'color': 'Sharpe Ratio'}, {'xaxis.title.text': col}],
        'label': col,
        'method': 'update',
    }
    for col in param_columns
]

fig.update_layout(
    updatemenus=[{
        'buttons': dropdown_buttons,
        'direction': 'down',
        'showactive': True,
        'x': 1.15,
        'xanchor': 'right',
        'y': 1.15,
        'yanchor': 'top'
    }]    
)

fig.show()

print("---")
print("Best iteration:")
display(results['best_iteration'])

#DEBUG#
qno.build_plot(results)

# ## Final Strategy Submission
# 
# This section runs the strategy with finalized parameters to generate the weight outputs for submission. Once you find the best combination of parameters, simply put them in the ```best_params``` dictionary, and it will run the backtest using them.

best_params = dict(p1=7.5, p2 = 3, p3 = 18, p4 = 6, p5 = 0.5, p6 = -17) #Choice of optimized parameters

def best_strategy(data, state):
    return strategy(data, state, **best_params)

weights, state = qnbt.backtest(
    competition_type="stocks_nasdaq100", 
    lookback_period=365,  # lookback in calendar days
    start_date="2006-01-01",
    strategy=best_strategy,
    analyze=True,
    build_plots=True,
    collect_all_states=False 
)

```

Shown in full with attribution under the source's licence. Licence: MIT

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.