Generate code in 3 minutes
Enter your idea and AI generates runnable code.
Here is a complete, robust momentum trading algorithm for ETFs implemented in Python for the QuantConnect Lean engine.
Entry Price - (2 * ATR).Entry Price + (4 * ATR) (2:1 Reward-to-Risk ratio).# region imports
from AlgorithmImports import *
# endregion
class MomentumETFWithBracketOrders(QCAlgorithm):
def initialize(self):
# 1. Strategy Setup
self.set_start_date(2018, 1, 1)
self.set_end_date(2024, 1, 1)
self.set_cash(100000)
# 2. Universe of ETFs
self.tickers = ["SPY", "QQQ", "IWM", "XLE", "XLF", "TLT", "GLD"]
self.symbols = []
# Dictionary to hold our custom data (indicators, order tickets) per symbol
self.symbol_data = {}
# 3. Parameters
self.lookback = 20 # Momentum lookback
self.trend_period = 50 # Trend filter SMA
self.atr_period = 14 # Volatility period
self.sl_multiplier = 2.0 # Stop Loss ATR multiple
self.tp_multiplier = 4.0 # Take Profit ATR multiple
self.allocation = 1.0 / len(self.tickers) # Equal weight
for ticker in self.tickers:
equity = self.add_equity(ticker, Resolution.DAILY)
symbol = equity.symbol
self.symbols.append(symbol)
# Initialize SymbolData helper for each ETF
self.symbol_data[symbol] = SymbolData(self, symbol, self.lookback, self.trend_period, self.atr_period)
# Warm up indicators
self.set_warm_up(max(self.trend_period, self.atr_period))
def on_data(self, data: Slice):
if self.is_warming_up:
return
for symbol in self.symbols:
# Ensure data exists for this symbol in this slice
if not data.bars.containsKey(symbol):
continue
sd = self.symbol_data[symbol]
# Ensure indicators are ready
if not sd.is_ready:
continue
# Check if we are already invested or have open orders
if self.portfolio[symbol].invested or self.transactions.get_open_orders(symbol):
continue
# --- Entry Logic ---
# 1. Price > 50 SMA (Uptrend)
# 2. ROC > 0 (Positive Momentum)
price = data.bars[symbol].close
if price > sd.sma.current.value and sd.roc.current.value > 0:
# We use MarketOrder to enter.
# The Stop/Limit logic is handled in OnOrderEvent to ensure we get the exact fill price.
quantity = self.calculate_order_quantity(symbol, self.allocation)
self.market_order(symbol, quantity)
def on_order_event(self, order_event: OrderEvent):
# Only process filled orders
if order_event.status != OrderStatus.FILLED:
return
order = self.transactions.get_order_by_id(order_event.order_id)
symbol = order_event.symbol
sd = self.symbol_data[symbol]
# --- Handle Entry (Buy) ---
# If we just bought the asset (Market Buy), place the Bracket Orders
if order.type == OrderType.MARKET and order.direction == OrderDirection.BUY:
fill_price = order_event.fill_price
atr_value = sd.atr.current.value
# Calculate levels
stop_price = fill_price - (atr_value * self.sl_multiplier)
profit_price = fill_price + (atr_value * self.tp_multiplier)
quantity = order_event.fill_quantity # Quantity to close
# Place Stop Loss
sd.stop_ticket = self.stop_market_order(symbol, -quantity, stop_price, "Stop Loss")
# Place Take Profit
sd.limit_ticket = self.limit_order(symbol, -quantity, profit_price, "Take Profit")
self.debug(f"{self.time} Entered {symbol} @ {fill_price}. SL: {stop_price:.2f}, TP: {profit_price:.2f}")
# --- Handle Exit (OCO Logic) ---
# If a Stop or Limit order is filled, cancel the other one to avoid "ghost" positions
if order.type == OrderType.STOP_MARKET or order.type == OrderType.LIMIT:
# If Stop Loss hit, cancel Take Profit
if sd.stop_ticket is not None and order.id == sd.stop_ticket.order_id:
if sd.limit_ticket is not None:
sd.limit_ticket.cancel("Stop Loss Hit")
sd.reset_tickets()
self.debug(f"{self.time} Stop Loss Hit for {symbol}")
# If Take Profit hit, cancel Stop Loss
elif sd.limit_ticket is not None and order.id == sd.limit_ticket.order_id:
if sd.stop_ticket is not None:
sd.stop_ticket.cancel("Take Profit Hit")
sd.reset_tickets()
self.debug(f"{self.time} Take Profit Hit for {symbol}")
class SymbolData:
"""
Helper class to manage indicators and order tickets for a specific symbol.
"""
def __init__(self, algorithm, symbol, roc_period, sma_period, atr_period):
self.symbol = symbol
# Indicators
self.roc = algorithm.roc(symbol, roc_period, Resolution.DAILY)
self.sma = algorithm.sma(symbol, sma_period, Resolution.DAILY)
self.atr = algorithm.atr(symbol, atr_period, MovingAverageType.WILDERS, Resolution.DAILY)
# Order Tickets (to manage OCO logic)
self.stop_ticket = None
self.limit_ticket = None
@property
def is_ready(self):
return self.roc.is_ready and self.sma.is_ready and self.atr.is_ready
def reset_tickets(self):
self.stop_ticket = None
self.limit_ticket = None
SymbolData Class:
ROC, SMA, and ATR specific to each ETF.stop_ticket and limit_ticket so we can reference them later to cancel orders.on_order_event Logic:
on_data.Risk Management:
Q: Why use on_order_event instead of placing all orders in on_data?
A: In live trading, market orders are not filled instantly. If you calculate your Stop Loss based on the current price but your order fills 10 seconds later at a different price, your risk calculations will be off. Using on_order_event ensures you attach the protection orders to the exact filled price.
Q: Can I change the Reward-to-Risk ratio?
A: Yes. Modify self.sl_multiplier and self.tp_multiplier in the initialize method. Currently, it is set to 2.0 (Stop) and 4.0 (Target), providing a 1:2 risk/reward ratio.
Q: How does the algorithm handle wash sales or re-entries?
A: The condition if self.portfolio[symbol].invested in on_data prevents the algorithm from adding to a position that is already open. It will only look for a new entry after the previous position has been fully closed (either by Stop Loss or Take Profit).