Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 33

Order Status Management

Recommended resources

Hello! Welcome back to our course on building your Python-based trading framework.

In the last lesson, we built the ExecutionHandler, which can successfully submit, modify, and cancel orders with the Interactive Brokers API. We implemented the basic EWrapper callbacks and saw them printing raw status updates to the console. While functional, this is like a car's engine running without a dashboard—the system is working, but you have no structured way to know what's happening.

Today, we'll build that dashboard. Our learning outcome is to implement logic to handle order status updates (submitted, filled, cancelled) from the broker. We will transform the raw, asynchronous feedback from the API into a structured, event-driven system that gives our framework a clear and actionable understanding of its orders.

1. From Raw Callbacks to a Structured Event System

In the previous lesson, we established that the IB API is event-driven. Your application sends a command and then listens for responses through callback methods. You might find this model familiar from network programming or UI development. A great analogy is a WebSocket client, which uses callback functions like on_message and on_close to react to data sent from a server.

Real-Time Streaming Stock Market Data with Python and Websockets

This video by Part Time Larry on building a real-time data stream with WebSockets offers an excellent parallel to what we are building. The core concept of using callback functions to handle asynchronous events is the same.

Watch from 07:50 to 09:16. Notice how he defines placeholder functions (on_open, on_message, on_close) that will be triggered by different events from the WebSocket connection. Our EWrapper callbacks (orderStatus, execDetails, etc.) serve the exact same purpose for the broker connection.

Our goal is to take the information arriving in our orderStatus and execDetails "on_message" handlers and make it useful for the rest of our application.


2. The Key Callbacks: orderStatus and execDetails

To build our logic, we first need to be crystal clear on what information the broker sends us and through which channels. The two most important callbacks for tracking an order's life are orderStatus and execDetails.

Let's quickly review the official IB video to see what data these callbacks provide.

Placing Orders using TWS Python API

This segment from the Interactive Brokers video we saw previously breaks down the openOrder, orderStatus, and execDetails callbacks.

Please re-watch the segment from 07:20 to 10:02. Pay close attention to: The parameters of orderStatus (status, filled, remaining, avgFillPrice). The role of execDetails in reporting actual fills, especially for partial executions. The sequence of events: openOrder is called once when the order is accepted, orderStatus is called for every state change, and execDetails is called for every fill.

From this, we can distill the roles of each callback:

  • openOrder: Confirms the broker has accepted our order and gives us the full details of the order as the broker sees it.
  • orderStatus: Provides high-level status changes. The status string can be 'ApiPending', 'PendingSubmit', 'Submitted', 'Filled', 'Cancelled', etc.
  • execDetails: Provides the "proof" of execution. It's called for every partial or full fill and gives concrete details like the exact number of shares filled and the execution price. For a single limit order, you might get one 'Filled' orderStatus update but several execDetails calls if the order was filled in parts.

3. Designing an Event-Driven Handling System

Simply printing this information inside the ExecutionHandler isn't enough. The Strategy component needs to know when an order is filled so it can stop trying to enter a position and switch to position management mode.

To achieve this, we will implement two key design patterns:

  1. State Management: The ExecutionHandler will maintain an internal dictionary to track the live status of every order it manages.
  2. Event Queue: The ExecutionHandler will place significant status updates into a shared queue, allowing other components of our framework to consume these events and react accordingly.

This is a robust and decoupled architecture. The ExecutionHandler's job is to talk to the broker; it shouldn't know or care what the Strategy does with a "fill" event. It just reports the event.

Step 3.1: Defining Event Objects

First, let's define simple data structures for our events. dataclasses are perfect for this.

from dataclasses import dataclass
from typing import Optional

@dataclass
class OrderStatusEvent:
    """Event for high-level order status changes."""
    order_id: int
    status: str
    filled: float
    remaining: float
    avg_fill_price: float

@dataclass
class OrderFillEvent:
    """Event for when an order is partially or fully filled."""
    order_id: int
    contract_symbol: str
    quantity: float
    fill_price: float
    exec_id: str

Step 3.2: Modifying the ExecutionHandler

Now, let's modify the ExecutionHandler from our previous lesson. We'll add an event queue and an internal log for order statuses.

import queue
from threading import Thread
from ibapi.client import EClient
from ibapi.wrapper import EWrapper
from ibapi.contract import Contract
from ibapi.order import Order

# Assume our dataclasses and translator functions are available

class ExecutionHandler(EWrapper, EClient):
    def __init__(self, event_queue: queue.Queue):
        EClient.__init__(self, self)
        # The queue for communicating events to the rest of the system
        self.event_queue = event_queue
        
        # Internal state management
        self.next_order_id = None
        self.order_status_log = {}  # {orderId: {status: 'Submitted', filled: 0, ...}}
        self._is_connected = False

    def nextValidId(self, orderId: int):
        # ... (same as previous lesson)
        super().nextValidId(orderId)
        self.next_order_id = orderId
        self._is_connected = True

    # ... (error, run_loop, connect_and_start methods are the same)

    def submit_order(self, internal_order):
        # ... (same as previous lesson, but now we log the initial state)
        ib_contract, ib_order = create_ib_combo_order(internal_order)
        order_id = self.get_next_order_id()
        ib_order.orderId = order_id
        
        # Log the initial state
        self.order_status_log[order_id] = {
            "status": "PendingSubmit", # Initial state
            "filled": 0.0,
            "remaining": ib_order.totalQuantity,
            "avg_fill_price": 0.0,
            "executions": []
        }
        
        self.placeOrder(order_id, ib_contract, ib_order)
        print(f"Placing order {order_id} for {internal_order.symbol}...")
        return order_id

    # --- NEW IMPLEMENTATION OF CALLBACKS ---

    def orderStatus(self, orderId, status, filled, remaining, avgFillPrice,
                    permId, parentId, lastFillPrice, clientId, whyHeld, mktCapPrice):
        super().orderStatus(orderId, status, filled, remaining, avgFillPrice, permId, parentId, lastFillPrice, clientId, whyHeld, mktCapPrice)
        
        print(f"OrderStatus. ID: {orderId}, Status: {status}, Filled: {filled}, "
              f"Remaining: {remaining}, AvgFillPrice: {avgFillPrice}")

        # Update our internal log
        if orderId in self.order_status_log:
            self.order_status_log[orderId].update({
                "status": status,
                "filled": filled,
                "remaining": remaining,
                "avg_fill_price": avgFillPrice
            })

        # Create an event and put it on the queue
        event = OrderStatusEvent(
            order_id=orderId,
            status=status,
            filled=filled,
            remaining=remaining,
            avg_fill_price=avgFillPrice
        )
        self.event_queue.put(event)

    def execDetails(self, reqId, contract: Contract, execution):
        super().execDetails(reqId, contract, execution)
        
        print(f"ExecDetails. OrderId: {execution.orderId}, Symbol: {contract.symbol}, "
              f"Shares: {execution.shares}, Price: {execution.price}")
              
        order_id = execution.orderId

        # Update internal log with execution details
        if order_id in self.order_status_log:
            self.order_status_log[order_id]["executions"].append(execution)

        # Create a fill event and put it on the queue
        fill_event = OrderFillEvent(
            order_id=order_id,
            contract_symbol=contract.symbol,
            quantity=execution.shares,
            fill_price=execution.price,
            exec_id=execution.execId
        )
        self.event_queue.put(fill_event)
Test your understanding!

An order to buy 10 contracts is partially filled (4 contracts are bought). Which callbacks will be triggered, and in what likely sequence? What events will be placed on the queue?

Show answer
  1. Callbacks: You will likely receive an execDetails call for the 4 contracts, followed by an orderStatus call with the status Submitted (or similar, depending on the broker) but with the filled quantity updated to 4 and remaining updated to 6. If the rest of the order is filled later, you'll get more execDetails and a final orderStatus with status Filled.

  2. Events on Queue:

    • An OrderFillEvent with quantity=4 and the fill price.
    • An OrderStatusEvent with status='Submitted', filled=4, and remaining=6.

4. Consuming Events in the Main Loop

Now that the ExecutionHandler is producing events, we need a consumer. In our final architecture (Module 9), this will be a central event loop that drives the entire system. For today, we can simulate this with a simple loop in our main script.

This approach—using a queue to pass events—is much more robust than polling or using complex locking mechanisms. The article "ib_insync Guide" explains why an event-driven (asynchronous) approach is superior to polling for order status.

ib_insync Guide - Interactive Brokers API

The article 'ib_insync Guide' from Algotrading101 provides a clear explanation of why asynchronous, event-driven designs are superior for trading systems. While it uses the ib_insync library, the core concepts apply directly to our native ibapi implementation.

Read the sections 'How can I fire an order through ib_insync?' and 'How to create a callback function for order status?'. Focus on the discussion that contrasts the small polling loop with the more effective callback function. This highlights the value of the event-driven system we are building.

Here's how we can put it all together in a test script. This script will connect, submit an order, and then listen on the event queue for updates.

# Full script for demonstration (includes mock classes from previous lesson)
import queue
import time

# --- Mock internal dataclasses and translator ---
# ... (copy the OrderLeg, InternalOrder, and create_ib_combo_order from previous lesson)

# --- ExecutionHandler and Event classes ---
# ... (copy the ExecutionHandler, OrderStatusEvent, OrderFillEvent from this lesson)

def main():
    # --- Configuration ---
    HOST = "127.0.0.1"
    PORT = 7497  # Use your TWS/Gateway paper trading port
    CLIENT_ID = 2 # Use a different client ID from the previous session

    # --- Setup ---
    event_queue = queue.Queue()
    app = ExecutionHandler(event_queue)
    app.connect_and_start(HOST, PORT, CLIENT_ID)

    # --- Define and submit order ---
    # Use valid conIds for contracts you can trade in your paper account
    # Example for SPY Bull Put Spread
    leg1 = OrderLeg(conId=645856403, ratio=1, action='SELL') # SPY 445P 15SEP23
    leg2 = OrderLeg(conId=645856434, ratio=1, action='BUY')  # SPY 444P 15SEP23
    
    # Use a limit price that is likely to fill in the paper account
    bull_put_order = InternalOrder(
        symbol='SPY', action='SELL', quantity=1, order_type='LMT',
        limit_price=0.25, legs=[leg1, leg2]
    )
    
    order_id = app.submit_order(bull_put_order)
    print(f"Submitted initial order with ID: {order_id}")

    # --- Main Event Loop (Simplified) ---
    try:
        while True:
            try:
                event = event_queue.get(block=True, timeout=5)
                print(f"\n[EVENT RECEIVED]: {event}")

                # If the order is filled or cancelled, we can exit the loop
                if isinstance(event, OrderStatusEvent):
                    if event.order_id == order_id and event.status in ['Filled', 'Cancelled', 'Inactive']:
                        print("Order is terminal. Exiting.")
                        break
            except queue.Empty:
                print("No new events. Waiting...")
    finally:
        # Gracefully disconnect
        app.disconnect()
        print("Disconnected.")

if __name__ == "__main__":
    main()

When you run this script (with TWS/Gateway open to a paper account), you will see the order being submitted. Then, as the broker processes the order, your main loop will print the OrderStatusEvent and OrderFillEvent objects as they are placed on the queue. This is the heart of your framework's nervous system.


Conclusion

In this lesson, we have significantly upgraded our ExecutionHandler. It no longer just sends commands into the void; it now actively listens for and processes the broker's feedback, turning it into a clean, structured stream of events.

Key Takeaways:

  • Handling order status requires processing asynchronous callbacks from the broker, chiefly orderStatus and execDetails.
  • A robust design uses an internal state log (like our order_status_log dictionary) to keep track of each order's lifecycle.
  • An event queue is an excellent mechanism for decoupling the ExecutionHandler from other components, allowing them to react to events like "order filled" or "order cancelled."
  • We created specific OrderStatusEvent and OrderFillEvent dataclasses to represent these updates in a structured way.

Preview of the Next Lesson:
We now have a stream of OrderFillEvents. The next logical step is to use these fill events to maintain an accurate, real-time record of our actual positions. In the next lesson, we will develop a position synchronizer to reconcile internal position state with the broker's records. This component will consume fill events to build our portfolio and periodically check against the broker's data to ensure everything stays in sync.