🚀 AI One-Click Generation of quantconnect Strategy Code

How to Emit Insights with Custom Data for Dynamic Stop-Loss and Take-Profit Levels in QuantConnect

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

Problem Description

How can I generate a trade signal and pass a calculated ATR value to set the stop-loss for that specific trade?

Solution

To generate a trade signal and use a specific ATR value for a stop-loss, the most robust approach in QuantConnect is to store the ATR value at the moment of signal generation and then retrieve it within the on_order_event handler.

This ensures that your stop-loss is calculated based on the volatility (ATR) that existed when the decision was made, but applied to the actual fill price of the trade.

Strategy Implementation

The following strategy demonstrates this workflow:

  1. Initialize: Set up the ATR indicator and a dictionary (self.signal_data) to store ATR values keyed by Symbol.
  2. OnData: Check for a Buy signal (e.g., Price > SMA). If triggered, store the current ATR in the dictionary and place a Market Order.
  3. OnOrderEvent: Detect when the entry order is FILLED. Retrieve the stored ATR for that symbol, calculate the stop price relative to the actual fill price, and submit the Stop Loss order.
# region imports
from AlgorithmImports import *
# endregion

class AtrStopLossAlgorithm(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 12, 1)
        self.set_cash(100000)

        # Add Equity
        self.symbol = self.add_equity("SPY", Resolution.DAILY).symbol

        # Initialize Indicators
        self.atr = self.atr(self.symbol, 14, MovingAverageType.WILDERS, Resolution.DAILY)
        self.sma = self.sma(self.symbol, 200, Resolution.DAILY)
        
        # Warm up indicators
        self.set_warm_up(200)

        # Dictionary to store the ATR value at the moment of the signal
        # Key: Symbol, Value: ATR float value
        self.signal_atr = {}
        
        # Multiplier for the stop loss (e.g., 2x ATR)
        self.sl_multiplier = 2.0

    def on_data(self, data: Slice):
        if self.is_warming_up or not self.atr.is_ready:
            return

        # Simple Logic: Buy if Price > 200 SMA and we are not invested
        if not self.portfolio.invested:
            if self.securities[self.symbol].price > self.sma.current.value:
                
                # 1. Capture the ATR value at this specific moment
                current_atr = self.atr.current.value
                
                # 2. Store it in our dictionary to pass it to OnOrderEvent
                self.signal_atr[self.symbol] = current_atr
                
                # 3. Place the entry order
                self.set_holdings(self.symbol, 1.0)
                self.log(f"Signal Generated. Stored ATR: {current_atr:.4f}")

    def on_order_event(self, order_event: OrderEvent):
        # We only care about Filled orders
        if order_event.status != OrderStatus.FILLED:
            return

        # We only care about Entry orders (Buying), not exit orders
        # order_event.quantity > 0 implies we bought
        if order_event.quantity > 0:
            
            # Retrieve the order details to confirm it's an equity trade
            order = self.transactions.get_order_by_id(order_event.order_id)
            
            # Check if we have a stored ATR for this symbol
            if order_event.symbol in self.signal_atr:
                
                # Retrieve the ATR value we stored in OnData
                atr_value = self.signal_atr[order_event.symbol]
                
                # Calculate Stop Price based on the ACTUAL Fill Price
                fill_price = order_event.fill_price
                stop_price = fill_price - (atr_value * self.sl_multiplier)
                
                # Place the Stop Market Order
                # Note: We negate the quantity to sell what we just bought
                self.stop_market_order(order_event.symbol, -order_event.quantity, stop_price)
                
                self.log(f"Entry Filled @ {fill_price}. Placing Stop @ {stop_price} (ATR: {atr_value})")
                
                # Optional: Clear the dictionary entry to keep it clean
                # del self.signal_atr[order_event.symbol]

Key Concepts Explained

  1. self.signal_atr Dictionary:
    Standard MarketOrder calls do not allow you to attach custom metadata (like an ATR value) directly to the order object that persists easily to the event handler. By using a class-level dictionary keyed by Symbol, you create a bridge between the logic in on_data and the execution in on_order_event.

  2. Why use on_order_event?
    You could technically place the stop_market_order immediately after the market_order in on_data. However, in on_data, you do not know the exact Fill Price of the market order yet (slippage).
    By waiting for the FILLED event in on_order_event, you ensure your stop loss is calculated mathematically correctly from the actual entry price (order_event.fill_price - 2 * ATR).

  3. Order Quantity Check:
    if order_event.quantity > 0: ensures we only place a stop loss when we enter a long position. Without this check, when your stop loss eventually fills (selling the stock), that fill event would trigger this logic again, potentially causing an infinite loop or errors.


Q&A

Q: Can I use this method with the Algorithm Framework (Alpha/Portfolio models)?
A: Yes. If you are using the Framework, you can calculate the ATR in your Alpha Model. When emitting an Insight, you can store the ATR in a shared dictionary or serialize it into the Insight.tag property (though parsing strings is slower). The on_order_event logic remains the same.

Q: What happens if I have multiple positions for the same symbol?
A: The dictionary approach above assumes one active "signal ATR" per symbol. If you scale into positions (pyramiding), you might overwrite the ATR value. For complex scaling, you would need to manage a list of ATRs or use the weighted average price of the portfolio holdings.

Q: How do I handle Trailing Stops with ATR?
A: To implement a trailing stop, you would not use stop_market_order. Instead, you would use self.train or on_data to monitor the price. If current_price - (ATR * multiplier) is higher than your existing stop price, you would use order.update() to move the stop price up.

Q: Does this work for Short selling?
A: The logic needs a slight adjustment for shorting.

  1. In on_data: self.set_holdings(symbol, -1.0).
  2. In on_order_event: Check if order_event.quantity < 0.
  3. Stop Calculation: stop_price = fill_price + (atr_value * self.sl_multiplier) (Stop is above entry for shorts).
  4. Order: self.stop_market_order(symbol, abs(quantity), stop_price).