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. Thestatusstring 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'orderStatusupdate but severalexecDetailscalls 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:
- State Management: The
ExecutionHandlerwill maintain an internal dictionary to track the live status of every order it manages. - Event Queue: The
ExecutionHandlerwill 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
-
Callbacks: You will likely receive an
execDetailscall for the 4 contracts, followed by anorderStatuscall with the statusSubmitted(or similar, depending on the broker) but with thefilledquantity updated to 4 andremainingupdated to 6. If the rest of the order is filled later, you'll get moreexecDetailsand a finalorderStatuswith statusFilled. -
Events on Queue:
- An
OrderFillEventwithquantity=4and the fill price. - An
OrderStatusEventwithstatus='Submitted',filled=4, andremaining=6.
- An
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
orderStatusandexecDetails. - A robust design uses an internal state log (like our
order_status_logdictionary) to keep track of each order's lifecycle. - An event queue is an excellent mechanism for decoupling the
ExecutionHandlerfrom other components, allowing them to react to events like "order filled" or "order cancelled." - We created specific
OrderStatusEventandOrderFillEventdataclasses 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.