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").

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:
- Manage Connection: Establish and maintain the connection to the IB Gateway or TWS.
- Manage Order IDs: Request and keep track of valid order IDs, as every order needs a unique ID.
- Provide an Interface for Order Actions: Offer simple methods to the rest of our framework, like
submit_order(),modify_order(), andcancel_order(). - Process Broker Feedback: Implement the
EWrappercallback 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
placeOrderagain, 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
cancelOrdermethod with the specificorderId.
# 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:
- The
order_idthat was returned when you originally submitted the order. - A new
InternalOrderobject that is identical to the original one, except itslimit_priceis now set to1.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
EClientto send requests and anEWrapperto handle asynchronous callbacks. - Our
ExecutionHandlercombines these roles to manage the full order lifecycle. placeOrder()is a versatile command used for both submitting new orders (with a neworderId) and modifying existing ones (by reusing anorderId).cancelOrder()is the dedicated method for cancelling an active order.- The
orderStatus,openOrder, andexecDetailscallbacks are the essentialEWrappermethods 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.