rustrade_execution/lib.rs
1#![forbid(unsafe_code)]
2#![deny(
3 clippy::unwrap_used,
4 clippy::expect_used,
5 clippy::cast_possible_truncation,
6 clippy::cast_sign_loss
7)]
8#![warn(
9 unused,
10 clippy::cognitive_complexity,
11 unused_crate_dependencies,
12 unused_extern_crates,
13 clippy::unused_self,
14 clippy::useless_let_if_seq,
15 missing_debug_implementations,
16 rust_2018_idioms
17)]
18#![allow(clippy::type_complexity, clippy::too_many_arguments, type_alias_bounds)]
19
20//! # Barter-Execution
21//! Stream private account data from financial venues, and execute (live or mock) orders. Also provides
22//! a feature rich MockExchange and MockExecutionClient to assist with backtesting and paper-trading.
23//!
24//! **It is:**
25//! * **Easy**: ExecutionClient trait provides a unified and simple language for interacting with exchanges.
26//! * **Normalised**: Allow your strategy to communicate with every real or MockExchange using the same interface.
27//! * **Extensible**: Barter-Execution is highly extensible, making it easy to contribute by adding new exchange integrations!
28//!
29//! See `README.md` for more information and examples.
30
31// Silence unused_crate_dependencies for dev-dependencies used only in tests
32#[cfg(test)]
33use serial_test as _;
34#[cfg(test)]
35use tracing_subscriber as _;
36#[cfg(test)]
37use wiremock as _;
38
39use crate::{
40 balance::{AssetBalance, AssetBalanceUpdate},
41 order::{Order, OrderSnapshot, request::OrderResponseCancel},
42 position::Position,
43 trade::Trade,
44};
45use chrono::{DateTime, Utc};
46use derive_more::{Constructor, From};
47use order::state::OrderState;
48use rust_decimal::Decimal;
49use rustrade_instrument::{
50 asset::{AssetIndex, name::AssetNameExchange},
51 exchange::{ExchangeId, ExchangeIndex},
52 instrument::{InstrumentIndex, name::InstrumentNameExchange},
53};
54use rustrade_integration::collection::snapshot::Snapshot;
55use serde::{Deserialize, Serialize};
56
57pub mod balance;
58pub mod client;
59pub mod error;
60pub mod exchange;
61pub mod fee;
62pub use fee::{FeeModel, FeeModelConfig, PerContractFeeModel, PercentageFeeModel, ZeroFeeModel};
63pub mod fill;
64pub use fill::{BidAskFillModel, FillModel, LastPriceFillModel, MidpointFillModel, SimFillConfig};
65pub mod indexer;
66pub mod map;
67pub mod order;
68pub mod position;
69pub mod trade;
70
71/// Convenient type alias for an [`AccountEvent`] keyed with [`ExchangeId`],
72/// [`AssetNameExchange`], and [`InstrumentNameExchange`].
73pub type UnindexedAccountEvent =
74 AccountEvent<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
75
76/// Convenient type alias for an [`AccountSnapshot`] keyed with [`ExchangeId`],
77/// [`AssetNameExchange`], and [`InstrumentNameExchange`].
78pub type UnindexedAccountSnapshot =
79 AccountSnapshot<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
80
81#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
82pub struct AccountEvent<
83 ExchangeKey = ExchangeIndex,
84 AssetKey = AssetIndex,
85 InstrumentKey = InstrumentIndex,
86> {
87 pub exchange: ExchangeKey,
88 pub kind: AccountEventKind<ExchangeKey, AssetKey, InstrumentKey>,
89}
90
91impl<ExchangeKey, AssetKey, InstrumentKey> AccountEvent<ExchangeKey, AssetKey, InstrumentKey> {
92 pub fn new<K>(exchange: ExchangeKey, kind: K) -> Self
93 where
94 K: Into<AccountEventKind<ExchangeKey, AssetKey, InstrumentKey>>,
95 {
96 Self {
97 exchange,
98 kind: kind.into(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, From)]
104#[non_exhaustive]
105pub enum AccountEventKind<ExchangeKey, AssetKey, InstrumentKey> {
106 /// Full [`AccountSnapshot`] - replaces all existing state.
107 Snapshot(AccountSnapshot<ExchangeKey, AssetKey, InstrumentKey>),
108
109 /// Single [`AssetBalance`] snapshot - replaces existing balance state.
110 ///
111 /// Sourced from a REST account snapshot: carries the **full** balance including any per-asset
112 /// margin debt (`borrowed`/`interest`). This is the authoritative source for debt totals.
113 BalanceSnapshot(Snapshot<AssetBalance<AssetKey>>),
114
115 /// Single [`AssetBalanceUpdate`] - applies a WS partial (`free`/`locked` only).
116 ///
117 /// Sourced from an exchange WS user-data stream (e.g. Binance `outboundAccountPosition`). It
118 /// carries **no** margin debt, so applying it updates `free`/`locked` while **preserving** any
119 /// existing [`MarginDetails`](balance::MarginDetails) — debt cannot be silently clobbered by a
120 /// stream update. Debt totals remain as fresh as the last [`BalanceSnapshot`](Self::BalanceSnapshot).
121 BalanceStreamUpdate(Snapshot<AssetBalanceUpdate<AssetKey>>),
122
123 /// Live per-instrument isolated-margin balance update (`free`/`locked` per side).
124 ///
125 /// The stream counterpart of [`InstrumentAccountSnapshot::isolated`] for venues with per-pair
126 /// isolated sub-accounts (e.g. Binance isolated margin). Carries a point-in-time `free`/`locked`
127 /// **snapshot** for the pair's `base` and `quote` assets — NOT a delta — keyed by instrument
128 /// rather than asset, because isolated balances are per-`(pair, asset)` and cannot be folded
129 /// into the asset-keyed balance state without collision (see [`InstrumentBalanceUpdate`]).
130 ///
131 /// The engine deliberately does **not** store this (the per-asset balance state is informational
132 /// only and the engine never reads it for sizing/gating); a consumer reads it off the account
133 /// event feed. Debt totals stay as fresh as the last
134 /// [`BalanceSnapshot`](Self::BalanceSnapshot) per the debt-freshness contract.
135 InstrumentBalanceUpdate(InstrumentBalanceUpdate<AssetKey, InstrumentKey>),
136
137 /// Single [`Order`] snapshot - used to upsert existing order state if it's more recent.
138 ///
139 /// This variant covers general order updates, and open order responses.
140 OrderSnapshot(Snapshot<Order<ExchangeKey, InstrumentKey, OrderState<AssetKey, InstrumentKey>>>),
141
142 /// Response to an [`OrderRequestCancel<ExchangeKey, InstrumentKey>`](order::request::OrderRequestOpen).
143 OrderCancelled(OrderResponseCancel<ExchangeKey, AssetKey, InstrumentKey>),
144
145 /// [`Order<ExchangeKey, InstrumentKey, Open>`] partial or full-fill.
146 ///
147 /// The fee asset (`AssetKey`) may be the quote asset, base asset, or a third-party
148 /// asset (e.g., BNB on Binance). Use `fees.fees_quote` for quote-equivalent value
149 /// when available.
150 Trade(Trade<AssetKey, InstrumentKey>),
151
152 /// WebSocket-level error from exchange. Connection may have dropped.
153 ///
154 /// Implementations send this when the underlying stream encounters an error.
155 /// Consumers should treat this as a signal that events may have been missed
156 /// and consider re-syncing via REST (e.g., `fetch_trades`, `account_snapshot`).
157 StreamError(String),
158}
159
160impl<ExchangeKey, AssetKey, InstrumentKey> AccountEvent<ExchangeKey, AssetKey, InstrumentKey>
161where
162 AssetKey: Eq,
163 InstrumentKey: Eq,
164{
165 pub fn snapshot(self) -> Option<AccountSnapshot<ExchangeKey, AssetKey, InstrumentKey>> {
166 match self.kind {
167 AccountEventKind::Snapshot(snapshot) => Some(snapshot),
168 _ => None,
169 }
170 }
171}
172
173#[derive(
174 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
175)]
176pub struct AccountSnapshot<
177 ExchangeKey = ExchangeIndex,
178 AssetKey = AssetIndex,
179 InstrumentKey = InstrumentIndex,
180> {
181 pub exchange: ExchangeKey,
182 pub balances: Vec<AssetBalance<AssetKey>>,
183 pub instruments: Vec<InstrumentAccountSnapshot<ExchangeKey, AssetKey, InstrumentKey>>,
184}
185
186/// serde `default` for an `Option` field whose inner type is generic — returns `None` without
187/// requiring the inner type to be `Default` (see the `isolated` field below).
188fn none_option<T>() -> Option<T> {
189 None
190}
191
192#[derive(
193 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
194)]
195pub struct InstrumentAccountSnapshot<
196 ExchangeKey = ExchangeIndex,
197 AssetKey = AssetIndex,
198 InstrumentKey = InstrumentIndex,
199> {
200 pub instrument: InstrumentKey,
201 #[serde(default = "Vec::new")]
202 pub orders: Vec<OrderSnapshot<ExchangeKey, AssetKey, InstrumentKey>>,
203 /// Open position for derivative instruments (perpetuals, futures, margin).
204 /// `None` for spot instruments where position is implicit in balances.
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub position: Option<Position>,
207 /// Per-pair isolated-margin balances and risk, for venues with isolated sub-accounts
208 /// (e.g. Binance isolated margin). `None` for cross margin, spot, and all other contexts.
209 ///
210 /// Surfaced here — attached to the instrument — rather than folded into the asset-keyed
211 /// [`AccountSnapshot::balances`] because isolated sub-accounts are per-`(pair, asset)`: the same
212 /// asset (e.g. `USDT`) in two isolated pairs is a separate pool, which the asset-keyed balance
213 /// model cannot represent without collision. The engine does not store this; a consumer reads
214 /// it off the snapshot to compose per-pair risk. See [`IsolatedInstrumentState`].
215 ///
216 // `default = "none_option"` (not a bare `#[serde(default)]`) avoids serde inferring a spurious
217 // `AssetKey: Default` bound: a bare default on a generic-typed field conservatively requires the
218 // field type to be `Default` (the `position` field escapes this only because `Position` is
219 // concrete). Naming a function makes serde *call* it instead, requiring only `AssetKey: Deserialize`.
220 #[serde(default = "none_option", skip_serializing_if = "Option::is_none")]
221 pub isolated: Option<IsolatedInstrumentState<AssetKey>>,
222}
223
224/// Per-pair isolated-margin state attached to an [`InstrumentAccountSnapshot`].
225///
226/// Binance isolated margin (and other CEX isolated/per-pair sub-accounts) hold balances
227/// per-`(pair, asset)`, not per-asset, so they are surfaced attached to the instrument rather than
228/// in the asset-keyed [`AccountSnapshot::balances`]. A consumer composes per-pair risk from `base`,
229/// `quote`, and `risk`.
230#[derive(
231 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
232)]
233pub struct IsolatedInstrumentState<AssetKey = AssetIndex> {
234 /// Base-asset balance of the pair's isolated sub-account (carries per-asset debt).
235 pub base: AssetBalance<AssetKey>,
236 /// Quote-asset balance of the pair's isolated sub-account (carries per-asset debt).
237 pub quote: AssetBalance<AssetKey>,
238 /// Per-pair risk metrics — snapshot-fresh, not live (see [`IsolatedMarginRisk`]).
239 pub risk: IsolatedMarginRisk,
240}
241
242/// Per-pair isolated-margin risk metrics, surfaced on [`IsolatedInstrumentState`].
243///
244/// Every field is `Option` — a venue may omit any given metric, and a missing metric must not
245/// drop the surrounding balance snapshot.
246///
247/// # Freshness
248/// These are **snapshot-only**: authoritative as of the last `account_snapshot` and refreshed on
249/// snapshot. Unlike balances, there is **no live-stream twin** — the WS `outboundAccountPosition`
250/// frame carries no margin-level / liquidation data. The live signal for risk crossing a threshold
251/// is the venue's `marginLevelStatusChange` event (surfaced observably, not accumulated here).
252#[derive(
253 Debug,
254 Copy,
255 Clone,
256 PartialEq,
257 Eq,
258 PartialOrd,
259 Ord,
260 Hash,
261 Default,
262 Deserialize,
263 Serialize,
264 Constructor,
265)]
266pub struct IsolatedMarginRisk {
267 /// Margin level of the isolated pair (collateral-to-debt ratio); higher is safer.
268 pub margin_level: Option<Decimal>,
269 /// Margin ratio of the isolated pair.
270 pub margin_ratio: Option<Decimal>,
271 /// Estimated liquidation price for the isolated pair.
272 pub liquidation_price: Option<Decimal>,
273}
274
275/// Live per-instrument isolated-margin balance update payload (the
276/// [`AccountEventKind::InstrumentBalanceUpdate`] counterpart of [`IsolatedInstrumentState`]).
277///
278/// Carries a point-in-time `free`/`locked` **snapshot** for the pair's `base` and `quote` assets —
279/// NOT a delta — keyed by instrument. Structurally analogous to [`AssetBalanceUpdate`] but
280/// per-instrument: it carries no debt, so applying it keeps `free`/`locked` live while preserving
281/// any known per-asset debt (use [`Balance::apply_stream_update`](balance::Balance::apply_stream_update)).
282#[derive(
283 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
284)]
285pub struct InstrumentBalanceUpdate<AssetKey = AssetIndex, InstrumentKey = InstrumentIndex> {
286 /// Instrument (isolated pair) the update applies to.
287 pub instrument: InstrumentKey,
288 /// Base-asset `free`/`locked` update for the pair's isolated sub-account.
289 pub base: AssetBalanceUpdate<AssetKey>,
290 /// Quote-asset `free`/`locked` update for the pair's isolated sub-account.
291 pub quote: AssetBalanceUpdate<AssetKey>,
292}
293
294impl<ExchangeKey, AssetKey, InstrumentKey> AccountSnapshot<ExchangeKey, AssetKey, InstrumentKey> {
295 pub fn time_most_recent(&self) -> Option<DateTime<Utc>> {
296 let order_times = self.instruments.iter().flat_map(|instrument| {
297 instrument
298 .orders
299 .iter()
300 .filter_map(|order| order.state.time_exchange())
301 });
302 let balance_times = self.balances.iter().map(|balance| balance.time_exchange);
303
304 order_times.chain(balance_times).max()
305 }
306
307 pub fn assets(&self) -> impl Iterator<Item = &AssetKey> {
308 self.balances.iter().map(|balance| &balance.asset)
309 }
310
311 pub fn instruments(&self) -> impl Iterator<Item = &InstrumentKey> {
312 self.instruments.iter().map(|snapshot| &snapshot.instrument)
313 }
314}