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}