rustrade_data/subscription/book.rs
1use super::SubscriptionKind;
2use crate::books::{Level, OrderBook, mid_price, volume_weighted_mid_price};
3use chrono::{DateTime, Utc};
4use derive_more::Constructor;
5use rust_decimal::Decimal;
6use rustrade_macro::{DeSubKind, SerSubKind};
7use serde::{Deserialize, Serialize};
8
9/// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields [`OrderBookL1`]
10/// market events.
11///
12/// Level 1 refers to the best non-aggregated bid and ask [`Level`] on each side of the
13/// [`OrderBook`].
14#[derive(
15 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeSubKind, SerSubKind,
16)]
17pub struct OrderBooksL1;
18
19impl SubscriptionKind for OrderBooksL1 {
20 type Event = OrderBookL1;
21 fn as_str(&self) -> &'static str {
22 "l1"
23 }
24}
25
26impl std::fmt::Display for OrderBooksL1 {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{}", self.as_str())
29 }
30}
31
32/// Normalised Barter [`OrderBookL1`] snapshot containing the latest best bid and ask.
33#[derive(
34 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Default, Deserialize, Serialize, Constructor,
35)]
36pub struct OrderBookL1 {
37 /// The instant this book state describes — the venue's own, where the venue publishes one.
38 ///
39 /// # Producer obligation
40 ///
41 /// A producer **must** set this to the same instant it puts in the wrapping
42 /// [`MarketEvent::time_exchange`](crate::event::MarketEvent::time_exchange), and it **should**
43 /// come from the venue rather than from a local or aggregator clock.
44 ///
45 /// Downstream state orders L1 updates on this field, not on the event's, so the two must not
46 /// disagree. A producer that lets them diverge gets no error and no log: a staleness guard keyed
47 /// on this field can reject legitimately newer updates and freeze the held book.
48 ///
49 /// # Not every in-tree producer has a venue instant to use
50 ///
51 /// The "should" above is not a "must", because some venues publish no book timestamp:
52 ///
53 /// - **Binance Spot** `bookTicker` carries `{u, s, b, B, a, A}` and no time field, so
54 /// [`BinanceOrderBookL1`](crate::exchange::binance::book::l1::BinanceOrderBookL1) falls back
55 /// to the host clock at deserialisation. (Binance USD-M futures *does* send `T`, and that path
56 /// uses it.)
57 /// - **IBKR** quotes are assembled from separate bid/ask/size ticks, which carry no per-tick
58 /// venue instant, so `exchange::ibkr` (behind the `ibkr` feature, hence no link) stamps the
59 /// assembly time.
60 ///
61 /// Both fill the field from a local clock, and both keep it consistent with `time_exchange` as
62 /// required above. Consumers must therefore **not** rank this field against a *tick* timestamp
63 /// from another source — a trade's `time_exchange`, say — since the gap between them is
64 /// milliseconds and the comparison would be decided by skew rather than by the market. Ranking
65 /// it against a coarser instant, such as a candle's `close_time`, is sound while the gap stays
66 /// well above the skew. `DefaultInstrumentMarketData::price` in the `rustrade` crate does
67 /// exactly that, and documents where the margin runs out.
68 pub last_update_time: DateTime<Utc>,
69 pub best_bid: Option<Level>,
70 pub best_ask: Option<Level>,
71}
72
73impl OrderBookL1 {
74 /// Calculate the mid-price by taking the average of the best bid and ask prices.
75 ///
76 /// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
77 pub fn mid_price(&self) -> Option<Decimal> {
78 match (self.best_ask, self.best_bid) {
79 (Some(best_ask), Some(best_bid)) => Some(mid_price(best_bid.price, best_ask.price)),
80 _ => None,
81 }
82 }
83
84 /// Calculate the volume weighted mid-price (micro-price), weighing the best bid and ask prices
85 /// with their associated amount.
86 ///
87 /// `None` when either side is absent, and when both sides carry a **zero amount** — a feed that
88 /// publishes prices without sizes produces exactly that book, and the weighting is undefined for
89 /// it. Fall back to [`mid_price`](Self::mid_price) when a price is needed regardless of size.
90 ///
91 /// See Docs: <https://www.quantstart.com/articles/high-frequency-trading-ii-limit-order-book>
92 pub fn volume_weighed_mid_price(&self) -> Option<Decimal> {
93 match (self.best_ask, self.best_bid) {
94 (Some(best_ask), Some(best_bid)) => volume_weighted_mid_price(best_bid, best_ask),
95 _ => None,
96 }
97 }
98}
99
100/// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields L2
101/// [`OrderBookEvent`] market events
102///
103/// Level 2 refers to an [`OrderBook`] with orders at each price level aggregated.
104#[derive(
105 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeSubKind, SerSubKind,
106)]
107pub struct OrderBooksL2;
108
109impl SubscriptionKind for OrderBooksL2 {
110 type Event = OrderBookEvent;
111 fn as_str(&self) -> &'static str {
112 "l2"
113 }
114}
115
116impl std::fmt::Display for OrderBooksL2 {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{}", self.as_str())
119 }
120}
121
122/// Barter [`Subscription`](super::Subscription) [`SubscriptionKind`] that yields
123/// L3 [`OrderBookEvent`] market events.
124///
125/// Level 3 refers to the non-aggregated [`OrderBook`]. This is a direct replication of the exchange
126/// [`OrderBook`].
127#[derive(
128 Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, DeSubKind, SerSubKind,
129)]
130pub struct OrderBooksL3;
131
132impl SubscriptionKind for OrderBooksL3 {
133 type Event = OrderBookEvent;
134
135 fn as_str(&self) -> &'static str {
136 "l3"
137 }
138}
139
140impl std::fmt::Display for OrderBooksL3 {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 write!(f, "{}", self.as_str())
143 }
144}
145
146#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
147pub enum OrderBookEvent {
148 Snapshot(OrderBook),
149 Update(OrderBook),
150}