ibapi 5.2.0

An unofficial Rust client for the Interactive Brokers TWS API. Async (Tokio) and blocking clients for TWS and IB Gateway.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# Migration Guide: 3.x to 4.x

This guide walks through the changes required to upgrade from `ibapi` 3.x to the 4.x line. Most of it describes the 4.0 release, but the 4.x line has continued to make breaking changes in minor releases, and each one is documented here rather than in a guide of its own — see [Which release introduced what](#which-release-introduced-what) for the mapping. The 4.x line ended with 4.2.0; for 4.x → 5.0, see [`migration-5.0.md`](migration-5.0.md). For 2.x → 3.x, see [`migration-3.0.md`](migration-3.0.md); for 1.x → 2.x, see [`MIGRATION.md`](../MIGRATION.md).

## Highlights

- Market-data sizes that IBKR models as decimals are now `Option<f64>` instead of `i32` / `f64` — fractional sizes (crypto, fractional shares) no longer truncate to `0`, and "TWS sent no value" is distinguishable from a real zero.
- `Liquidity` preserves unrecognized execution liquidity codes as `Liquidity::Unknown(i32)` instead of collapsing them to `Liquidity::None`. Exhaustive matches need a new arm.
- `Client::wsh_event_data_by_contract` / `wsh_event_data_by_filter` return builders instead of taking positional `Option` arguments.
- `Client::check_server_version` is crate-private; compare against `Client::server_version()` directly.
- `Notice` gains a `request_id` field — the originating request or order id, `None` for request-less notices.
- `MarketDataBuilder` moves from `market_data::builder` to `market_data::realtime`, beside its sibling builders.
- Transport hardening (behavioral, not API-breaking): frame-length validation with automatic reconnect, wrong-wire-format detection, unified retry-on-reset across one-shot requests, a configurable reconnect attempt limit (`ClientBuilder::max_reconnect_attempts` / `reconnect_forever`), observable consumer lag (`SUBSCRIPTION_LAG_CODE` notices, `ClientBuilder::channel_capacity`, sync backlog watermarks), and byte-level stream capture via `IBAPI_RAW_CAPTURE_DIR`.

## Which release introduced what

Section numbers are stable.

| Release | Sections |
|---|---|
| 4.0.0 | [§1](#1-market-data-sizes-are-optionf64), [§2](#2-liquidity-gains-unknowni32), [§3](#3-wsh-event-data-goes-through-builders), [§4](#4-clientcheck_server_version-is-crate-private), [§5](#5-notice-gains-request_id), [§7](#7-marketdatabuilder-moves-to-market_datarealtime), [§8](#8-the-realtimesyncmarket_data-free-function-is-crate-private), [§9](#9-orderstatuskind-gains-unknownstring), [§10](#10-option_chain-goes-through-a-builder) |
| 4.1.0 | [§6](#6-data_advisory_codes-is-a-i32-slice), [§11](#11-orderupdate-gains-orderbound) |
| 4.2.0 | [§12](#12-the-async-subscriptionnewreceiver-constructor-is-removed), [§13](#13-one-timeinforce-ordersbuildertimeinforce-is-removed-and-the-variants-are-spelled-till), [§14](#14-order-enums-parse-through-fromstr-and-preserve-unrecognized-wire-values), [§15](#15-orderbuilder-covers-the-integer-coded-order-enums-and-auctionstrategy-is-removed) |

## Breaking changes

### 1. Market-data sizes are `Option<f64>`

IBKR models market-data sizes as decimals and ships them as strings on the wire. Historical tick and histogram sizes were typed `i32`, so a fractional wire value such as `"0.5"` failed integer parsing and silently decoded as `0` — real data loss on crypto and fractional-share feeds. Those fields, plus the `ContractDetails` size rules, are now `Option<f64>`.

| Type | Field | 3.x | 4.0 |
|---|---|---|---|
| `TickMidpoint` | `size` | `i32` | `Option<f64>` |
| `TickLast` | `size` | `i32` | `Option<f64>` |
| `TickBidAsk` | `size_bid`, `size_ask` | `i32` | `Option<f64>` |
| `HistogramEntry` | `size` | `i32` | `Option<f64>` |
| `ContractDetails` | `min_size`, `size_increment`, `suggested_size_increment` | `f64` | `Option<f64>` |

`None` means TWS sent no value — the field was absent, empty, or carried one of TWS's "unset" sentinels (`2147483647`, `9223372036854775807`, `-9223372036854775808`, `1.7976931348623157E308`). `Some(0.0)` is a real zero. A malformed size is no longer swallowed: it surfaces as `Error::Parse` and fails the request or subscription.

**The `Ord` gotcha.** `f64` does not implement `Ord`, so `max_by_key` / `min_by_key` on a size no longer compiles. This is the most likely break in existing code:

```rust,ignore
// 3.x — compiled against i32
let mode = histogram.iter().max_by_key(|e| e.size);

// 4.0 — pick out the present sizes, then compare with total_cmp
let mode = histogram
    .iter()
    .filter_map(|e| e.size.map(|s| (e, s)))
    .max_by(|a, b| a.1.total_cmp(&b.1));
```

**Accumulating.** Treat "unset" as contributing nothing. Use `unwrap_or(0.0)` when you are folding one value at a time into a running total, and `filter_map` when you are reducing a collection — the latter also matters for `min`/`max`, where a substituted `0.0` would skew the result:

```rust,ignore
// 3.x
let mut total_volume = 0;
total_volume += tick.size;

// 4.0
let mut total_volume = 0.0;
total_volume += tick.size.unwrap_or(0.0);

// Or, over a collection:
let total: f64 = histogram.iter().filter_map(|e| e.size).sum();
```

**Displaying.** `Option<f64>` is not `Display`, so `{}` no longer works. Prefer surfacing the missing case over hiding it behind a zero:

```rust,ignore
// 3.x
println!("Size: {}", tick.size);

// 4.0
fn fmt_size(size: Option<f64>) -> String {
    size.map_or_else(|| "n/a".to_string(), |s| format!("{s:.0}"))
}
println!("Size: {}", fmt_size(tick.size));
```

**Serialized shape changes too.** These types derive `Serialize`/`Deserialize` (and `utoipa::ToSchema` under the `utoipa` feature), so the JSON changes in two ways beyond the compile errors: a present size now serializes as `100.0` rather than `100`, and an absent one as `null` rather than `0`. Under `utoipa` the generated schema goes from `integer` to a nullable `number`. If you publish an OpenAPI contract or have strict JSON consumers downstream, that is a breaking change with no compile-time signal.

`Option<f64>` is an intermediate step. A dedicated decimal quantity type is planned, so sizes will eventually round-trip the wire's decimal representation exactly rather than through binary floating point.

### 2. `Liquidity` gains `Unknown(i32)`

`Execution.last_liquidity` used to collapse any liquidity code outside the documented 0–3 range to `Liquidity::None` — indistinguishable from a genuine "no liquidity information". Unrecognized codes are now preserved:

```rust,ignore
// 3.x — exhaustive match compiled
match execution.last_liquidity {
    Liquidity::None => {}
    Liquidity::AddedLiquidity => {}
    Liquidity::RemovedLiquidity => {}
    Liquidity::LiquidityRoutedOut => {}
}

// 4.0 — add an arm for the new variant
match execution.last_liquidity {
    Liquidity::None => {}
    Liquidity::AddedLiquidity => {}
    Liquidity::RemovedLiquidity => {}
    Liquidity::LiquidityRoutedOut => {}
    Liquidity::Unknown(code) => log::warn!("unrecognized liquidity code {code}"),
}
```

The documented codes 0–3 decode exactly as before. No official client or `Execution.proto` defines a code outside that range today, so `Unknown` is forward compatibility — a code IBKR adds later surfaces as `Unknown(code)` you can log, store, or reject, instead of silently masquerading as `None`. `Liquidity` is deliberately exhaustive (no `#[non_exhaustive]`), so the compiler points at every match that needs the new arm.

### 3. WSH event data goes through builders

`wsh_event_data_by_contract` took one required argument and four `Option`s; `wsh_event_data_by_filter` took one and two. Every call site in this repository — both examples and both integration tests — passed `None` for all of them. They now return builders, with one setter per optional value:

```rust,ignore
// 3.x
let events = client.wsh_event_data_by_contract(contract_id, None, None, None, None)?;
let events = client.wsh_event_data_by_contract(
    contract_id,
    Some(date!(2024 - 01 - 01)),
    Some(date!(2024 - 03 - 31)),
    Some(50),
    Some(auto_fill),
)?;
let subscription = client.wsh_event_data_by_filter(filter, None, None)?;

// 4.0
let events = client.wsh_event_data_by_contract(contract_id).fetch()?;
let events = client
    .wsh_event_data_by_contract(contract_id)
    .starting(date!(2024 - 01 - 01))
    .ending(date!(2024 - 03 - 31))
    .limit(50)
    .auto_fill(auto_fill)
    .fetch()?;
let subscription = client.wsh_event_data_by_filter(filter).subscribe()?;
```

The terminals differ because the requests do: `.fetch()` returns a single `WshEventData`, `.subscribe()` returns a `Subscription<WshEventData>`. Async is identical with `.await` on the terminal.

Each setter carries its own server-version requirement — `.starting()` / `.ending()` / `.limit()` need `WSH_EVENT_DATA_FILTERS_DATE`, `.auto_fill()` needs `WSH_EVENT_DATA_FILTERS` — so a bare request still works against a gateway that supports none of them. That was true before too; the builder just makes it visible which argument costs which version.

### 4. `Client::check_server_version` is crate-private

The async `Client` exposed `check_server_version(required_version, feature)` as `pub` while the blocking `Client` kept the same method `pub(crate)`. That was drift, not design — the method is the internal guard each version-gated API calls before encoding a request, and nothing in `examples/` or the integration crates ever called it. Both are now `pub(crate)`.

If you were calling it to branch on server support, compare against `Client::server_version()` directly:

```rust,ignore
// 3.x — async only
client.check_server_version(server_versions::SIZE_RULES, "size rules")?;

// 4.0 — the constants are public; the guard is not
if client.server_version() < server_versions::SIZE_RULES {
    // fall back
}
```

The version-gated methods still perform this check themselves and return `Error::ServerVersion` when the gateway is too old, so an explicit pre-check is only needed when you want to branch instead of erroring.

### 5. `Notice` gains `request_id`

`Notice` now carries the originating request or order id — `None` for request-less notices. `Notice` is not `#[non_exhaustive]`, so struct literals must add the field:

```rust,ignore
// 3.x
let notice = Notice {
    code: 2104,
    message: "Market data farm connection is OK".into(),
    error_time: None,
    advanced_order_reject_json: String::new(),
};

// 4.0
let notice = Notice {
    request_id: None,
    code: 2104,
    message: "Market data farm connection is OK".into(),
    error_time: None,
    advanced_order_reject_json: String::new(),
};
```

Consumers are unaffected at compile time but gain information: a notice delivered to a subscription or the order-update stream now names the request or order it belongs to. The serde shape is backward compatible — `request_id` is `#[serde(default, skip_serializing_if = "Option::is_none")]`, so 3.x JSON still deserializes and the field only appears in output when present.

### 6. `DATA_ADVISORY_CODES` is a `&[i32]` slice

The advisory list grew from `[10089, 10167]` in 3.x to `[2188, 10089, 10090, 10167]` in 4.0.0 and `[2188, 10089, 10090, 10091, 10167]` after — see the notice-classification changes under [Behavioral changes](#behavioral-changes). Each addition changed the array type, so the constant is now a slice and future additions are value changes only. This is breaking only for code binding the const with an explicit type:

```rust,ignore
// 3.x
let advisories: [i32; 2] = ibapi::DATA_ADVISORY_CODES;

// 4.x — let the type follow the const
let advisories = ibapi::DATA_ADVISORY_CODES;
```

Iteration yields `&i32`: write `for &code in ibapi::DATA_ADVISORY_CODES` where 3.x wrote `for code in ...`.

### 7. `MarketDataBuilder` moves to `market_data::realtime`

`MarketDataBuilder` lived at `market_data::builder` — a module whose only content was that one type — while its three siblings (`RealtimeBarsBuilder`, `MarketDepthBuilder`, `TickByTickBuilder`) lived under `market_data::realtime`. It now lives beside them, and the `market_data::builder` module is gone:

```rust,ignore
// 3.x
use ibapi::market_data::builder::MarketDataBuilder;

// 4.0
use ibapi::market_data::realtime::MarketDataBuilder;
```

Only code that names the type breaks — imports, or a function signature taking/returning the builder. The usual form, `client.market_data(&contract).subscribe()`, never names it and is unaffected. It is also in `ibapi::prelude` now, along with the other three realtime builders.

`MarketDataBuilder::new` is `pub(crate)` in the same move — the sibling builders never exposed a public constructor, and `client.market_data(&contract)` is the supported way to obtain one.

### 8. The `realtime::sync::market_data` free function is crate-private

The low-level request function under the market-data builder was `pub` where its siblings (`realtime_bars`, `market_depth`, `tick_by_tick`) were already `pub(crate)` — reachable as `market_data::realtime::sync::market_data(...)`, and in sync-only builds also as `market_data::realtime::market_data(...)`. It is crate-private now. The builder takes the same inputs:

```rust,ignore
// 3.x (sync-only build)
let subscription = ibapi::market_data::realtime::market_data(&client, &contract, &["233"], false, false)?;

// 4.0
let subscription = client.market_data(&contract).generic_ticks(&["233"]).subscribe()?;
```

The `snapshot` and `regulatory_snapshot` booleans map to the `.snapshot()` / `.regulatory_snapshot()` setters. Nothing in `examples/` or the integration crates called the free function, and the async side never had a public one.

### 9. `OrderStatusKind` gains `Unknown(String)`

An `OrderStatus` frame whose status string this crate did not model used to fail decoding with `Error::Parse` — which terminated the subscription delivering it, including the long-lived `order_update_stream()`. A status IBKR ships in the future would have taken down the one stream a trading application cannot afford to lose. Unrecognized statuses are now preserved (the official IB client does the same — its `OrderStatus` enum has an `Unknown` fallback):

```rust,ignore
// 3.x — exhaustive match compiled
match status.status {
    OrderStatusKind::Filled => {}
    OrderStatusKind::Cancelled => {}
    // ... the other seven
}

// 4.0 — add an arm for the new variant
match status.status {
    OrderStatusKind::Filled => {}
    OrderStatusKind::Cancelled => {}
    // ... the other seven
    OrderStatusKind::Unknown(raw) => log::warn!("unrecognized order status {raw:?}"),
}
```

What changes for compiling code:

- **`Copy` is gone** (the variant carries a `String`); `OrderStatusKind` is still `Clone`. Code that relied on implicit copies needs `.clone()` or a borrow.
- **`as_str()` returns `&str`**, not `&'static str` — for `Unknown` it borrows the raw value.
- **`is_active()` / `is_terminal()` take `&self`** and both return `false` for `Unknown`, as they do for `ApiPending` — don't assume `!is_active() ⇒ is_terminal()`.
- **Exhaustive matches need the new arm.** Like `Liquidity`, the enum is deliberately exhaustive (no `#[non_exhaustive]`), so the compiler points at every site.

The nine known statuses parse exactly as before; matching stays exact and case-sensitive, so a case-variant of a known status also lands in `Unknown` rather than being coerced. Empty input is still `Error::Parse` — absence of a value remains an error. Serialization is unchanged in shape: the enum still serializes as the plain status string in both directions (`Unknown("X")` round-trips as `"X"`), and the `utoipa` schema is a plain `string` to match (hand-written, since the derive would describe the tagged `{"Unknown":"X"}` shape the serde impls avoid).

### 10. `option_chain` goes through a builder

`option_chain` took four positional arguments. `exchange` documented `""` as "all exchanges" and was the one with a default; it is now a setter, and the three required arguments stay positional:

```rust,ignore
// 3.x
let subscription = client.option_chain("AAPL", "", SecurityType::Stock, 265598)?;

// 4.0
let subscription = client.option_chain("AAPL", SecurityType::Stock, 265598).subscribe()?;
```

Async is identical with `.await` on `.subscribe()`.

Two things the old signature let callers get wrong, both verified against a live paper gateway while making this change:

- **`exchange` is TWS's `futFopExchange`** — a filter for *futures* options. For a stock underlying, leave it unset: TWS returns one `OptionChain` per listing exchange (twenty for AAPL). Naming any exchange, `"SMART"` included, returns an empty chain. Only call `.exchange("CME")` for a futures underlying.
- **`contract_id` is required.** `0` is rejected with code 321 "Invalid contract id". The crate's own async example passed `0` (and `"SMART"`), so it had been receiving nothing.

An unset exchange is now absent from the request rather than sent as `""`; TWS treats the two identically.

### 11. `OrderUpdate` gains `OrderBound`

TWS sends an `OrderBound` notification when it binds a permanent order ID to an API client ID and an order ID in that client's namespace. The official client documents it as the response to an order-binding request: client ID 0 can take over orders submitted manually in TWS via `reqAutoOpenOrders`, and each order it takes over is announced this way. The wire type existed in 3.x but no transport delivered it, so it was silently dropped. `order_update_stream()` now yields it as `OrderUpdate::OrderBound(OrderBound { perm_id, client_id, order_id })`:

```rust,ignore
// 3.x — exhaustive match compiled
match update? {
    OrderUpdate::OrderStatus(status) => {}
    OrderUpdate::OpenOrder(order) => {}
    OrderUpdate::ExecutionData(exec) => {}
    OrderUpdate::CommissionReport(report) => {}
}

// 4.0 — add an arm for the new variant
match update? {
    OrderUpdate::OrderStatus(status) => {}
    OrderUpdate::OpenOrder(order) => {}
    OrderUpdate::ExecutionData(exec) => {}
    OrderUpdate::CommissionReport(report) => {}
    OrderUpdate::OrderBound(binding) => println!("perm {} is client {} order {}", binding.perm_id, binding.client_id, binding.order_id),
}
```

What changes for compiling code:

- **Exhaustive matches need the new arm.** Like `Liquidity` and `OrderStatusKind`, the enum stays exhaustive (no `#[non_exhaustive]`), so the compiler points at every site. Matches with a `_` arm compile unchanged.
- **Bindings arrive only on `order_update_stream()`.** They do not reach the per-order `place_order` subscription, even for this client's own orders: API order IDs are scoped to a client ID, and the transport does not filter on it, so a binding for another client's order 42 must not land on a local subscription for order 42. This matches the official client, which delivers `orderBound` only to the global wrapper callback. Consume bindings from the update stream and key on `perm_id` when you need an account-wide identity.
- **A binding grants nothing.** Knowing another client's `(client_id, order_id)` does not let this client modify or cancel that order.

### 12. The async `Subscription::new(receiver)` constructor is removed

The async `Subscription` had a second constructor, `Subscription::new(rx)`, that wrapped a `tokio::sync::mpsc::UnboundedReceiver<Result<T, Error>>` of items already decoded by the caller. It was a leftover from before the async client decoded through `StreamDecoder`: a subscription built that way had no message bus, could not be cancelled, and panicked on `clone()`. Nothing in `examples/` or the integration crates called it. It is gone, along with the type-erased decoder path it needed inside `Subscription`. The blocking `Subscription` never had it.

What changes for compiling code:

- `Subscription<T>` now requires `T` to be one of the crate's stream item types (`TickTypes`, `Bar`, `OrderUpdate`, ...), as the blocking `Subscription` always has. `Subscription<i32>` or `Subscription<String>` no longer names a type.
- Generic code over the async subscription, such as `async fn drain<T: Send + 'static>(sub: &mut Subscription<T>)`, no longer compiles: the bound the type now needs is a crate-private trait, so downstream generics cannot spell it. Write such helpers against a concrete item type, or against `impl Stream<Item = Result<SubscriptionItem<T>, Error>> + Unpin`. The blocking `Subscription` has always been in this position.
- `Subscription::clone()` has no panicking path; every async subscription clones.

If you were using `Subscription::new(rx)` to give your own channel the `Stream` interface, wrap the receiver directly. `UnboundedReceiverStream` lives in the `tokio-stream` crate, which `ibapi` does not re-export, so add it to your own `Cargo.toml`:

```rust,ignore
// before — async only
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut stream = ibapi::prelude::Subscription::new(rx);

// after — tokio-stream = "0.1"
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
let mut stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
```

Two differences to carry across, neither of which the compiler will point at:

- **Item shape.** The removed constructor yielded `SubscriptionItem::Data(item)`; the wrapper yields `item` as sent. `SubscriptionItemStreamExt::filter_data` does not apply to it, because there are no `Notice` items to filter.
- **Errors are no longer terminal.** `Subscription::new(rx)` treated the first `Err` as the end of the stream: it latched an internal flag and every later poll returned `None` without reading the channel again, so items queued behind an error were never yielded. `UnboundedReceiverStream` has no such rule — an `Err` is an ordinary item and the stream keeps going. If you relied on the old behavior, restore it explicitly, for example with `.take_while(|item| futures::future::ready(item.is_ok()))` plus one final yield of the error, or by having the producer close the channel when it sends one.

Subscriptions returned by `Client` methods are unaffected.

### 13. One `TimeInForce`: `orders::builder::TimeInForce` is removed and the variants are spelled "till"

3.x exported two `TimeInForce` enums naming the same wire values with different identifiers: `orders::TimeInForce` on `Order.tif` (spelled `GoodTilCanceled`), and `orders::builder::TimeInForce` taken by `OrderBuilder::time_in_force` (spelled `GoodTillCancel`). The builder converted between them by formatting to the wire string and re-parsing, and `orders::TimeInForce` had no `GTX` variant, so `builder::TimeInForce::GoodTillCrossing` was silently submitted as `DAY`.

In 4.0 the builder enum is gone and `OrderBuilder::time_in_force` takes `orders::TimeInForce`. Its variants now use "till", matching IB's own `goodTillDate`, the `Order.good_till_date` field and the builder setters, and it gains `GoodTillCrossing` (`GTX`). TWS wire strings are unchanged.

```rust,ignore
// 3.x
use ibapi::orders::builder::TimeInForce;
client.order(&contract).buy(100).limit(50.0).time_in_force(TimeInForce::GoodTillCancel).submit()?;
let order = Order { tif: ibapi::orders::TimeInForce::GoodTilCanceled, ..Default::default() };

// 4.0
use ibapi::orders::TimeInForce;
client.order(&contract).buy(100).limit(50.0).time_in_force(TimeInForce::GoodTillCanceled).submit()?;
let order = Order { tif: TimeInForce::GoodTillCanceled, ..Default::default() };
```

Rename the variants from either 3.x enum:

| `orders::builder::TimeInForce` (3.x) | `orders::TimeInForce` (3.x) | `orders::TimeInForce` (4.0) |
|---|---|---|
| `Day` | `Day` | `Day` |
| `GoodTillCancel` | `GoodTilCanceled` | `GoodTillCanceled` |
| `ImmediateOrCancel` | `ImmediateOrCancel` | `ImmediateOrCancel` |
| `GoodTillDate { date }` | `GoodTilDate` | `GoodTillDate`, with the date passed to `.good_till_date(date)` or `.good_till_time(date)` |
| `FillOrKill` | `FillOrKill` | `FillOrKill` |
| `GoodTillCrossing` | (none; parsed as `Day`) | `GoodTillCrossing` (new) |
| `DayTillCanceled` | `DayTilCanceled` | `DayTillCanceled` |
| `Auction` | `Auction` | `Auction` |
| `OpeningAuction` | `OnOpen` | `OnOpen` |

The named setters (`.day_order()`, `.good_till_date(..)`, `.fill_or_kill()`, `.immediate_or_cancel()`) are unchanged; `.good_till_cancel()` is renamed `.good_till_canceled()`, matching the variant it sets and its `.day_till_canceled()` neighbour, and `.good_till_crossing()` and `.day_till_canceled()` are added. `.time_in_force(TimeInForce::GoodTillDate)` without a date still fails `build()` with `ValidationError::MissingRequiredField("good_till_date")`. The builder keeps the GTD date only in the field written by `.good_till_date(..)` / `.good_till_time(..)`; in 3.x a date carried in the `GoodTillDate { date }` payload took precedence over one set through those methods; now the last one set wins.

`orders::TimeInForce` is also an open enum now, like `OrderStatusKind` in [§9](#9-orderstatuskind-gains-unknownstring): a TIF string this crate does not model decodes as `Unknown(raw)` carrying the value TWS sent, instead of being coerced to `Day`. Sending an `Unknown` back — through `.time_in_force(..)` or `Order.tif` — puts the raw string on the wire unchanged, so an order read from TWS round-trips. Matching is exact and case-sensitive, so a case-variant such as `"gtc"` lands in `Unknown` rather than being coerced to the nearest known variant.

Exhaustive matches on `orders::TimeInForce` need both a `GoodTillCrossing` and an `Unknown(raw)` arm.

**If you serialize orders to JSON**, `TimeInForce` now serializes as the TWS wire string — `"GTC"`, not the variant name `"GoodTilCanceled"` — in both directions, and its `utoipa` schema is a plain string. That keeps the JSON stable against Rust-side renames and keeps `Unknown("GTZ")` as `"GTZ"` rather than the `{"Unknown":"GTZ"}` a derive would emit. Update stored documents and downstream consumers that read the old variant names.

### 14. Order enums parse through `FromStr` and preserve unrecognized wire values

If IB adds a value to one of these enums, 3.x silently mishandled it: `Action::from(&str)` panicked, `Rule80A::from(&str)` and `OrderOpenClose::from(&str)` returned `None` (so an inbound code the crate did not model read as "field absent"), and the `From<i32>` impls on `OcaType`, `OrderOrigin`, `ShortSaleSlot`, `VolatilityType`, `ReferencePriceType` and `TriggerMethod` each collapsed an unrecognized code to one of their known variants. All of them now follow the shape `Liquidity`, `OrderStatusKind` and (per [§13](#13-one-timeinforce-ordersbuildertimeinforce-is-removed-and-the-variants-are-spelled-till)) `TimeInForce` already use:

- **String enums parse through `FromStr<Err = Error>`.** The three inherent `from` methods are gone. `Action` is closed: an unrecognized side is `Error::Parse`. `Rule80A` and `OrderOpenClose` are open: an unrecognized non-empty value parses as `Unknown(String)` carrying the raw wire value, which `Display` writes back unchanged. Empty input is `Error::Parse` for all three.
- **Integer-coded enums gain `Unknown(i32)`.** `From<i32>` on the six enums above now returns `Unknown(code)` for a code outside the table, and `i32::from(Unknown(code))` gives the code back. The known codes convert exactly as before.
- **`decode_order` treats `action` as required.** An inbound order that omits it, or carries it empty, fails the decode with `Error::Parse` instead of reading as `Buy`; as with any decode error, any subscription that receives `OpenOrder` / `CompletedOrder` frames - `open_orders` and its `all_` / `auto_` variants, `completed_orders`, `order_update_stream`, `exercise_options`, a `place_order` subscription - yields the error and ends rather than skipping the frame. `rule_80_a` and `open_close` stay optional: absent or empty is `None`. `tif` is deliberately different: an absent or empty `tif` decodes as `Order::default().tif` (per [§13](#13-one-timeinforce-ordersbuildertimeinforce-is-removed-and-the-variants-are-spelled-till)), because the reference client omits an empty `Tif` rather than sending one, so absence is an unset field with a known default; a missing side has no safe default, and defaulting it to `Buy` is exactly the mishandling this change removes.
- **A frame missing a whole submessage is an error too.** An `OpenOrder` or `CompletedOrder` whose `contract`, `order` or `order_state` is absent — or an `ExecutionDetails` missing `contract` or `execution` — fails with `Error::Parse` rather than yielding a default-constructed one. The required-`action` rule above only bites when the `order` submessage is *present*; without this, an absent one still produced `Order::default()` — `action == Buy`. The reference client drops such a frame outright (`EDecoder.cs`'s `OpenOrderEventProtoBuf` returns before `eWrapper.openOrder(..)`); this crate has no skip channel, so it surfaces the error.

```rust,ignore
// 3.x
let action = Action::from("SELL");                       // panicked on anything else
let rule: Option<Rule80A> = Rule80A::from("A");          // unknowns became None
let oc: Option<OrderOpenClose> = OrderOpenClose::from("O");
let code = OcaType::CancelWithBlock as i32;

// 4.0
let action: Action = "SELL".parse()?;                    // Err(Error::Parse) on anything else
let rule: Rule80A = "A".parse()?;                        // unknowns are Unknown(raw); "" is an error
let oc: OrderOpenClose = "O".parse()?;                   // same
let code = i32::from(OcaType::CancelWithBlock);          // `as i32` no longer compiles
```

What changes for compiling code:

- **Exhaustive matches need the new arm** on `Rule80A`, `OrderOpenClose`, `OcaType`, `OrderOrigin`, `ShortSaleSlot`, `VolatilityType`, `ReferencePriceType` and `TriggerMethod`. Like `Liquidity`, they stay exhaustive (no `#[non_exhaustive]`), so the compiler points at every site. `Action` is unchanged in shape.
- **`variant as i32` no longer compiles** on the six integer-coded enums, because a payload variant removes the fieldless-enum cast. Use `i32::from(variant)` or `.into()`; `ToField` already does. For the same reason these enums (and `Liquidity`) carry neither `#[repr(i32)]` nor explicit discriminants: nothing reads them, so the hand-written `From<T> for i32` is the only source of truth and each variant's wire code is in its rustdoc.
- **`as_str()` is added** to `Action` (returns `&'static str`) and to `Rule80A` and `OrderOpenClose` (returns `&str`; for `Unknown` it borrows the raw value).
- **`Copy` is unaffected.** `Action` keeps it; the integer-coded enums keep it (`Unknown(i32)` is `Copy`); `Rule80A` and `OrderOpenClose` never had it.
- **`Display` and wire strings are unchanged** for every existing variant. `ToField` still writes the wire string or code.
- **Serde and `utoipa` are unchanged for existing variants.** These enums keep their derives (variant names, not wire strings), so `Unknown` serializes in the derive's externally tagged form (`{"Unknown":"Z"}`, `{"Unknown":9}`) and the generated schema gains a matching `Unknown` object branch — unlike `OrderStatusKind` and `TimeInForce`, whose serde form is the wire string.

### 15. `OrderBuilder` covers the integer-coded order enums, and `AuctionStrategy` is removed

Six public order enums sat on public `Order` fields that reach TWS, with no way to set them through the fluent builder — the only path was constructing `Order` by hand, which is the surface the builder replaces. `OcaType` was half-exposed: reachable, but as a bare `i32`.

| Field | 4.1 | 4.2 |
|---|---|---|
| `oca_type` | `.oca_group("MyOCA", 1)` | `.oca_group("MyOCA", OcaType::CancelWithBlock)` |
| `trigger_method` | hand-built `Order` | `.trigger_method(TriggerMethod::Last)` |
| `origin` | hand-built `Order` | `.origin(OrderOrigin::Firm)` |
| `short_sale_slot` | hand-built `Order` | `.short_sale_slot(ShortSaleSlot::ThirdParty)` |
| `designated_location` | hand-built `Order` | `.designated_location("ABC SECURITIES")` |
| `volatility_type` | hand-built `Order` | `.volatility_type(VolatilityType::Annual)` |
| `reference_price_type` | hand-built `Order` | `.reference_price_type(ReferencePriceType::NBBO)` |

Only `oca_group` is source-breaking. The bare code maps straight onto the variant — `0` → `OcaType::None`, `1` → `CancelWithBlock`, `2` → `ReduceWithBlock`, `3` → `ReduceWithoutBlock` — and anything else onto `OcaType::Unknown(code)`, so `.oca_group(g, n)` becomes `.oca_group(g, OcaType::from(n))` if you are forwarding a runtime integer:

```rust,ignore
// 4.1
let order = client.order(&contract).buy(100).limit(50.0).oca_group("MyOCA", 1).build()?;

// 4.2
use ibapi::orders::OcaType;
let order = client.order(&contract).buy(100).limit(50.0).oca_group("MyOCA", OcaType::CancelWithBlock).build()?;
```

`volatility_type` is the one with a builder field already: a private `Option<i32>` that `build()` read only when `volatility()` had also been called. Nothing ever wrote it — there was no setter — so it was always `None` and the guard never ran. The new setter is applied unconditionally, which is why the guard is gone rather than preserved.

**`AuctionStrategy` and `Order::auction_strategy` are gone.** The seventh integer-coded enum had nothing behind it. IBKR's `source/proto/Order.proto` declares no `auctionStrategy` field, and the reference client's `EClientUtils.createOrderProto` never sets one — the field exists only on the legacy text encoding this crate dropped at server version 213. So `Order::auction_strategy` was written by `auction_limit` and discarded by `encode_order`, and no decoder ever produced one: unreachable in both directions, the same call 3.0 made on `TickEFP`.

`orders::order_builder::auction_limit` loses its fourth parameter as a result. There is no replacement for setting the strategy from the API; TWS applies the account's configured BOX default.

```rust,ignore
// 4.1 — the strategy never reached TWS
let order = auction_limit(Action::Buy, 100.0, 50.0, AuctionStrategy::Improvement);

// 4.2
let order = auction_limit(Action::Buy, 100.0, 50.0);
```

`orders::builder::AuctionType` is removed in the same pass. It was public, had no caller anywhere in the crate, and its `to_strategy()` codes (`1` / `2` / `4`) matched no TWS auction field; its variants (`Opening` / `Closing` / `Volatility`) were not auction strategies. Nothing replaces it.

## Behavioral changes

No code changes required, but observable at runtime:

- **Wrong wire format is an error, not a skip.** A text-framed message reaching a proto-only decoder fails the subscription with the new `Error::UnexpectedWireFormat` instead of being silently dropped — at server version 213+ that framing means the gateway broke protocol, and previously the subscription just yielded nothing. Wrong-*message-type* frames are still skipped, which is what shared channels need.
- **One-shot requests narrow to the message type they asked for.** A foreign frame surfaces as `Error::UnexpectedResponse` naming both the expected and received type, instead of being fed to the wrong payload decoder — where overlapping protobuf field numbers usually produced a plausible struct full of wrong values.
- **Retry-on-reset is uniform.** `market_rule`, `family_codes`, `calculate_option_price`, `calculate_implied_volatility`, and `next_valid_order_id` now retry a connection reset up to three times like every other one-shot; `head_timestamp`, `histogram_data`, `market_depth_exchanges`, `historical_data(..).fetch()`, and `historical_schedules(..).fetch()` now retry *at most* three times instead of unboundedly (or, on the async side, not at all), and sync/async agree on what a closed stream returns.
- **Frames are validated.** A length prefix that cannot describe a TWS message (shorter than the message id, or over the official 16 MiB cap) raises `Error::InvalidFrame` and drives a reconnect instead of a multi-gigabyte allocation and a permanently mis-framed stream; a body too short for the message id no longer panics the dispatcher. A frame whose message id maps to no known type raises an `UNKNOWN_MESSAGE_TYPE_CODE` (`-5`) notice on `Client::notice_stream` — the observable form of a framing desynchronization. Both new `Error` variants arrive via `#[non_exhaustive]`, so they are not compile-breaking.
- **Notices reclassified.** Codes 2188 and 10090 are data advisories (TWS keeps delivering data after sending them, but the subscription used to be torn down); code-399 order messages whose text carries a `Warning:` line classify as warnings instead of order rejections; a notice with no `error_code` field (code 0) classifies as a warning instead of failing every in-flight shared one-shot. After 4.0.1, `WARNING_CODE_RANGE` covers the whole `21xx` band (`2100..=2199`), 317 (market depth RESET) is a data advisory, and `DATA_ADVISORY_CODES` resolves before the ranges in `category()` — see the CHANGELOG entries for #805 / #806.
- **The order-update stream delivers order-bound errors as notices.** Order-bound error frames arrive as `SubscriptionItem::Notice` (with `request_id`, code, and message) instead of raw frames that failed to decode as `OrderUpdate`. Note `filter_data()` / `iter_data()` drop notices — match on `SubscriptionItem::Notice` to 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.
- **Real errors instead of empty results.** `OrderBuilder::analyze()` returns the TWS rejection (e.g. code 201) instead of `Error::UnexpectedEndOfStream`; blocking `matching_symbols()` returns the TWS error instead of `Ok(vec![])`.
- **Malformed decimals fail instead of decoding as `0`.** Beyond the size fields whose types changed in [§1](#1-market-data-sizes-are-optionf64), every decimal-typed wire field — order quantities, execution shares, positions, bar volume/WAP, market-depth sizes — now surfaces a malformed value as `Error::Parse` instead of silently substituting `0`. TWS's "unset" sentinels are also recognized on all of these fields (previously only a few), decoding to `None` — or `0.0` where the field stays `f64` — instead of leaking as a literal 2.1-billion value.
- **`TickTypes::MarketDataType` actually arrives.** The variant existed but was never routed to `Client::market_data` subscriptions; TWS's market-data-type notifications (real-time / frozen / delayed / delayed-frozen, sent on subscribe and whenever the feed switches) now reach them, so a match that never saw this variant will start seeing it.
- **Falling behind is observable.** An async subscription whose consumer lags its broadcast channel (capacity 1024, now settable via `ClientBuilder::channel_capacity`) receives a non-terminal `SUBSCRIPTION_LAG_CODE` (`-6`) notice naming the number of evicted frames, plus a `warn!`, where it previously resumed with no signal at any level; reconcile as after a reconnect gap. The async notice stream (`Client::notice_stream`) rides the same bounded fan-out at the fixed default capacity, and a lagging consumer receives a `NOTICE_STREAM_LAG_CODE` (`-7`) notice in place of the evicted notices; because that stream carries the 1100/1101/1102 connection-status notices, resynchronize (re-baseline link state, re-establish subscriptions) rather than resume. A finished socket reconnect publishes a `TRANSPORT_RECONNECT_CODE` (`-8`) notice to the notice stream on both sides, since TWS never frames the reconnect and does not replay 1101/1102 on the new connection; treat it the same way. On the sync side, whose channels are unbounded and never drop, a stalled consumer now triggers `warn!` watermarks at every 10,000 queued messages on the subscription, shared, and order-update send paths (the sync notice fan-out has no watermark).
- **Reconnection is configurable and more resilient.** `ClientBuilder::max_reconnect_attempts` / `reconnect_forever` control the retry budget (default unchanged: 20 attempts, ~7.5 minutes). A session-establishment failure (handshake, `startAPI`, account info) consumes one attempt and backs off instead of aborting the loop — common during an automated TWS restart — and when every attempt fails, `reconnect` returns the last real error instead of a generic `Error::ConnectionFailed`.

## Quick migration checklist

1. Add a `Liquidity::Unknown(code)` arm to exhaustive matches on `Execution.last_liquidity` — the compiler finds them for you.
2. Unwrap market-data sizes: historical tick / histogram sizes and the `ContractDetails` size rules are `Option<f64>`. Watch for `max_by_key` on a size (`f64` isn't `Ord`), `+=` into an integer accumulator, and `{}` formatting — see [§1](#1-market-data-sizes-are-optionf64).
3. Rewrite `wsh_event_data_by_contract` / `wsh_event_data_by_filter` calls as builder chains ending in `.fetch()` / `.subscribe()`, and `option_chain(symbol, exchange, security_type, contract_id)` as `option_chain(symbol, security_type, contract_id).subscribe()` — drop the exchange unless the underlying is a future; see [§10](#10-option_chain-goes-through-a-builder).
4. Replace `client.check_server_version(..)` with a comparison against `client.server_version()`.
5. Add `request_id: None` (or a real id) to any `Notice` struct literals.
6. Re-point `use ibapi::market_data::builder::MarketDataBuilder` imports at `ibapi::market_data::realtime::MarketDataBuilder`.
7. Replace direct `realtime::sync::market_data(..)` calls with the `client.market_data(&contract)` builder — see [§8](#8-the-realtimesyncmarket_data-free-function-is-crate-private).
8. If you consume the order-update stream through `filter_data()` / `iter_data()`, decide whether you need a `SubscriptionItem::Notice` arm to observe order rejections.
9. Add an `OrderStatusKind::Unknown(raw)` arm to exhaustive matches on order statuses, and `.clone()` (or borrow) where code relied on the removed `Copy` — see [§9](#9-orderstatuskind-gains-unknownstring).
10. If you serialize market-data types to JSON, update downstream consumers: sizes are now `number | null` instead of `integer`, and notices may carry `request_id`.
11. Add an `OrderUpdate::OrderBound(binding)` arm to exhaustive matches on order updates, and read bindings from `order_update_stream()` — they never reach `place_order` subscriptions; see [§11](#11-orderupdate-gains-orderbound).
12. Replace any `Subscription::new(rx)` over your own channel with `tokio_stream::wrappers::UnboundedReceiverStream::new(rx)` (add `tokio-stream` to your dependencies), and give any generic helper over the async `Subscription<T>` a concrete item type — see [§12](#12-the-async-subscriptionnewreceiver-constructor-is-removed).
13. Use `ibapi::orders::TimeInForce` everywhere (`ibapi::orders::builder::TimeInForce` is gone) and spell the variants "till": `GoodTilCanceled` → `GoodTillCanceled`, `GoodTilDate` → `GoodTillDate`, `DayTilCanceled` → `DayTillCanceled`; from the builder enum, `GoodTillCancel` → `GoodTillCanceled`, `OpeningAuction` → `OnOpen`, `GoodTillDate { date }` → `GoodTillDate` plus `.good_till_date(date)`. Rename `.good_till_cancel()` calls to `.good_till_canceled()`, add `GoodTillCrossing` and `Unknown(raw)` arms to exhaustive matches, and re-read any stored JSON — the field is the wire string now — see [§13](#13-one-timeinforce-ordersbuildertimeinforce-is-removed-and-the-variants-are-spelled-till).
14. Replace `Action::from(s)`, `Rule80A::from(s)` and `OrderOpenClose::from(s)` with `s.parse()?`, and add an `Unknown(..)` arm to exhaustive matches on `Rule80A`, `OrderOpenClose`, `OcaType`, `OrderOrigin`, `ShortSaleSlot`, `VolatilityType`, `ReferencePriceType` and `TriggerMethod` — see [§14](#14-order-enums-parse-through-fromstr-and-preserve-unrecognized-wire-values).
15. Pass an `OcaType` to `OrderBuilder::oca_group` instead of an `i32`, drop the fourth argument from `auction_limit(..)` calls, remove any use of `orders::AuctionStrategy`, `Order::auction_strategy` or `orders::builder::AuctionType`, and replace hand-built `Order` structs that only existed to set `trigger_method` / `origin` / `short_sale_slot` / `designated_location` / `volatility_type` / `reference_price_type` with the new builder setters — see [§15](#15-orderbuilder-covers-the-integer-coded-order-enums-and-auctionstrategy-is-removed).
16. Re-run `cargo fmt`, `cargo clippy --all-targets --all-features -- -D warnings`, and your test suite for each feature flag you support.

## Need help?

- Examples: `examples/async` and `examples/sync`
- README: [Handling notifications](../README.md#handling-notifications)
- Issues: <https://github.com/wboayue/rust-ibapi/issues>