Quantstrip docs
Open Dashboard

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()
The strategy and broker referenced here (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"
This is the same event vocabulary — OPEN, INCREASE/DECREASE, CLOSE — that the Trade Operations page's Add Event panel uses for manual attribution. Automated clients and manual repair both write into the same strategy_event/position_event tables.

For the underlying IB connection wrapper and ib_contract/ ib_order helpers used above, see Integration templates.