Expand description
Interactive Brokers ExecutionClient implementation.
Uses the ibapi crate for IB TWS/Gateway connectivity. Supports equities,
futures, options, and forex.
§Testing Status
NOT TESTED in CI. IBKR has not confirmed permission to use credentials for CI, and requires IB Gateway/TWS running locally.
Tested locally: All execution tests (connection, orders, account streaming) run on paper trading accounts — no market data subscriptions required, except for shortability.
§Connection
Requires TWS or IB Gateway running locally with API enabled:
| Application | Live Port | Paper Port |
|---|---|---|
| TWS | 7496 | 7497 |
| IB Gateway | 4001 | 4002 |
Enable API in TWS/Gateway: Configure → API → Settings → Enable ActiveX and Socket Clients. For order placement, uncheck “Read-Only API”.
§Architecture
- Connection: TCP socket to TWS/Gateway
- Orders: Subscription-based events (OrderStatus, ExecutionData, CommissionReport)
- Account: Subscription-based position and balance updates
§Balances and carrying costs
Balances are read from IB’s account summary, one per currency in IB’s ledger ($LEDGER:ALL),
named by the currency’s code. Every account the login manages is summed per currency.
ExecutionClient::account_stream delivers no balances.
totalis the currency’sTotalCashBalance(elseCashBalance): its cash, negative while the account borrows it. It is not converted into the base currency.accrued(Balance::accrued) is the currency’sAccruedCash: interest accrued this month and not yet posted, positive when the account earns it.freeistotal, capped at the account’sAvailableFundsconverted into the currency at IB’s ledger rate. IB reportsAvailableFundsonly for the whole account, so every currency is capped by the same funds and thefreevalues do not add up. It is negative whentotalis. With several accounts, each is capped by its own funds before they are summed. Without a rate to convert the funds,freeistotal, logged at WARN.
The ledger’s BASE row, every currency converted into the base currency and summed, is not
returned.
As IB documents it, interest accrues daily through the month in AccruedCash and is posted to
cash at the month’s end or in the first days of the next. Until then total leaves it out, so
the cash the account will hold is total + accrued. Stock borrow fees accrue daily too; IB
does not document whether they accrue in AccruedCash or when they post. Fills carry none of
these charges, so PnL computed from them excludes them too.
§Fill fees
A fill’s Trade::fees is the commission of the
CommissionReport IB sends for its execution, in that report’s currency, unchanged. That
figure includes the regulatory fees IB passes through and any tax IB charges on the commission
itself, such as GST. Taxes IB bills separately from commissions, such as stamp duty or a
financial transaction tax, are not known to be included, so on markets that levy them fees
may understate what a fill cost. A commission that is not a finite number, or is too large
for a Decimal, becomes a zero fee, logged at WARN; IB should never send one for a real fill.
An execution whose report IB did not send has a zero fee in
UNKNOWN_FEE_ASSET.
§Shortability
ShortabilityClient reads what IB says about lending a
stock from its shortable market data ticks, so unlike the rest of this client it uses one of
the account’s market data lines while it runs. It asks for IB’s delayed data, which IB serves
only where the account has no live subscription, so an account without one still gets an
answer. See fetch_shortability under IbkrClient’s trait implementations, and
IbkrConfig::shortability_timeout.
§Limitations
- Order types: Market, Limit, Stop, StopLimit, TrailingStop, TrailingStopLimit, and Bracket (entry + take-profit + stop-loss) supported. No Algo orders.
- TimeInForce: No
post_only(IB has no maker-only orders) - Client order ids travel as each order’s IB order reference, so they are at most 128 ASCII characters
§Reconnection & Recovery
ibapi reconnects its socket to TWS/Gateway by itself, with the same client ID.
In-flight requests fail with ConnectionReset at the drop, and new sends are refused
until the reconnect completes. Order sends classify that as a transient
OrderError::Connectivity. If the reconnect fails, ibapi shuts the client down.
ExecutionClient::account_stream stays open across a successful reconnect. It
detects the gap and, once delivery is restored, recovers the fills TWS sent while
the socket was down, emitting each exactly once: as a Trade, or as a
TradeAmended when it corrects an earlier execution. It then reports how each order
it held as live ended during the gap, if it did. The same applies when TWS loses its
own link to IB’s servers (notice 1100) and later restores it (1101/1102). See that
method for the details.
Each order carries its client order id as its IB order reference, which IB lists with
the order, its executions and its completion. So a client finds its orders by the id
they were placed with even across a restart: ExecutionClient::account_snapshot and
ExecutionClient::fetch_open_orders list them under it and track them again, and
OrderStatusClient finds how they ended.
Fills of orders that other API clients or TWS placed on the account are reported only when
IbkrConfig::other_clients_fills is set, as trades of origin
External. A fill IB flags as an IB-initiated
liquidation is reported either way, as a
Liquidation, on the stream, by fill recovery and by
fetch_trades, whenever IB sends it to this connection. The stream reports one as it happens
only when IB tags its execution with this client’s ID: ibapi drops a live execution tagged
with another client’s, so such a fill reaches only fill recovery and fetch_trades. Which ID
IB gives a liquidation’s execution is not documented.
§Caller Responsibilities
- Fill reconciliation: when fill recovery fails repeatedly, the stream reports the
gap as
AccountEventKind::FillRecoveryGaveUpand stays open. Read the gap’s fills withExecutionClient::fetch_trades. AVenueRetentiongap is the part IB no longer holds:fetch_tradescannot read it, so reconcile it from another source. - Permanent disconnect: when
ibapigives up reconnecting,IbkrClient::disconnectis called, or TWS/Gateway ends the API session,account_streamends withStreamTerminated, andaccount_streamon the shut-down client fails, so replace the client, by reconnecting withIbkrClient::connect_syncand choosing the client ID: the caller’s decision. The old client’s connection is closed in each case, so TWS/Gateway frees its ID once it has processed the close (seeIbkrClient::disconnect). A newIbkrClienttracks the orders the old one placed once an account snapshot orfetch_open_orderslists them; until then their events are dropped. - Stale state cleanup: Periodically call
IbkrClient::clear_stale_executions,IbkrClient::clear_stale_order_ids, andIbkrClient::clear_stale_pending_cancels
Rationale: IBKR uses TCP to local TWS/Gateway, not cloud WebSocket. ibapi
owns the transient reconnect. Replacing a client that is gone for good requires IB
Gateway availability and client ID coordination, decisions that belong in the
caller’s wrapper, not the library.
§See Also
- IB API Documentation
rustrade_data::exchange::ibkrfor market data
Re-exports§
pub use order::BracketOrderRequest;pub use order::BracketOrderResult;pub use execution::parse_ib_timestamp;
Modules§
Structs§
- Connect
Outcome - The result of
IbkrClient::connect_sync_lenient: the connected client, and the configured contracts it connected without. - Contract
Config - Pre-configured contract for startup registration.
- Ibkr
Client - Interactive Brokers execution client.
- Ibkr
Config - Configuration for the IBKR execution client.
- Skipped
Contract - A contract in
IbkrConfig::contractsthat could not be registered when the client connected.
Enums§
- Contract
Skip Reason - Why a configured contract could not be registered. Each variant carries the error of the step that failed: building, resolving or registering it.
- Ibkr
Connect Error - Why
IbkrClient::connect_syncfailed.
Constants§
- DEFAULT_
SHORTABILITY_ TIMEOUT - How long
IbkrClient::fetch_shortabilitywaits for IB’s shortability ticks, unlessIbkrConfig::shortability_timeoutsays otherwise.