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:
- Pull positions, balances, and executions from IBKR.
- Diff against local state.
- Classify each divergence: expected (an in-flight order), explained (a corporate action, a fee), or unexplained.
- 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:
- Read-only adapter against an IBKR paper account — connect, positions, balances, reconciliation. No write path compiled.
- Divergence monitor comparing modelled fills against IBKR paper fills, run for a full quarter.
- Write path against the paper account, with hard caps and the idempotency key.
- 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.