Skip to main content

rustrade_execution/client/
mod.rs

1//! Execution client implementations for various exchanges.
2//!
3//! # Connector Comparison
4//!
5//! | Connector | Reconnect | Dedup | Fill Recovery | Ended-order Recovery | Heartbeat | Cancel answers once |
6//! |-----------|-----------|-------|---------------|----------------------|-----------|---------------------|
7//! | [`binance`] | Auto (1s→30s backoff, 10 tries) | 10k LRU | REST after reconnect | REST after reconnect | 30s | cancelled |
8//! | [`alpaca`] | Auto (1s→30s backoff, 10 tries) | 2k LRU | REST after reconnect | REST after reconnect | 35s | accepted |
9//! | [`ibkr`] | ibapi-managed | 10k LRU, fills only | Executions request after reconnect | None | N/A | submitted |
10//! | [`hyperliquid`] | SDK-managed (every 1s, no limit) | 10k LRU, fills only | Venue snapshot on resubscribe | REST after reconnect | SDK-managed | cancelled |
11//!
12//! "Reconnect" is how the account stream comes back after its connection drops, and when it gives
13//! up: Binance and Alpaca end the stream once 10 reconnects in a row have failed (see
14//! [`binance`](binance#account-stream-reconnection) and
15//! [`AlpacaClient`](alpaca::AlpacaClient)'s `account_stream`).
16//!
17//! "Ended-order Recovery" is whether a reconnect reports how an order the client held as live
18//! ended while the stream was down, and only those clients implement [`OrderStatusClient`]. The
19//! last column is when [`ExecutionClient::cancel_order`] answers `Ok`. Only for "cancelled" has the
20//! order ended by then; see that method.
21//!
22//! # Resilience Philosophy
23//!
24//! **WebSocket-based connectors** (Binance, Alpaca) implement auto-reconnection with
25//! fill recovery and deduplication. After reconnect, they query REST APIs for missed
26//! fills and deduplicate against the LRU cache to prevent duplicate processing.
27//!
28//! **IBKR** uses TCP to local TWS/Gateway. `ibapi` reconnects that socket itself; the
29//! account stream stays open across it and recovers the missed fills from TWS's executions,
30//! deduplicating against the LRU cache. Replacing a client that is gone for good requires IB
31//! Gateway availability and client ID coordination — decisions that belong in the caller's
32//! wrapper. See [`ibkr`] module docs for caller responsibilities.
33//!
34//! **Hyperliquid** delegates reconnection to the official SDK's `with_reconnect()` mechanism, but
35//! deduplicates fills itself: the SDK resubscribes on reconnect and the venue opens a `userFills`
36//! subscription with a snapshot, so every reconnect redelivers fills already seen. That snapshot
37//! is the only fill recovery: it holds recent fills, how many Hyperliquid does not say, so callers
38//! needing older ones call [`ExecutionClient::fetch_trades`], whose results are not deduplicated
39//! against the stream. Once the snapshot has been sent on, a reconnect checks how the orders the
40//! client holds as live ended, like Binance's and Alpaca's.
41//!
42//! # Known Limitations
43//!
44//! Only Binance (Spot and Margin), Alpaca and Hyperliquid (perpetuals and spot) recover the **order
45//! lifecycle events** (cancelled, expired, rejected, a fill that completes an order) they missed
46//! while disconnected, and only for the orders the client holds as live; IBKR recovers fills only
47//! (#370). [`OrderStatusClient`] is the lookup that recovery is built on: given the orders a caller
48//! still holds as live, it says how each that has ended did end. Binance Spot, Binance Margin,
49//! Alpaca, both Hyperliquid clients and the mock client implement it.
50//!
51//! The engine closes part of that gap from the account snapshot each reconnect produces: an order a
52//! complete list no longer shows is retired (see [`ExecutionClient::account_snapshot`]). That covers
53//! the connectors whose snapshot declares its orders complete — Binance Spot and Margin,
54//! Hyperliquid, Alpaca, and the mock venue — but not IBKR (#371), and it cannot tell how an order
55//! ended. A caller that needs more reconciles against
56//! [`ExecutionClient::fetch_open_orders`] after a reconnect.
57
58use crate::{
59    UnindexedAccountEvent, UnindexedAccountSnapshot,
60    balance::AssetBalance,
61    borrow_capacity::BorrowCapacity,
62    error::UnindexedClientError,
63    order::{
64        Order, UnindexedInactiveOrder, UnindexedOrderKey,
65        bracket::{BracketOrderRequest, BracketOrderResult},
66        request::{
67            CancelReplaceAnswer, OnCancelFailure, OrderRequestCancel, OrderRequestOpen,
68            UnindexedOrderResponseCancel,
69        },
70        state::{Open, UnindexedOrderState},
71    },
72    shortability::Shortability,
73    trade::TradesRead,
74};
75use chrono::{DateTime, Utc};
76use futures::{Stream, stream::BoxStream};
77use rustrade_instrument::{
78    Side,
79    asset::name::AssetNameExchange,
80    exchange::ExchangeId,
81    instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
82};
83use std::future::Future;
84
85// Account-event deduplication over rustrade's own event type. Alpaca deduplicates too, but against
86// a raw `SmolStr` fill key rather than this cache. Always compiled: `AccountEventDeduper` is public.
87pub mod dedup;
88
89// What a client knows of its live orders, and the reconnect check of how those orders ended while
90// its account stream was down. Gated on the clients that use it.
91#[cfg(any(
92    feature = "alpaca",
93    feature = "binance",
94    feature = "hyperliquid",
95    feature = "ibkr"
96))]
97pub(crate) mod order_recovery;
98
99// An estimate of a venue's clock from the times it stamps on its events. Always compiled: the
100// execution manager's fill recovery uses it whatever the client.
101pub mod venue_clock;
102
103// The account streams' reconnect budget. Gated on the clients that use it.
104#[cfg(any(feature = "alpaca", feature = "binance"))]
105pub(crate) mod reconnect_budget;
106
107// A REST stub on the test's own runtime, for the reconnect tests on a paused clock.
108#[cfg(all(test, any(feature = "alpaca", feature = "binance")))]
109#[allow(clippy::unwrap_used)] // Test code: panics on bad input are acceptable
110pub(crate) mod test_http;
111
112// Alpaca ExecutionClient implementation (options, equities, crypto — single unified API)
113#[cfg(feature = "alpaca")]
114pub mod alpaca;
115
116// Binance ExecutionClient implementations: Spot (live) and Cross Margin (identity/config scaffolding)
117#[cfg(feature = "binance")]
118pub mod binance;
119
120// Hyperliquid perpetual futures and spot ExecutionClient implementations
121#[cfg(feature = "hyperliquid")]
122pub mod hyperliquid;
123
124// Interactive Brokers ExecutionClient implementation (equities, futures, options, forex)
125#[cfg(feature = "ibkr")]
126pub mod ibkr;
127
128pub mod mock;
129
130/// An instrument an [`ExecutionClient`] is about to be built to trade, as
131/// [`ExecutionClient::validate_config`] sees it.
132///
133/// `#[non_exhaustive]`, so further detail can be added without breaking an implementation that
134/// reads these fields; construct one with [`Self::new`].
135#[derive(Debug, Clone, Copy, Eq, PartialEq)]
136#[non_exhaustive]
137pub struct ClientInstrument<'a> {
138    /// The name the venue knows the instrument by, which is what a client's requests carry.
139    pub name_exchange: &'a InstrumentNameExchange,
140    /// The instrument's kind.
141    pub kind: InstrumentKindDiscriminant,
142}
143
144impl<'a> ClientInstrument<'a> {
145    /// The view of an instrument named `name_exchange`, of kind `kind`.
146    pub fn new(
147        name_exchange: &'a InstrumentNameExchange,
148        kind: InstrumentKindDiscriminant,
149    ) -> Self {
150        Self {
151            name_exchange,
152            kind,
153        }
154    }
155}
156
157// `+ Send` bounds on async method return types required for multi-threaded
158// Tokio runtime. This is a breaking change vs upstream — any `!Send` executor
159// implementation would fail to compile.
160/// A venue's account and order API, as the engine's `ExecutionManager` drives it.
161///
162/// # Wrapping a client
163///
164/// To add behaviour around a venue client without reimplementing it, wrap it in a generic
165/// decorator that implements `ExecutionClient` itself. Typical uses are choosing which fills on a
166/// shared account reach the engine, logging, and metrics.
167///
168/// - **Take the inner client's identity.** Set `const EXCHANGE: ExchangeId = C::EXCHANGE` and
169///   `SUPPORTED_KINDS = C::SUPPORTED_KINDS`. The `ExecutionManager` refuses a venue client whose
170///   `EXCHANGE` differs from its instrument map's exchange (only the `Mock` and `Simulated` ids
171///   are exempt), so one generic wrapper then serves every venue client.
172/// - **Forward every method, the provided ones included.** That is
173///   [`open_orders`](Self::open_orders), [`cancel_orders`](Self::cancel_orders),
174///   [`validate_config`](Self::validate_config) and
175///   [`process_account_events`](Self::process_account_events): left to the trait's defaults, they
176///   would bypass any batching, config check or processing the inner client has. Implement the
177///   extension traits the inner client implements as well, with impls bounded on it, forwarding
178///   their provided methods too:
179///   - [`OrderStatusClient`], which the `ExecutionManager` requires of every client it runs;
180///   - [`BracketOrderClient`]
181///   - [`ShortabilityClient`]
182///   - [`BorrowCapacityClient`]
183/// - **Withhold a fill from [`account_stream`](Self::account_stream) and
184///   [`fetch_trades`](Self::fetch_trades) alike, by the same rule.** One fill can reach the engine
185///   either way: live on the stream, or in a read made after a disconnect. A fill withheld from
186///   only one of them still reaches the engine once.
187///   - **Choose a key both paths carry.** [`Trade::client_order_id`] marks the orders a consumer
188///     placed, but not every venue reports it on `fetch_trades` (its rustdoc lists which do).
189///     Binance's is one that does not. A rule keyed on it has to decide what to do with a fill it
190///     cannot classify. Keeping it, as the example below does, delivers too much rather than too
191///     little: on such a venue every fill a `fetch_trades` read returns is kept, and only the
192///     stream is filtered. To withhold those fills as well, match them another way, such as by
193///     [`Trade::order_id`] against the orders the wrapper placed.
194///   - **Keep fills the venue forced** ([`TradeOrigin::is_forced`]). They moved the position,
195///     whoever placed the order.
196///   - **Leave `resume` alone.** Filtering a [`TradesRead`] keeps its `resume` as it was: it marks
197///     where the venue's read stopped, not which fills the wrapper kept.
198/// - **Keep state over the fills in [`process_account_events`](Self::process_account_events).**
199///   A wrapper that tracks what the engine holds, attributes fills, or acts on each one needs to
200///   see each fill once. `account_stream` and `fetch_trades` overlap on purpose, around a
201///   reconnect, so a fill can pass through both; `process_account_events` sees it once, after the
202///   engine's `ExecutionManager` drops the copy, with the recovered fills and the snapshot in
203///   order. The method takes `&self` and returns a `'static` stream, so hold that state in an
204///   `Arc` and move a clone into the stream. A rule that only withholds fills can stay on the two
205///   paths above.
206/// - **One inner client per engine.** A wrapper that owns one client counts as that one client.
207///   Two clients on one account whose account streams both reach one engine deliver every fill
208///   twice.
209/// - **Withhold here, not later.** The engine applies a fill that matches none of its orders too.
210///   In `OmsMode::Netting` it moves the netting position like any other fill, and the engine
211///   cannot tell it from a fill of its own. In `OmsMode::Hedging` an ordinary fill
212///   ([`TradeOrigin::Order`] or [`TradeOrigin::External`]) opens a position of its own, and a
213///   settlement fill is handled as [`InstrumentState::update_from_trade`] describes.
214///
215/// ```
216/// use chrono::{DateTime, Utc};
217/// use futures::{Stream, StreamExt, stream::BoxStream};
218/// use rustrade_execution::{
219///     AccountEventKind, UnindexedAccountEvent, UnindexedAccountSnapshot,
220///     balance::AssetBalance,
221///     client::{ClientInstrument, ExecutionClient, OrderStatusClient, VenueOrderStatus},
222///     error::UnindexedClientError,
223///     order::{
224///         Order, UnindexedInactiveOrder, UnindexedOrderKey,
225///         request::{
226///             CancelReplaceAnswer, OnCancelFailure, OrderRequestCancel, OrderRequestOpen,
227///             UnindexedOrderResponseCancel,
228///         },
229///         state::{Open, UnindexedOrderState},
230///     },
231///     trade::{Trade, TradesRead},
232/// };
233/// use rustrade_instrument::{
234///     asset::name::AssetNameExchange,
235///     exchange::ExchangeId,
236///     instrument::{kind::InstrumentKindDiscriminant, name::InstrumentNameExchange},
237/// };
238///
239/// /// Delivers the fills of orders whose client order id starts with `prefix`, fills whose
240/// /// client order id the venue did not report, and every fill the venue forced.
241/// ///
242/// /// Keeping a fill without a client order id is a choice: on a venue whose `fetch_trades`
243/// /// reports none, this filters the account stream only.
244/// #[derive(Debug, Clone)]
245/// pub struct OwnFills<C> {
246///     inner: C,
247///     prefix: String,
248/// }
249///
250/// #[derive(Debug, Clone)]
251/// pub struct OwnFillsConfig<Config> {
252///     pub inner: Config,
253///     pub prefix: String,
254/// }
255///
256/// fn keeps<AssetKey, InstrumentKey>(prefix: &str, trade: &Trade<AssetKey, InstrumentKey>) -> bool {
257///     trade.origin.is_forced()
258///         || trade
259///             .client_order_id
260///             .as_ref()
261///             .is_none_or(|cid| cid.0.starts_with(prefix))
262/// }
263///
264/// // `Sync` makes the wrapper's method futures `Send`, as the trait requires. `'static` lets the
265/// // filtered account stream be boxed.
266/// impl<C> ExecutionClient for OwnFills<C>
267/// where
268///     C: ExecutionClient + Sync,
269///     C::AccountStream: 'static,
270/// {
271///     const EXCHANGE: ExchangeId = C::EXCHANGE;
272///     const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = C::SUPPORTED_KINDS;
273///     type Config = OwnFillsConfig<C::Config>;
274///     type AccountStream = BoxStream<'static, UnindexedAccountEvent>;
275///
276///     fn validate_config(
277///         config: &Self::Config,
278///         instruments: &[ClientInstrument<'_>],
279///     ) -> Result<(), String> {
280///         C::validate_config(&config.inner, instruments)
281///     }
282///
283///     fn new(config: Self::Config) -> Self {
284///         Self {
285///             inner: C::new(config.inner),
286///             prefix: config.prefix,
287///         }
288///     }
289///
290///     async fn account_stream(
291///         &self,
292///         assets: &[AssetNameExchange],
293///         instruments: &[InstrumentNameExchange],
294///     ) -> Result<Self::AccountStream, UnindexedClientError> {
295///         let prefix = self.prefix.clone();
296///         let stream = self.inner.account_stream(assets, instruments).await?;
297///         Ok(stream
298///             .filter(move |event| {
299///                 let keep = match &event.kind {
300///                     AccountEventKind::Trade(trade) => keeps(&prefix, trade),
301///                     _ => true,
302///                 };
303///                 std::future::ready(keep)
304///             })
305///             .boxed())
306///     }
307///
308///     async fn fetch_trades(
309///         &self,
310///         start: DateTime<Utc>,
311///         end: DateTime<Utc>,
312///         instruments: &[InstrumentNameExchange],
313///     ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError> {
314///         let mut read = self.inner.fetch_trades(start, end, instruments).await?;
315///         read.trades.retain(|trade| keeps(&self.prefix, trade));
316///         Ok(read)
317///     }
318///
319///     // Everything else is forwarded unchanged.
320///
321///     fn process_account_events(
322///         &self,
323///         events: BoxStream<'static, UnindexedAccountEvent>,
324///     ) -> BoxStream<'static, UnindexedAccountEvent> {
325///         self.inner.process_account_events(events)
326///     }
327///
328///     async fn account_snapshot(
329///         &self,
330///         assets: &[AssetNameExchange],
331///         instruments: &[InstrumentNameExchange],
332///     ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
333///         self.inner.account_snapshot(assets, instruments).await
334///     }
335///
336///     async fn cancel_order(
337///         &self,
338///         request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
339///     ) -> UnindexedOrderResponseCancel {
340///         self.inner.cancel_order(request).await
341///     }
342///
343///     fn cancel_orders<'a>(
344///         &self,
345///         requests: impl IntoIterator<Item = OrderRequestCancel<ExchangeId, &'a InstrumentNameExchange>>,
346///     ) -> impl Stream<Item = UnindexedOrderResponseCancel> + Send {
347///         self.inner.cancel_orders(requests)
348///     }
349///
350///     async fn open_order(
351///         &self,
352///         request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
353///     ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
354///         self.inner.open_order(request).await
355///     }
356///
357///     fn open_orders<'a>(
358///         &self,
359///         requests: impl IntoIterator<Item = OrderRequestOpen<ExchangeId, &'a InstrumentNameExchange>>,
360///     ) -> impl Stream<Item = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>> + Send
361///     {
362///         self.inner.open_orders(requests)
363///     }
364///
365///     // Forwarded so that the inner client's one-request cancel-replace is kept: left out, the
366///     // wrapper's groups would go the slower, sequential way.
367///     fn cancel_replace(
368///         &self,
369///         cancel: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
370///         open: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
371///         on_cancel_failure: OnCancelFailure,
372///     ) -> Option<impl Future<Output = CancelReplaceAnswer> + Send> {
373///         self.inner.cancel_replace(cancel, open, on_cancel_failure)
374///     }
375///
376///     async fn fetch_balances(
377///         &self,
378///         assets: &[AssetNameExchange],
379///     ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
380///         self.inner.fetch_balances(assets).await
381///     }
382///
383///     async fn fetch_open_orders(
384///         &self,
385///         instruments: &[InstrumentNameExchange],
386///     ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError> {
387///         self.inner.fetch_open_orders(instruments).await
388///     }
389/// }
390///
391/// impl<C> OrderStatusClient for OwnFills<C>
392/// where
393///     C: OrderStatusClient + Sync,
394///     C::AccountStream: 'static,
395/// {
396///     async fn fetch_ended_orders(
397///         &self,
398///         orders: &[UnindexedOrderKey],
399///     ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
400///         self.inner.fetch_ended_orders(orders).await
401///     }
402///
403///     async fn fetch_order_status(
404///         &self,
405///         key: &UnindexedOrderKey,
406///     ) -> Result<VenueOrderStatus, UnindexedClientError> {
407///         self.inner.fetch_order_status(key).await
408///     }
409/// }
410/// ```
411///
412/// [`Trade::client_order_id`]: crate::trade::Trade::client_order_id
413/// [`Trade::order_id`]: crate::trade::Trade::order_id
414/// [`TradeOrigin::is_forced`]: crate::trade::TradeOrigin::is_forced
415/// [`TradeOrigin::Order`]: crate::trade::TradeOrigin::Order
416/// [`TradeOrigin::External`]: crate::trade::TradeOrigin::External
417/// [`TradesRead`]: crate::trade::TradesRead
418/// [`InstrumentState::update_from_trade`]: https://docs.rs/rustrade/latest/rustrade/engine/state/instrument/struct.InstrumentState.html#method.update_from_trade
419pub trait ExecutionClient
420where
421    Self: Clone,
422{
423    const EXCHANGE: ExchangeId;
424
425    /// The [`InstrumentKindDiscriminant`]s this client can trade.
426    ///
427    /// A client works off [`InstrumentNameExchange`] strings and never inspects an instrument's
428    /// kind, so nothing downstream can infer this from the implementation — routing a `Future`
429    /// through a spot-only venue produces a rejected or, worse, misinterpreted order rather than a
430    /// type error. Declaring the set here lets [`ExecutionBuilder`] reject an unsupported
431    /// instrument set at build time, before any order is sent.
432    ///
433    /// Declare what the *venue and this implementation together* can actually route, not what the
434    /// exchange's product catalogue advertises. A kind the exchange offers but this client builds
435    /// no request for does not belong in the set.
436    ///
437    /// There is deliberately no default. A new client must state its capabilities rather than
438    /// inherit a permissive set that would silently admit every kind.
439    ///
440    /// It is a `const` and not a `fn supported_kinds(&self)` because [`ExecutionBuilder`] validates
441    /// the instrument set *before* [`Self::new`] is ever called — there is no instance to ask at the
442    /// point the answer is needed, and deferring validation until after construction would mean
443    /// standing up a client for a venue the engine is about to reject. A capability set that varies
444    /// with `Self::Config` therefore cannot be expressed here; it would need a separate
445    /// pre-construction hook on the config.
446    ///
447    /// [`ExecutionBuilder`]: https://docs.rs/rustrade/latest/rustrade/execution/builder/struct.ExecutionBuilder.html
448    const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant];
449
450    type Config: Clone;
451
452    /// Checks `config` against the instruments this client is about to be built to trade, before
453    /// [`Self::new`] is called. `Err` names what is wrong.
454    ///
455    /// [`SUPPORTED_KINDS`](Self::SUPPORTED_KINDS) rejects a kind the client cannot route at all.
456    /// This catches what only the config knows: a per-instrument entry that is invalid, or that
457    /// disagrees with the instrument it is keyed to, such as a contract registered as a stock for
458    /// an instrument modelled as an option. Each such entry would otherwise be skipped or acted on
459    /// silently once the client runs.
460    ///
461    /// The default accepts every config. Override it when `Self::Config` carries per-instrument
462    /// detail.
463    ///
464    /// [`ExecutionBuilder`] calls it, after checking `SUPPORTED_KINDS`, with the instruments
465    /// registered on this client's exchange. A client constructed by calling `new` directly is not
466    /// checked; call this first to get the same check.
467    ///
468    /// [`ExecutionBuilder`]: https://docs.rs/rustrade/latest/rustrade/execution/builder/struct.ExecutionBuilder.html
469    fn validate_config(
470        config: &Self::Config,
471        instruments: &[ClientInstrument<'_>],
472    ) -> Result<(), String> {
473        let _ = (config, instruments);
474        Ok(())
475    }
476
477    // `+ Send` required so generic code (e.g. ExecutionManager) can pass
478    // the stream to tokio::spawn, which requires Send.
479    type AccountStream: Stream<Item = UnindexedAccountEvent> + Send;
480
481    fn new(config: Self::Config) -> Self;
482
483    /// Reads the account's balances, and its open orders and positions for `instruments`, from the
484    /// venue.
485    ///
486    /// Besides startup, the engine's `ExecutionManager` calls this again on every account-stream
487    /// reconnect, and emits the result ahead of the new stream's updates. It is how the engine
488    /// catches up on what changed while the stream was down.
489    ///
490    /// # Assets
491    ///
492    /// `assets` are the ones the caller registered. Return balances for those only when the venue
493    /// lets you ask, which saves bandwidth. A balance for another asset is not an error: the
494    /// `ExecutionManager` drops it and logs the asset names once per snapshot.
495    ///
496    /// Balances are stamped as [`fetch_balances`](Self::fetch_balances) describes.
497    ///
498    /// # Order completeness
499    ///
500    /// Each [`InstrumentAccountSnapshot`](crate::InstrumentAccountSnapshot) states through
501    /// `orders_complete` whether its `orders` list is every order open at the venue for that
502    /// instrument. Set it only when it is true: the engine retires a tracked order that a complete
503    /// list omits, so a list that can miss an open order (a capped page, an order dropped on
504    /// conversion, an order reported under an id other than the one it was placed with) must say
505    /// `false`. `false` is always safe; it only forgoes that reconciliation.
506    fn account_snapshot(
507        &self,
508        assets: &[AssetNameExchange],
509        instruments: &[InstrumentNameExchange],
510    ) -> impl Future<Output = Result<UnindexedAccountSnapshot, UnindexedClientError>> + Send;
511
512    /// Returns a live stream of account events (fills, order updates, balance changes).
513    ///
514    /// # Startup race window
515    ///
516    /// There is an unavoidable gap between the WebSocket subscribe response and the
517    /// first event being delivered: fills arriving in this window (typically milliseconds,
518    /// no sub-millisecond guarantee) are silently dropped. `account_snapshot` reconciles
519    /// open-order state, but TRADE fills in this window are not recoverable from the stream
520    /// alone. Callers that require fill completeness at startup **must** call
521    /// [`ExecutionClient::fetch_trades`] with at least a 1-second lookback after this method returns.
522    ///
523    /// # Backpressure
524    ///
525    /// Implementations use unbounded internal channels. If the consumer cannot keep up,
526    /// events queue in memory rather than being dropped — per library philosophy, OOM
527    /// crashes are preferable to silent data loss. Consumers requiring backpressure
528    /// should implement it at their boundary (e.g., bounded channel with overflow policy).
529    fn account_stream(
530        &self,
531        assets: &[AssetNameExchange],
532        instruments: &[InstrumentNameExchange],
533    ) -> impl Future<Output = Result<Self::AccountStream, UnindexedClientError>> + Send;
534
535    /// Processes the account events of one open of the account stream, after the engine's
536    /// `ExecutionManager` has dropped the fills it delivered before, and before it indexes them for
537    /// the engine. The default passes them on unchanged.
538    ///
539    /// The manager calls it each time it opens the account stream, the first time and on every
540    /// re-initialisation, with that open's events in the order it delivers them: the fills it read
541    /// for the span the stream was down, an [`AccountEventKind::FillRecoveryGaveUp`] for any span
542    /// it could not read, the [`AccountEventKind::Snapshot`], and then the stream's updates,
543    /// [`AccountEventKind::StreamTerminated`] included. Its own reports of the reconnect
544    /// (`Reconnecting`, [`AccountEventKind::ReinitFailed`]) do not pass through it.
545    ///
546    /// A fill reaches it once, whichever path brought it. [`account_stream`](Self::account_stream)
547    /// does not have that property: the manager opens the stream before it reads the span the
548    /// stream was down, so a fill made during the read arrives both ways, and a venue can replay
549    /// fills when a stream opens. Processing that keeps state over the fills the engine receives
550    /// belongs here (see [Wrapping a client](Self#wrapping-a-client)).
551    ///
552    /// # Contract
553    /// - **Keep the order** of the events passed on: process them with `then`, `flat_map` or
554    ///   `buffered`, not `buffer_unordered`.
555    /// - **End only when `events` ends.** When the returned stream ends, the manager opens the
556    ///   account stream again. Each fill `events` yielded is recorded as seen by then, so one this
557    ///   stage took and did not pass on is not read again: it is lost.
558    /// - **Retry a step that fails here, with backoff**, such as a REST call to attribute a fill.
559    ///   To give up on a fill, report it as an [`AccountEventKind::FillRecoveryGaveUp`] whose scope
560    ///   is its instrument, whose span is its time, and whose reason is
561    ///   [`FillRecoveryFailure::Processing`]. A fill dropped without a report is lost.
562    /// - **Pass on the events it does not process**, the snapshot included.
563    /// - The manager sets no time limit: a slow step delays every event behind it.
564    ///
565    /// # What the manager does around it
566    /// - Every fill the returned stream yields is recorded as seen, and none is dropped. A fill
567    ///   this stage adds is recorded as it leaves the stage, so a copy the venue sends after that
568    ///   is dropped before it reaches the stage; one it adds that was delivered before is
569    ///   delivered again. A copy the stage has already taken from `events` is not caught: a stage
570    ///   that adds fills while it reads ahead, as `buffered` does, drops the venue's copy itself.
571    /// - Where the next read of a missed span starts is measured on `events`, so a fill this stage
572    ///   drops or holds does not widen it, and the stage's own delay does not skew it.
573    /// - The returned events are indexed. A snapshot this stage changes so that it no longer
574    ///   indexes is dropped and logged as an error, as is any other event the indexer rejects.
575    /// - Events this stage holds when the manager stops are not delivered.
576    ///
577    /// [`FillRecoveryFailure::Processing`]: crate::fill_recovery::FillRecoveryFailure::Processing
578    /// [`AccountEventKind::FillRecoveryGaveUp`]: crate::AccountEventKind::FillRecoveryGaveUp
579    /// [`AccountEventKind::Snapshot`]: crate::AccountEventKind::Snapshot
580    /// [`AccountEventKind::StreamTerminated`]: crate::AccountEventKind::StreamTerminated
581    /// [`AccountEventKind::ReinitFailed`]: crate::AccountEventKind::ReinitFailed
582    fn process_account_events(
583        &self,
584        events: BoxStream<'static, UnindexedAccountEvent>,
585    ) -> BoxStream<'static, UnindexedAccountEvent> {
586        events
587    }
588
589    /// Cancel an order at the venue.
590    ///
591    /// # Return value
592    ///
593    /// Every request gets exactly one answer, which the engine's `ExecutionManager` forwards as the
594    /// order's response. An implementation must not swallow a request: a failure to send it, or to
595    /// read the venue's answer, is an `Err`.
596    ///
597    /// - `Ok(Cancelled)`: the venue took the cancel. Whether the order has *ended* by then depends
598    ///   on the venue; see below.
599    /// - `Err`: the venue refused the cancel, or it could not be sent or answered. The order may
600    ///   still be open, or may have ended some other way, such as by filling. The account stream
601    ///   reports which.
602    ///
603    /// # A taken cancel is not always an ended order
604    ///
605    /// Some venues answer a cancel before carrying it out. Until they do, the order is live and can
606    /// still fill. From those venues `Ok(Cancelled)` means only that the cancel was accepted, and
607    /// the account stream reports how the order actually ended, including any fill in between. The
608    /// engine stops tracking an order on `Ok(Cancelled)` from any venue.
609    ///
610    /// Binance, Hyperliquid and the mock venue answer once the order is cancelled. Alpaca answers
611    /// once it has accepted the cancel, and IBKR once the cancel is submitted. The
612    /// [connector comparison](crate::client#connector-comparison) lists the same for each
613    /// connector; the mock venue is not one, so it has no row there.
614    ///
615    /// # `filled_quantity` and `time_exchange`
616    ///
617    /// `Cancelled::filled_quantity` is the quantity the venue reported filled where the answer
618    /// carries one, which Binance's and the mock venue's do. Alpaca, IBKR and Hyperliquid answer
619    /// without it and report zero, which does not mean nothing filled. The fill total comes from
620    /// the account stream, or from [`Self::fetch_trades`]. Likewise `time_exchange` is the venue's
621    /// time where the answer carries one, and the local time the answer arrived otherwise.
622    fn cancel_order(
623        &self,
624        request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
625    ) -> impl Future<Output = UnindexedOrderResponseCancel> + Send;
626
627    // `+ Send` on default method return types for multi-threaded Tokio runtime
628    fn cancel_orders<'a>(
629        &self,
630        requests: impl IntoIterator<Item = OrderRequestCancel<ExchangeId, &'a InstrumentNameExchange>>,
631    ) -> impl Stream<Item = UnindexedOrderResponseCancel> + Send {
632        futures::stream::FuturesUnordered::from_iter(
633            requests
634                .into_iter()
635                .map(|request| self.cancel_order(request)),
636        )
637    }
638
639    /// Place an order on the exchange.
640    ///
641    /// # Return value
642    ///
643    /// Every request gets exactly one answer, which the engine's `ExecutionManager` forwards as the
644    /// order's response. An implementation must not swallow a request: a failure to send it, or to
645    /// read the venue's answer, is `OpenFailed`.
646    ///
647    /// Returns `OrderState` directly rather than `Result<Open, OrderError>`:
648    /// - `OrderState::Active(Open)` - order is resting on the order book
649    /// - `OrderState::Inactive(FullyFilled)` - order was immediately filled (includes `avg_price` when available)
650    /// - `OrderState::Inactive(Expired)` - order ended in the response itself without filling
651    ///   completely, carrying what filled: an IOC or FOK order that found too little liquidity,
652    ///   or one expired by self-trade prevention
653    /// - `OrderState::Inactive(Cancelled)` - order the venue reports cancelled in the response
654    ///   itself, carrying what filled
655    /// - `OrderState::Inactive(OpenFailed)` - order placement failed (API error, connectivity, a
656    ///   rejection the venue reports in the response, etc.)
657    ///
658    /// An implementation returns `Expired` or `Cancelled` only when its venue reports the order's
659    /// status in the placement response (currently Binance Spot and Margin). The others return
660    /// `Open` for such an order, and their account stream reports how it ended.
661    ///
662    /// This design allows immediate fills to carry metadata (e.g., `avg_price`) that
663    /// would be lost if we had to infer terminal state from `Open::filled_quantity`.
664    fn open_order(
665        &self,
666        request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
667    ) -> impl Future<Output = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>> + Send;
668
669    // `+ Send` on default method return types for multi-threaded Tokio runtime
670    fn open_orders<'a>(
671        &self,
672        requests: impl IntoIterator<Item = OrderRequestOpen<ExchangeId, &'a InstrumentNameExchange>>,
673    ) -> impl Stream<Item = Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>> + Send
674    {
675        futures::stream::FuturesUnordered::from_iter(
676            requests.into_iter().map(|request| self.open_order(request)),
677        )
678    }
679
680    /// Send a cancel and the open that waits on it as one venue request, where the venue has one
681    /// that does exactly what a [`CancelThenOpen`] group of that cancel and that open asks.
682    ///
683    /// The engine's `ExecutionManager` offers each group of one cancel and one open here first.
684    /// When this returns `None` it sequences the group itself: it sends the cancel, and sends or
685    /// drops the open once the cancel is answered. A native request saves the open's round trip,
686    /// which is how long the group leaves nothing resting.
687    ///
688    /// The default returns `None` for every pair. Binance Spot overrides it.
689    ///
690    /// # Implementation obligations
691    /// - **Decline without sending.** Return `None`, having sent nothing, for a pair the venue's
692    ///   request cannot carry exactly as asked, including under `on_cancel_failure`. The
693    ///   sequential path then serves it.
694    /// - **Keep the group's order.** The venue must finish the cancel before it places the open.
695    ///   Under [`OnCancelFailure::DropOpens`] it must not place the open when the cancel fails.
696    ///   Under [`OnCancelFailure::SendOpens`] it must attempt the open whatever the cancel's
697    ///   answer, unless the venue refuses the whole request (below).
698    /// - **Answer what the venue did with each leg,** in the form a leg sent alone is answered:
699    ///   [`CancelReplaceAnswer::cancel`] as [`cancel_order`](Self::cancel_order) answers, and
700    ///   [`CancelReplaceAnswer::open`] as [`open_order`](Self::open_order) answers. `open` is
701    ///   `None` only when the cancel failed under `DropOpens` and the venue did not attempt the
702    ///   open.
703    /// - **A request refused whole changed nothing.** A venue may check the request before it
704    ///   acts on either leg, and refuse it whole: for a filter, a limit, or a malformed or
705    ///   unauthorised request. The order to cancel then still rests, so the cancel is answered as
706    ///   failed with that refusal, and the open as not attempted under `DropOpens` or as refused
707    ///   with the same reason under `SendOpens`. Sent apart, the cancel might have gone through
708    ///   and only the open been refused, leaving nothing resting, where the refused request
709    ///   leaves the order to cancel in place.
710    /// - **Unknown stays unknown.** When the venue's answer is lost, the cancel is an error, and
711    ///   the open is `Some`, with an outcome that is unknown (see
712    ///   [`OrderState::is_outcome_unknown`](crate::order::state::OrderState::is_outcome_unknown)).
713    ///   Then the manager looks the order up, as it does for an open sent alone.
714    /// - **Answer once.** The future must resolve. The manager bounds it with its
715    ///   `request_timeout`, and a timeout counts as the cancel failing and the open's outcome
716    ///   being unknown.
717    ///
718    /// # Wrapping clients
719    /// A client that wraps another and does not forward this method keeps the default, so its
720    /// groups go the sequential way. That is still correct, just slower.
721    ///
722    /// [`CancelThenOpen`]: crate::order::request::CancelThenOpen
723    /// [`OnCancelFailure::DropOpens`]: crate::order::request::OnCancelFailure::DropOpens
724    /// [`OnCancelFailure::SendOpens`]: crate::order::request::OnCancelFailure::SendOpens
725    fn cancel_replace(
726        &self,
727        cancel: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
728        open: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
729        on_cancel_failure: OnCancelFailure,
730    ) -> Option<impl Future<Output = CancelReplaceAnswer> + Send> {
731        let _ = (cancel, open, on_cancel_failure);
732        None::<std::future::Ready<CancelReplaceAnswer>>
733    }
734
735    /// Fetch current balances for the specified assets.
736    ///
737    /// An empty `assets` slice is the "return all" sentinel: implementations must return
738    /// balances for every asset held. When non-empty, only the listed assets are returned.
739    ///
740    /// # Balance time
741    ///
742    /// Each balance's `time_exchange` must be no later than the state it describes, because the
743    /// engine orders balance and debt state by it: a balance stamped later than its state would
744    /// block the stream's balance and debt events from between the two (see
745    /// [`AssetBalance::time_exchange`]). Stamp a balance with this host's time taken before the
746    /// request that returned it was sent, less how far the venue's REST view can trail its
747    /// account stream where the stream carries balances, never with a time taken after the
748    /// response. A time the venue states for the account's state can be used instead.
749    ///
750    /// A host clock ahead of the venue's carries into a stamp taken from this host's time. An
751    /// implementation that keeps an estimate of the venue's clock that is never ahead of it takes
752    /// the earlier of the two times instead, and says so in its docs. Otherwise keeping the two
753    /// clocks within bounds is the caller's obligation.
754    fn fetch_balances(
755        &self,
756        assets: &[AssetNameExchange],
757    ) -> impl Future<Output = Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError>> + Send;
758
759    /// Fetch currently open orders, optionally filtered by instrument.
760    ///
761    /// An empty `instruments` slice is the "return all" sentinel: implementations must
762    /// return open orders across all instruments. When non-empty, only orders for the
763    /// listed instruments are returned.
764    fn fetch_open_orders(
765        &self,
766        instruments: &[InstrumentNameExchange],
767    ) -> impl Future<
768        Output = Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError>,
769    > + Send;
770
771    /// Read the trades (fills) from `start` to `end`, both inclusive, optionally filtered by
772    /// instrument.
773    ///
774    /// An empty `instruments` slice is the "return all" sentinel: implementations must
775    /// return trades across all instruments. When non-empty, only trades for the listed
776    /// instruments are returned.
777    ///
778    /// # Bounded, resumable reads
779    ///
780    /// One call is bounded by the venue's limits, so it may stop before `end`.
781    /// [`TradesRead::resume`] says whether it did:
782    /// - `None`: every fill in the span the venue still holds was read.
783    /// - `Some(t)`: call again with `start = t` and the same `end` to read on. That read can
784    ///   return again fills the previous one returned, so match them by instrument and
785    ///   [`TradeId`](crate::trade::TradeId).
786    ///
787    /// The caller owns the loop, and with it how many calls, and how long, it spends on a span.
788    /// A `start` after `end` is an empty span, read as no trades and `resume: None`.
789    ///
790    /// `end` can lie past this host's present. A read placed on the venue's clock, as the
791    /// execution manager's missed-fill recovery is, ends past it to cover a venue clock ahead of
792    /// this host's. Implementations read to `end` or the venue's present, whichever is earlier,
793    /// and must not cut `end` to this host's clock.
794    ///
795    /// # Venue limits
796    ///
797    /// `resume: None` covers what the venue still holds, which can be less than the span:
798    /// - IBKR lists the last 7 days' executions at most.
799    /// - Hyperliquid keeps only each wallet's 10,000 most recent fills.
800    ///
801    /// Binance needs instruments: an empty slice reads nothing. Each client documents its limits.
802    ///
803    /// # Errors
804    ///
805    /// [`Truncated`](crate::error::ClientError::Truncated) when a read cannot advance, because
806    /// more fills share the span's first moment than one call can read past.
807    ///
808    /// The fee asset (`AssetNameExchange`) may be quote, base, or third-party (e.g., BNB).
809    /// Use `fees.fees_quote` for quote-equivalent value when available.
810    fn fetch_trades(
811        &self,
812        start: DateTime<Utc>,
813        end: DateTime<Utc>,
814        instruments: &[InstrumentNameExchange],
815    ) -> impl Future<
816        Output = Result<
817            TradesRead<AssetNameExchange, InstrumentNameExchange>,
818            UnindexedClientError,
819        >,
820    > + Send;
821}
822
823/// Extension trait for exchanges that support native bracket orders.
824///
825/// A bracket order consists of three linked orders:
826/// 1. **Entry**: Limit order to enter the position
827/// 2. **Take Profit**: Limit order to exit at profit target
828/// 3. **Stop Loss**: Stop (or stop-limit) order to exit at loss limit
829///
830/// When either exit leg fills, the exchange automatically cancels the other.
831///
832/// # Type-Level Capability
833///
834/// This is a supertrait of [`ExecutionClient`], enabling compile-time capability checks:
835/// - `impl ExecutionClient` — basic order operations
836/// - `impl ExecutionClient + BracketOrderClient` — includes bracket orders
837///
838/// This follows Rust idioms like `Read + Seek` or `Iterator + ExactSizeIterator`.
839///
840/// # Why Supertrait Over Alternatives
841///
842/// **vs. associated types on `ExecutionClient`**: Callers can't construct
843/// `Self::BracketRequest` without knowing the concrete type — adds trait surface
844/// without enabling generic use.
845///
846/// **vs. default impl returning `Unsupported`**: Puts a "dead method" on every
847/// client (MockExecution, BinanceSpot, HyperliquidClient). Compile-time capability
848/// via trait bounds is better than runtime errors.
849///
850/// # Result Types
851///
852/// [`BracketOrderResult`] uses `Option<Order>` for child legs to document API divergence:
853///
854/// | Exchange | `take_profit` | `stop_loss` | Reason |
855/// |----------|---------------|-------------|--------|
856/// | IBKR     | `Some(...)` | `Some(...)` | Returns all three orders immediately |
857/// | Alpaca   | `None` | `None` | Child legs created server-side |
858///
859/// # Example
860///
861/// ```ignore
862/// use rustrade_execution::client::{ExecutionClient, BracketOrderClient};
863/// use rustrade_execution::order::bracket::{BracketOrderRequest, RequestOpenBracket};
864///
865/// async fn place_bracket<C: ExecutionClient + BracketOrderClient>(
866///     client: &C,
867///     request: BracketOrderRequest<ExchangeId, &InstrumentNameExchange>,
868/// ) -> BracketOrderResult {
869///     client.open_bracket_order(request).await
870/// }
871/// ```
872pub trait BracketOrderClient: ExecutionClient {
873    /// Place a bracket order (entry + take-profit + stop-loss).
874    ///
875    /// # Request
876    ///
877    /// The [`BracketOrderRequest`] contains:
878    /// - `key`: Order key (exchange, instrument, strategy, client order ID)
879    /// - `state`: [`RequestOpenBracket`](crate::order::bracket::RequestOpenBracket) with
880    ///   side, quantity, prices, and optional stop-loss limit price
881    ///
882    /// # Constraints
883    ///
884    /// - `time_in_force` must be `Day` or `GoodUntilCancelled` on most exchanges
885    /// - Entry order type is always `Limit`
886    /// - Price ordering must be valid for the side (see [`RequestOpenBracket`](crate::order::bracket::RequestOpenBracket))
887    ///
888    /// # Exchange-Specific Field Handling
889    ///
890    /// `RequestOpenBracket::stop_loss_limit_price` is **not honored uniformly**:
891    /// - **Alpaca**: When `Some`, the stop-loss leg becomes a stop-limit order at that price.
892    /// - **IBKR**: Silently ignored — the stop-loss leg is always a stop (market) order.
893    ///
894    /// Generic callers `T: BracketOrderClient` must treat this field as advisory.
895    ///
896    /// # Return Value
897    ///
898    /// Returns [`BracketOrderResult`] with:
899    /// - `parent`: Always present (entry order)
900    /// - `take_profit`: `Some` if exchange returns legs immediately (IBKR), `None` otherwise (Alpaca)
901    /// - `stop_loss`: `Some` if exchange returns legs immediately (IBKR), `None` otherwise (Alpaca)
902    ///
903    /// Either all orders are `Active(Open)` or all are `Inactive` (placement failed), with one
904    /// exception: a leg of a failed placement whose fate at the venue is unknown (no status
905    /// arrived, or it was accepted and its rollback cancel could not be sent) is returned
906    /// `Active(Open)`, so the account stream can resolve it. Only IBKR returns such legs today;
907    /// see its `open_bracket_order`.
908    fn open_bracket_order(
909        &self,
910        request: BracketOrderRequest<ExchangeId, &InstrumentNameExchange>,
911    ) -> impl Future<Output = BracketOrderResult> + Send;
912}
913
914/// Extension trait for clients that can say how an order ended at the venue.
915///
916/// A client that loses its account stream recovers the fills it missed when it reconnects, but not
917/// an order that was cancelled, expired or rejected in the meantime: the event that said so went
918/// out while nothing was listening. The engine then keeps such an order as live until a complete
919/// account snapshot no longer lists it, and even then cannot tell how it ended. This trait is the
920/// lookup that answers that: given the orders a caller still holds as live, it returns how each one
921/// that has ended did end.
922///
923/// # By order, not by time
924///
925/// The obvious alternative, asking the venue for every order closed since the disconnect, does not
926/// work. Binance's `allOrders` and Alpaca's `GET /v2/orders?status=closed` both filter their time
927/// window on when an order was **created**, so a window opening at the disconnect cannot see an
928/// order placed before it and cancelled during it — which is the order that matters, because the
929/// caller is tracking it. Looking each order up by its client order id finds it whenever it was
930/// placed.
931///
932/// # Type-Level Capability
933///
934/// A supertrait of [`ExecutionClient`], for the reason [`BracketOrderClient`] is one: a client that
935/// cannot look orders up does not implement it, rather than carrying a method that fails at run
936/// time. A caller that needs the lookup bounds on `ExecutionClient + OrderStatusClient`.
937pub trait OrderStatusClient: ExecutionClient {
938    /// How each of `orders` ended, for those that have.
939    ///
940    /// # Contract
941    ///
942    /// - One entry per order that has ended, in no particular order:
943    ///   [`FullyFilled`](crate::order::state::InactiveOrderState::FullyFilled),
944    ///   [`Cancelled`](crate::order::state::InactiveOrderState::Cancelled) carrying what filled
945    ///   before it, [`Expired`](crate::order::state::InactiveOrderState::Expired), or
946    ///   [`OpenFailed`](crate::order::state::InactiveOrderState::OpenFailed) for an order the venue
947    ///   accepted for processing and later rejected (Binance `REJECTED`, Alpaca `rejected`). An
948    ///   order rejected in the response to its open request was answered there and is not expected
949    ///   here.
950    /// - An order still live is **omitted**, and so is one the venue does not know. Neither is an
951    ///   error. A caller compares the result with what it asked for.
952    /// - An empty `orders` returns an empty list. Unlike
953    ///   [`ExecutionClient::fetch_open_orders`]'s empty slice, it does not mean "all".
954    /// - An order is found by its [`cid`](crate::order::OrderKey::cid), and each returned order
955    ///   carries the key it was asked for, so a `strategy` the venue does not record survives.
956    /// - A key whose `instrument` is not the one the order traded is treated as unknown. The
957    ///   `exchange` is not checked: a client answers for its own venue.
958    /// - A `cid` asked about more than once is reported at most once, under the first key that
959    ///   finds it.
960    ///
961    /// # Errors
962    ///
963    /// `Err` if the venue could not answer for every order. A partial list is never returned,
964    /// because an order missing from it would read as still live.
965    fn fetch_ended_orders(
966        &self,
967        orders: &[UnindexedOrderKey],
968    ) -> impl Future<Output = Result<Vec<UnindexedInactiveOrder>, UnindexedClientError>> + Send;
969
970    /// What the venue knows of the order under `key`: live, ended, or nothing.
971    ///
972    /// This is how an open request whose outcome is unknown is settled (see
973    /// [`OrderError::is_outcome_unknown`](crate::error::OrderError::is_outcome_unknown)).
974    ///
975    /// # Contract
976    ///
977    /// - [`VenueOrderStatus::Unknown`] means the venue lists the order neither as live nor as
978    ///   ended, under the key's `instrument`. A venue may take a moment to list an order it has
979    ///   just accepted, so one `Unknown` does not prove the order was never placed. A caller asks
980    ///   again before it concludes that.
981    /// - The order is found by its [`cid`](crate::order::OrderKey::cid), and a found order
982    ///   carries `key`.
983    ///
984    /// The provided method asks [`fetch_ended_orders`](Self::fetch_ended_orders) about `key`
985    /// first, then lists the open orders of its instrument with
986    /// [`ExecutionClient::fetch_open_orders`]. In that order, an order that is placed between
987    /// the two reads is listed as live, so it is not missed. An order that ends between them is
988    /// found by neither, and is reported `Unknown` once; the next lookup finds it ended. A client
989    /// whose venue answers in one request may override it.
990    ///
991    /// # Errors
992    ///
993    /// `Err` if either read fails. A failed read is never reported as `Unknown`.
994    fn fetch_order_status(
995        &self,
996        key: &UnindexedOrderKey,
997    ) -> impl Future<Output = Result<VenueOrderStatus, UnindexedClientError>> + Send
998    where
999        Self: Sync,
1000    {
1001        async move {
1002            let ended = self.fetch_ended_orders(std::slice::from_ref(key)).await?;
1003            if let Some(order) = ended.into_iter().find(|order| order.key.cid == key.cid) {
1004                return Ok(VenueOrderStatus::Ended(Box::new(Order {
1005                    key: key.clone(),
1006                    ..order
1007                })));
1008            }
1009
1010            let open = self
1011                .fetch_open_orders(std::slice::from_ref(&key.instrument))
1012                .await?;
1013            Ok(open
1014                .into_iter()
1015                .find(|order| order.key.cid == key.cid && order.key.instrument == key.instrument)
1016                .map_or(VenueOrderStatus::Unknown, |order| {
1017                    VenueOrderStatus::Live(Box::new(Order {
1018                        key: key.clone(),
1019                        ..order
1020                    }))
1021                }))
1022        }
1023    }
1024}
1025
1026/// What a venue knows of one order, as
1027/// [`OrderStatusClient::fetch_order_status`] reads it.
1028#[derive(Debug, Clone, Eq, PartialEq)]
1029pub enum VenueOrderStatus {
1030    /// The order is live at the venue, in this state. Boxed, as `Unknown` carries nothing.
1031    Live(Box<Order<ExchangeId, InstrumentNameExchange, Open>>),
1032    /// The order has ended, as this says.
1033    Ended(Box<UnindexedInactiveOrder>),
1034    /// The venue lists the order neither as live nor as ended.
1035    Unknown,
1036}
1037
1038/// Extension trait for clients that can say what the venue would lend for a short sale.
1039///
1040/// Answers, before an order is sent, the question a short sale otherwise learns only from its
1041/// rejection: whether the instrument may be sold short, whether it is easy to borrow, how much the
1042/// venue has to lend and at what fee. Each venue reports a different subset, so each field of the
1043/// [`Shortability`] it returns is optional; the implementations say which fields they fill.
1044///
1045/// # Type-Level Capability
1046///
1047/// A supertrait of [`ExecutionClient`], for the reason [`BracketOrderClient`] is one: a client
1048/// whose venue has nothing to lend, or that does not report it, does not implement it. A simulated
1049/// venue reads the same [`Shortability`] from a
1050/// [`ShortabilityProvider`](crate::shortability::ShortabilityProvider) instead.
1051pub trait ShortabilityClient: ExecutionClient {
1052    /// What the venue says about lending `instrument` for a short sale.
1053    ///
1054    /// # Contract
1055    ///
1056    /// - **Advisory.** `Ok` describes the venue's lending as it reported it, not a promise to lend:
1057    ///   a short sale must still handle
1058    ///   [`ApiError::BorrowRejected`](crate::error::ApiError::BorrowRejected).
1059    /// - **Not this account's capacity.** [`Shortability::available`] is what the venue has to
1060    ///   lend, not what is left of it for this account; that is
1061    ///   [`BorrowCapacityClient::fetch_borrow_capacity`], where a venue reports it.
1062    /// - **May be expensive.** An implementation may make several requests, some of them heavy
1063    ///   against the venue's rate limit, or hold a scarce resource such as a market data line;
1064    ///   each says what it costs. Fetch it when deciding on a short, not on every tick.
1065    /// - **May be slow.** Where the venue streams its answer rather than replying to a request,
1066    ///   the call waits for it, up to a timeout the implementation documents: seconds, not
1067    ///   milliseconds.
1068    /// - **May outlive its future.** An implementation that reads on a blocking thread finishes
1069    ///   that read, and releases what it holds, even when the returned future is dropped first;
1070    ///   each says so.
1071    ///
1072    /// # Errors
1073    ///
1074    /// - [`ApiError::InstrumentInvalid`](crate::error::ApiError::InstrumentInvalid) for an
1075    ///   instrument the venue does not know, or one of a kind that is not sold short by borrowing
1076    ///   it, such as an option.
1077    /// - An error, rather than a [`Shortability`] with nothing known, when the venue does not
1078    ///   answer, including when the account is not entitled to the answer, such as one without the
1079    ///   market data subscription a venue sends it through.
1080    fn fetch_shortability(
1081        &self,
1082        instrument: &InstrumentNameExchange,
1083    ) -> impl Future<Output = Result<Shortability, UnindexedClientError>> + Send;
1084}
1085
1086/// Extension trait for clients that can say how much more this account can borrow.
1087///
1088/// Where [`ShortabilityClient`] says what the venue has to lend, this says how much of it this
1089/// account could take now, given its collateral, its limit and what it has already borrowed. The
1090/// two are separate types because they are different facts; see [`BorrowCapacity`].
1091///
1092/// # Type-Level Capability
1093///
1094/// A supertrait of [`ExecutionClient`], for the reason [`BracketOrderClient`] is one. Only a venue
1095/// that lends against an account's collateral and reports what is left of it implements it. The
1096/// mock client implements it whether or not its simulated venue lends, and answers
1097/// [`ApiError::InstrumentInvalid`](crate::error::ApiError::InstrumentInvalid) when it does not.
1098pub trait BorrowCapacityClient: ExecutionClient {
1099    /// How much this account can borrow now for an order on `instrument` on `side`.
1100    ///
1101    /// A sell borrows the instrument's base asset, which is a short; a buy borrows its quote asset,
1102    /// which is a leveraged long. The result names that asset, and its amounts are in its units.
1103    ///
1104    /// # Contract
1105    ///
1106    /// **Advisory**, as [`BorrowCapacity`] says: the amounts are not reserved, so an order sized to
1107    /// them must still handle [`ApiError::BorrowRejected`](crate::error::ApiError::BorrowRejected).
1108    ///
1109    /// # Errors
1110    ///
1111    /// [`ApiError::InstrumentInvalid`](crate::error::ApiError::InstrumentInvalid) for an instrument
1112    /// the client cannot borrow against, such as a pair the venue does not trade on margin.
1113    fn fetch_borrow_capacity(
1114        &self,
1115        instrument: &InstrumentNameExchange,
1116        side: Side,
1117    ) -> impl Future<Output = Result<BorrowCapacity, UnindexedClientError>> + Send;
1118}
1119
1120/// The capability table, pinned.
1121///
1122/// [`ExecutionClient::SUPPORTED_KINDS`] is a `const`: widening one, or adding a kind a client builds
1123/// no request for, compiles and passes clippy. The only thing it changes is which instruments
1124/// [`ExecutionBuilder`](https://docs.rs/rustrade/latest/rustrade/execution/builder/struct.ExecutionBuilder.html)
1125/// admits — so the failure mode is not a broken build but an instrument reaching a venue that cannot
1126/// encode it, as a silently wrong order. These assertions exist so that changing a client's declared
1127/// capabilities is a deliberate edit to a test that says what the venue can route, rather than a
1128/// one-word diff nothing observes.
1129///
1130/// The real-client cases are each gated on their own feature, matching the module gates above; the
1131/// mock case is ungated, like its module. So with `default = []`, a bare
1132/// `cargo test -p rustrade-execution` compiles only the mock assertion — pinning a real client's
1133/// capabilities requires `--features <name>` or `--all-features`.
1134#[cfg(test)]
1135mod tests {
1136    use super::*;
1137    use crate::client::mock::MockExecution;
1138    use futures::StreamExt;
1139
1140    /// The provided stage passes each event on unchanged, in order.
1141    #[test]
1142    fn the_provided_account_event_stage_passes_events_on_unchanged() {
1143        use crate::{AccountEventKind, error::StreamTerminationReason};
1144        use rustrade_instrument::exchange::ExchangeId;
1145        use std::time::Duration;
1146
1147        let (_event_tx, event_rx) = tokio::sync::broadcast::channel(1);
1148        let (request_tx, _request_rx) = tokio::sync::mpsc::unbounded_channel();
1149        let client = MockExecution::new(ExchangeId::Mock, Utc::now, request_tx, event_rx);
1150        let terminated = |secs| UnindexedAccountEvent {
1151            exchange: ExchangeId::Mock,
1152            kind: AccountEventKind::StreamTerminated(StreamTerminationReason::RateLimited {
1153                retry_after: Duration::from_secs(secs),
1154            }),
1155        };
1156        let events = vec![terminated(1), terminated(2)];
1157
1158        let processed = futures::executor::block_on(
1159            client
1160                .process_account_events(futures::stream::iter(events.clone()).boxed())
1161                .collect::<Vec<_>>(),
1162        );
1163
1164        assert_eq!(processed, events);
1165    }
1166
1167    #[test]
1168    fn mock_supports_only_the_kinds_it_can_model() {
1169        // No funding schedule or futures margin in `MockExchange`, so `Perpetual` and `Future` have
1170        // no faithful projection onto it. Kept in step with the projection in
1171        // `generate_mock_exchange_instruments`, which panics on a kind it cannot project.
1172        assert_eq!(
1173            <MockExecution<fn() -> DateTime<Utc>> as ExecutionClient>::SUPPORTED_KINDS,
1174            &[
1175                InstrumentKindDiscriminant::Spot,
1176                InstrumentKindDiscriminant::Cfd,
1177                InstrumentKindDiscriminant::Option,
1178            ]
1179        );
1180    }
1181
1182    #[cfg(feature = "binance")]
1183    #[test]
1184    fn binance_spot_and_margin_are_spot_only() {
1185        use crate::client::binance::{BinanceMargin, BinanceSpot};
1186
1187        assert_eq!(
1188            BinanceSpot::SUPPORTED_KINDS,
1189            &[InstrumentKindDiscriminant::Spot]
1190        );
1191        // Cross margin trades the same spot instruments on borrowed funds -- the leverage is in the
1192        // account, not the instrument kind.
1193        assert_eq!(
1194            BinanceMargin::SUPPORTED_KINDS,
1195            &[InstrumentKindDiscriminant::Spot]
1196        );
1197    }
1198
1199    #[cfg(feature = "hyperliquid")]
1200    #[test]
1201    fn hyperliquid_splits_perpetual_and_spot_across_two_clients() {
1202        use crate::client::hyperliquid::{HyperliquidClient, spot::HyperliquidSpotClient};
1203
1204        assert_eq!(
1205            HyperliquidClient::SUPPORTED_KINDS,
1206            &[InstrumentKindDiscriminant::Perpetual]
1207        );
1208        assert_eq!(
1209            HyperliquidSpotClient::SUPPORTED_KINDS,
1210            &[InstrumentKindDiscriminant::Spot]
1211        );
1212    }
1213
1214    #[cfg(feature = "alpaca")]
1215    #[test]
1216    fn alpaca_trades_equities_and_options() {
1217        use crate::client::alpaca::AlpacaClient;
1218
1219        assert_eq!(
1220            AlpacaClient::SUPPORTED_KINDS,
1221            &[
1222                InstrumentKindDiscriminant::Spot,
1223                InstrumentKindDiscriminant::Option
1224            ]
1225        );
1226    }
1227
1228    #[cfg(feature = "ibkr")]
1229    #[test]
1230    fn ibkr_omits_cfd_because_it_builds_no_contract_for_one() {
1231        use crate::client::ibkr::IbkrClient;
1232
1233        // `ContractConfig::to_contract` builds `STK`, `FUT`, `OPT` and `CASH` and nothing else, so a
1234        // CFD routed here would be encoded as some other security type -- exactly the silent
1235        // wrong-symbol order this const exists to reject. Adding `Cfd` to the list requires adding
1236        // the arm first.
1237        assert_eq!(
1238            IbkrClient::SUPPORTED_KINDS,
1239            &[
1240                InstrumentKindDiscriminant::Spot,
1241                InstrumentKindDiscriminant::Future,
1242                InstrumentKindDiscriminant::Option
1243            ]
1244        );
1245    }
1246
1247    /// Answers `fetch_ended_orders` and `fetch_open_orders` with fixed lists, whatever is asked.
1248    #[derive(Debug, Clone, Default)]
1249    struct ListingClient {
1250        ended: Vec<UnindexedInactiveOrder>,
1251        open: Vec<Order<ExchangeId, InstrumentNameExchange, Open>>,
1252    }
1253
1254    impl ExecutionClient for ListingClient {
1255        const EXCHANGE: ExchangeId = ExchangeId::Simulated;
1256        const SUPPORTED_KINDS: &'static [InstrumentKindDiscriminant] = &[];
1257        type Config = ();
1258        type AccountStream = futures::stream::Empty<UnindexedAccountEvent>;
1259
1260        fn new(_: Self::Config) -> Self {
1261            Self::default()
1262        }
1263
1264        async fn account_snapshot(
1265            &self,
1266            _: &[AssetNameExchange],
1267            _: &[InstrumentNameExchange],
1268        ) -> Result<UnindexedAccountSnapshot, UnindexedClientError> {
1269            unreachable!("not a lookup")
1270        }
1271
1272        async fn account_stream(
1273            &self,
1274            _: &[AssetNameExchange],
1275            _: &[InstrumentNameExchange],
1276        ) -> Result<Self::AccountStream, UnindexedClientError> {
1277            unreachable!("not a lookup")
1278        }
1279
1280        async fn cancel_order(
1281            &self,
1282            _: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
1283        ) -> UnindexedOrderResponseCancel {
1284            unreachable!("not a lookup")
1285        }
1286
1287        async fn open_order(
1288            &self,
1289            _: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
1290        ) -> Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState> {
1291            unreachable!("not a lookup")
1292        }
1293
1294        async fn fetch_balances(
1295            &self,
1296            _: &[AssetNameExchange],
1297        ) -> Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError> {
1298            unreachable!("not a lookup")
1299        }
1300
1301        async fn fetch_open_orders(
1302            &self,
1303            _: &[InstrumentNameExchange],
1304        ) -> Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError>
1305        {
1306            Ok(self.open.clone())
1307        }
1308
1309        async fn fetch_trades(
1310            &self,
1311            _: DateTime<Utc>,
1312            _: DateTime<Utc>,
1313            _: &[InstrumentNameExchange],
1314        ) -> Result<TradesRead<AssetNameExchange, InstrumentNameExchange>, UnindexedClientError>
1315        {
1316            unreachable!("not a lookup")
1317        }
1318    }
1319
1320    impl OrderStatusClient for ListingClient {
1321        async fn fetch_ended_orders(
1322            &self,
1323            _: &[UnindexedOrderKey],
1324        ) -> Result<Vec<UnindexedInactiveOrder>, UnindexedClientError> {
1325            Ok(self.ended.clone())
1326        }
1327    }
1328
1329    /// An order under `cid` on `instrument`, as a venue lists it: with no strategy.
1330    fn listed<State>(
1331        instrument: &str,
1332        cid: &str,
1333        quantity: rust_decimal::Decimal,
1334        state: State,
1335    ) -> Order<ExchangeId, InstrumentNameExchange, State> {
1336        Order {
1337            key: crate::order::OrderKey {
1338                exchange: ExchangeId::Simulated,
1339                instrument: InstrumentNameExchange::new(instrument),
1340                strategy: crate::order::id::StrategyId::unknown(),
1341                cid: crate::order::id::ClientOrderId::new(cid),
1342            },
1343            side: Side::Buy,
1344            price: None,
1345            quantity,
1346            kind: crate::order::OrderKind::Market,
1347            time_in_force: crate::order::TimeInForce::ImmediateOrCancel,
1348            state,
1349        }
1350    }
1351
1352    fn open_state() -> Open {
1353        Open::new(
1354            crate::order::id::VenueOrderId::Assigned(crate::order::id::OrderId::new("v")),
1355            DateTime::<Utc>::MIN_UTC,
1356            rust_decimal::Decimal::ZERO,
1357        )
1358    }
1359
1360    fn cancelled_state() -> crate::order::state::UnindexedInactiveOrderState {
1361        crate::order::state::InactiveOrderState::Cancelled(crate::order::state::Cancelled::new(
1362            crate::order::id::OrderId::new("v"),
1363            DateTime::<Utc>::MIN_UTC,
1364            None,
1365        ))
1366    }
1367
1368    /// The provided lookup picks the order under the asked client order id and instrument out of
1369    /// what each read lists, ended first, and reports it under the asked key.
1370    #[tokio::test]
1371    async fn fetch_order_status_finds_the_order_asked_about_under_its_key() {
1372        use rust_decimal::Decimal;
1373
1374        let key = UnindexedOrderKey {
1375            exchange: ExchangeId::Simulated,
1376            instrument: InstrumentNameExchange::new("btc_usdt"),
1377            strategy: crate::order::id::StrategyId::new("asked"),
1378            cid: crate::order::id::ClientOrderId::new("target"),
1379        };
1380        // Decoys come first: another order on the instrument, and the id on another instrument.
1381        let open = vec![
1382            listed("btc_usdt", "other", Decimal::ONE, open_state()),
1383            listed("eth_usdt", "target", Decimal::TWO, open_state()),
1384            listed("btc_usdt", "target", Decimal::TEN, open_state()),
1385        ];
1386
1387        let live = ListingClient {
1388            ended: Vec::new(),
1389            open: open.clone(),
1390        };
1391        match live.fetch_order_status(&key).await {
1392            Ok(VenueOrderStatus::Live(order)) => {
1393                assert_eq!(order.key, key, "reported under the asked key");
1394                assert_eq!(order.quantity, Decimal::TEN, "the asked order: {order:?}");
1395            }
1396            other => panic!("expected live, got {other:?}"),
1397        }
1398
1399        let ended = ListingClient {
1400            ended: vec![
1401                listed("btc_usdt", "other", Decimal::ONE, cancelled_state()),
1402                listed("btc_usdt", "target", Decimal::TEN, cancelled_state()),
1403            ],
1404            open,
1405        };
1406        match ended.fetch_order_status(&key).await {
1407            Ok(VenueOrderStatus::Ended(order)) => {
1408                assert_eq!(order.key, key, "reported under the asked key");
1409                assert_eq!(order.quantity, Decimal::TEN, "the asked order: {order:?}");
1410            }
1411            other => panic!("expected ended, got {other:?}"),
1412        }
1413
1414        let unknown = ListingClient {
1415            ended: vec![listed("btc_usdt", "other", Decimal::ONE, cancelled_state())],
1416            open: vec![
1417                listed("btc_usdt", "other", Decimal::ONE, open_state()),
1418                listed("eth_usdt", "target", Decimal::TWO, open_state()),
1419            ],
1420        };
1421        assert_eq!(
1422            unknown.fetch_order_status(&key).await,
1423            Ok(VenueOrderStatus::Unknown)
1424        );
1425    }
1426}