Problem Description
Solution
To create a trading indicator with a dynamic lookback period based on market volatility, you typically follow these steps:
- Measure Volatility: Calculate the standard deviation of prices over a fixed period (e.g., 30 days).
- Calculate Change: Compare the current volatility to a previous baseline (e.g., yesterday's volatility or a moving average of volatility).
- 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).
- 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
-
Volatility Calculation (
np.std):
We useself.historyto get the last 31 days of data. We slice this array to get "Today's Volatility" (indices1to30) and "Yesterday's Volatility" (indices0to29). This allows us to determine if the market is becoming more or less volatile. -
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. -
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. -
Dynamic History Request:
Unlike standard indicators where the history length is constant, here we callself.history(self.symbol, self.current_lookback, ...)insideon_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.