Skip to main content

rustrade_execution/client/
mod.rs

1use crate::{
2    UnindexedAccountEvent, UnindexedAccountSnapshot,
3    balance::AssetBalance,
4    error::UnindexedClientError,
5    order::{
6        Order,
7        request::{OrderRequestCancel, OrderRequestOpen, UnindexedOrderResponseCancel},
8        state::{Open, UnindexedOrderState},
9    },
10    trade::Trade,
11};
12use chrono::{DateTime, Utc};
13use futures::Stream;
14use rustrade_instrument::{
15    asset::name::AssetNameExchange, exchange::ExchangeId, instrument::name::InstrumentNameExchange,
16};
17use std::future::Future;
18
19// Alpaca ExecutionClient implementation (options, equities, crypto — single unified API)
20#[cfg(feature = "alpaca")]
21pub mod alpaca;
22
23// BinanceSpot ExecutionClient implementation
24#[cfg(feature = "binance")]
25pub mod binance;
26
27// Hyperliquid perpetual futures and spot ExecutionClient implementations
28#[cfg(feature = "hyperliquid")]
29pub mod hyperliquid;
30
31// Interactive Brokers ExecutionClient implementation (equities, futures, options, forex)
32#[cfg(feature = "ibkr")]
33pub mod ibkr;
34
35pub mod mock;
36
37// `+ Send` bounds on async method return types required for multi-threaded
38// Tokio runtime. This is a breaking change vs upstream — any `!Send` executor
39// implementation would fail to compile.
40pub trait ExecutionClient
41where
42    Self: Clone,
43{
44    const EXCHANGE: ExchangeId;
45
46    type Config: Clone;
47    // `+ Send` required so generic code (e.g. ExecutionManager) can pass
48    // the stream to tokio::spawn, which requires Send.
49    type AccountStream: Stream<Item = UnindexedAccountEvent> + Send;
50
51    fn new(config: Self::Config) -> Self;
52
53    fn account_snapshot(
54        &self,
55        assets: &[AssetNameExchange],
56        instruments: &[InstrumentNameExchange],
57    ) -> impl Future<Output = Result<UnindexedAccountSnapshot, UnindexedClientError>> + Send;
58
59    /// Returns a live stream of account events (fills, order updates, balance changes).
60    ///
61    /// # Startup race window
62    ///
63    /// There is an unavoidable gap between the WebSocket subscribe response and the
64    /// first event being delivered: fills arriving in this window (typically milliseconds,
65    /// no sub-millisecond guarantee) are silently dropped. `account_snapshot` reconciles
66    /// open-order state, but TRADE fills in this window are not recoverable from the stream
67    /// alone. Callers that require fill completeness at startup **must** call
68    /// [`ExecutionClient::fetch_trades`] with at least a 1-second lookback after this method returns.
69    ///
70    /// # Backpressure
71    ///
72    /// Implementations use unbounded internal channels. If the consumer cannot keep up,
73    /// events queue in memory rather than being dropped — per library philosophy, OOM
74    /// crashes are preferable to silent data loss. Consumers requiring backpressure
75    /// should implement it at their boundary (e.g., bounded channel with overflow policy).
76    fn account_stream(
77        &self,
78        assets: &[AssetNameExchange],
79        instruments: &[InstrumentNameExchange],
80    ) -> impl Future<Output = Result<Self::AccountStream, UnindexedClientError>> + Send;
81
82    fn cancel_order(
83        &self,
84        request: OrderRequestCancel<ExchangeId, &InstrumentNameExchange>,
85    ) -> impl Future<Output = Option<UnindexedOrderResponseCancel>> + Send;
86
87    // `+ Send` on default method return types for multi-threaded Tokio runtime
88    fn cancel_orders<'a>(
89        &self,
90        requests: impl IntoIterator<Item = OrderRequestCancel<ExchangeId, &'a InstrumentNameExchange>>,
91    ) -> impl Stream<Item = Option<UnindexedOrderResponseCancel>> + Send {
92        futures::stream::FuturesUnordered::from_iter(
93            requests
94                .into_iter()
95                .map(|request| self.cancel_order(request)),
96        )
97    }
98
99    /// Place an order on the exchange.
100    ///
101    /// # Return value
102    ///
103    /// Returns `OrderState` directly rather than `Result<Open, OrderError>`:
104    /// - `OrderState::Active(Open)` - order is resting on the order book
105    /// - `OrderState::Inactive(FullyFilled)` - order was immediately filled (includes `avg_price` when available)
106    /// - `OrderState::Inactive(OpenFailed)` - order placement failed (API error, connectivity, etc.)
107    ///
108    /// This design allows immediate fills to carry metadata (e.g., `avg_price`) that
109    /// would be lost if we had to infer terminal state from `Open::filled_quantity`.
110    fn open_order(
111        &self,
112        request: OrderRequestOpen<ExchangeId, &InstrumentNameExchange>,
113    ) -> impl Future<Output = Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>>
114    + Send;
115
116    // `+ Send` on default method return types for multi-threaded Tokio runtime
117    fn open_orders<'a>(
118        &self,
119        requests: impl IntoIterator<Item = OrderRequestOpen<ExchangeId, &'a InstrumentNameExchange>>,
120    ) -> impl Stream<Item = Option<Order<ExchangeId, InstrumentNameExchange, UnindexedOrderState>>> + Send
121    {
122        futures::stream::FuturesUnordered::from_iter(
123            requests.into_iter().map(|request| self.open_order(request)),
124        )
125    }
126
127    /// Fetch current balances for the specified assets.
128    ///
129    /// An empty `assets` slice is the "return all" sentinel: implementations must return
130    /// balances for every asset held. When non-empty, only the listed assets are returned.
131    fn fetch_balances(
132        &self,
133        assets: &[AssetNameExchange],
134    ) -> impl Future<Output = Result<Vec<AssetBalance<AssetNameExchange>>, UnindexedClientError>> + Send;
135
136    /// Fetch currently open orders, optionally filtered by instrument.
137    ///
138    /// An empty `instruments` slice is the "return all" sentinel: implementations must
139    /// return open orders across all instruments. When non-empty, only orders for the
140    /// listed instruments are returned.
141    fn fetch_open_orders(
142        &self,
143        instruments: &[InstrumentNameExchange],
144    ) -> impl Future<
145        Output = Result<Vec<Order<ExchangeId, InstrumentNameExchange, Open>>, UnindexedClientError>,
146    > + Send;
147
148    /// Fetch trades (fills) since `time_since`, optionally filtered by instrument.
149    ///
150    /// An empty `instruments` slice is the "return all" sentinel: implementations must
151    /// return trades across all instruments. When non-empty, only trades for the listed
152    /// instruments are returned.
153    ///
154    /// The fee asset (`AssetNameExchange`) may be quote, base, or third-party (e.g., BNB).
155    /// Use `fees.fees_quote` for quote-equivalent value when available.
156    ///
157    /// Note: `MockExecution` currently ignores `instruments` and always returns all trades.
158    fn fetch_trades(
159        &self,
160        time_since: DateTime<Utc>,
161        instruments: &[InstrumentNameExchange],
162    ) -> impl Future<
163        Output = Result<
164            Vec<Trade<AssetNameExchange, InstrumentNameExchange>>,
165            UnindexedClientError,
166        >,
167    > + Send;
168}