Repository navigation
5.4 adds a client-wide timeout for one-shot requests, cancels abandoned one-shots at TWS instead of leaving them running, stops redundant cancels after rejections and reconnects, and fixes several decoding and docs bugs found in live testing. No breaking changes.
What's New
Client-wide one-shot timeout (#1095)
ClientBuilder::one_shot_timeout bounds every one-shot request (one that returns a value rather than a subscription) with Error::Timeout. Without it, requests answered on their own request id (historical_data(..).fetch(), head_timestamp, the option calculations, ...) still wait without a limit, and the rest keep their 30-second bound. On timeout, a request TWS can cancel is cancelled. Any one-shot timeout, the default included, now bounds the whole call, across resends after a connection reset.
let client = Client::builder()
.address("127.0.0.1:4002")
.client_id(100)
.one_shot_timeout(Duration::from_secs(60))
.connect()
.await?;
let contract = Contract::stock("AAPL").build();
match client.head_timestamp(&contract, WhatToShow::Trades, TradingHours::Regular).await {
Ok(first) => println!("first bar: {first}"),
Err(Error::Timeout) => println!("no answer within 60s; the request was cancelled at TWS"),
Err(e) => return Err(e),
}Accounts from the handshake (#1092)
Client::handshake_managed_accounts() (sync and async) returns the accounts TWS sent at connect, with no round trip. It isn't updated on reconnect; managed_accounts() still fetches the current list.
for account in client.handshake_managed_accounts() {
println!("{account}");
}Named order types: VOL and REL combos (#1082, #1076)
OrderBuilder::volatility_order sets the VOL order type with its volatility and VolatilityType. The figure is a percentage (40.0 = 40%), checked against a live TWS. relative_limit_combo and relative_market_combo set REL + LMT and REL + MKT. All three were previously reachable only through .order_type(..).
let option = Contract::call("SPY").strike(770.0).expires_on(2026, 10, 30).build();
// 40.0 = 40% annualized volatility; TWS derives the limit price.
client.order(&option).buy(1).volatility_order(40.0, VolatilityType::Annual).submit().await?;
let capped = Order::builder().buy(1).relative_limit_combo(2.5).non_guaranteed().build()?; // REL + LMT
let uncapped = Order::builder().buy(1).relative_market_combo().build()?; // REL + MKTChanged
ExecutionFilterSide,OptionRightandLegActionare no longer#[non_exhaustive], so amatchon them can be exhaustive; an existing_ =>arm now warns as unreachable. The asyncTickSubscriptionno longer requiresT: Send(#1081).
Bug Fixes
Cancels at TWS (#1094, #1107, #1097, #1098, #1105)
historical_data(..).fetch(),head_timestamp,histogram_data,historical_schedules(..).fetch(),calculate_option_price,calculate_implied_volatility,wsh_metadataandwsh_event_data_by_contract(..).fetch()cancel the request at TWS when dropped before the answer (a dropped future, a timeout, a local error), instead of leaving it running against pacing limits.- A subscription that TWS already closed with a rejection no longer sends a cancel when dropped, so TWS stops answering with "not found" errors (366, 300, 310, 365, 10185/10186). This covers one-shot historical fetches and, for live-verified codes, market data, realtime bars, tick-by-tick, depth, streaming historical bars, PnL and scanners. Other rejections still cancel, so nothing leaks at TWS.
- A subscription opened before a reconnect no longer sends a cancel for the dead session's request id.
Reconnect and connect (#1105, #1091)
Client::server_version()and the version checks that use it follow the version negotiated on reconnect, instead of the first connection's.- Connecting with a client id another connection holds returns
Error::ClientIdInUse { client_id }(TWS 326) instead of an I/O end-of-file error.connectfails fast; the automatic reconnect retries it, since TWS can briefly hold the old session's id.
Orders and positions (#1116, #1089, #1088, #1069, #1073)
wait_for_fillno longer reports a negativeremainingfor an order found among completed orders. TWS sends a completed order'stotal_quantityas the quantity left unfilled (0once filled); thecompleted_ordersandOrder::total_quantitydocs now say so.- Order status, position, account-update and PnL frames with a missing or "unset" quantity return
Error::Parseinstead of decoding as0. A missingremainingcould makewait_for_fillreport a partial fill asFilled. TWS sends an explicit"0"in normal frames, so these are unaffected. order_status_streamandwait_for_fillreturnError::OrderIdInRequestRangefor order ids at or above 1,500,000,000 instead of timing out.order_type(OrderType::MarketOnOpen),LimitOnOpenandAtAuctionset the time in force those types imply, as the named setters do.
Docs corrected against live TWS (#1114, #1115)
calculate_option_pricetakes volatility as a fraction (0.2= 20%). The docs said a percentage and passed100.0, so following them priced options at 100 times the intended volatility.- An order with
what_if_typebut notwhat_ifis rejected by TWS (error 321); the docs said it was placed live.
Also fixed
- Async
collect_to_end_within,next_batch,contract_detailsand account-summary snapshots returnError::NoticewithSUBSCRIPTION_LAG_CODEwhen the consumer fell behind, instead ofOkwith rows missing (#1090). Contract::last_trade_dateis populated on decoded option and future contracts; it was alwaysNone(#1087).Strike::newrejects NaN and infinite prices (#1072).news_articlereturnsError::Parsefor an unknown article type instead of treating it as text (#1070).
See CHANGELOG.md for the full list.