Problem Description
Solution
To find the best input parameters for your trading strategy in QuantConnect, you must expose specific variables to the LEAN Optimizer. This is done using the self.get_parameter() method.
When you run an optimization job (Grid Search or Euler) in the QuantConnect cloud or local environment, the engine iterates through the ranges you define, injects those values into your algorithm via get_parameter, and calculates the objective function (e.g., Sharpe Ratio) to find the optimal combination.
Key Steps to Implement Parameter Optimization
- Define Parameters: Replace hard-coded values (like moving average lengths or thresholds) with
self.get_parameter(). - Cast Types: The optimizer passes values as strings. You must cast them to
int,float, orboolbefore using them. - Set Defaults: Always provide a default value so the algorithm works in standard backtests without the optimizer.
Code Example: Optimizing a Dual EMA Crossover
The following strategy optimizes the lookback periods for a fast and slow Exponential Moving Average (EMA).
from AlgorithmImports import *
class ParameterOptimizationStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2023, 1, 1)
self.set_cash(100000)
self.symbol = self.add_equity("SPY", Resolution.DAILY).symbol
# ------------------------------------------------------------
# 1. GET PARAMETERS
# The first argument is the name used in the Optimizer UI.
# The second argument is the default value for standard backtests.
# Note: get_parameter returns a STRING, so we must cast to int.
# ------------------------------------------------------------
# Parameter: Length of the fast moving average
self.fast_period = int(self.get_parameter("fast_period", 12))
# Parameter: Length of the slow moving average
self.slow_period = int(self.get_parameter("slow_period", 26))
# ------------------------------------------------------------
# 2. VALIDATION
# Ensure logic holds (e.g., fast period should be < slow period)
# ------------------------------------------------------------
if self.fast_period >= self.slow_period:
# If the optimizer injects invalid logic, we can handle it
# or simply return to skip this iteration.
self.debug(f"Invalid parameters: Fast {self.fast_period} >= Slow {self.slow_period}")
self.quit()
return
# ------------------------------------------------------------
# 3. INITIALIZE INDICATORS WITH PARAMETERS
# ------------------------------------------------------------
self.fast_ema = self.ema(self.symbol, self.fast_period, Resolution.DAILY)
self.slow_ema = self.ema(self.symbol, self.slow_period, Resolution.DAILY)
# Warm up indicators
self.set_warm_up(self.slow_period)
def on_data(self, data: Slice):
if self.is_warming_up:
return
if not self.fast_ema.is_ready or not self.slow_ema.is_ready:
return
# Trading Logic using the optimized indicators
if self.fast_ema.current.value > self.slow_ema.current.value:
if not self.portfolio.invested:
self.set_holdings(self.symbol, 1.0)
elif self.fast_ema.current.value < self.slow_ema.current.value:
if self.portfolio.invested:
self.liquidate(self.symbol)
def on_end_of_algorithm(self):
# Log the final parameters used for this specific run
self.log(f"Finished run with Fast: {self.fast_period}, Slow: {self.slow_period}")
Best Practices for Optimization
- Avoid Overfitting: Do not optimize every single variable. If you optimize for specific market noise (e.g., a moving average of exactly 13.4 days), your strategy will likely fail in live trading. Look for "parameter stability"—a range of parameters where the strategy performs well, rather than a single spike of high returns.
- Out-of-Sample Testing: Split your data. Run the optimization on data from 2015–2019, pick the best parameters, and then test those specific parameters on data from 2020–2024. If the performance degrades significantly, the strategy is overfitted.
- Constraint Logic: As shown in the code (
if self.fast_period >= self.slow_period), ensure your code handles illogical parameter combinations gracefully, as the optimizer (especially Grid Search) might try every combination blindly.
Q&A: Parameter Optimization in QuantConnect
Q: What happens if I don't provide a default value in get_parameter?
A: If you are running a standard backtest (not an optimization job) and the parameter is not defined in the configuration, get_parameter may return None. It is best practice to always provide a default value (e.g., self.get_parameter("my_var", 10)) to ensure the code runs locally or in standard backtests.
Q: Can I optimize non-numeric values?
A: Yes. Since get_parameter returns a string, you can map strings to logic. For example, you could optimize a parameter named resolution_type with values "minute" or "hour" and use an if/else block in initialize to set the resolution based on that string.
Q: Which optimization target should I use?
A: The Sharpe Ratio is generally preferred over Net Profit. Net Profit can be skewed by a single lucky trade or high leverage/risk. The Sharpe Ratio balances return against the volatility (risk) taken to achieve that return.
Q: How do I define the range for the parameters?
A: You do not define the range (min, max, step) inside the Python code. You define the range in the QuantConnect web UI (Optimization tab) or in the config.json if using the local CLI (LEAN CLI). The Python code only receives the single value for the current iteration.