All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
DATA_ADVISORY_CODESis a&[i32]slice instead of a fixed-size array, so adding an advisory code is no longer a type change. Code binding the constant with an explicit array type, or iterating it by value, must adjust; seedocs/migration-4.0.md§6 (#807). -
WARNING_CODE_RANGEwidens from2100..=2169to2100..=2199: IB keeps adding warnings above the old ceiling (2176, 2187), and each one was a hard error that failed in-flight one-shots and ended subscriptions. Codes 2170–2199 now route as non-terminal notices andNotice::category()reports them asWarning(#805). -
Notice::category()resolvesDATA_ADVISORY_CODESahead of the warning and order-rejection bands instead of after them, so 2188 staysDataAdvisoryinside the widened warning band (and 317 below categorises the same way). The range predicates are unchanged:Notice::is_warning()is now true for 2188 andNotice::is_order_rejection()remains true for 317 (#806).
-
Error 317 ("Market depth data has been RESET. Please empty deep book contents before applying any new entries.") is a data advisory: it is published as a non-terminal
SubscriptionItem::Noticeon the market-depth subscription instead of ending it, so the rows that rebuild the book still arrive. Consumers discard their book on this notice and apply the updates that follow. Its sibling 316 (market depth HALTED) remains terminal (#806). -
Error 10091 ("Part of requested market data requires additional subscription for API") is classified as a data advisory like 10089, 10090, and 10167: it is published as a non-terminal notice instead of ending the market-data subscription, so ticks that follow it — including delayed option computations — still arrive (#804).
-
Disconnecting while the client is reconnecting no longer hangs: the reconnect backoff now observes the shutdown request and the dispatcher exits with
Error::Shutdown. Previously the syncClient::drop/disconnect()blocked until every reconnect attempt was exhausted (forever withreconnect_forever), and the async dispatcher task leaked (#795). -
System messages (codes 1100, 1101, 1102, 1300) no longer fail every in-flight one-shot request. They are routed as non-terminal notices, like the order-cancellation confirmation (202);
Notice::is_warning()andNotice::category()are unchanged for these codes (#800).
4.0.1 - 2026-09-06
TCP_NODELAYis now on by default (ClientBuilder::tcp_no_delaydefaults totrue), matching the official IB clients. With Nagle's algorithm on, a request written while the previous one is still unacknowledged waits for that ACK — up to ~40 ms on Linux and ~200 ms on macOS against TWS's delayed ACK — so a burst of orders placed in quick succession was serialised on the ACK of each preceding one. A single request was never affected. Pass.tcp_no_delay(false)to restore the old behaviour.
4.0.0 - 2026-09-03
-
The prelude now re-exports the account-parameter newtypes
AccountGroup,AccountId,ContractId, andModelCode(fromibapi::accounts::types). These are required arguments ofaccount_summary,account_updates,pnl,pnl_single, andpositions_multi, but were the only such parameter types missing fromibapi::prelude— the quick-start example could not be written with prelude-only imports. -
SUBSCRIPTION_LAG_CODE(-6), a synthesized notice code delivered in-band on an async subscription whose consumer fell behind its broadcast channel: the channel evicts the oldest frames, and the subscription now receives a non-terminalSubscriptionItem::Noticenaming the dropped count (plus awarn!) where it previously resumed silently — the frames were simply gone with no signal at any level. Reconcile as after a reconnect gap (order streams:open_orders(); market data self-corrects with the next tick). Step 1 of the #779 plan (plans/broadcast-lag-visibility.md): loss is now observable; the bounded-lossy semantic itself is unchanged (#779). -
ClientBuilder::channel_capacity(async only), setting the per-subscription broadcast channel capacity (default unchanged: 1024). Raise it if consumers legitimately fall behind during bursts;0is rejected asError::InvalidArgument. The notice fan-out keeps the default (#779). -
ClientBuilder::max_reconnect_attemptsandClientBuilder::reconnect_forever(sync and async), configuring how many times the automatic reconnect loop retries before giving up. The default is unchanged: 20 attempts, ~7.5 minutes with the capped Fibonacci backoff — too short to span a TWS/IB Gateway nightly restart that needs a manual re-login, which is whatreconnect_forever(retry every 30 s until the connection returns) is for (#762, #763). -
IBAPI_RAW_CAPTURE_DIR, a byte-level tap on the inbound stream. Set it to a directory and every connection writes<stamp>-<n>-inbound-<NNN>.bin, a verbatim copy of what the socket delivered — including the 4-byte length prefix — alongside an.idxofseq,utc_timestamp,offset,declared_lengthper frame.IBAPI_RECORDING_DIRcannot substitute: it is handed an already-parsed message and re-frames it, so the prefix it writes is one this crate computed. That makes it blind to a framing desync, where the prefix is the corrupted field. The tap sits below validation, so a prefix rejected asError::InvalidFramestill reaches the capture — that prefix is the evidence. A reconnect starts a new file, so no.binsplices two TCP streams. Because prefixes are preserved, a capture replays through the frame reader unchanged;cargo run --example replay_raw_capture -- <file>.binwalks one and reports the first frame that cannot be read. Captures are unredacted wire bytes, so they carry account ids, positions, and orders. -
UNKNOWN_MESSAGE_TYPE_CODE(-5), a synthesized notice code published toClient::notice_streamwhen a frame's message id maps to no knownIncomingMessageskind. Joins the existing client-side sentinels (HANDSHAKE_UNKNOWN_FRAME_CODE,HANDSHAKE_DECODE_FAILURE_CODE); TWS itself only uses codes 0 and up. This is the observable form of a framing desynchronization. The notice text names the offending id, which is what separates the two explanations: a slipped stream yields scattered ids, while a message type IBKR has added repeats one. -
Error::InvalidFrame, returned when a frame's 4-byte length prefix cannot describe a TWS message — shorter than the message id, or larger than the 16 MiB cap the official client enforces asConstants.MaxMsgSize. Classified asError::is_connection_lost, so both dispatchers reconnect: the framing is positional, with no delimiter to re-anchor on, and a reconnect is the only recovery.Erroris#[non_exhaustive], so the new variant is not a breaking change. -
Error::UnexpectedWireFormat, returned when a message arrives in the wrong wire format for the reader handling it — text framing at a proto-only decoder, or proto framing at a text-field accessor. Previously this sharedError::UnexpectedResponsewith the unrelated "message is not for this decoder" case, which the dispatcher skips silently.Erroris#[non_exhaustive], so the new variant is not a breaking change (#731). -
Notice.request_id, the originating request or order id (Nonefor request-less notices).Noticeis not#[non_exhaustive], so struct literals must add the field (#759). -
ORDER_MESSAGE_CODE(399), the generic order-message code whose message text decides severity (#759).
-
Client::option_chainreturns a builder instead of takingexchangepositionally:client.option_chain(symbol, security_type, contract_id)with an optional.exchange(..)setter and a.subscribe()terminal. The old signature'sexchange: &strdocumented""as "all exchanges", and live checks against TWS show that is the only value that returns anything for a stock underlying — the field is TWS'sfutFopExchange, a futures-options filter, and"SMART"(which the async example passed) yields an empty chain.contract_idstays required:0(which two examples passed) is rejected with code 321 "Invalid contract id". An unset exchange is now omitted from the wire rather than sent as an empty string, matching the official client. Seedocs/migration-4.0.md§10. -
Sync transport: a stalled consumer now triggers
warn!watermarks at every 10,000 queued messages on the subscription, shared-channel, and order-update send paths. Sync channels are unbounded and never drop; their failure mode — silent memory growth — is now loud. The notice fan-out (NoticeBroadcaster) is excluded for now; seeplans/broadcast-lag-visibility.md(#779). -
OrderStatusKindgains anUnknown(String)variant preserving unrecognized status strings (see the Fixed entry below). Breaking: the enum stays deliberately exhaustive likeLiquidity::Unknown(#760), so downstream exhaustive matches need a new arm;Copyis gone (stillClone);as_str()returns&str;is_active()/is_terminal()take&selfand arefalseforUnknown. Serialization stays a plain string in both directions, and theutoipaschema is now a plainstringto match. Details and examples indocs/migration-4.0.md§9 (#774). -
MarketDataBuildermoved frommarket_data::buildertomarket_data::realtime, alongside the sibling builders (RealtimeBarsBuilder,MarketDepthBuilder,TickByTickBuilder) it always belonged with; themarket_data::buildermodule is gone.MarketDataBuilder::newis alsopub(crate)now, matching its siblings — construct viaclient.market_data(&contract). Only code naming the type — imports, signatures, or a directnewcall — breaks;client.market_data(&contract)call sites are unaffected. The prelude now exports all four realtime builders (MarketDataBuilderandRealtimeBarsBuilderjoin the two already there). Seedocs/migration-4.0.md§7 (#772). -
A text-framed message reaching a proto-only decoder now fails the subscription instead of being skipped. At
server_versions::PROTOBUF_REST_MESSAGES_3every message with a proto decoder arrives proto-framed, so reaching this means the gateway broke protocol — previously the message was dropped and the subscription silently yielded nothing. Wrong-message-type frames are still skipped, which is what shared channels need (#731). -
Historical tick and histogram sizes are now
Option<f64>instead ofi32:TickMidpoint.size,TickLast.size,TickBidAsk.size_bid/size_ask, andHistogramEntry.size. IBKR models these as decimals on the wire, and the oldi32parse silently truncated fractional sizes — a crypto tick of0.5decoded as0.Nonemeans TWS sent no value (field absent, empty, or an "unset" sentinel);Some(0.0)is a real zero. This also changes the serialized shape — a size is now100.0rather than100, absent isnullrather than0, and theutoipaschema becomes a nullablenumber(#716). -
ContractDetails.min_size,size_increment, andsuggested_size_incrementare nowOption<f64>instead off64. Contracts without size rules omit these on the wire, where the old0.0was indistinguishable from a real value and asize_incrementof0.0is nonsense (#716). -
Every one-shot request now narrows the inbound frame to the message type it asked for before decoding it. Narrowing used to be opt-in — each domain hand-wrote a
decode_*_messagewrapper for it, and 26 of the 50 sites had no wrapper, decoding whatever arrived on the shared channel. A foreign frame now surfaces asError::UnexpectedResponsenaming both the expected and received type, rather than being fed to the wrong payload decoder — where overlapping protobuf field numbers usually produce a plausible struct full of wrong values instead of an error. Affectsnext_valid_order_idin particular, which reads the sharedRequestIdschannel (#738). -
market_rule,family_codes,calculate_option_price,calculate_implied_volatility, andnext_valid_order_idnow retry a connection reset up to three times, like every other one-shot request. They previously surfacedError::ConnectionResetto the caller on the first reset — a gateway reconnect mid-request failed the call outright. Nothing about these five made them unsafe to retry; they were the sites that had picked a helper without retry,market_ruleandfamily_codesbecause the only helper bundling a server-version check was the non-retrying one (#741). -
head_timestamp,histogram_data,market_depth_exchanges, andhistorical_schedules(..).fetch()retry a connection reset at most three times instead of unboundedly. The asynchead_timestamprecursed on a closed stream and the asynchistogram_data,market_depth_exchanges, and schedule fetch looped on one; a gateway that keeps resetting would hang the call rather than return. They also now agree with their blocking twins on what a closed stream means:Error::UnexpectedEndOfStreamforhead_timestampand the schedule fetch, an empty list forhistogram_dataandmarket_depth_exchanges(#738). -
Liquiditynow preserves unrecognized execution liquidity codes as a newUnknown(i32)variant instead of collapsing them toLiquidity::None. The documented codes 0–3 decode as before; any other value surfaces asUnknown(code)so callers can log, store, or reject it — previously an unknown code was indistinguishable from a genuine "no liquidity information", which is silent data loss (the official C# client has the same coercion). No official client orExecution.protodefines a code outside 0–3 today, so this is forward compatibility.Liquidityis deliberately exhaustive (no#[non_exhaustive]), so downstream exhaustive matches need a new arm — breaking (#760). -
Client::wsh_event_data_by_contractandClient::wsh_event_data_by_filterreturn builders instead of taking optional arguments positionally. The first took one required argument and fourOptions, the second one and two; every call site in this repository passedNonefor all of them. Narrowing is now.starting(date)/.ending(date)/.limit(n)/.auto_fill(spec), with.fetch()(single result) and.subscribe()(subscription) as the terminals. Seedocs/migration-4.0.md§3 (#752). -
Code 399 order messages whose text carries a
Warning:line now classify as warnings (Notice::is_warning,NoticeCategory::Warning) and route as non-terminal notices. TWS uses 399 for both severities — e.g. "order will not be placed at the exchange until …" for outside-RTH stops — and the warning form previously terminated the owning subscription as an order rejection (#759). -
The order-update stream delivers order-bound error frames as
SubscriptionItem::Notice(carryingrequest_id, code, and message) instead of raw error frames thatOrderUpdatecould not decode — consumers previously sawErr(Error::UnexpectedResponse). Notefilter_data()/iter_data()drop notices; match onSubscriptionItem::Noticeto observe rejections of fire-and-forget orders. Request-less errors and errors owned by a data-request subscription no longer reach the stream at all (#759).
-
The free function
market_data::realtime::sync::market_data()is crate-private, matching therealtime_bars/market_depth/tick_by_tickfree functions beside it (in sync-only builds it was also reachable asmarket_data::realtime::market_datathrough the glob re-export). It was the low-level request under theclient.market_data(&contract)builder, which is the supported path and takes the same inputs via setters; no call site inexamples/or the integration crates used the free function. Seedocs/migration-4.0.md§8 (#780). -
Client::check_server_version()is crate-private. It waspubon the async client andpub(crate)on the blocking one — drift rather than design, since it is the internal guard every version-gated API already calls. Compare againstClient::server_version()directly if you need to branch on gateway support (#728).
-
ComboLegOpenClose::from(i32)no longer panics on a value outside0..=3; unrecognised values decode toComboLegOpenClose::Unknown. TheFrom<i32>impl runs inside contract-details decoding, so an unexpectedopenClosefrom TWS took the dispatcher down instead of surfacing as anUnknownleg. -
HistoricalDataEndand historical-schedule decoding no longer panics when a wall-clock time from TWS falls in a DST fold or gap, and the connection time reported at handshake is no longer dropped for a fold. A reading in a repeated hour resolves to its earlier occurrence (the pre-transition offset). A reading in a skipped hour never showed on a clock, so it can only come from TWS doing date arithmetic on wall-clock time — a window start computed as "end minus 300 days", say — and is pushed forward by the gap: 02:30 on a US spring-forward day becomes 03:30 EDT. Previously a 300-dayhistorical_data()request whose start edge landed on a fall-back night panicked withOffsetResult::unwrap(#790). -
An
OrderStatusorOpenOrderframe carrying an unrecognized status string no longer terminates the subscription delivering it. Decoding failed withError::Parse, which endedplace_order,cancel_order,open_orders, and — worst — the long-livedorder_update_streamthe moment TWS shipped a status this crate had not modeled; the official IB client parses such statuses into anUnknownfallback instead of failing. The status now arrives asOrderStatusKind::Unknown(raw)and the stream continues (#774). -
Async
open_orders(),all_open_orders(),completed_orders(), and the other streaming shared-channel subscriptions (positions, account data, news bulletins) no longer hang forever when a reconnect happens mid-request. After a reconnect,reset_channelsnotified request-id and order-id subscriptions withError::ConnectionResetbut left shared channels untouched — and the end marker such a subscription awaits is a response to a request sent on the dead connection, so nothing ever arrived. The async reset now notifies shared-channel senders too, mirroring the sync transport (which already did this). Subscriptions created after the reset are unaffected: they subscribe at the channel's current tail and never see the injected error (#776). -
Dropping a subscription no longer unregisters a newer subscription under the same key. Drop-signal cleanup crosses a channel to a separate task/thread, so it can run arbitrarily late — after
place_orderthencancel_orderre-registered the same order id, or after the order-update stream was recreated — and removal was unconditional, silently disconnecting the live replacement (data and error frames for that order went nowhere). The async cleanup now removes a registration only when its channel has no receivers left (a dropping subscription detaches its receivers first, so the count is authoritative); the sync drop signal now carries the dropped subscription's sender and cleanup removes only the matching registration. Dropping a clone while a sibling is still consuming no longer unregisters the shared channel either (#773). -
order_update_stream()can be dropped and immediately recreated. Recreation returnedError::AlreadySubscribeduntil the asynchronous cleanup ran, and a stale cleanup signal could then clear the replacement's registration, after which the new stream silently received nothing. The async transport now replaces a registration whose channel has no receivers instead of refusing, and stale signals cannot clear a live replacement on either transport. Sync note: recreation still returnsAlreadySubscribedfor the instant between drop and the cleanup thread running (crossbeam senders expose no receiver count); retry onAlreadySubscribedthere (#778). -
The order cancellation confirmation (code 202) is routed to request-bound subscriptions as a non-terminal
SubscriptionItem::Noticeinstead of a stream-terminatingErr. Successfully cancelling an order ended thecancel_order(orplace_order) subscription with an error whose payload the crate itself classifies as informational (Notice::is_cancellation(),NoticeCategory::Cancellation), while the order-update stream received the identical frame as a non-terminal notice. Routing now agrees with that classification. Behavior note: the 202-Errwas also the only thing that ended acancel_ordersubscription; it now stays open until dropped — consume it like the order-update stream and break onnotice.is_cancellation()(#775). -
FibonacciBackoffno longer overflowsu64on the 93rd consecutivenext_delay()call. The backoff state kept growing pastmax(only the returned delay was capped), so a reconnect loop with a largemax_reconnect_attemptsorreconnect_foreverwould panic withattempt to add with overflowin debug builds, or wrap to nonsense delays in release, once an outage outlasted ~92 attempts. The state is now clamped atmax. Returned delays are unchanged. -
Notice 2188 ("Up-to-the-second historical data requires additional subscription for the API.") is classified as a data advisory instead of a hard error. TWS sends it and then delivers the historical bars anyway — the account merely lacks the up-to-the-second tail — but
historical_data()returnedErron the notice and discarded the bars that followed, so accounts without the real-time entitlement for a listing got no history at all for those symbols.DATA_ADVISORY_CODESwidens from[i32; 2]to[i32; 3], which is breaking only for code binding the const with an explicit array type (#765). -
Error 10090 ("Part of requested market data is not subscribed. Subscription-independent ticks are still active") is classified as a data advisory like 10089 and 10167: it is published as a non-terminal notice instead of ending the market-data subscription. TWS sends it on partial entitlement — commonly an options subscription without the underlying — and keeps delivering the ticks the account is entitled to, but the subscription was torn down before they could arrive.
DATA_ADVISORY_CODESwidens again to[i32; 4](#768). -
A notice whose protobuf
error_codefield is absent no longer fails every in-flight one-shot shared request (server_time,managed_accounts,next_valid_order_id, ...). The absent field decodes to code 0 — outside every warning range — so IB Gateway's code-less informational notices (e.g. "Warning: Approaching max rate of 50 messages per second (42)") were treated as request-less hard errors. Code 0 now classifies as a warning throughout: request-less frames reachClient::notice_streamwithout failing anything, frames carrying a request id are delivered to that subscription as a notice instead of terminating it, andNotice::is_warning()/Notice::category()agree with the routing. An error frame that fails proto decode falls back to the same code-0 path and now logs a warning naming the byte length (#766). -
The frame reader now validates the 4-byte length prefix before using it, instead of trusting it to size an allocation and a
read_exact. Nothing bounded it: four garbage bytes were read as a body length of up to 4 GiB, which allocated that much and then blocked until that many bytes arrived — consuming and destroying every real message in between, then yielding one bogus frame with the stream left permanently mis-framed. Because the framing is positional, nothing re-synchronizes it, and a mis-framed protobuf payload still decodes without error (prost skips unrecognized field numbers), so the visible symptom was plausible-looking wrong field values that never recovered. Out-of-range prefixes now raiseError::InvalidFrameand drive a reconnect. The cap matches the official client'sConstants.MaxMsgSize(EReader.readSingleMessage, which raisesBAD_LENGTH). -
A frame that no channel claims is now reported instead of dropped. An unrecognized message id raises a
UNKNOWN_MESSAGE_TYPE_CODEnotice and a warning; a known type with no current subscriber stays atinfo, as before, since that is ordinary steady state. Previously the blocking client logged every such frame atinfowithout distinguishing the two, and the async client logged nothing at all — so a desynchronized stream was indistinguishable from an idle one, which is why the incident that prompted this work surfaced data-farm notices and no decode error. -
A frame whose body is too short to hold the 4-byte message id no longer panics the dispatcher.
parse_raw_messageindexed the first four bytes unguarded, so a body of 0–3 bytes aborted withindex out of bounds— killing the dispatcher thread on the blocking client and the dispatcher task on the async one. It returnsError::InvalidFramenow. -
IBAPI_RECORDING_DIRpointing at an unwritable path no longer panics duringClient::connect. Creating the recording directory wasunwraped, so a typo or a read-only mount aborted the process rather than the recording. It now warns and disables recording, matchingIBAPI_RAW_CAPTURE_DIR— a diagnostic aid must not be the reason a connection fails. -
IBAPI_RECORDING_DIRrecords an unrecognized message id as itself rather than as-1. The recorder reconstructed the frame's id from the resolvedIncomingMessageskind, and every unrecognized id resolves toNotValid, whose discriminant is-1— so the capture of a framing desync, which is exactly the capture worth replaying, carried a fabricated id where the offending one should have been. Recognized ids are unaffected:IncomingMessages::frommaps a value to the variant with that discriminant, so the two agreed for every frame the client understands. -
IBAPI_RECORDING_DIRrecords the actual response payload again. Recording wrote a response's parsed text fields, and a protobuf frame has none — so since the protobuf-only transition everyNNNN-response.msgheld the bare message id and nothing else. Responses are now written as their wire frame (4-byte big-endian message id followed by the payload), matching whatrecord_requestalready wrote for outbound messages. Text-framed responses keep their pipe-delimited form (#748). -
historical_data(..).fetch()on the async client now retries a connection reset instead of surfacing it, and no longer retries a closed stream. It had the two cases backwards relative to its blocking twin: a routedError::ConnectionResetreturned to the caller on the first occurrence, while an empty stream was re-sent five times before failing asError::ConnectionReset. Both sides now shareretry_on_connection_reset— up to three retries on a reset,Error::UnexpectedEndOfStreamon a closed stream, on the first attempt. An error on the follow-onHistoricalDataEndframe is also propagated rather than dropped, on both sides (#744). -
OrderBuilder::analyze()(what-if orders, blocking and async) now returns the TWS rejection instead ofError::UnexpectedEndOfStream. A rejected what-if order arrives as a routed error, which the response read discarded — the blocking path viaif let Ok(..)inside the loop, the async path by ending itswhile let Some(Ok(..))loop — so the caller lost the reason (e.g. code 201,Order rejected - reason:...) and got a generic end-of-stream error. Rejection is a routine outcome for a what-if order, so this was the likeliest path to hit it (#735). -
Reconnection now survives the window where TWS accepts the TCP connection but its API handshake is not yet ready — common during an automated restart. A session-establishment failure (handshake,
startAPI, account info) used to abort the reconnect loop on the first occurrence; it now consumes one attempt and follows the same fibonacci backoff as a socket failure, on both the blocking and async clients. When every attempt fails,reconnectreturns the last attempt's error instead of a bareError::ConnectionFailed, so a permanent cause — say, an incompatible server version — is named rather than hidden behind a generic failure (#761). -
matching_symbols()on the blocking client now returns the TWS error instead of an empty list. A routed error arrives asSome(Err(_)), which theif let Some(Ok(_))read discarded, so a rejected pattern silently returnedOk(vec![])— indistinguishable from "no symbols matched". The async client already propagated it (#735). -
TickTypes::MarketDataTypenow reachesClient::market_datasubscriptions. The message type was missing from the request-id routing allow-list, so TWS's market-data-type notifications (real-time / frozen / delayed / delayed-frozen, sent on subscribe and whenever the feed switches) were routed to a shared channel nobody subscribes to and dropped. The decoder has produced the variant since #516; nothing could ever yield it (#730). -
Decimal-typed wire fields no longer fall back to
0when the value fails to parse; a malformed value now surfaces asError::Parseand fails the request or subscription instead of being silently swallowed. Covers order quantities, execution shares, positions, contract-detail sizes, bar volume/WAP, tick and market-depth sizes (#716). -
TWS "unset" sentinels (
2147483647,9223372036854775807,-9223372036854775808,1.7976931348623157E308) are now recognized on those same fields rather than onlyOrderState.suggested_sizeandOrderAllocation.*. They decode toNone, or to0.0on fields still typedf64, instead of leaking as a literal 2.1-billion size (#716).
3.3.0 - 2026-07-16
Client::config()(sync + async) to read the TWS/Gateway configuration (API settings, order precautions, smart-routing, lock-and-exit) added upstream for server version 219 (TWS 10.43); returns the newconfig::Configsnapshot type and is gated behindserver_versions::CONFIG.Client::update_config()(sync + async) to write partial edits to the TWS/Gateway configuration via a fluentUpdateConfigBuilder(.api()/.orders()/.lock_and_exit()/.message()/.messages()/.accept_warning()/.accept_warnings()/.reset_api_order_sequence()→.submit()), added upstream for server version 221 (TWS 10.44); returns the newconfig::UpdateConfigResponse(withconfig::ConfigWarning) and is gated behindserver_versions::UPDATE_CONFIG.Order.deactivate(bool) flagging a de-activated (inactive) order; since TWS 10.48reqOpenOrdersreturns de-activated orders, so this distinguishes them from active ones. Round-trips through the order proto in both directions; no server-version gate (#709).Order.hedge_max_size(Option<i32>) for the maximum size of a hedge order, added upstream for server version 223 (TWS 10.45); gated behindserver_versions::HEDGE_MAX_SIZEwhen placing orders (#706).- Server-version support advertised through 225 (
ODD_LOT_BID_ASK_QUOTES), with newserver_versionsconstantsFRACTIONAL_LAST_SIZE(222),HEDGE_MAX_SIZE(223),USE_PRECISION_FROM_SEC_DEF(224), andODD_LOT_BID_ASK_QUOTES(225) (#706). generic_tick::ODD_LOT("787") request-side generic tick to subscribe odd-lot bid/ask quotes (server 225 / TWS 10.46) viareqMktData(#706).BarSize::Min4("4 mins") historical bar size, added upstream in TWS API v10.44 (#704).- Odd-lot bid/ask
TickTypevariants (OddLotBid,OddLotAsk,OddLotBidSize,OddLotAskSize,OddLotBidExch,OddLotAskExch, ids 105–110) so odd-lot market-data ticks (server 225 / TWS 10.46) decode to typed variants instead ofUnknown(#703).
- Fundamental data support: the
fundamentalmodule (FundamentalData,FundamentalReportType) andClient::fundamental_data. IBKR removed the fundamental-data feature (reqFundamentalData/cancelFundamentalData) from the TWS API in 10.47 with no replacement. TheTickType::FundamentalRatiosvariant (tick id 47) is also removed; id 47 now decodes toTickType::Unknown.
3.2.1 - 2026-07-13
- Request-less TWS errors on shared one-shot requests (e.g. read-only-mode 321, unknown market rule 322) now fail the awaiting call fast with the real error instead of hanging; streaming shared subscriptions are unaffected (#698).
3.2.0 - 2026-07-06
AggTradesvariant (wire valueAGGTRADES) on the historical and realtimeWhatToShowenums, required to request trade bars for crypto contracts (TWS rejectsTRADESwith error 10299) (#693).ConnectivityStatusenum withConnectivityStatus::from_code()andNotice::connectivity_status()to expose data-farm connectivity sub-states (Ok / Broken / Inactive / Connecting) within the 2100–2169 warning band (#684).Error::is_connection_lost()predicate so reconnect loops can branch on connection loss without matching internal error variants (#690).Subscription::collect_for(timeout)/collect_until(timeout, predicate)terminals andMarketDataBuilder::snapshot_once(timeout)to collect a one-shot snapshot into aVecwithout hand-writing a collect-with-timeout loop (#686).
- Connectivity restored notices no longer log at
error: code 1102 ("data maintained") now logs atinfoand code 1101 ("data lost — resubscribe") atwarn, so routine overnight reconnects stop tripping error-level alerting (#695). - Async snapshot market-data subscriptions no longer send a redundant cancel after the snapshot completes, matching the sync side (#686).
3.1.0 - 2026-06-19
DATA_ADVISORY_CODES,Notice::is_data_advisory(), and theNoticeCategory::DataAdvisoryvariant for delayed-market-data advisory codes (#680).
- Benign data-farm connectivity notices (codes 2104/2106/2158, "…connection is OK") now log at
infoinstead ofwarn, removing warn-level spam on long-running sessions (#678).
- Delayed-data advisories (codes 10089/10167) no longer terminate a market-data subscription before its data arrives (#677).
TickSubscriptionnow carries theSubscriptionItemenvelope (#675).
Versions up to and including 3.0.1 predate this changelog; see the GitHub Releases page for their notes.