Generate code in 3 minutes
Enter your idea and AI generates runnable code.
To add stop-loss and take-profit orders immediately after a main trade is executed, you should utilize the on_order_event event handler. This method is triggered automatically whenever an order status changes (e.g., from SUBMITTED to FILLED).
By checking for OrderStatus.FILLED inside this handler, you ensure that your protection orders are only placed once the entry position is confirmed and you have the exact execution price.
on_order_event: This is where the logic resides. You must filter events to ensure you are reacting only to entry fills, not exit fills (to avoid infinite loops).stop_market_order for the Stop Loss.limit_order for the Take Profit.Here is a complete, runnable algorithm demonstrating this pattern.
from AlgorithmImports import *
class EntryWithExitProtection(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 6, 1)
self.set_cash(100000)
self.symbol = self.add_equity("SPY", Resolution.MINUTE).symbol
# Parameters for risk management
self.stop_loss_pct = 0.02 # 2% Stop Loss
self.take_profit_pct = 0.04 # 4% Take Profit
# Variable to track our entry ticket to distinguish it from other orders
self.entry_ticket = None
def on_data(self, data: Slice):
# Simple Logic: Enter if not invested
if not self.portfolio.invested and self.entry_ticket is None:
# Place a Market Order to enter
self.entry_ticket = self.market_order(self.symbol, 100)
def on_order_event(self, order_event: OrderEvent):
# 1. Check if the order is completely filled
if order_event.status != OrderStatus.FILLED:
return
# 2. Check if this fill belongs to our specific entry ticket
# We do this to ensure we don't place stops/limits when our stops/limits get filled
if self.entry_ticket is not None and self.entry_ticket.order_id == order_event.order_id:
fill_price = order_event.fill_price
quantity = order_event.fill_quantity
# Logic for LONG positions (Quantity > 0)
if quantity > 0:
stop_price = fill_price * (1 - self.stop_loss_pct)
limit_price = fill_price * (1 + self.take_profit_pct)
# Place Stop Loss (Sell to Close)
self.stop_market_order(self.symbol, -quantity, stop_price, "Stop Loss")
# Place Take Profit (Sell to Close)
self.limit_order(self.symbol, -quantity, limit_price, "Take Profit")
self.log(f"Long Entry Filled at {fill_price}. SL: {stop_price}, TP: {limit_price}")
# Logic for SHORT positions (Quantity < 0)
elif quantity < 0:
stop_price = fill_price * (1 + self.stop_loss_pct)
limit_price = fill_price * (1 - self.take_profit_pct)
# Place Stop Loss (Buy to Close)
self.stop_market_order(self.symbol, -quantity, stop_price, "Stop Loss")
# Place Take Profit (Buy to Close)
self.limit_order(self.symbol, -quantity, limit_price, "Take Profit")
self.log(f"Short Entry Filled at {fill_price}. SL: {stop_price}, TP: {limit_price}")
# Reset entry ticket so we don't process this again
self.entry_ticket = None
# 3. Handle the "One-Cancels-Other" (OCO) logic manually
# If a Stop or Limit is filled, we should cancel the remaining open orders for this symbol
# to prevent leaving a "dangling" order in the market.
else:
# If we are no longer invested (position closed by SL or TP), cancel all other orders
if not self.portfolio[self.symbol].invested:
self.transactions.cancel_open_orders(self.symbol)
self.entry_ticket = None
self.transactions.cancel_open_orders.order_event.fill_quantity (or -quantity for the exit) rather than self.portfolio[symbol].quantity. This ensures that if you are scaling into a position (partial fills), you only place exits for the amount actually filled.on_order_event runs asynchronously. This method is the safest way to chain orders because it guarantees the entry order has been processed by the exchange before you attempt to place the exits.on_order_event instead of placing orders immediately in on_data?In on_data, you do not yet know the execution price of a market order. If you place a Stop Loss immediately after a Market Order line in on_data, the Market Order might not have filled yet, or you might calculate the stop price based on the current price rather than the actual fill price. on_order_event guarantees you have the confirmed fill price.
Ensure your logic calculates the stop price correctly based on direction. For a Long position, the stop price must be below the fill price. For a Short position, it must be above. The code provided handles this in the if quantity > 0 and elif quantity < 0 blocks.
stop_limit_order instead of stop_market_order?Yes. A stop_market_order guarantees execution but not price (slippage may occur). A stop_limit_order guarantees price but not execution (if the market gaps past your limit, you might not get filled). Change self.stop_market_order to self.stop_limit_order if you prefer the latter, but be aware of the risk of holding a losing position that skipped your stop.