use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tick {
pub ts: DateTime<Utc>,
pub price: f64,
pub size: f64,
pub side: Option<TradeSide>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TradeSide {
Buy,
Sell,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Level1Quote {
pub ts: DateTime<Utc>,
pub symbol: String,
pub bid: f64,
pub bid_size: f64,
pub ask: f64,
pub ask_size: f64,
}
impl Level1Quote {
pub fn spread(&self) -> f64 {
self.ask - self.bid
}
pub fn spread_bps(&self) -> f64 {
if self.bid == 0.0 {
return 0.0;
}
(self.spread() / self.bid) * 10000.0
}
pub fn mid_price(&self) -> f64 {
(self.bid + self.ask) / 2.0
}
pub fn total_size(&self) -> f64 {
self.bid_size + self.ask_size
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookLevel {
pub price: f64,
pub size: f64,
pub order_cnt: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBook {
pub ts: DateTime<Utc>,
pub symbol: String,
pub bids: Vec<OrderBookLevel>,
pub asks: Vec<OrderBookLevel>,
}
impl OrderBook {
pub fn best_bid(&self) -> Option<&OrderBookLevel> {
self.bids.first()
}
pub fn best_ask(&self) -> Option<&OrderBookLevel> {
self.asks.first()
}
pub fn to_level1(&self) -> Option<Level1Quote> {
let bid = self.best_bid()?;
let ask = self.best_ask()?;
Some(Level1Quote {
ts: self.ts,
symbol: self.symbol.clone(),
bid: bid.price,
bid_size: bid.size,
ask: ask.price,
ask_size: ask.size,
})
}
pub fn total_bid_volume(&self) -> f64 {
self.bids.iter().map(|level| level.size).sum()
}
pub fn total_ask_volume(&self) -> f64 {
self.asks.iter().map(|level| level.size).sum()
}
pub fn imbalance_ratio(&self) -> f64 {
let bid_vol = self.total_bid_volume();
let ask_vol = self.total_ask_volume();
let total = bid_vol + ask_vol;
if total == 0.0 {
return 0.5;
}
bid_vol / total
}
pub fn mid_price(&self) -> Option<f64> {
let bid = self.best_bid()?.price;
let ask = self.best_ask()?.price;
Some((bid + ask) / 2.0)
}
}
pub trait QuoteSource: Send + Sync {
fn subscribe_quotes(&mut self, symbol: String) -> Result<(), QuoteSourceError>;
fn unsubscribe_quotes(&mut self, symbol: String) -> Result<(), QuoteSourceError>;
fn poll_quotes(&mut self) -> Vec<Level1Quote>;
fn latest_quote(&self, symbol: &str) -> Option<&Level1Quote>;
}
pub trait OrderBookSource: Send + Sync {
fn subscribe_order_book(
&mut self,
symbol: String,
depth: usize,
) -> Result<(), QuoteSourceError>;
fn unsubscribe_order_book(&mut self, symbol: String) -> Result<(), QuoteSourceError>;
fn poll_order_books(&mut self) -> Vec<OrderBook>;
fn latest_order_book(&self, symbol: &str) -> Option<&OrderBook>;
}
pub trait TickSource: Send + Sync {
fn subscribe_ticks(&mut self, symbol: String) -> Result<(), QuoteSourceError>;
fn unsubscribe_ticks(&mut self, symbol: String) -> Result<(), QuoteSourceError>;
fn poll_ticks(&mut self) -> Vec<Tick>;
fn get_ticks(&self, symbol: &str, limit: usize) -> Vec<Tick>;
}
#[derive(Debug, Clone)]
pub enum QuoteSourceError {
InvalidSymbol(String),
ConnError(String),
SubscriptionLimitReached,
Other(String),
}
impl std::fmt::Display for QuoteSourceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidSymbol(s) => write!(f, "Invalid symbol: {s}"),
Self::ConnError(e) => write!(f, "Conn error: {e}"),
Self::SubscriptionLimitReached => write!(f, "Subscription limit reached"),
Self::Other(e) => write!(f, "Error: {e}"),
}
}
}
impl std::error::Error for QuoteSourceError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_level1_quote_calculations() {
let quote = Level1Quote {
ts: Utc::now(),
symbol: "BTCUSDT".to_string(),
bid: 50000.0,
bid_size: 1.5,
ask: 50010.0,
ask_size: 2.0,
};
assert_eq!(quote.spread(), 10.0);
assert_eq!(quote.mid_price(), 50005.0);
assert_eq!(quote.total_size(), 3.5);
assert!((quote.spread_bps() - 2.0).abs() < 0.01);
}
#[test]
fn test_order_book_imbalance() {
let order_book = OrderBook {
ts: Utc::now(),
symbol: "BTCUSDT".to_string(),
bids: vec![
OrderBookLevel {
price: 50000.0,
size: 3.0,
order_cnt: Some(5),
},
OrderBookLevel {
price: 49990.0,
size: 2.0,
order_cnt: Some(3),
},
],
asks: vec![
OrderBookLevel {
price: 50010.0,
size: 1.0,
order_cnt: Some(2),
},
OrderBookLevel {
price: 50020.0,
size: 1.0,
order_cnt: Some(1),
},
],
};
let imbalance = order_book.imbalance_ratio();
assert!((imbalance - 0.714).abs() < 0.01);
assert_eq!(order_book.total_bid_volume(), 5.0);
assert_eq!(order_book.total_ask_volume(), 2.0);
}
#[test]
fn test_order_book_to_level1() {
let order_book = OrderBook {
ts: Utc::now(),
symbol: "BTCUSDT".to_string(),
bids: vec![OrderBookLevel {
price: 50000.0,
size: 1.5,
order_cnt: None,
}],
asks: vec![OrderBookLevel {
price: 50010.0,
size: 2.0,
order_cnt: None,
}],
};
let level1 = order_book.to_level1().unwrap();
assert_eq!(level1.bid, 50000.0);
assert_eq!(level1.ask, 50010.0);
assert_eq!(level1.bid_size, 1.5);
assert_eq!(level1.ask_size, 2.0);
}
}