IBKR template
Two examples, ordered by complexity: sending a single order, and a full client that reconciles order status, executions, positions, and commissions against Interactive Brokers every second.
Sending an order
ib_send_order.py connects, places one market order, records it, then stops.
This is the minimal pattern for placing an order Quantstrip tracks in its database.
from quantstrip import ClientBase, db_handler
from IBKR.ib_connect import IB
from IBKR.ib_objects import ib_contract, ib_order
SYMBOL = "SPY"
ORDER_TYPE = "MKT"
QUANTITY = 100
TARGET_POSITION = 100
EVENT_TYPE = "OPEN"
contract = ib_contract(SYMBOL)
order = ib_order(quantity=QUANTITY, orderType=ORDER_TYPE)
class Client(ClientBase):
def __init__(self, *args):
super().__init__()
self.display_name = "IB Send Order Test"
self.scheduler.every(1).seconds.do(self.job)
def job(self):
ib = IB()
order_id = db_handler.next_order_id()
try:
if ib.connect_client(client_id=5):
# 1. Place the order with IB
ib.placeOrder(order_id, contract, order)
# 2. Record the order in Quantstrip's DB
db_handler.insert_order(
order_id=order_id, strategy_id=1, broker_id=1,
account="Account1", symbol=SYMBOL,
side="BUY" if QUANTITY > 0 else "SELL",
order_type=ORDER_TYPE, total_quantity=QUANTITY)
# 3. Record the strategy's intent
db_handler.insert_strategy_event(
strategy_id=1, broker_id=1, symbol=SYMBOL,
order_id=order_id, position=TARGET_POSITION,
event_type=EVENT_TYPE)
finally:
ib.disconnect_client()
self.stop_client()
strategy_id=1,
broker_id=1) must already exist in the reference tables. Create them on the
Trade Operations page's Static Data tab first.
Full trade lifecycle
ib_trade_cycle.py runs every second and reconciles three things against IB:
open-order status, new executions, and commissions, writing each into the canonical DB
records read by other pages.
It connects using client_id = 0. In the IB API, this client ID receives
executions from every client connected to the same TWS/Gateway session, not only its own,
which is required to capture fills placed by other clients:
def job(self):
ib = IB()
try:
with IB(client_id=0) as ib: # client_id 0 sees all executions from all clients
# 1. Reconcile order status for open orders
for order_id, status in ib.get_order_status().items():
order = self.db.get_order(order_id)
if not order.empty:
self.insert_order_status(status)
# 2. Insert new executions -> position events -> confirm strategy state
for execution in ib.get_executions():
order_id = execution["execution"]["orderId"]
order = self.db.get_order(order_id)
if not order.empty:
order = order.iloc[0].to_dict()
self.insert_execution(execution, order)
new_position, _ = self.insert_position_event(execution, order)
strategy_event = self.db.get_last_strategy_event_by_order(order_id)
if new_position == strategy_event["position"]:
self.db.update_strategy_event_status(order_id, status="CONFIRMED")
# 3. Insert commissions for executions we've actually stored
stored_ids = self.db.get_executions()['exec_id'].to_list()
for commission in ib.get_commissions():
if commission['execId'] in stored_ids:
self.insert_commission(commission)
except Exception as e:
logger.info(f"Failed to run IB trade life-cycle process: {e}")
self.stop_client()
insert_position_event loads the strategy's previous position and classifies
the resulting state: a new open, an addition to an existing position, a partial close, a
full close, or a flip through zero:
prev = self.db.get_last_position_event(strategy_id, symbol)
old_pos = prev["position"] if prev else 0.0
old_avg = prev["avg_price"] if prev else None
if old_pos == 0:
new_pos, new_avg = qty, trade_price
event_type = "OPEN_LONG" if qty > 0 else "OPEN_SHORT"
elif old_pos > 0 and qty > 0: # adding to long
new_pos = old_pos + qty
new_avg = (old_avg * old_pos + trade_price * qty) / new_pos
event_type = "OPEN_LONG"
elif old_pos > 0 and qty < 0 and old_pos + qty > 0: # partial close of long
new_pos, new_avg = old_pos + qty, old_avg
event_type = "PARTIAL_CLOSE"
elif old_pos + qty == 0: # full close or cover
new_pos, new_avg = 0, None
event_type = "CLOSE" if old_pos > 0 else "COVER"
else: # flip through zero
new_pos, new_avg = old_pos + qty, trade_price
event_type = "FLIP"
strategy_event/position_event tables.
For the underlying IB connection wrapper and ib_contract/
ib_order helpers used above, see
Integration templates.