use std::sync::Arc;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use super::client::StreamResult;
use super::handle::{RECONNECT_BACKOFF, SourceStream, stream_builder, stream_handle};
use super::polygon::PolygonBookSource;
use super::source::ReconnectConfig;
const CHANNEL_CAPACITY: usize = 1024;
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct BookLevel {
pub price: f64,
pub size: f64,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct OrderBookUpdate {
pub symbol: String,
pub bids: Vec<BookLevel>,
pub asks: Vec<BookLevel>,
pub exchange: Option<i32>,
pub time: i64,
}
impl OrderBookUpdate {
pub fn best_bid(&self) -> Option<BookLevel> {
self.bids.first().copied()
}
pub fn best_ask(&self) -> Option<BookLevel> {
self.asks.first().copied()
}
pub fn spread(&self) -> Option<f64> {
Some(self.best_ask()?.price - self.best_bid()?.price)
}
pub fn mid(&self) -> Option<f64> {
Some((self.best_ask()?.price + self.best_bid()?.price) / 2.0)
}
pub fn depth(&self) -> (f64, f64) {
(
self.bids.iter().map(|l| l.size).sum(),
self.asks.iter().map(|l| l.size).sum(),
)
}
}
stream_handle! {
DepthStream(OrderBookUpdate);
add: add_pairs = "Add pairs to the subscription.",
remove: remove_pairs = "Remove pairs from the subscription.",
}
impl DepthStream {
pub async fn subscribe<S, I>(pairs: I) -> StreamResult<Self>
where
S: Into<String>,
I: IntoIterator<Item = S>,
{
DepthStreamBuilder::new().pairs(pairs).build().await
}
}
pub struct DepthStreamBuilder {
pairs: Vec<String>,
retry_delay: Duration,
max_reconnect_attempts: Option<u32>,
}
impl DepthStreamBuilder {
pub fn new() -> Self {
Self {
pairs: Vec::new(),
retry_delay: RECONNECT_BACKOFF,
max_reconnect_attempts: None,
}
}
pub async fn build(self) -> StreamResult<DepthStream> {
let reconnect =
ReconnectConfig::new(self.retry_delay).max_attempts(self.max_reconnect_attempts);
Ok(DepthStream {
inner: SourceStream::start(
Arc::new(PolygonBookSource),
self.pairs,
reconnect,
CHANNEL_CAPACITY,
),
})
}
}
stream_builder!(
DepthStreamBuilder,
pairs = "Add crypto pairs to subscribe to."
);
#[cfg(test)]
mod tests {
use super::*;
fn book() -> OrderBookUpdate {
OrderBookUpdate {
symbol: "BTC-USD".into(),
bids: vec![
BookLevel {
price: 100.0,
size: 2.0,
},
BookLevel {
price: 99.0,
size: 3.0,
},
],
asks: vec![
BookLevel {
price: 101.0,
size: 1.0,
},
BookLevel {
price: 102.0,
size: 4.0,
},
],
..Default::default()
}
}
#[test]
fn top_of_book_helpers_use_the_first_level() {
let book = book();
assert_eq!(book.best_bid().unwrap().price, 100.0);
assert_eq!(book.best_ask().unwrap().price, 101.0);
assert!((book.spread().unwrap() - 1.0).abs() < 1e-9);
assert!((book.mid().unwrap() - 100.5).abs() < 1e-9);
}
#[test]
fn depth_sums_each_side() {
let (bid_depth, ask_depth) = book().depth();
assert!((bid_depth - 5.0).abs() < 1e-9);
assert!((ask_depth - 5.0).abs() < 1e-9);
}
#[test]
fn an_empty_side_has_no_spread() {
let empty = OrderBookUpdate::default();
assert!(empty.spread().is_none());
assert!(empty.mid().is_none());
}
}