Problem Description
Solution
To add a Stop Loss and Take Profit in QuantConnect, the most robust method is to place these orders inside the on_order_event handler immediately after your entry order is filled. This ensures your protection levels are calculated based on the actual execution price, not the theoretical market price.
Here is a complete, runnable strategy that demonstrates how to enter a position and manage a "One-Cancels-Other" (OCO) logic manually.
Strategy Logic
- Entry: Buy SPY when not invested.
- Protection: Upon entry fill, place a Stop Market Order (Stop Loss) 5% below the fill price and a Limit Order (Take Profit) 10% above the fill price.
- Management: If the Stop Loss is hit, cancel the Take Profit. If the Take Profit is hit, cancel the Stop Loss.
Python Code Implementation
# region imports
from AlgorithmImports import *
# endregion
class StopLossTakeProfitExample(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 12, 1)
self.set_cash(100000)
self.symbol = self.add_equity("SPY", Resolution.MINUTE).symbol
# Track order tickets to manage cancellations
self.entry_ticket = None
self.stop_loss_ticket = None
self.take_profit_ticket = None
def on_data(self, data: Slice):
# Simple Entry Logic: Enter if we have no position and no pending entry
if not self.portfolio.invested and self.entry_ticket is None:
quantity = self.calculate_order_quantity(self.symbol, 0.9)
self.entry_ticket = self.market_order(self.symbol, quantity)
self.debug(f"Entry Order Placed: {quantity} shares")
def on_order_event(self, order_event: OrderEvent):
# We only care about filled orders
if order_event.status != OrderStatus.FILLED:
return
# 1. Handle Entry Fill: Place SL and TP
if self.entry_ticket is not None and order_event.order_id == self.entry_ticket.order_id:
fill_price = order_event.fill_price
quantity = order_event.fill_quantity
# Calculate prices (Long Position Example)
# Stop Loss at 5% loss
stop_price = fill_price * 0.95
# Take Profit at 10% gain
limit_price = fill_price * 1.10
# Place orders (Quantity is negative to sell)
self.stop_loss_ticket = self.stop_market_order(self.symbol, -quantity, stop_price)
self.take_profit_ticket = self.limit_order(self.symbol, -quantity, limit_price)
self.log(f"Entry Filled at {fill_price}. SL: {stop_price}, TP: {limit_price}")
# 2. Handle Stop Loss Fill: Cancel Take Profit
if self.stop_loss_ticket is not None and order_event.order_id == self.stop_loss_ticket.order_id:
self.log("Stop Loss Hit. Cancelling Take Profit.")
if self.take_profit_ticket is not None:
self.take_profit_ticket.cancel()
self.reset_tickets()
# 3. Handle Take Profit Fill: Cancel Stop Loss
if self.take_profit_ticket is not None and order_event.order_id == self.take_profit_ticket.order_id:
self.log("Take Profit Hit. Cancelling Stop Loss.")
if self.stop_loss_ticket is not None:
self.stop_loss_ticket.cancel()
self.reset_tickets()
def reset_tickets(self):
"""Resets tickets to None to allow new trades."""
self.entry_ticket = None
self.stop_loss_ticket = None
self.take_profit_ticket = None
Key Concepts Explained
1. stop_market_order (Stop Loss)
This creates an order that triggers a market sell once the price drops to (or below) a specific level.
- Syntax:
self.stop_market_order(symbol, quantity, stop_price) - Note: For a long position,
quantitymust be negative (selling).
2. limit_order (Take Profit)
This creates an order to sell at a specific price or better.
- Syntax:
self.limit_order(symbol, quantity, limit_price)
3. on_order_event
This is the event handler where you manage the lifecycle of your orders.
- Why use it? In live trading, slippage occurs. If you calculate your SL/TP in
on_datausing the current close price, your risk calculations might be off. Usingorder_event.fill_priceensures your SL/TP are calculated exactly from where you entered the market.
4. Ticket Management
The OrderTicket objects returned by order methods allow you to update or cancel orders later.
- One-Cancels-Other (OCO): QuantConnect does not have a native "OCO" order type that sits on the broker server for all brokerages. You must simulate it in code. When the SL fills, you explicitly call
self.take_profit_ticket.cancel(), and vice versa.
Q&A: Common Questions about SL/TP in QuantConnect
Q: Can I use a Trailing Stop Loss?
A: Yes. You can use self.stop_market_order initially. Then, in on_data, check if the price has moved in your favor and use self.stop_loss_ticket.update() to modify the stop price upwards.
Q: What happens if I am Shorting?
A: The logic reverses.
- Entry: Sell (
quantity< 0). - Stop Loss: Buy Stop (
quantity> 0, price > entry). - Take Profit: Buy Limit (
quantity> 0, price < entry).
Q: Is there an easier way without writing all this code?
A: Yes, you can use the Risk Management Framework. In your initialize method, you can add:
self.add_risk_management(MaximumDrawdownPercentPerSecurity(0.05))
However, this only handles the Stop Loss (liquidating positions that lose 5%). It does not handle Take Profits or specific price targets as precisely as the manual method above.