Problem Description
Solution
To execute a buy order immediately after a sell order is filled, you should utilize the on_order_event event handler. This method is triggered whenever an order's status changes (e.g., Submitted, Filled, Canceled).
By tracking the OrderTicket of your sell order, you can check inside on_order_event if that specific order has a status of OrderStatus.FILLED. If it does, you then place your buy order.
Here is a complete, runnable strategy demonstrating this logic.
Strategy Logic
- Initialize: We subscribe to
SPY(to sell) andTLT(to buy). - Setup: We initially buy
SPYto establish a position to sell. - Trigger: We place a market sell order for
SPYand save the returnedOrderTicketto the variableself.sell_ticket. - Execution: In
on_order_event, we check if the incoming event matches ourself.sell_ticket.order_idand if the status isFILLED. If true, we immediately place a buy order forTLT.
Python Code
from AlgorithmImports import *
class SequentialOrderExecution(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 6, 1)
self.set_cash(100000)
# Assets: We will sell SPY to buy TLT
self.symbol_to_sell = self.add_equity("SPY", Resolution.MINUTE).symbol
self.symbol_to_buy = self.add_equity("TLT", Resolution.MINUTE).symbol
# Variable to store the ticket of the sell order we are tracking
self.sell_ticket = None
# Flag to ensure we only run the demo logic once
self.demo_complete = False
def on_data(self, slice: Slice):
if self.demo_complete:
return
# 1. Setup Phase: Buy SPY initially so we have something to sell
if not self.portfolio.invested and self.sell_ticket is None:
self.set_holdings(self.symbol_to_sell, 1.0)
self.debug("Setup: Bought SPY")
return
# 2. Trigger Phase: Sell SPY
# We wait until we actually have holdings before selling
if self.portfolio[self.symbol_to_sell].invested and self.sell_ticket is None:
quantity = self.portfolio[self.symbol_to_sell].quantity
# Place the sell order and capture the OrderTicket
self.sell_ticket = self.market_order(self.symbol_to_sell, -quantity)
self.debug(f"Trigger: Placed sell order for {self.symbol_to_sell}")
def on_order_event(self, order_event: OrderEvent):
# We only care about Filled events
if order_event.status != OrderStatus.FILLED:
return
# 3. Execution Phase: Check if this event belongs to our specific sell order
if self.sell_ticket is not None and order_event.order_id == self.sell_ticket.order_id:
self.debug(f"Confirmed: Sell order filled for {order_event.symbol} at {order_event.fill_price}")
# Calculate buying power or specific quantity for the next asset
# Here we simply allocate 100% of equity to the new asset
self.set_holdings(self.symbol_to_buy, 1.0)
self.debug(f"Action: Placed buy order for {self.symbol_to_buy}")
self.demo_complete = True
Key Concepts
on_order_event: This is the most reliable place to chain orders. In live trading, order fills are asynchronous. If you place a sell order inon_data, the fill might not happen immediately.on_order_eventguarantees you only act once the broker confirms the trade is done.OrderTicket: When you callself.market_order(or any order method), it returns anOrderTicket. This object contains the uniqueorder_id. We store this inself.sell_ticketso we can identify exactly which order triggered the event.OrderStatus.FILLED: We strictly check for this status. We do not want to buy the second asset if the first order wasCANCELEDorINVALID.
Q&A
Q: Can I use this for Limit Orders as well?
A: Yes. The logic is identical. You would place a limit_order instead of a market_order, store the ticket, and wait for the FILLED event in on_order_event.
Q: What happens if the sell order is only partially filled?
A: The code above checks for OrderStatus.FILLED, which usually implies a complete fill. If you want to react to partial fills, you can check for OrderStatus.PARTIALLY_FILLED. However, you must be careful not to place the buy order multiple times if multiple partial fills occur for a single order.
Q: Why not just place both orders in on_data?
A: If you place a sell order and immediately place a buy order in the same on_data step, you might not have the cash available yet (buying power) because the sell hasn't settled or processed. Chaining them via events ensures the proceeds from the sale are recognized by the algorithm before the buy is attempted.