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