finance_query/streaming/
book.rs1use std::sync::Arc;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use super::client::StreamResult;
12use super::handle::{RECONNECT_BACKOFF, SourceStream, stream_builder, stream_handle};
13use super::polygon::PolygonBookSource;
14use super::source::ReconnectConfig;
15
16const CHANNEL_CAPACITY: usize = 1024;
18
19#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22#[non_exhaustive]
23pub struct BookLevel {
24 pub price: f64,
26 pub size: f64,
28}
29
30#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33#[non_exhaustive]
34pub struct OrderBookUpdate {
35 pub symbol: String,
37 pub bids: Vec<BookLevel>,
39 pub asks: Vec<BookLevel>,
41 pub exchange: Option<i32>,
43 pub time: i64,
45}
46
47impl OrderBookUpdate {
48 pub fn best_bid(&self) -> Option<BookLevel> {
50 self.bids.first().copied()
51 }
52
53 pub fn best_ask(&self) -> Option<BookLevel> {
55 self.asks.first().copied()
56 }
57
58 pub fn spread(&self) -> Option<f64> {
60 Some(self.best_ask()?.price - self.best_bid()?.price)
61 }
62
63 pub fn mid(&self) -> Option<f64> {
65 Some((self.best_ask()?.price + self.best_bid()?.price) / 2.0)
66 }
67
68 pub fn depth(&self) -> (f64, f64) {
70 (
71 self.bids.iter().map(|l| l.size).sum(),
72 self.asks.iter().map(|l| l.size).sum(),
73 )
74 }
75}
76
77stream_handle! {
78 DepthStream(OrderBookUpdate);
100 add: add_pairs = "Add pairs to the subscription.",
101 remove: remove_pairs = "Remove pairs from the subscription.",
102}
103
104impl DepthStream {
105 pub async fn subscribe<S, I>(pairs: I) -> StreamResult<Self>
107 where
108 S: Into<String>,
109 I: IntoIterator<Item = S>,
110 {
111 DepthStreamBuilder::new().pairs(pairs).build().await
112 }
113}
114
115pub struct DepthStreamBuilder {
117 pairs: Vec<String>,
118 retry_delay: Duration,
119 max_reconnect_attempts: Option<u32>,
120}
121
122impl DepthStreamBuilder {
123 pub fn new() -> Self {
125 Self {
126 pairs: Vec::new(),
127 retry_delay: RECONNECT_BACKOFF,
128 max_reconnect_attempts: None,
129 }
130 }
131
132 pub async fn build(self) -> StreamResult<DepthStream> {
134 let reconnect =
135 ReconnectConfig::new(self.retry_delay).max_attempts(self.max_reconnect_attempts);
136 Ok(DepthStream {
137 inner: SourceStream::start(
138 Arc::new(PolygonBookSource),
139 self.pairs,
140 reconnect,
141 CHANNEL_CAPACITY,
142 ),
143 })
144 }
145}
146
147stream_builder!(
148 DepthStreamBuilder,
149 pairs = "Add crypto pairs to subscribe to."
150);
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 fn book() -> OrderBookUpdate {
157 OrderBookUpdate {
158 symbol: "BTC-USD".into(),
159 bids: vec![
160 BookLevel {
161 price: 100.0,
162 size: 2.0,
163 },
164 BookLevel {
165 price: 99.0,
166 size: 3.0,
167 },
168 ],
169 asks: vec![
170 BookLevel {
171 price: 101.0,
172 size: 1.0,
173 },
174 BookLevel {
175 price: 102.0,
176 size: 4.0,
177 },
178 ],
179 ..Default::default()
180 }
181 }
182
183 #[test]
184 fn top_of_book_helpers_use_the_first_level() {
185 let book = book();
186 assert_eq!(book.best_bid().unwrap().price, 100.0);
187 assert_eq!(book.best_ask().unwrap().price, 101.0);
188 assert!((book.spread().unwrap() - 1.0).abs() < 1e-9);
189 assert!((book.mid().unwrap() - 100.5).abs() < 1e-9);
190 }
191
192 #[test]
193 fn depth_sums_each_side() {
194 let (bid_depth, ask_depth) = book().depth();
195 assert!((bid_depth - 5.0).abs() < 1e-9);
196 assert!((ask_depth - 5.0).abs() < 1e-9);
197 }
198
199 #[test]
200 fn an_empty_side_has_no_spread() {
201 let empty = OrderBookUpdate::default();
202 assert!(empty.spread().is_none());
203 assert!(empty.mid().is_none());
204 }
205}