Repository navigation
Port the Interactive Brokers adapter onto ibapi 5.0.0 - #5041
Conversation
0fd49cd to
d4bcc92
Compare
|
Note that 4.2.0 just got released. |
|
@tradatious thanks there would be a cooldown period for the new verison so it depends on when the PR will get merged. We'll update to latest version at some point anyway, it's simple to upgrade. On the other hand we could also avoid a cooldown, as it's a library regularly used and if there's no new dependency. |
d4bcc92 to
a9a73b8
Compare
|
Two issues that appear to be new: Stale cache tears down sessionA stale cached instrument stops the execution client from connecting. The old adapter preloaded too, but only for spread instruments. It also only warned and continued on a miss (core_tracking.rs#L55-L79 at the base). The rewrite expands the preload to every cached order and makes a miss fatal. An expired contract or a delisted symbol on a months-old cached order would now stop the client starting, requiring a cache purge to recover. Reconciling before trading makes sense, but refusing to start over an unresolvable instrument doesn't seem great. Perhaps restore the old warn-and-skip behaviour for that order, or gate the strictness behind a config flag? Wrong account code sent for non-default client namesThe account code sent to IB is now wrong for any client not named To talk to IB the adapter has to recover the raw code, and This change now makes orders go through Maybe the raw code should be derived once from the configured |
|
Thanks for the review, I'll update. |
a9a73b8 to
2fe1bc5
Compare
|
Hi @tradatious, thanks for the careful read. Both are right and both are fixed in 2fe1bc5. Stale cache. Account code. The raw IB code is derived once at construction from the configured |
8553534 to
865737c
Compare
|
Thank you, that was fast! And the changes look great. One small issue, in case you want to close it: if a consumer configures |
865737c to
3c271bb
Compare
|
Thanks @tradatious, both done in 3c271bb. The |
|
Awesome, thank you @faysou! |
|
You're welcome, thank you for reviewing too. |
2a91d1e to
e2fc015
Compare
|
Does this port support requesting a previously unseen instrument after the node starts, then trading it without reconnecting while another position remains active? At Is there a supported higher-level path I’ve missed, or is this a remaining gap in the port? |
a18992a to
31d2067
Compare
|
Test report from #5057: we built this PR at 31d2067 and ran it against an IB paper account (CME MNQ futures). The setup is a The #5057 restart case now recovers. A process was stopped with a position open and two orders still working at IB in one OCA group (
There was no "Trader ID not found", no The in-session path also passed, long and short:
The Two observations with this build:
Build: Rust 1.98.1, maturin 1.15.0, release profile with LTO off, macOS arm64, Python 3.12.12, IB Gateway (paper). We're happy to rerun on a later commit. |
|
The complexity of IB never stops. Luckily we have several people reporting bugs and the help of modern tools. Using a cache like redis improves stability between sessions, but if we can rebuild everything without a cache even better. |
8709ab7 to
8af617e
Compare
|
Hi @honvl, thanks for the detailed test report, it helped a lot. Both issues are fixed in 8af617e, which is also rebased on the latest
I checked both on a paper account: a Python strategy submitted two GTD limit orders (one from inside a callback), modified one, and read the cache in every callback without errors. After a restart with the same client ID, both orders were reconciled as A rerun on 8af617e would be welcome if you have time. |
|
Retested at 8af617e (built from source), same setup: IB paper account, CME MNQ futures,
Thanks for the quick fixes. |
7a88a0c to
48d12dc
Compare
|
FWIW in case you want to update all in one go, rust-ibapi just released 5.0. |
|
ok thanks |
3752611 to
17683a3
Compare
Rewrite the Interactive Brokers adapter against the published ibapi 4.2.0 release, replacing the 3.3.0 surface. The adapter owns the two IB behaviors that no other venue reports: - A second permanent order ID behind one client order reference is reported under its own client order ID, so the engine creates it as an external order. Cancelling the original still cancels the whole group. - An execution for an order the adapter does not track is held while the adapter queries the order details and executions, then emitted as an order report with its real fills. A failed or empty query forwards the held fills unchanged. Outside the adapter, the change adds generic spread IDs and futures symbol parsing to the model identifiers, and lets strategies modify the trigger of trailing stop orders. Futures and futures-option instrument identifiers are unchanged: the adapter still emits the local symbol IB reports, such as `YMM6.XCBT`.
- Keep futures symbol parsing with its IB consumers - Preserve parser behavior and tests while removing model exports
The execution client emitted one AccountState at connect and never refreshed it: the account summary subscription was dropped after the first End marker, so balances and margins stayed at their connect-time values for the whole session. query_account also built its AccountState on a tokio worker thread and sent it through the thread-local message bus, which has no registered endpoint there, so the result was dropped. The client now keeps the account summary subscription open in a session task. After the initial snapshot IB pushes changed values without a further End marker, so a refresh completes once rows stop arriving for one second. Connect, refreshes, and query_account all deliver through an execution event sender captured before spawning. Fixes #5193 Fixes #5194
A market depth Reset notice now clears the instrument book and the L2 order id map, because TWS discards its side of the book and the stream stays open. The unused realtime bar size accessor is removed because 5.0 dropped the type it returned, and test fixtures drop the removed trade tick type field. ibapi 5.0.0 is inside the 3 day dependency cooldown, so it is allowed in Cargo.toml and the cargo-vet exemption moves to 5.0.0. The cooldown check still fails until the version is 3 days old or an audit is recorded.
Clippy on the pinned 1.99.0 toolchain rejects the loop over one key. The strike check now matches the includeExpired check below it.
ibapi 5.0.0 is inside the 3 day dependency cooldown. The audit is recorded at the PR author's direction, who states they vetted the release, so CI can run before the cooldown ends. Remove the cooldown allow entry in Cargo.toml once the release is 3 days old.
The Rust formatting hook requires a blank line above a while loop that does not share an identifier with the line above it.
Report collection for single orders, bulk orders, fills, and positions decoded IB responses on the single-threaded live node core, even though the requests were asynchronous. The collection reads no cache: it needs only the IB client, the instrument provider, the account codes, and the request timeout. The collection moves to an owned report client that holds those inputs, and the four execution report task hooks return it as an ExecutionReportTask, so decoding and report construction run on a runtime worker. The inline report methods call the same code, so both paths share one implementation and the same filters, report identity, and errors. While the client is disconnected the hooks return None and the inline path reports the same not connected error. Mass status collection still uses the inline path. Part of the adapter rollout tracked in #5216.
IB reports the combo execution of a filled spread before the leg executions and sends no commissionReport for it. The adapter held every execution until its commission report arrived, so the spread fill only went out when the 5 second commission timeout fired, after the legs and with a warning on every combo fill. Combo executions no longer wait for a commission report, the commission is charged on the legs. The spread fill is held on its tracked order until the sent leg quantities cover it, then goes out, so it is the last event of its group and code reacting to it sees the leg positions and cash already applied. A spread fill whose legs do not arrive within 5 seconds is sent with a warning. Checked on a paper account: a market ES put spread and its flattening order each emitted both leg fills and then the spread fill, within 2 ms.
c3b7544 to
dc0d506
Compare
cjdsellers
left a comment
There was a problem hiding this comment.
Hey @faysou,
Thank you for all the work on this and for your patience through the review rounds. I appreciate the time you've put into addressing the feedback, adding tests, and validating the fixes on paper accounts.
Summary
This PR rewrites the Rust Interactive Brokers adapter on the published
ibapi 5.0.0release(from
3.3.0) and fixes the correctness gaps an audit and paper-account runs found in it:venue rejections that never reached the strategy, reconciliation against the wrong account code,
no resubscription after a socket loss, IB sentinel prices emitted as quotes, and fills replayed
after a restart. The execution engine, live reconciliation, risk engine, and portfolio fixes this
work needed are now on
develop, so outside the adapter the PR only adds model identifiers and astrategy fix. Futures and futures-option identifiers are unchanged: the adapter still emits the IB
local symbol, such as
YMM6.XCBT.ibapi 5.0.0: restructured modules and the execution, data, and connection fixes.flowchart LR subgraph IB["crates/adapters/interactive_brokers"] EX[Execution client] DC[Data client] SC[Shared connection] end subgraph Core["Core crates"] EE["ExecutionEngine (nautilus-execution)"] MI["Model identifiers"] end IBAPI["ibapi 5.0.0 (crates.io)"] EX -- "OrderWithFills, external DUP- orders" --> EE EX -- spread IDs --> MI SC --> IBAPI EX --> SC DC --> SCThe PR is one commit on top of
develop.Changes outside the adapter
Trailing stop trigger modifies
Strategy::modify_orderandmodify_ordersrejected a trigger price for trailing stop orders(
orders do not have a STOP trigger price), although the risk engine already projects it and theorder model applies it on
OrderUpdated. Both now accept a trigger forTRAILING_STOP_MARKETandTRAILING_STOP_LIMIT; other order types without a trigger are still refused.Model identifiers
new_generic_spread_idandparse_generic_spread_id_legsjoinnautilus_model::identifierswith Python bindings, so IB combo contracts round-trip through generic spread IDs from both
languages. Leg ratios parse as digits only, because
parse::<i64>accepts a leading sign andwould flip the sign the surrounding parentheses encode.
nautilus_model::identifiers::futures. It has one consumertoday; the Databento alignment that would share it is a follow-up. Keeping it in the adapter
crate changes no behavior if a reviewer prefers that.
Interactive Brokers adapter
Module layout
The previous crate mounted
core_orders.rs,core_updates.rs, andcore_tracking.rsinto oneimplblock through#[path], threaded about twenty separately locked maps as parameters, andrepeated its stream loop eight times. The rewrite splits by responsibility:
execution/core_orders.rsexecution/commands.rs: submit, modify, cancel, and their denialsexecution/core_updates.rs,core_tracking.rsexecution/updates.rs,order_state.rs,incarnations.rsArc<Mutex<map>>fieldsOrderTrackerinexecution/core.rs: per-order state behind one lockexecution/core_tests.rsexecution/core/tests.rsdata/core_streams.rsdata/streams.rs: oneStreamContextandrun_streamdriver for every streamcommon/parse.rscommon/symbology.rs(Symbologyvalue) andcommon/spreads.rscommon/connection.rs,common/types.rs,error.rsflowchart TD subgraph execution core[core: client, lifecycle, OrderTracker] commands[commands] updates[updates] order_state[order_state] incarnations[incarnations] account[account] espreads[spreads: combo legs] transform[transform: policy and tags] end subgraph data dcore[core: subscriptions] streams[streams: run_stream] dcache[cache: quotes and sentinels] convert[convert and parse] end subgraph common shared[shared_client] symbology[symbology] cspreads[spreads] end commands --> core updates --> core updates --> incarnations core --> order_state core --> account updates --> espreads commands --> transform dcore --> streams streams --> dcache streams --> convert core --> shared dcore --> shared core --> symbology dcore --> symbology symbology --> cspreadsExecution
openOrder), not local send success.PreSubmittedmaps toAccepted, so orders held forgoodAfterTimereconcile as open.Inactivestatus emitOrderRejected. Other error codes, which IB also sends for orders it keepsworking (10349 sets the time in force from an order preset), trigger a venue query, and the order
is rejected only when IB lists it neither as open nor as completed. For an accepted order, a notice
or
Inactiveemits a modify rejection when a modify is pending and leaves the order working; anotice also emits a cancel rejection when a cancel is pending (161), and otherwise schedules a
venue query. A venue query that fails, times out, or ends early leaves the order working. A
pending modify is resolved first, so
Inactiveafter a refused modify keeps even a partiallyfilled order working; otherwise
Inactiveafter a partial fill emits a cancel, since the orderstate machine forbids rejecting a partially filled order. Order-bound notices come from
OrderBoundinibapi 4.2.0.reference. A binding notice binds a route only in the subscription that owns the identity, and a
fill never rewrites a route already used for cancellation. A route with no binding, owned by
another API client, or mapping to several permanent IDs stays unresolved and is reported, never
resolved by an account-wide cancel.
engine models one order per client order ID, so each additional order is reported under its own
ID,
DUP-<account length>:<account ID>:PERM-<permanent ID>, and the engine creates it as anexternal order under the instrument's external order claim, otherwise
EXTERNAL. Late statusescannot reopen a filled duplicate, and cancelling the original also cancels the group's working
members, including after the original closes ([Interactive Brokers] Detect duplicate live orders after modify during connectivity loss #4564).
adapter queries the order details and executions (
order_details_with_fills). The adapter thenemits
ExecutionReport::OrderWithFills, so the engine creates the order at its full quantity andapplies the real fills with their trade IDs and commissions. One query runs per order, bounded by
request_timeoutand at most 3 seconds, which stays below the live node's defaultposition_check_threshold_msof 5 seconds. A failed, empty, or timed-out query logs a warning andforwards the held fills as plain reports. A duplicate status that shows fills the engine has not
received resolves them the same way, so the engine never infers a fill that a real execution
later repeats.
flowchart TD X[execDetails] --> T{Order tracked?} T -- yes --> F[OrderFilled] T -- no --> H["Hold fill (one query per order)"] H --> Q["Order details and executions min(request_timeout, 3 s)"] Q -- found --> W[OrderWithFills] W --> R[Held fills the query missed] Q -- failed, empty, or timed out --> P[Held fills as plain reports]reqExecutionsreplies, which ibapi alsobroadcasts on the update stream, are ignored there, so reconciliation queries no longer replay
the day's fills.
reports commissions on the legs only. Each leg execution resolves from its own contract and
becomes an
OrderFilledevent on the leg instrument under the spread order's strategy, which theengine applies to the leg position without an order of its own. Fill deduplication survives
terminal eviction long enough to absorb replays.
commissionReportfor it,so combo executions no longer wait for one. The spread fill is held until the sent leg
quantities cover it, then goes out as the last event of its group, so code reacting to it sees
the leg positions and cash already applied. A spread fill whose legs do not arrive within 5
seconds is sent with a warning. Previously the spread fill waited for the 5 second commission
timeout on every combo fill.
contract ID, builds the spread, and publishes both, so startup reconciliation restores combo
orders the node did not load. IB serves no contract details for a BAG. Startup fill reports
resolve the combo-level execution's spread through its order's contract, and leave out the leg
executions, which share the combo order's IDs; leg positions reconcile from IB's position reports.
Mass status sets a missing average price on a filled order report from its fills.
price, so
goodAfterTime, the OCA group, andoutsideRthsurvive, including on restored orders.A trailing order's trigger is IB's trailing stop price, both in
openOrderupdates and in thepending-modify acknowledgement; a modify that leaves it unchanged ignores the stop IB moves with
the market. IB accepts a new trailing stop price only with a new trailing amount (10067), so a
trigger modify of a trailing order carries
params={"trailing_offset": <offset>}in the order'soffset units; without it the modify is rejected before it is sent.
BASIS_POINTSoffsets (sent as IB's trailing percent) as well asPRICEoffsets, for single orders as for order lists; other offset types are denied.
lookup; a duplicate member, which shares its raw order ID, uses its permanent ID. A cancel
resolved from the venue emits
OrderCanceledunder the target order's own strategy. A strategycancel, which already published
OrderPendingCancel, only updates the adapter's tracking;cancel-all and group cancels, which the strategy does not mark, still emit it.
instrument, and side filters across cached orders, tracked orders, and duplicate groups; an empty
side-filtered selection cancels nothing, and duplicate members of unknown side are skipped.
be prepared is denied as a whole (
ORDER_LIST_INVALID). A partially failed order list gives everyorder a definite state; sent predecessors are cancelled at the venue. The order ID counter stays inside the client's partition when seeded from open
orders. Pre-submit denials carry coded reasons.
account_idand used for orders,modifies, cancels, account, position, and PnL subscriptions, and execution filters. A client
named
IB_LIVEsendsU1234567, notIB_LIVE-U1234567, so startup fill reports no longer failwith TWS error 321 ([Interactive Brokers]
generate_fill_reportssends the prefixedAccountIdas the execution filter's account code: rejects with error 321 #5007).reqAccountUpdatessnapshot after the summary and stores every key inAccountState.info, so values such asPostExpirationExcessreach Python ([Interactive Brokers] Account values outside the fixed reqAccountSummary tag list are unreachable #5032). Balances andmargins use each row's own currency; ratio rows such as
Cushionare skipped without a warning([Interactive Brokers] Cannot build an Account when the account summary carries no
Currencytag #4987).Endafter the first snapshot, so a refresh completes once rows stop arriving for one second,and the latest value per tag and currency persists across cycles.
query_accountand the streamsend
ExecutionEvent::Accountthrough an execution event sender captured on the core thread,not the thread-local message bus from a runtime worker ([Interactive Brokers] Execution client never refreshes AccountState after connect #5193, [Interactive Brokers] query_account sends AccountState via the thread-local msgbus from a tokio worker (suspected) #5194).
Resetnotice, delivered as data byibapi 5.0.0, clears the instrument book andthe L2 order ID map.
ibapi 5.0.0is inside the 3 day dependency cooldown, so it is allowed inCargo.tomland the cargo-vet exemption moves to 5.0.0 until an audit is recorded.through the four
ExecutionReportTaskhooks. An owned report client inreports.rsholds the IBclient, the instrument provider, the account codes, and the request timeout, and reads no cache.
The inline report methods call the same code, so both paths share filters, report identity, and
errors. While the client is disconnected the hooks return
Noneand the inline path reports thesame not connected error.
generate_mass_statuskeeps the inline path (Standardize execution report generation hooks across adapters #5216).race a fill into a spurious external position change. Position tracking keeps streaming past
PositionEnd.Reconciliation and restart recovery
PERM-venue order IDs, so their updates, fills, and cancels reach the strategy. Reports carrythe bracket parent (
parent_order_id,OTO), OCA group (OCOorOUOby OCA type), displayquantity, completion time, and GTD
expire_time. The firstopenOrderafter a restart sends noOrderUpdatedunless a value changed.set_report_window. Unresolvable ordersand positions and missing commission reports fail the request instead of dropping records. Live
execution still emits a zero-commission fill with a warning after five seconds without a
commission report, because a venue fill cannot stay withheld.
returns a position report, and the engine's inferred-fill reconciliation resolves it.
Completed orders that an OCA group reduced to zero without a fill are skipped.
so startup no longer aborts with
instrument missing from cache. Cached orders whose instrumentIB no longer resolves log a warning instead of failing
connect(Interactive Brokers: a replayed execution is dropped when the instrument provider has not yet loaded its instrument #4932).exactly its position quantity, the position report carries the entry price of those fills, so a
position opened with a commission reconciles across a restart; otherwise it keeps IB's average
cost.
Market data
bar type, so 1-minute and 5-minute or bid and ask bars on one instrument stream together;
unsubscribe cancels only the matching stream, and a
SubscriptionGuardremoves the entry when a stream ends permanentlyor panics.
-1without a size, or0with zero size) are filtered before buildingquotes, trades, and index ticks; legitimate zero and negative spread prices, including
-1with asize, pass.
Clearbefore new deltas, and unknown depthoperations are skipped with a warning instead of mapped to add.
handle_revised_bars=False, a keepUpToDate bar is published once it completes: on IB'supdate after its close (about 0.5 seconds), 2 seconds after its close, or on the next bar's
first update, whichever comes first. Replayed history cannot publish a bar twice.
ibapi 4.2.0routes as anotice (IB adapter: historical bar requests are ended on IB warning 2188 (delayed data), delayed bars dropped #5142).
the last listed session, so they expire at the close instead of midnight ([Interactive Brokers] v2: option expiration_ns is midnight instead of market close (regression of #3331) #5136).
requested one, since IB returns no chain when a stock's exchange is named ([Interactive Brokers] build_options_chain loads no options for stock underlyings #5135). A continuous
future loads the futures options of its futures regardless of the options' expiry window.
HISTORICAL_TICK_DEFAULT_LIMIT, 10,000) and deduplicatedat the boundary second. A historical request that fails partway emits the rows it obtained and
warns.
all_last_trades, [Interactive Brokers] Make the tick-by-tick trade type selectable (Last vs AllLast) #4960), optionalInteractiveBrokersSubscriptionIdlecustom data (subscription_idle_timeout_secs, [Interactive Brokers] No liveness detection for subscriptions that are open but delivering no data #4949), awarning before resubscribing an unexpectedly ended stream ([Interactive Brokers] Trade, realtime bar and market depth streams end with no log line #4947), and DEBUG logs for expected
unrepresentable fractional sizes ([Interactive Brokers] Unrepresentable fractional trade sizes log at WARN once per tick (99.7% of session log) #4948).
Connection
run_streamand reportsdisconnection when the transport shuts down or a required stream cannot be restored.
on one connection, and a cancelled waiter leaves no reserved reference.
TaskHandlesand cancellation instead of best-efforttry_lockcleanup. Dockerized gateway readiness reflects the current session's login.StartApiwith an explicit error, andconnection timeouts are bounded ([Interactive Brokers] v2.0.0rc2 historical client connect leaves IB Gateway 10.41 API unresponsive (accept-queue wedge requiring Gateway restart) #4796).
Configuration and Python surface
load_contractsis a typedConfiguredContractlist, including the per-contract chain keys, andfilter_sec_typesparses intoIbSecurityTypewith errors on unknown values. AconIdalone isa valid contract.
DebugandSerialize.that
ibapi 4.2.0reports asUnknown(andGTX) map to the defaults with a warning, matchinghow
4.1.0decoded them.ibapi 4.2.0bounds async subscriptions with a crate-private decoder trait, sorun_streamiterates over a small private trait implemented per streamed item type. The dependency carries
no
git,branch,rev, orpathkey.__all__, and__init__.pyiaddsall_last_trades,subscription_idle_timeout_secs, andInteractiveBrokersSubscriptionIdle.pass-through without a configuration field, because switching to two-digit years would rewrite
identifiers already stored in catalogs and caches.
Related issues/PRs
StartApiwith an explicit error. Connection timeouts stay bounded.develop; the adapter relies on the risk engine's account routing for broker-routed instruments.all_last_tradesdefaults to AllLast;Falseselects Last.Cushionno longer log currency warnings.reqAccountUpdatessnapshot merges intoAccountState.infokeyed by IB name, so values such asPostExpirationExcessreach Python.order_sidein every selection path; an empty side-filtered selection cancels nothing, and duplicate members of unknown side are skipped.PreSubmittedmaps toAccepted, so orders held forgoodAfterTimereconcile as open. External orders restored at startup join the adapter's order tracking, so their later IB updates reach the strategy and cancels route. AnopenOrdersendsOrderUpdatedonly when quantity, price, or trigger price change. Restored orders keep their bracket and OCA links, display quantity, and completion time, and a modify keeps IB-only attributes such asgoodAfterTime, the OCA group, andoutsideRth. Filled orders from the previous session reconcile from their real fills, and GTD orders carry theirgoodTillDateasexpire_time. Startup acceptance ordering is ondevelop.build_options_chainloads the options of stock and index underlyings: the chain request names no exchange for them and keeps the chains of the requested exchange, and loaded options are looked up on that exchange. A continuous future loads the futures options of its futures whatever their expiry.NVDA 261030C00230000expires at 16:00 New York.ibapi 4.2.0routes it as a notice and delivers the delayed bars. The v1 adapter's request error handling is not part of v2.reqExecutionsreplies that ibapi also broadcasts on the order update stream are ignored there, so startup and periodic execution queries no longer replay the day's fills as live fills. The adapter holds a fill for an untracked order until it reports the order with its real fills, so the fill does not create a reduce-only order. The position-check grace period is ondevelop.AccountStateeach time IB pushes changed values. IB sends no furtherEndafter the first snapshot, so a refresh completes after one second without new rows, and the latest value per tag and currency persists across cycles.query_accountand the account stream sendExecutionEvent::Accountthrough an execution event sender captured on the core thread, not the thread-local message bus from a runtime worker.The issues stay open; this table records implementation coverage.
Type of change
Breaking change details
ibapimoves from=3.3.0to=4.2.0. Nothing in the repository outside the adapter uses it.ConnectionManagerandConnectionWatchdog, theerror.rstaxonomy (
InteractiveBrokersError,InteractiveBrokersErrorKind,ErrorCategory, and theclassification functions), the
ib_contract_to_instrument_id_*andinstrument_id_to_ib_contractfunctions (now
Symbology), and the provider'sload_async,load_ids_async, andload_contract_with_return_async.parse_market_depth_operationreturnsOption<BookAction>.ErrorCategoryandInteractiveBrokersErrorKindare no longer exported.load_contractsandfilter_sec_typesare typed and reject unknown keys orsecurity types that were previously ignored.
Deferred follow-ups
futures_year_digitsconfiguration surface, and theDatabento symbology alignment that depends on it.
streaming, and historical-execution modules that read it.
as its only user.
Documentation
docs/integrations/interactive_brokers.mdis rewritten for the new adapter: connection andmarket data modes, an architecture section with diagrams (reconnect and resubscribe, order
identity and cancellation, execution order flow), data and execution capability matrices,
account state, reconciliation including restart recovery and why execution-query replies are not
live fills, symbology and generic spreads, a configuration reference, the Python enum reference,
testing, and troubleshooting. Futures identifiers are documented as the IB local symbol.
examples/live/interactive_brokersruns at any date: contract selection goes through_common.py, the notebooks are inlined instead of routing throughorder_example_driver.py,notebooks/option_chain_example.pyloads the SPY, SPX, ES, and ESTX50 option chains through oneinstrument request, and
historical_download.pywrites throughParquetDataCatalog. The Rust exec tester requiresNAUTILUS_IB_RUN=1to connect andNAUTILUS_IB_LIVE_ORDERS=1to submit orders.Release notes
Not added.
RELEASES.mdis maintainer-owned.Testing
Core tests:
parsing, formatting, and year resolution.
Adapter tests (offline, against parsed ibapi structs):
orders, and duplicate groups, excluding other instruments and accounts.
PreSubmittedmapping,OrderUpdatedonly on changed values, and execution details with requestID -1, 0, and a positive query ID, where only the first two record a live fill.
reported price of market, stop, and limit orders, and a modify that changes only the requested
field of limit, stop, and trailing orders.
-1sentinel, informational error notices beforeacceptance resolving through the venue query,
Inactiveon an accepted order with and without apending modify, a refused cancel emitting
OrderCancelRejected, a trailing modify acknowledged byits trailing stop price, the cancel selector for tracked and duplicate orders, and an order list
whose trailing child cannot be transformed failing before any order is sent.
trailing_offset, the local rejectionwithout it, the trailing offset types a single order accepts, and pending-cancel tracking that
marks an order once without emitting.
DUP-client order ID, resolvedduplicate fills gating later statuses, and one held-fill resolution per order.
risk engine.
Endcycles, the quiet-period flush for refreshes without anEnd, anddelivery of an account state from a worker thread.
Resetclears the book and the next update restarts the sequence.report) emits both leg fills and then the spread fill, and a replay emits nothing. A spread fill
waits until split leg executions cover its quantity, and one without legs is sent after 5 seconds.
Nonewhile disconnected, and the inline fill report fails with the samenot connected error. The fill-report parsing tests run through the shared report context.
Paper TWS runs (account
DU187075):USD within two seconds and produced a new
AccountState. Before the fix the account stayed atits connect-time values.
query_accountitself was not called in these runs.tokio-rt-workerthread and returned successfully (12 and 4 times). The single-order and fillhooks did not run, because normal operation never triggered them.
IB_PAPERreceived its account summary and positions; before the fix thesummary was filtered out.
ESZ6put positions the tester never loads are recovered from venue reports at startupinstead of aborting it. A one-lot
ESZ6.XCMEorder, previously denied by the risk engine, wassubmitted, accepted, and filled.
goodAfterTimemarket order placed before the node started reconciled asACCEPTEDand its fill during the run took theESZ6.XCMEposition from 1 to 0. That order wasknown from startup, so the run does not exercise the held-fill path, which the adapter tests
cover.
avgCost231.89) aborted startupreconciliation before the average cost fix and reconciled after it.
combo execution first, then the leg executions, with commission reports for the legs only.
spread_example.pythen bought and flattened a one-lot ES put spread: each order emitted bothleg fills and then the spread fill within 2 ms, with no commission timeout warning, and both leg
positions opened and closed.
option_chain_example.pyloaded 8,536 options: SPY, SPX, ESZ6 futures options, and ESTX50 withintheir expiry windows. NVDA options loaded through
build_options_chainexpire at 16:00 New York(20:00Z on 2026-10-30, 21:00Z on 2026-11-06 after the DST change).
goodAfterTimemarket order from one OCA group asOUOlinked
ACCEPTEDorders, and a bracket as anOTOparent with display quantity 1. Strategycancels completed; in another run the held order filled at its
goodAfterTimeand IB cancelledthe stop. Modifying the restored stop's trigger from 5000 to 4900 left its OCA group and
outsideRthunchanged.node matched IB throughout. A restart with the day's filled external orders applied every fill in
venue order after a synthetic opening for the prior-day position and ended at +1.
a restarted node reconciled both as
ACCEPTEDGTD orders and the strategy cancelled them.ESZ6were accepted and theiropenOrderupdates carried IB's trailing stopprice as the trigger; limit orders were modified seven times, each acknowledged by
OrderUpdated,and cancelled, with no errors.
trailing_offset: all 24 modifies that changed the amountwere acknowledged with the new trailing stop price, and all 22 that repeated it were rejected by
IB with 10067. A direct probe confirmed IB rejects a stop-only change for price and percent
offsets alike. Each strategy cancel produced one
OrderPendingCancel.leg
OrderFilledevents. A restart afterwards reconciled the day's 20 orders, applied one combofill to each of the 8 combo orders, matched IB's leg positions, and logged no warnings.
Local runs after the rebase onto
develop:nautilus-interactive-brokers, lib and testsnautilus-modelandnautilus-tradingtests/unit/adapters/interactive_brokersib-exec-testerexample,--features examplescargo-test-core-localmake pytestmake pre-commitThe core-local failure,
websocket::client::rust_tests::connection_rate_limit_gates_initial_connect_and_reconnectinnautilus-network, fails the same way ondevelop.