Skip to main content

Welcome to a Grasp lesson

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

Lesson 32

Order Management with ExecutionHandler

Recommended resources

Hello! Welcome to the next lesson in our journey to build a robust trading framework.

In our last session, we created a vital "translation" function, create_ib_combo_order, that converts our abstract, internal Order object into the specific Contract and Order objects required by the Interactive Brokers API. This was a crucial step in maintaining a clean separation between our strategy logic and the broker's implementation details.

Today, we will build the component that uses this translator to communicate with the broker. The learning outcome for this lesson is to build an ExecutionHandler to submit, modify, and cancel orders through the broker API. This handler will be the engine of our trading system, responsible for taking instructions from our strategy and turning them into real-world actions.

1. The Asynchronous Heartbeat: An Event-Driven Architecture

Before we write any code, it's essential to understand the design pattern that underpins the Interactive Brokers API. It is not a simple request-response system where you send a request and wait for an immediate answer. Instead, it operates on an event-driven architecture.

Given your background in IT, you're likely familiar with this paradigm from areas like graphical user interfaces or network programming. The core idea is that you send commands without blocking, and the system notifies you of results and state changes through asynchronous callbacks.

The ibapi library implements this through two fundamental classes:

  • EClient: This is the "client" part. It's what you use to send requests to the IB TWS/Gateway (e.g., "place this order," "cancel that order," "what's the next valid order ID?").
  • EWrapper: This is the "wrapper" or callback handler. It's a collection of methods that the IB servers call on your application to send information back to you (e.g., "here is the next valid ID," "the status of order X has changed," "order Y has been filled").
Event-Driven Architecture Diagram
This diagram of an event-driven architecture illustrates the model used by the IB API. Your `EClient` calls generate events (requests). The API server processes them and sends back events (data, status updates), which are caught by your `EWrapper` methods (the event handlers).

Our ExecutionHandler will inherit from both EClient and EWrapper, allowing it to both send commands and handle the responses in a single, cohesive unit.


2. Designing the ExecutionHandler

We will design a class, ExecutionHandler, with a clear set of responsibilities:

  1. Manage Connection: Establish and maintain the connection to the IB Gateway or TWS.
  2. Manage Order IDs: Request and keep track of valid order IDs, as every order needs a unique ID.
  3. Provide an Interface for Order Actions: Offer simple methods to the rest of our framework, like submit_order(), modify_order(), and cancel_order().
  4. Process Broker Feedback: Implement the EWrapper callback methods to receive and log order status updates.

To get a clear picture of how these pieces fit together, let's watch the official Interactive Brokers video on order placement.

Placing Orders using TWS Python API

This video from Interactive Brokers provides a concise overview of the entire order placement lifecycle using the native Python API. It demonstrates the key methods and callbacks we will be implementing.

Please watch from 02:50 to 12:54. As you watch, focus on: How the class inherits from both EClient and EWrapper (02:50). The use of nextValidId to get an order ID (03:20). The placeOrder method for submitting both market and limit orders (05:25). The openOrder, orderStatus, and execDetails callbacks that provide feedback (07:20). How an order is modified by calling placeOrder again with the same order ID (11:20).

This video gives us the complete blueprint for what our ExecutionHandler needs to do. Now, let's implement it.


3. Implementing the ExecutionHandler Class

We will build the class step-by-step. For this lesson, we will assume the create_ib_combo_order function and the internal Order dataclasses from our previous lesson are available.

Step 3.1: Basic Structure and Connection

First, we define the class structure, inheriting from EWrapper and EClient. The __init__ method initializes the parent classes and sets up some internal state, like next_order_id. We'll also include basic connection logic.

import time
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 internal classes and translator function from the previous lesson are here
# from framework.models import InternalOrder
# from framework.translators import create_ib_combo_order

class ExecutionHandler(EWrapper, EClient):
    def __init__(self):
        EClient.__init__(self, self)
        self.next_order_id = None
        
        # A simple flag to signal when the connection is established
        self._is_connected = False

    def nextValidId(self, orderId: int):
        """Callback for receiving the next valid order ID."""
        super().nextValidId(orderId)
        self.next_order_id = orderId
        print(f"Next valid order ID: {orderId}")
        
        # Signal that we are connected and have a valid ID
        self._is_connected = True

    def error(self, reqId, errorCode, errorString, advancedOrderReject=""):
        """Callback for handling errors."""
        super().error(reqId, errorCode, errorString, advancedOrderReject)
        # Ignore informational messages
        if errorCode in [2104, 2106, 2158]: 
            return
        print(f"Error. ReqId: {reqId}, Code: {errorCode}, Msg: {errorString}")

    def run_loop(self):
        """Runs the EClient's message-processing loop."""
        self.run()

    def connect_and_start(self, host: str, port: int, clientId: int):
        """Connects to TWS/Gateway and starts the event loop in a new thread."""
        self.connect(host, port, clientId)
        print(f"Connecting to {host}:{port} with clientId {clientId}...")

        # Start the message loop in a background thread
        thread = Thread(target=self.run_loop, daemon=True)
        thread.start()

        # Wait for the connection to be established and nextValidId to be received
        while not self._is_connected:
            print("Waiting for connection...")
            time.sleep(1)
        print("Connection successful.")

Note: The nextValidId callback is crucial. It's triggered automatically by the API after a successful connection. We must wait for it to return before we can place any orders.

Step 3.2: Submitting an Order

The submit_order method will be our public-facing function. It takes our internal Order object, translates it, assigns a new orderId, and calls the placeOrder method from EClient.

# Add this method to the ExecutionHandler class

def get_next_order_id(self) -> int:
    """Gets the next available order ID and increments the internal counter."""
    if self.next_order_id is None:
        raise ConnectionError("Not connected or next_order_id not received.")
    
    current_id = self.next_order_id
    self.next_order_id += 1
    return current_id

def submit_order(self, internal_order: InternalOrder):
    """Translates and submits an internal order to the broker."""
    # Step 1: Translate our internal order to IB-specific objects
    ib_contract, ib_order = create_ib_combo_order(internal_order)
    
    # Step 2: Get a new order ID and assign it
    order_id = self.get_next_order_id()
    ib_order.orderId = order_id
    
    # Step 3: Place the order
    print(f"Placing order {order_id} for {internal_order.quantity} {ib_contract.symbol} {ib_contract.secType}...")
    self.placeOrder(order_id, ib_contract, ib_order)
    
    return order_id

Step 3.3: Modifying and Cancelling Orders

Modifying and cancelling orders are simpler.

  • To modify, you call placeOrder again, but you reuse the original order's ID. The TWS/Gateway recognizes this and treats it as a modification/replacement request.
  • To cancel, you call the cancelOrder method with the specific orderId.
# Add these methods to the ExecutionHandler class

def modify_order(self, order_id: int, internal_order: InternalOrder):
    """Modifies an existing order."""
    ib_contract, ib_order = create_ib_combo_order(internal_order)
    
    # CRITICAL: Use the existing order_id
    ib_order.orderId = order_id
    
    print(f"Modifying order {order_id}...")
    self.placeOrder(order_id, ib_contract, ib_order)
    
def cancel_order(self, order_id: int):
    """Cancels an existing order."""
    print(f"Cancelling order {order_id}...")
    self.cancelOrder(order_id)

Step 3.4: Handling Order Status Updates

To know what happens after we send a command, we must implement the relevant EWrapper callbacks. These methods will be called automatically by the API server whenever there's an update. For now, we'll just print the information. In our next lesson, we will build more sophisticated logic around these updates.

# Add these methods to the ExecutionHandler class

def openOrder(self, orderId, contract: Contract, order: Order, orderState):
    """Callback for receiving open order details."""
    super().openOrder(orderId, contract, order, orderState)
    print(f"OpenOrder. ID: {orderId}, Symbol: {contract.symbol}, Action: {order.action}, "
          f"Quantity: {order.totalQuantity}, Status: {orderState.status}")

def orderStatus(self, orderId, status, filled, remaining, avgFillPrice, permId,
                parentId, lastFillPrice, clientId, whyHeld, mktCapPrice):
    """Callback for receiving order status updates."""
    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}")

def execDetails(self, reqId, contract, execution):
    """Callback for receiving execution details."""
    super().execDetails(reqId, contract, execution)
    print(f"ExecDetails. ReqId: {reqId}, Symbol: {contract.symbol}, "
          f"ExecId: {execution.execId}, OrderId: {execution.orderId}, "
          f"Shares: {execution.shares}, Price: {execution.price}")
Test your understanding!

You submit a limit order to sell a credit spread for a net credit of $1.50. The market moves, and you decide you are now willing to accept a credit of $1.45. Which method of the ExecutionHandler would you call, and what would be the key parameters you pass to it?

Show answer

You would call the modify_order method.

The key parameters would be:

  1. The order_id that was returned when you originally submitted the order.
  2. A new InternalOrder object that is identical to the original one, except its limit_price is now set to 1.45.

The ExecutionHandler will then call placeOrder using the same order_id, which the broker interprets as a request to modify the existing order's limit price.


4. Putting It All Together: A Test Run

Let's see how this would work in a simple script. This example will connect to the TWS/Gateway, submit a bull put spread order, wait 15 seconds, modify its price, wait another 15 seconds, and finally cancel it.

Note: You must have TWS or IB Gateway running and configured to accept API connections for this to work. It is strongly recommended to use a paper trading account.

# This is a full script for demonstration purposes.
# It assumes the ExecutionHandler class and helper functions are defined above.

# --- Mock internal dataclasses and translator for a complete example ---
from dataclasses import dataclass, field
from typing import Optional, List, Tuple

@dataclass
class OrderLeg:
    conId: int
    ratio: int
    action: str
    exchange: str = 'SMART'

@dataclass
class InternalOrder:
    symbol: str
    action: str
    quantity: int
    order_type: str
    limit_price: Optional[float] = None
    time_in_force: str = 'DAY'
    legs: List[OrderLeg] = field(default_factory=list)

def create_ib_combo_order(internal_order: InternalOrder) -> Tuple[Contract, Order]:
    if not internal_order.legs:
        raise ValueError("Cannot create combo order with no legs.")
    ib_contract = Contract()
    ib_contract.symbol = internal_order.symbol
    ib_contract.secType = "BAG"
    ib_contract.currency = "USD"
    ib_contract.exchange = "SMART"
    ib_contract.comboLegs = []
    for leg in internal_order.legs:
        from ibapi.contract import ComboLeg
        combo_leg = ComboLeg()
        combo_leg.conId = leg.conId
        combo_leg.ratio = leg.ratio
        combo_leg.action = leg.action
        combo_leg.exchange = leg.exchange
        ib_contract.comboLegs.append(combo_leg)
    ib_order = Order()
    ib_order.action = internal_order.action
    ib_order.orderType = internal_order.order_type
    ib_order.totalQuantity = internal_order.quantity
    ib_order.lmtPrice = internal_order.limit_price
    ib_order.tif = internal_order.time_in_force
    return ib_contract, ib_order
# --- End of mock classes ---

def main():
    # --- Configuration ---
    HOST = "127.0.0.1"
    PORT = 7497  # 7497 for TWS Paper, 4002 for Gateway Paper
    CLIENT_ID = 1

    # --- Instantiate and connect the handler ---
    app = ExecutionHandler()
    app.connect_and_start(HOST, PORT, CLIENT_ID)

    # --- Define the order ---
    # NOTE: These conIds are examples. You must use valid conIds for your contracts.
    leg1 = OrderLeg(conId=645856403, ratio=1, action='SELL') # SPY 445P 15SEP23
    leg2 = OrderLeg(conId=645856434, ratio=1, action='BUY')  # SPY 444P 15SEP23
    
    original_order = InternalOrder(
        symbol='SPY', action='SELL', quantity=1, order_type='LMT',
        limit_price=0.20, legs=[leg1, leg2]
    )
    
    # 1. Submit the original order
    order_id = app.submit_order(original_order)
    print(f"Submitted initial order with ID: {order_id}")
    time.sleep(15)

    # 2. Modify the order
    modified_order = InternalOrder(
        symbol='SPY', action='SELL', quantity=1, order_type='LMT',
        limit_price=0.18, legs=[leg1, leg2] # Change price to 0.18
    )
    app.modify_order(order_id, modified_order)
    time.sleep(15)

    # 3. Cancel the order
    app.cancel_order(order_id)
    time.sleep(5)

    # Disconnect
    app.disconnect()
    print("Disconnected.")

if __name__ == "__main__":
    main()


Conclusion

In this lesson, we have constructed the ExecutionHandler, the central nervous system for your trading framework's order management. By inheriting from EClient and EWrapper, this class encapsulates the logic for sending commands and processing the asynchronous feedback from the broker.

Key Takeaways:

  • The IB API is event-driven, using an EClient to send requests and an EWrapper to handle asynchronous callbacks.
  • Our ExecutionHandler combines these roles to manage the full order lifecycle.
  • placeOrder() is a versatile command used for both submitting new orders (with a new orderId) and modifying existing ones (by reusing an orderId).
  • cancelOrder() is the dedicated method for cancelling an active order.
  • The orderStatus, openOrder, and execDetails callbacks are the essential EWrapper methods for monitoring the state of your orders.

Preview of the Next Lesson:
We are now receiving status updates, but we are only printing them to the console. This is not enough for an automated system. In our next lesson, we will implement logic to handle order status updates (submitted, filled, cancelled) from the broker. We will build a system to parse these updates and use them to maintain an accurate, real-time picture of our portfolio and open orders, which is crucial for any automated strategy.