1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
//! Market-data and account streaming types.
//!
//! These are the items a pull-based stream yields from [`poll_events`]. Prices
//! and quantities are exact [`Decimal`]s; the order-book types carry a sequence
//! id so the local builder can apply diffs and detect gaps. They are all
//! `serde`-serializable, which is what the replay-parity corpus pins across the
//! language bindings.
//!
//! [`poll_events`]: crate::MarketData::poll_events
use crate::symbol::Symbol;
use crate::types::{Balance, Order, OrderSide, Ticker};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
/// A single resting price level (exchange-precision `Decimal`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BookLevel {
/// Price of the level.
pub price: Decimal,
/// Resting quantity at this price. A quantity of zero in a [`BookDelta`]
/// means the level was removed.
pub quantity: Decimal,
}
impl BookLevel {
/// Construct a level.
#[must_use]
pub fn new(price: Decimal, quantity: Decimal) -> Self {
Self { price, quantity }
}
}
/// A depth snapshot with exact levels, best-first on each side.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OrderBookSnapshot {
/// The market.
pub symbol: Symbol,
/// The venue's sequence id for this snapshot (for diff alignment).
pub last_update_id: u64,
/// Bid levels, best (highest price) first.
pub bids: Vec<BookLevel>,
/// Ask levels, best (lowest price) first.
pub asks: Vec<BookLevel>,
/// Venue timestamp (milliseconds since the Unix epoch), or `0` when the
/// venue did not supply one.
///
/// Without this a consumer cannot tell how old a quote is, which is the
/// difference between acting on the market and acting on a memory of it. A
/// venue that publishes no timestamp reports `0` rather than the local
/// clock: a locally-stamped quote looks fresh by construction, which is the
/// one thing a staleness check must never be told.
pub timestamp: i64,
}
impl OrderBookSnapshot {
/// The best (highest-price) bid, or `None` if the bid side is empty.
#[must_use]
pub fn best_bid(&self) -> Option<&BookLevel> {
self.bids.first()
}
/// The best (lowest-price) ask, or `None` if the ask side is empty.
#[must_use]
pub fn best_ask(&self) -> Option<&BookLevel> {
self.asks.first()
}
/// The mid price `(best_bid + best_ask) / 2`, or `None` if either side is empty.
#[must_use]
pub fn mid_price(&self) -> Option<Decimal> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some((bid.price + ask.price) / Decimal::from(2)),
_ => None,
}
}
/// The bid/ask spread `best_ask - best_bid`, or `None` if either side is empty.
#[must_use]
pub fn spread(&self) -> Option<Decimal> {
match (self.best_bid(), self.best_ask()) {
(Some(bid), Some(ask)) => Some(ask.price - bid.price),
_ => None,
}
}
}
/// A depth diff to apply to a locally maintained order book.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BookDelta {
/// The market.
pub symbol: Symbol,
/// The first sequence id covered by this diff.
pub first_update_id: u64,
/// The last sequence id covered by this diff.
pub final_update_id: u64,
/// Changed bid levels (quantity zero = remove).
pub bids: Vec<BookLevel>,
/// Changed ask levels (quantity zero = remove).
pub asks: Vec<BookLevel>,
/// Venue timestamp (milliseconds since the Unix epoch), or `0` when the
/// venue did not supply one.
///
/// Without this a consumer cannot tell how old a quote is, which is the
/// difference between acting on the market and acting on a memory of it. A
/// venue that publishes no timestamp reports `0` rather than the local
/// clock: a locally-stamped quote looks fresh by construction, which is the
/// one thing a staleness check must never be told.
pub timestamp: i64,
}
/// A single executed public trade.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TradePrint {
/// The market.
pub symbol: Symbol,
/// Execution price.
pub price: Decimal,
/// Executed quantity.
pub quantity: Decimal,
/// The taker (aggressor) side.
pub aggressor: OrderSide,
/// Venue timestamp (milliseconds since the Unix epoch).
pub timestamp: i64,
}
/// An item yielded by a pull-based stream.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Event {
/// A public trade print.
Trade(TradePrint),
/// A ticker update.
Ticker(Ticker),
/// A full order-book snapshot (e.g. after subscribe or resync).
BookSnapshot(OrderBookSnapshot),
/// An incremental order-book diff.
BookDelta(BookDelta),
/// An update to one of the account's orders (user-data stream).
OrderUpdate(Order),
/// An account balance update (user-data stream).
BalanceUpdate(Vec<Balance>),
/// A subscription was acknowledged by the venue.
Subscribed {
/// The channel that was subscribed.
channel: String,
},
/// The stream disconnected; the client will attempt to reconnect.
Disconnected,
/// The stream reconnected and resubscribed.
Reconnected,
/// A derivatives microstructure print: funding, mark/index, or a forced
/// liquidation. Arrives only on a futures client that subscribed to the
/// channel through
/// [`DerivativesStream`](crate::traits::DerivativesStream).
Derivatives(crate::feeds::DerivativesFeed),
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn book() -> OrderBookSnapshot {
OrderBookSnapshot {
symbol: Symbol::new("BTC", "USDT"),
last_update_id: 42,
bids: vec![
BookLevel::new(dec!(100), dec!(1)),
BookLevel::new(dec!(99), dec!(2)),
],
asks: vec![
BookLevel::new(dec!(101), dec!(1)),
BookLevel::new(dec!(102), dec!(3)),
],
timestamp: 0,
}
}
#[test]
fn book_best_levels_mid_and_spread() {
let b = book();
assert_eq!(b.best_bid(), Some(&BookLevel::new(dec!(100), dec!(1))));
assert_eq!(b.best_ask(), Some(&BookLevel::new(dec!(101), dec!(1))));
assert_eq!(b.mid_price(), Some(dec!(100.5)));
assert_eq!(b.spread(), Some(dec!(1)));
}
#[test]
fn empty_sides_yield_no_mid_or_spread() {
let empty = OrderBookSnapshot {
symbol: Symbol::new("BTC", "USDT"),
last_update_id: 0,
bids: vec![],
asks: vec![],
timestamp: 0,
};
assert!(empty.best_bid().is_none());
assert!(empty.best_ask().is_none());
assert!(empty.mid_price().is_none());
assert!(empty.spread().is_none());
let bid_only = OrderBookSnapshot {
asks: vec![],
..book()
};
assert!(bid_only.mid_price().is_none());
assert!(bid_only.spread().is_none());
}
#[test]
fn event_is_tagged_in_json() {
let event = Event::Trade(TradePrint {
symbol: Symbol::new("BTC", "USDT"),
price: dec!(20000),
quantity: dec!(0.5),
aggressor: OrderSide::Buy,
timestamp: 1_700_000_000_000,
});
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"type\":\"trade\""));
assert_eq!(serde_json::from_str::<Event>(&json).unwrap(), event);
}
#[test]
fn lifecycle_events_round_trip() {
for event in [Event::Disconnected, Event::Reconnected] {
let json = serde_json::to_string(&event).unwrap();
assert_eq!(serde_json::from_str::<Event>(&json).unwrap(), event);
}
let sub = Event::Subscribed {
channel: "btcusdt@trade".into(),
};
let json = serde_json::to_string(&sub).unwrap();
assert_eq!(serde_json::from_str::<Event>(&json).unwrap(), sub);
}
}