🚀 AI One-Click Generation of quantconnect Strategy Code

How to Implement a Dynamic Lookback Period for Technical Indicators in QuantConnect

quantconnect | Published on: 2025/12/4
The following content is generated by EasyQuant .

Problem Description

How can I create a trading indicator with a lookback period that changes based on market volatility?

Solution

To create a trading indicator with a dynamic lookback period based on market volatility, you typically follow these steps:

  1. Measure Volatility: Calculate the standard deviation of prices over a fixed period (e.g., 30 days).
  2. Calculate Change: Compare the current volatility to a previous baseline (e.g., yesterday's volatility or a moving average of volatility).
  3. Adjust Lookback:
    • If volatility increases, you might increase the lookback period to filter out noise (reduce sensitivity).
    • If volatility decreases, you might decrease the lookback period to react faster to new trends (increase sensitivity).
  4. Clamp Values: Ensure the calculated lookback period stays within reasonable bounds (e.g., minimum 10 days, maximum 60 days).

Below is a complete, runnable QuantConnect algorithm. It implements a Dynamic Simple Moving Average (SMA). The length of the SMA changes daily based on the volatility of the underlying asset.

Dynamic Lookback Strategy Code

# region imports
from AlgorithmImports import *
import numpy as np
# endregion

class DynamicLookbackIndicatorAlgorithm(QCAlgorithm):

    def initialize(self):
        # 1. Set Setup Parameters
        self.set_start_date(2018, 1, 1)
        self.set_end_date(2023, 1, 1)
        self.set_cash(100000)

        # 2. Add Assets
        self.ticker = "SPY"
        self.symbol = self.add_equity(self.ticker, Resolution.DAILY).symbol

        # 3. Define Volatility Settings
        self.volatility_window = 30  # Days used to calculate StdDev
        
        # 4. Define Dynamic Lookback Constraints
        self.current_lookback = 20   # Starting lookback period
        self.min_lookback = 10       # Floor
        self.max_lookback = 60       # Ceiling

        # 5. Warm up period (max lookback + volatility window)
        self.set_warm_up(self.max_lookback + self.volatility_window)

    def on_data(self, data: Slice):
        # Ensure we have data for the symbol
        if not data.contains_key(self.symbol) or data[self.symbol] is None:
            return

        # 1. Fetch History for Volatility Calculation
        # We need enough data for the volatility window + 1 day for 'yesterday' comparison
        history_vol = self.history(self.symbol, self.volatility_window + 1, Resolution.DAILY)
        
        if history_vol.empty or len(history_vol) < self.volatility_window + 1:
            return

        # Extract close prices
        closes = history_vol['close'].values

        # 2. Calculate Volatility (Standard Deviation)
        # Today's volatility (last 30 days)
        today_vol = np.std(closes[1:]) 
        # Yesterday's volatility (previous 30 days shifted back by 1)
        yesterday_vol = np.std(closes[:-1])

        # Avoid division by zero
        if today_vol == 0:
            return

        # 3. Calculate the Change in Volatility
        # Formula: percent change in volatility
        delta_vol = (today_vol - yesterday_vol) / today_vol

        # 4. Adjust the Lookback Period
        # If volatility increases, we increase the lookback (to smooth noise).
        # If volatility decreases, we decrease the lookback (to catch trends).
        raw_new_lookback = self.current_lookback * (1 + delta_vol)
        
        # Round to nearest integer
        new_lookback = int(round(raw_new_lookback))

        # 5. Clamp the Lookback Period (Apply Floor and Ceiling)
        if new_lookback > self.max_lookback:
            self.current_lookback = self.max_lookback
        elif new_lookback < self.min_lookback:
            self.current_lookback = self.min_lookback
        else:
            self.current_lookback = new_lookback

        # 6. Calculate the Dynamic Indicator (Dynamic SMA)
        # Fetch history based on the NEW dynamic lookback period
        history_dynamic = self.history(self.symbol, self.current_lookback, Resolution.DAILY)
        
        if history_dynamic.empty:
            return
            
        dynamic_sma = history_dynamic['close'].mean()
        current_price = data[self.symbol].close

        # 7. Trading Logic (Simple Crossover)
        # If price is above Dynamic SMA, Buy. Else, Liquidate.
        if not self.portfolio.invested:
            if current_price > dynamic_sma:
                self.set_holdings(self.symbol, 1.0)
        else:
            if current_price < dynamic_sma:
                self.liquidate(self.symbol)

        # 8. Plotting for Visualization
        self.plot("Indicators", "Price", current_price)
        self.plot("Indicators", "Dynamic SMA", dynamic_sma)
        self.plot("Lookback Period", "Days", self.current_lookback)

Key Implementation Details

  1. Volatility Calculation (np.std):
    We use self.history to get the last 31 days of data. We slice this array to get "Today's Volatility" (indices 1 to 30) and "Yesterday's Volatility" (indices 0 to 29). This allows us to determine if the market is becoming more or less volatile.

  2. The Adjustment Formula:

    raw_new_lookback = self.current_lookback * (1 + delta_vol)
    

    This logic implies that as volatility rises (delta_vol > 0), the lookback period extends. A longer lookback period creates a "slower" moving average, which helps prevent getting stopped out by market noise during choppy/volatile periods. Conversely, as volatility drops, the lookback shortens to capture the trend faster.

  3. Clamping (min_lookback, max_lookback):
    It is critical to set boundaries. Without a floor, the lookback could become 1 or 2, making the indicator uselessly erratic. Without a ceiling, it could become so long that it lags the market by months.

  4. Dynamic History Request:
    Unlike standard indicators where the history length is constant, here we call self.history(self.symbol, self.current_lookback, ...) inside on_data. This ensures the SMA is calculated using exactly the number of days determined by our volatility logic.

Q&A: Dynamic Indicators

Q: Can I use Average True Range (ATR) instead of Standard Deviation?
A: Yes. ATR is often a better measure of volatility for trading because it accounts for gaps (high-low range) rather than just the close price variance. You would replace np.std with a manual calculation of TR (True Range) over the history window or use a helper method to compute the average.

Q: Why does the code increase the lookback when volatility increases?
A: This is a defensive mechanism. High volatility often implies "noise" or "chop." By increasing the lookback period, you smooth out the data, preventing the algorithm from reacting to false breakouts. However, some aggressive strategies do the opposite (shorten the lookback) to scalp rapid movements. You can invert the logic by changing (1 + delta_vol) to (1 - delta_vol).

Q: How do I visualize the changing lookback period?
A: The code provided includes self.plot("Lookback Period", "Days", self.current_lookback). When you run the backtest, a secondary chart will appear showing exactly how the lookback period expanded and contracted over time alongside the price chart.