book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Order Management

Order Tickets

Introduction

When you create an order, you get an OrderTicket object to manage your order.

Properties

OrderTicket objects have the following attributes:

Track Orders

As the state of your order updates over time, your order ticket automatically updates. To track an order, you can check any of the preceding order ticket properties.

To get an order field, call the get method with an OrderField.

Select Language:
def initialize(self) -> None:
    self._symbol = self.add_equity("SPY").symbol
    self._ticket = None
    
def on_data(self, slice: Slice) -> None:
    # Place order if not invested and save the order ticket for later retrival.
    if not self.portfolio.invested and self._symbol in slice.bars:
        self._ticket = self.limit_order(self._symbol, 10, slice.bars[self._symbol].close)
    # Get the limit price if the order is filled.
    elif self.portfolio.invested:
        limit_price = self._ticket.get(OrderField.LIMIT_PRICE)

The OrderField enumeration has the following members:

In addition to using order tickets to track orders, you can receive order events through the on_order_event event handler.

Update Orders

To update an order, use its OrderTicket. You can update other orders until they are filled or the brokerage prevents modifications. You just can't update orders during warm up and initialization.

Updatable Properties

The specific properties you can update depends on the order type. The following table shows the properties you can update for each order type.

Order TypeUpdatable Properties
TagQuantityLimitPriceTriggerPriceStopPrice
Market Order
Limit Ordergreen checkgreen checkgreen check
Limit If Touched Ordergreen checkgreen checkgreen checkgreen check
Stop Market Ordergreen checkgreen checkgreen check
Stop Limit Ordergreen checkgreen checkgreen checkgreen check
Market On Open Ordergreen checkgreen check
Market On Close Ordergreen checkgreen check

Update Methods

To update an order, pass an UpdateOrderFields object to the update method. The method returns an OrderResponse to signal the success or failure of the update request.

Select Language:
def initialize(self) -> None:
    self._symbol = self.add_equity("SPY").symbol
    self._ticket = None
    
def on_data(self, slice: Slice) -> None:
    # Place order if not invested and save the order ticket for later retrival.
    if not self._ticket and self._symbol in slice.bars:
        self._ticket = self.limit_order("SPY", 100, slice.bars[self._symbol].close * 0.98)
    # If order is placed, update the limit price to be 90% of the orginal.
    elif self._ticket != None and self._ticket.status == OrderStatus.SUBMITTED:
        # Update the order tag and limit price
        update_settings = UpdateOrderFields()
        update_settings.limit_price = self._ticket.get(OrderField.LIMIT_PRICE) * 0.9
        update_settings.tag = "Limit Price Updated for SPY Trade"
        response = self._ticket.update(update_settings)

        # Check if the update request is successfully submitted to the broker.
        # Note that it may not represent the order is updated successfully: during the order updating process, it may be filled or canceled.
        if response.is_success:
            self.debug("Order update request is submitted successfully")

To update individual fields of an order, call any of the following methods:

  • update_limit_price
  • update_quantity
  • update_stop_price
  • update_tag
Select Language:
response = ticket.update_limit_price(limit_price, tag)

response = ticket.update_quantity(quantity, tag)

response = ticket.update_stop_price(stop_price, tag)

response = ticket.update_tag(tag)

Update Order Requests

When you update an order, LEAN creates an UpdateOrderRequest object, which have the following attributes:

To get a list of UpdateOrderRequest objects for an order, call the update_requests method.

Select Language:
update_requests = ticket.update_requests()

Workaround for Brokerages That Don't Support Updates

Not all brokerages fully support order updates. To check what functionality your brokerage supports for order updates, see the Orders section of the documentation for your brokerage model. If your brokerage doesn't support order updates and you want to update an order, cancel the order. When you get an order event that confirms the order is no longer active, place a new order.

Select Language:
def initialize(self) -> None:
    self._symbol = self.add_equity("SPY").symbol
    self._ticket = None
    
def on_data(self, slice: Slice) -> None:
    # Place order if not invested and save the order ticket for later retrival.
    if not self._ticket and self._symbol in slice.bars:
        self._ticket = self.limit_order("SPY", 100, slice.bars[self._symbol].close)
    # If order is placed, cancel the order and place a new one as substituent.
    elif self._ticket != None and self._ticket.status == OrderStatus.SUBMITTED:
        self._ticket.cancel()

def on_order_event(self, order_event: OrderEvent) -> None:
    if (self._ticket and order_event.order_id == self._ticket.order_id and
        order_event.status == OrderStatus.CANCELED):
        # Place a new order
        quantity = self._ticket.quantity - self._ticket.quantity_filled
        limit_price = self.securities[self._ticket.symbol].price + 1
        self._ticket = self.limit_order(self._ticket.symbol, quantity, limit_price)

Cancel Orders

To cancel an order, call the cancel method on the OrderTicket. The method returns an OrderResponse object to signal the success or failure of the cancel request.

Select Language:
def initialize(self) -> None:
    self._symbol = self.add_equity("SPY").symbol
    self._ticket = None
    
def on_data(self, slice: Slice) -> None:
    # Place order if not invested and save the order ticket for later retrival.
    if not self._ticket and self._symbol in slice.bars:
        self._ticket = self.limit_order("SPY", 100, slice.bars[self._symbol].close)
    # If order is placed, cancel the order if it is not filled within 2 minutes.
    elif self._ticket != None and self._ticket.time < slice.time - timedelta(minutes=2) and self._ticket.status == OrderStatus.SUBMITTED:
        response = self._ticket.cancel("Canceled SPY trade")
    
        # Check if the cancel request is successfully submitted to the broker.
        # Note that it may not represent the order is canceled successfully: during the order updating process, it may be filled.
        if response.is_success:
            self.debug("Order cancel request successfully submitted")

You can't cancel market orders because they are immediately transmitted to the brokerage. You also can't cancel any orders during warm up and initialization.

When you cancel an order, LEAN creates a CancelOrderRequest, which have the following attributes:

To get the CancelOrderRequest for an order, call the cancel_order_request method. The method returns None if the order hasn't been cancelled.

Select Language:
request = ticket.cancel_order_request()

Order Response

When you update or cancel an order, LEAN returns an OrderReponse object, which have the following attributes:

If your order changes fail, check the ErrorCode or ErrorMessage. For more information about specific order errors, see the Order Response Error Reference.

To get most recent order response, call the get_most_recent_order_response method.

Select Language:
response = ticket.get_most_recent_order_response()

Examples

The following examples demonstrate some common practices for order ticket management.

Example 1: Timed Exit

The following algorithm is an EMA-crossover strategy. Using the filled time of the order ticket, we can control to exit the position after 45 minutes.

Select Language:
class OrderTicketExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2019, 1, 1)
        self.set_end_date(2019, 4, 1)
        self._symbol = self.add_equity("SPY").symbol
        self._ticket = None
        # Set up EMA indicator for trade signal generation.
        self._ema = self.ema("SPY", 20, Resolution.DAILY)
        # Warm up the indicator for its readiness to use immediately.
        self.warm_up_indicator(self._symbol, self._ema)
        
    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self._symbol)
        # Place order if not order yet and save the order ticket for later retrival.
        if not self._ticket and bar:
            # EMA crossover: buy if price is above EMA, which indicate uptrend.
            if self._ema.current.value < bar.close:
                self._ticket = self.limit_order("SPY", 100, slice.bars[self._symbol].close * 0.995)
            # EMA crossover: sell if price is below EMA, which indicate down trend.
            else:
                self._ticket = self.limit_order("SPY", -100, slice.bars[self._symbol].close * 1.005)
        # Exit position if it is in the portfolio for more than 45 minutes.
        elif self._ticket and self._ticket.time + timedelta(minutes=45) < self.utc_time:
            self.liquidate()
            self._ticket = None

Example 2: Intraday Stop Loss

The following algorithm demonstrates an intraday long position with a 10% stop loss order. Before the market closes, cancel the stop loss order if it is not filled and exit the position at market close with a market on close order.

Select Language:
class OrderTicketExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 7, 1)
        self._symbol = self.add_equity("QQQ").symbol
        self._ticket = None
        self._day = -1

        # Schedule an event to handle exiting position before market close if stop loss is not hit.
        # Market on close order requires ordering 15 minutes before market close. 
        self.schedule.on(
            self.date_rules.every_day(self._symbol),
            self.time_rules.before_market_close(self._symbol, 16),
            self.exit_position
        )
        
    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self._symbol)
        # Place order if not invested. Make sure only trade once a day to avoid chasing the loss.
        if not self.portfolio.Invested and self._day != slice.time.day and bar:
            self.market_order(self._symbol, 10)
            # Update the day variable to avoid repeat ordering.
            self._day = slice.time.day

    def on_order_event(self, order_event: OrderEvent) -> None:
        # Make sure to order the stop loss order on the filled market order only.
        if order_event.status == OrderStatus.FILLED and order_event.fill_quantity > 0:
            # Place a 10% stop loss order to avoid large loss. Save the order ticket for tracking its status later.
            self._ticket = self.stop_market_order(order_event.symbol, -order_event.fill_quantity, order_event.fill_price*0.9)

    def exit_position(self) -> None:
        # Only need to cancel the order and handle exit position if the stop loss order is not fully filled.
        if self._ticket and self._ticket.status != OrderStatus.FILLED:
            self._ticket.cancel()
            # Market on close order to exit position at market close with the remaining quantity.
            self.market_on_close_order(self._symbol, -10 - self._ticket.quantity_filled)

Example 3: Crypto Trailing Stop Order

This example trades BTCUSD with a similar logic of Example 2, but apply a trailing stop loss of 10% instead. Crypto exchanges does not support trailing stop loss order. Yet, we can cancel and reorder a stop loss order with the updated stop loss price. If the market price of the crypto pair is above the previous level, update the stop loss order by the above.

Select Language:
class OrderTicketExampleAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2023, 1, 1)
        self.set_end_date(2023, 7, 1)
        # Set the market argument to trade in specific exchange.
        self._symbol = self.add_crypto("BTCUSD", market=Market.COINBASE).symbol
        # A variable to hold the stop loss order to track if it should be updated.
        self._ticket = None
        # A day variable to avoid over-ordering (trade once per day)
        self._day = -1
        # A variable to hold the price of the high price in the lifetime of the open position for updating the stop loss price.
        self.high_price = 0
        
    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self._symbol)
        if bar:
            # Place order if not invested. Make sure only trade once a day to avoid chasing the loss.
            if not self.portfolio.Invested and self._day != slice.time.day:
                self.market_order(self._symbol, 1)
                # Update the day variable to avoid repeat ordering.
                self._day = slice.time.day

            # Update the trailing stop order. Since order updating is not supported in crypto exchanges, we cancel and reorder.
            # Trailing stop price will only need to be updated when the market price is higher than the current price.
            if self._ticket and self._ticket.status in [OrderStatus.SUBMITTED, OrderStatus.PARTIALLY_FILLED]\
            and bar.high > self.high_price:
                self._ticket.cancel()
                self._ticket = self.stop_market_order(self._symbol, -self.portfolio[self._symbol].quantity, bar.high*0.9)
                self.high_price = bar.high

    def on_order_event(self, order_event: OrderEvent) -> None:
        # Make sure to order the stop loss order on the filled market order only.
        if order_event.status == OrderStatus.FILLED and order_event.fill_quantity > 0:
            # Place a 10% stop loss order to avoid large loss. Save the order ticket for tracking its status later.
            self._ticket = self.stop_market_order(order_event.symbol, -order_event.fill_quantity, order_event.fill_price*0.9)
            # Save the price to check if the stop loss price should be increased.
            self.high_price = order_event.fill_price

Other Examples

For more examples, see the following algorithms:

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: