book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Optimization

Parameters

Introduction

Parameters are project variables that your algorithm uses to define the value of internal variables like indicator arguments or the length of lookback windows. Parameters are stored outside of your algorithm code, but we inject the values of the parameters into your algorithm when you run a backtest, deploy a live algorithm, or launch an optimization job. To use parameters, set some parameters in your project and then load them into your algorithm.

Set Parameters

The process to set parameter values depends on the environment you use to write algorithms. See the tutorial in one of the following environments:

Get Parameters

To get a parameter value into your algorithm, call the GetParameterget_parameter method of the algorithm class.

// Get the parameter string-value with this matching key. 
// Project parameters are useful for optimization or securing variables outside the source-code.
var parameterValue = GetParameter("Parameter Name");
# Get the parameter string-value with this matching key. 
# Project parameters are useful for optimization or securing variables outside the source-code.
parameter_value = self.get_parameter("Parameter Name")

The GetParameterget_parameter method returns a string by default. If you provide a default parameter value, the method returns the parameter value as the same data type as the default value. If there are no parameters in your project that match the name you pass to the method and you provide a default value to the method, it returns the default value. The following table describes the arguments the GetParameterget_parameter method accepts:

ArgumentData TypeDescriptionDefault Value
namestringstrThe name of the parameter to get
defaultValuestring/int/double/decimalThe default value to returnnull
default_valuestr/int/doubleThe default value to returnNone

The following example algorithm gets the values of parameters of each data type:

// Example to get the values of parameters of each data type:
namespace QuantConnect.Algorithm.CSharp
{
    public class ParameterizedAlgorithm : QCAlgorithm
    {
        public override void Initialize()
        {
            // Get the parameter value and return an integer.
            var intParameterValue = GetParameter("intParameterName", 100);

            // Get the parameter value and return a double.
            var doubleParameterValue = GetParameter("doubleParameterName", 0.95);

            // Get the parameter value and return a decimal.
            var decimalParameterValue = GetParameter("decimalParameterName", 0.05m);

            // Get the parameter value as a string.
            var stringParameterValue = GetParameter("parameterName", "defaultStringValue")

            // Cast the string to an integer.
            var castedParameterValue = Convert.ToInt32(stringParameterValue);
        }
    }
}
# Example to get the values of parameters of each data type:
class ParameterizedAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        # Get the parameter value and return an integer.
        int_parameter_value = self.get_parameter("int_parameter_name", 100)

        # Get the parameter value and return a double.
        float_parameter_value = self.get_parameter("float_parameter_name", 0.95)

        # Get the parameter value as a string.
        string_parameter_value = self.get_parameter("parameter_name", "default_string_value")

        # Cast the string to an integer.
        parameter_value = int(string_parameter_value)

Alternatively, you can use the Parameter(name) attribute on top of class fields or properties to set their values. If there are no parameters in your project that match the name you pass to the attribute and you provide a default value to the method, it returns the default value.

// Use the Parameter attribute to set field values externally, enabling flexible configuration with default fallback.
namespace QuantConnect.Algorithm.CSharp
{
    public class ParameterizedAlgorithm : QCAlgorithm
    {
        [Parameter("intParameterName")]
        public int IntParameterValue = 100;

        [Parameter("doubleParameterName")]
        public double DoubleParameterValue = 0.95;

        [Parameter("decimalParameterName")]
        public decimal DecimalParameterValue = 0.05m;
    }
}

The parameter values are sent to your algorithm when you deploy the algorithm, so it's not possible to change the parameter values while the algorithm runs.

Overfitting

Overfitting occurs when a function is fit too closely fit to a limited set of training data. Overfitting can occur in your trading algorithms if you have many parameters or select parameters values that worked very well in the past but are sensitive to small changes in their values. In these cases, your algorithm will likely be fine-tuned to fit the detail and noise of the historical data to the extent that it negatively impacts the live performance of your algorithm. The following image shows examples of underfit, optimally-fit, and overfit functions:

Overfitting an optimization job

An algorithm that is dynamic and generalizes to new data is more likely to survive across different market conditions and apply to other markets.

Look-Ahead Bias

Look-ahead bias occurs when an algorithm makes decisions using data that would not have yet been available. For instance, in optimization jobs, you optimize a set of parameters over a historical backtesting period. After the optimizer finds the optimal parameter values, the backtest period becomes part of the in-sample data. If you run a backtest over the same period using the optimal parameters, look-ahead bias has seeped into your research. In reality, it would not be possible to know the optimal parameters during the testing period until after the testing period is over. To avoid issues with look-ahead bias, optimize on older historical data and test the optimal parameter values on recent historical data. Alternatively, apply walk forward optimization to optimize the parameters on smaller batches of history.

Live Trading Considerations

To update parameters in live mode, add a Schedule Event that downloads a remote file and uses its contents to update the parameter values.

private Dictionary _parameters = new();
public override void Initialize()
{
    if (LiveMode)
    {
        Schedule.On(
            DateRules.EveryDay(),
            TimeRules.Every(TimeSpan.FromMinutes(1)),
            ()=>
            {
                var content = Download(urlToRemoteFile);
                // Convert content to _parameters
            });
    }
}
def initialize(self):
    self.parameters = { }
    if self.live_mode:
        def download_parameters():
            content = self.download(url_to_remote_file)
            # Convert content to self.parameters

        self.schedule.on(self.date_rules.every_day(), self.time_rules.every(timedelta(minutes=1)), download_parameters)

Examples

The following examples demonstrate common practices for using optimization parameters.

Example 1: Bond Candidate Selection

The following algorithm holds a 60/40 stock-bond portfolio rebalancing monthly. To select the best bond ETF to act as a proxy to trade bonds, we create a list of bond tickers and grid search the best using each to backtest. Using a bond-id parameter, which is an integer ranging from 0 to the list size - 1 with a step size of 1, we can perform optimization to test each of them.

public class OptimizationParameterAlgorithm : QCAlgorithm
{
    // A list of bond ETFs candidates to trade as bond proxies. 
    private static readonly List<string> _bondList = new()
    {
        "TLT", "IEF", "SHY", "VCSH", "VCLT"
    };
    private Symbol _spy, _bond;

    public override void Initialize()
    {
        SetStartDate(2023, 1, 1);
        SetEndDate(2024, 1, 1);

        // Get the bond candidate to be tested.
        var bondId = GetParameter("bond-id", 0);
        
        // Request SPY and the selected bond data for trading.
        _spy = AddEquity("SPY", Resolution.Minute).Symbol;
        _bond = AddEquity(_bondList[bondId], Resolution.Minute).Symbol;

        // Rebalance for a stock-bond portfolio monthly.
        Schedule.On(
            DateRules.MonthStart(_spy),
            TimeRules.AfterMarketOpen(_spy, 1),
            Rebalance
        );
    }

    private void Rebalance()
    {
        // Hold an all-weather stock-bond 60/40 portfolio.
        SetHoldings(_spy, 0.6m);
        SetHoldings(_bond, 0.4m);
    }
}
class OptimizationParameterAlgorithm(QCAlgorithm):
    # A list of bond ETFs as candidates to trade as bond proxies. 
    _bond_list = [
        "TLT", "IEF", "SHY", "VCSH", "VCLT"
    ]

    def initialize(self) -> None:
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2024, 1, 1)
    
        # Get the bond candidate to be tested.
        bond_id = self.get_parameter("bond-id", 0)
    
        # Request SPY and the selected bond data for trading.
        self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol
        self.bond = self.add_equity(self._bond_list[bond_id], Resolution.MINUTE).symbol
    
        # Rebalance for a stock-bond portfolio monthly.
        self.schedule.on(
            self.date_rules.month_start(self.spy),
            self.time_rules.after_market_open(self.spy, 1),
            self.rebalance
        )
    
    def rebalance(self) -> None:
        # Hold an all-weather stock-bond 60/40 portfolio.
        self.set_holdings(self.spy, 0.6)
        self.set_holdings(self.bond, 0.4)

Example 2: Test Different Period

This algorithm holds SPY for the whole day following the direction from the previous day. To test which period is the most effective for this momentum-following strategy, we can set an offset parameter and adjust the backtest period accordingly.

You can also load the parameter values with the Parameter attribute.

[Parameter("offset")]
private int _offset = 0;
public class OptimizationParameterAlgorithm : QCAlgorithm
{
    // Get the number of months to offset the start date.
    [Parameter("offset")]
    private int _offset = 0;

    private Symbol _spy;
    private decimal _lastPrice = 0m;

    public override void Initialize()
    {
        // Create a variable start date with offset to test which periods the algorithm performs better.
        var startDate = new DateTime(2019, 1, 1);
        SetStartDate(startDate.AddMonths(_offset));
        // One-month fixed period test per backtest.
        SetEndDate(StartDate.AddMonths(1));
        
        // Request daily SPY data for trading.
        _spy = AddEquity("SPY", Resolution.Daily).Symbol;

        SetWarmUp(1);
    }

    public override void OnData(Slice slice)
    {
        // Trade based on updated data.
        if (slice.Bars.TryGetValue(_spy, out var bar))
        {
            // Trade with riding on the last day's momentum.
            if (_lastPrice != 0m && bar.Close > _lastPrice && !Portfolio[_spy].IsLong)
            {
                SetHoldings(_spy, 0.5m);
            }
            else if (_lastPrice != 0m && bar.Close < _lastPrice && !Portfolio[_spy].IsShort)
            {
                SetHoldings(_spy, -0.5m);
            }

            _lastPrice = bar.Close;
        }
    }
}
from dateutil.relativedelta import relativedelta 
class OptimizationParameterAlgorithm(QCAlgorithm):
    last_price = 0

    def initialize(self) -> None:
        # Get the number of months to offset the start date.
        offset = self.get_parameter("offset", 0)
        # Create a variable start date with offset to test which periods the algorithm performs better.
        start_date = datetime(2019, 1, 1)
        self.set_start_date(start_date + relativedelta(months=offset))
        # One-month fixed period test per backtest.
        self.set_end_date(self.start_date + relativedelta(months=1))
    
        # Request daily SPY data for trading.
        self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
    
        self.set_warm_up(1)
    
    def on_data(self, slice: Slice) -> None:
        # Trade based on updated data.
        bar = slice.bars.get(self.spy)
        if bar:
            # Trade with riding on the last day's momentum.
            if self.last_price != 0 and bar.close > self.last_price and not self.portfolio[self.spy].is_long:
                self.set_holdings(self.spy, 0.5)
            elif self.last_price != 0 and bar.close < self.last_price and not self.portfolio[self.spy].is_short:
                self.set_holdings(self.spy, -0.5)

            self.last_price = bar.close

Example 3: Hyperparameter Tuning

The following example algorithm demonstrates an EMA cross strategy. To optimize for the best setup to attain the greatest Sharpe Ratio, the algorithm grid searches the best hyperparameters (the EMA periods) that do so.

public class ParameterizedAlgorithm : QCAlgorithm
{
    private Symbol _spy;
    private ExponentialMovingAverage _fast;
    private ExponentialMovingAverage _slow;

    public override void Initialize()
    {
        SetStartDate(2020, 1, 1);
        SetCash(100000);
        // For warming up indicators.
        Settings.AutomaticIndicatorWarmUp = true;

        // Request SPY data to feed the indicator and trade.
        _spy = AddEquity("SPY").Symbol;

        // Get and store the EMA settings to local parameters.
        var fastPeriod = GetParameter("ema-fast", 50);
        var slowPeriod = GetParameter("ema-slow", 200);

        // Set up the 2 EMA indicators for trade signal generation with the fast and slow periods from parameters.
        _fast = EMA("SPY", fastPeriod, Resolution.Daily);
        _slow = EMA("SPY", slowPeriod, Resolution.Daily);
    }

    public override void OnData(Slice slice)
    {
        if (slice.Bars.TryGetValue(_spy, out var bar))
        {
            // Following the uptrend indicated by price up in nearer periods.
            if (bar.Close > _fast && _fast > _slow)
            {
                SetHoldings(_spy, 0.5m);
            }
            // Following the downtrend indicated by price down in nearer periods.
            else if (bar.Close < _fast && _fast < _slow)
            {
                SetHoldings(_spy, -0.5m);
            }
            // Liquidate if the trend is not strong.
            else
            {
                Liquidate();
            }
        }
    }
}
class ParameterizedAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2020, 1, 1)
        self.set_cash(100000)
        # For warming up indicators.
        self.settings.automatic_indicator_warm_up = True

        # Request SPY data to feed the indicator and trade.
        self.spy = self.add_equity("SPY").symbol
        
        # Get and store the EMA settings to local parameters.
        fast_period = self.get_parameter("ema-fast", 50)
        slow_period = self.get_parameter("ema-slow", 200)
    
        # Set up the 2 EMA indicators for trade signal generation with the fast and slow periods from parameters.
        self._fast = self.ema("SPY", fast_period, Resolution.DAILY)
        self._slow = self.ema("SPY", slow_period, Resolution.DAILY)
    
    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self.spy)
        if bar:
            # Following the uptrend indicated by price up in nearer periods.
            if bar.close > self._fast.current.value > self._slow.current.value:
                self.set_holdings(self.spy, 0.5)
            # Following the downtrend indicated by price down in nearer periods.
            elif bar.close < self._fast.current.value < self._slow.current.value:
                self.set_holdings(self.spy, -0.5)
            # Liquidate if the trend is not strong.
            else:
                self.liquidate()

Other Examples

For more examples, see the following algorithms:

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: