Skip to content

15 — Broker Integration (Interactive Brokers)

1. Position

IBKR is the primary broker integration. It is reached through a narrow adapter interface that the rest of the system programs against, so adding a second broker later changes one module and nothing above it.

The invariant that shapes everything here: no AI component ever talks to the broker. Agents produce structured signals; they do not hold a broker handle, and there is no tool in any agent's toolset that reaches one. See ADR-0007.

2. The mandatory pipeline

Every order, in every live mode, passes through the same sequence. There is no path that skips a stage.

graph LR
    A["AI Analysis<br/>signals + score"] --> B["Shariah<br/>Validation"]
    B --> C["Risk<br/>Validation"]
    C --> D["Portfolio<br/>Validation"]
    D --> E["Execution<br/>Engine"]
    E --> F["Broker<br/>Adapter"]
    F --> G["IBKR API"]
    B -.reject.-> X["Blocked<br/>+ reason stored"]
    C -.reject.-> X
    D -.reject.-> X
    E -.reject.-> X
Stage Decides Fails closed on
AI Analysis What to consider Insufficient evidence → no recommendation
Shariah Validation Whether it is permitted at all NON_COMPLIANT or UNCERTAIN (05)
Risk Validation How much, and whether at all Zero size, breached limit, active circuit breaker (07)
Portfolio Validation Whether it fits what is already held Concentration, correlation cluster, cash floor
Execution Engine Order type, timing, slicing No acceptable execution — e.g. spread too wide
Broker Adapter Protocol translation and hard caps Cap breach, disconnection, rejected order

The adapter enforces hard caps independently of the risk engine: maximum order value, maximum daily notional, maximum open orders. Duplicated deliberately — a single implementation of a limit is a single point of failure for the limit.

3. Why IBKR, and which API

IBKR is the right primary because it offers global market access, a genuine paper account that mirrors the live API surface, and fee transparency that makes cost modelling honest. Its weakness is a famously awkward API, which the adapter exists to contain.

Two access routes, both viable:

TWS / IB Gateway API Client Portal Web API
Transport Socket to a local gateway process REST + WebSocket
Session Gateway must run; daily re-auth Token, with keep-alive
Coverage Complete Broad, some gaps
Ops burden A process to supervise Fewer moving parts

Chosen: IB Gateway with ib_insync-style async access, for coverage and because the gateway can be supervised in the same Docker Compose stack as everything else. The Client Portal route is kept as an adapter variant behind the same interface, because IBKR's daily re-authentication is genuinely irritating and the trade-off may look different in practice.

The gateway runs paper and live as separate endpoints. Mode 2 and Mode 4+ therefore differ by which endpoint the adapter is constructed against — and the live endpoint is unreachable from lower modes because its credentials are mode-derived (14 §4).

4. Adapter interface

The whole surface the rest of the system may use:

class BrokerAdapter(Protocol):
    """Everything above this line is broker-agnostic."""

    # --- read ---
    async def account_summary(self) -> AccountSummary: ...
    async def positions(self) -> list[BrokerPosition]: ...
    async def open_orders(self) -> list[BrokerOrder]: ...
    async def executions(self, since: datetime) -> list[Execution]: ...

    # --- write: live modes only ---
    async def place(self, order: PreparedOrder) -> BrokerOrderId: ...
    async def cancel(self, order_id: BrokerOrderId) -> None: ...
    async def cancel_all(self) -> int: ...

    # --- lifecycle ---
    async def connect(self) -> None: ...
    async def disconnect(self) -> None: ...
    def revoke_credentials(self) -> None: ...   # the kill switch


class ReadOnlyBrokerAdapter(Protocol):
    """What Mode 3 gets. The write methods do not exist."""
    async def account_summary(self) -> AccountSummary: ...
    async def positions(self) -> list[BrokerPosition]: ...

ReadOnlyBrokerAdapter is a separate type rather than a flag on the first. Mode 3 receives an object with no place method at all, so "send an order in Recommend mode" is a type error, not a runtime check.

5. Reconciliation

The broker is the source of truth for what you own. The application's view is a cache, and caches drift.

A reconciliation loop runs on connect, after every execution report, and on a schedule:

  1. Pull positions, balances, and executions from IBKR.
  2. Diff against local state.
  3. Classify each divergence: expected (an in-flight order), explained (a corporate action, a fee), or unexplained.
  4. On any unexplained divergence — halt new orders, raise a critical alert, and present the diff. Do not auto-correct.

That last point is the important one. Silently overwriting local state to match the broker would hide the bug that caused the divergence, and the bug is more dangerous than the discrepancy. A trading system that quietly reconciles away differences is a trading system that cannot tell you when it is wrong.

Fills also feed the live-versus-paper divergence monitor: modelled fill price against actual. That comparison is the only honest test of whether the spread and impact model in 06 §4 was telling the truth, and it is what would justify — or discredit — the paper track record that gated live trading in the first place.

6. Failure handling

Failure Response
Gateway down / disconnected Halt new orders immediately; alert; reconcile fully on reconnect before resuming
Order rejected by IBKR Store the broker's reason verbatim, surface it, do not retry automatically
Partial fill Track remaining quantity; re-evaluate against risk limits before continuing
Duplicate risk on retry Every order carries a client-generated idempotency key; a retry after a network failure can never create a second order
Market halted Cancel resting orders, mark the instrument, block new entries
Daily re-auth lapsed Degrade to Recommend, alert, never queue orders to send later

Queuing orders through a disconnection is specifically rejected. An order composed against a market that existed twenty minutes ago is not the order you would place now.

7. Security

Credentials live in encrypted storage, never in the database, environment files, or logs, and are decryptable only by a process running in a live mode (10). The kill switch revokes them rather than merely stopping the sender, so a compromised or malfunctioning process cannot resume by restarting.

The adapter is the only component with network egress to IBKR, and that host is the only broker destination on the egress allowlist.

8. Build sequence

Nothing in this document is built. It is written now so the paper engine's interfaces are shaped correctly, and it is deliberately last in the roadmap:

  1. Read-only adapter against an IBKR paper account — connect, positions, balances, reconciliation. No write path compiled.
  2. Divergence monitor comparing modelled fills against IBKR paper fills, run for a full quarter.
  3. Write path against the paper account, with hard caps and the idempotency key.
  4. Live endpoint, behind the enablement ceremony in 14 §3.

Step 2 is the one that must not be skipped. It is where you find out whether the simulator was honest before that question costs money.