Skip to content

v5.4.0

Latest

Choose a tag to compare

@wboayue wboayue released this 27 Oct 18:35
· 9 commits to main since this release
ee6dc96

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 + MKT

Changed

  • ExecutionFilterSide, OptionRight and LegAction are no longer #[non_exhaustive], so a match on them can be exhaustive; an existing _ => arm now warns as unreachable. The async TickSubscription no longer requires T: 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_metadata and wsh_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. connect fails 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_fill no longer reports a negative remaining for an order found among completed orders. TWS sends a completed order's total_quantity as the quantity left unfilled (0 once filled); the completed_orders and Order::total_quantity docs now say so.
  • Order status, position, account-update and PnL frames with a missing or "unset" quantity return Error::Parse instead of decoding as 0. A missing remaining could make wait_for_fill report a partial fill as Filled. TWS sends an explicit "0" in normal frames, so these are unaffected.
  • order_status_stream and wait_for_fill return Error::OrderIdInRequestRange for order ids at or above 1,500,000,000 instead of timing out.
  • order_type(OrderType::MarketOnOpen), LimitOnOpen and AtAuction set the time in force those types imply, as the named setters do.

Docs corrected against live TWS (#1114, #1115)

  • calculate_option_price takes volatility as a fraction (0.2 = 20%). The docs said a percentage and passed 100.0, so following them priced options at 100 times the intended volatility.
  • An order with what_if_type but not what_if is rejected by TWS (error 321); the docs said it was placed live.

Also fixed

  • Async collect_to_end_within, next_batch, contract_details and account-summary snapshots return Error::Notice with SUBSCRIPTION_LAG_CODE when the consumer fell behind, instead of Ok with rows missing (#1090).
  • Contract::last_trade_date is populated on decoded option and future contracts; it was always None (#1087).
  • Strike::new rejects NaN and infinite prices (#1072).
  • news_article returns Error::Parse for an unknown article type instead of treating it as text (#1070).

See CHANGELOG.md for the full list.