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