Problem Description
Solution
Based on your request, here is a Python strategy for QuantConnect that implements a Dynamic Breakout system.
Strategy Logic
This strategy adapts to market conditions by adjusting its "lookback period" based on recent volatility changes:
- Volatility Measurement: It calculates the standard deviation of closing prices over two different windows (current vs. previous).
- Adaptive Lookback:
- If volatility increases, the lookback period increases. This widens the breakout channel to avoid "whipsaws" (false signals) during choppy/volatile markets.
- If volatility decreases, the lookback period decreases. This tightens the channel to catch new trends early during quiet periods.
- Entry: Buys if the price breaks the
n-dayHigh or Sells if it breaks then-dayLow. - Exit: Uses a Bollinger Band midpoint (Simple Moving Average) as a liquidation target to secure profits or cut losses when the trend reverts.
Python Code (PEP8 Compliant)
# region imports
from AlgorithmImports import *
import numpy as np
# endregion
class AdaptiveVolatilityBreakout(QCAlgorithm):
def initialize(self):
# 1. Set Setup Parameters
self.set_start_date(2019, 1, 1)
self.set_end_date(2024, 1, 1)
self.set_cash(100000)
# 2. Add Forex Asset
# We use Hourly resolution for trading, but calculations will be based on Daily data
self.pair = self.add_forex("EURUSD", Resolution.HOUR, Market.OANDA).symbol
# 3. Define Strategy Parameters
self.lookback = 20 # Initial lookback days
self.ceiling = 60 # Max lookback days
self.floor = 20 # Min lookback days
# Variables to store calculated levels
self.buy_point = None
self.sell_point = None
self.liquidation_point = None
# 4. Initialize Indicators
# We use Bollinger Bands to help determine the liquidation point (the middle band)
self.bolband = self.bb(self.pair, 20, 2, MovingAverageType.SIMPLE, Resolution.DAILY)
# 5. Schedule the adaptive calculation to run every day before market close
self.schedule.on(
self.date_rules.every_day(self.pair),
self.time_rules.before_market_close(self.pair, 1),
self.recalibrate_lookback
)
# Warm up period to ensure indicators and history are ready
self.set_warm_up(self.ceiling + 30)
def recalibrate_lookback(self):
"""
Adjusts the lookback period based on volatility changes.
"""
# Get 60 days of history to calculate volatility shifts
history = self.history(self.pair, 60, Resolution.DAILY)
if history.empty or 'close' not in history.columns:
return
closes = history['close']
# Calculate volatility (Standard Deviation) for two windows
# Recent 30 days vs Previous 30 days
if len(closes) < 60:
return
current_vol = np.std(closes[-30:])
prev_vol = np.std(closes[-60:-30])
# Avoid division by zero
if current_vol == 0:
return
# Calculate percentage change in volatility
delta_vol = (current_vol - prev_vol) / current_vol
# Adjust lookback:
# If volatility increases, increase lookback (widen channel to avoid noise).
# If volatility decreases, decrease lookback (tighten channel to catch moves).
self.lookback = int(round(self.lookback * (1 + delta_vol)))
# Clamp the lookback between floor and ceiling
self.lookback = max(self.floor, min(self.ceiling, self.lookback))
# Calculate Breakout Levels based on the new dynamic lookback
# We look at the Highs and Lows of the last N days
highs = history['high'][-self.lookback:]
lows = history['low'][-self.lookback:]
self.buy_point = max(highs)
self.sell_point = min(lows)
# Set liquidation point (Mean of recent closes - essentially the BB Middle Band)
self.liquidation_point = np.mean(closes[-self.lookback:])
# Log the adaptation for debugging
# self.debug(f"Vol Delta: {delta_vol:.2f} | New Lookback: {self.lookback}")
def on_data(self, data: Slice):
"""
Event handler for new data. Checks for breakout signals.
"""
if self.is_warming_up:
return
if not self.bolband.is_ready or self.buy_point is None:
return
price = self.securities[self.pair].price
holdings = self.portfolio[self.pair].quantity
# 1. Entry Logic: Breakout
# Buy if price breaks above the dynamic N-day high
if price >= self.buy_point:
if holdings <= 0:
self.set_holdings(self.pair, 1.0)
# Sell if price breaks below the dynamic N-day low
elif price <= self.sell_point:
if holdings >= 0:
self.set_holdings(self.pair, -1.0)
# 2. Exit Logic: Mean Reversion / Trailing
# If we are Long and price falls below the liquidation point (Moving Average)
if holdings > 0 and price <= self.liquidation_point:
self.liquidate(self.pair, "Long Exit: Crossed Mean")
# If we are Short and price rises above the liquidation point
elif holdings < 0 and price >= self.liquidation_point:
self.liquidate(self.pair, "Short Exit: Crossed Mean")
Key Implementation Details
-
PEP8 Standards:
- Methods use snake_case (e.g.,
recalibrate_lookback,set_holdings). - Properties use snake_case (e.g.,
self.portfolio,self.securities). - This ensures compatibility with the modern QuantConnect API.
- Methods use snake_case (e.g.,
-
Volatility Calculation:
- We use
numpyto calculate standard deviation. delta_voldetermines the momentum of volatility. A positive delta implies the market is becoming more erratic, so we increaseself.lookbackto filter out noise.
- We use
-
Data Handling:
- The strategy trades on
Resolution.HOURto allow for timely entries during the day. - However, the logic (Highs/Lows/Volatility) is calculated on
Resolution.DAILYdata to maintain the significance of the breakout levels.
- The strategy trades on
-
Risk Management:
- The
liquidation_pointacts as a dynamic trailing stop. As the price moves, the moving average (calculated from the dynamic lookback) moves with it, securing profits if the trend reverses.
- The
Q&A
Q: Why adjust the lookback period based on volatility?
A: In high volatility environments, price swings are wider. A short lookback period might trigger false breakouts (whipsaws). Increasing the lookback requires the price to break a more significant historical high to trigger a trade. Conversely, in low volatility, trends often start quietly; a shorter lookback helps enter the trade earlier.
Q: Can I use this for Crypto or Equities?
A: Yes. You simply need to change the add_forex line to add_crypto or add_equity. However, for Equities, you should ensure you handle extended_market_hours if you want to trade pre/post market, though breakouts are usually most reliable during regular hours.
Q: How does the Exit logic work?
A: The exit is based on a "reversion to the mean." If you are Long (betting price goes up), and the price falls back down to the average price of the last N days, the strategy assumes the momentum is lost and closes the position.