work on new engine

This commit is contained in:
Boki 2025-07-04 11:24:27 -04:00
parent 44476da13f
commit a1e5a21847
126 changed files with 3425 additions and 6695 deletions

View file

@ -0,0 +1,374 @@
use crate::{OrderBookSnapshot, PriceLevel};
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookAnalytics {
pub spread: f64,
pub spread_bps: f64,
pub mid_price: f64,
pub micro_price: f64, // Size-weighted mid price
pub imbalance: f64, // -1 to 1 (negative = bid pressure)
pub depth_imbalance: OrderBookImbalance,
pub liquidity_score: f64,
pub effective_spread: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookImbalance {
pub level_1: f64,
pub level_5: f64,
pub level_10: f64,
pub weighted: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidityProfile {
pub bid_liquidity: Vec<LiquidityLevel>,
pub ask_liquidity: Vec<LiquidityLevel>,
pub total_bid_depth: f64,
pub total_ask_depth: f64,
pub bid_depth_weighted_price: f64,
pub ask_depth_weighted_price: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiquidityLevel {
pub price: f64,
pub size: f64,
pub cumulative_size: f64,
pub cost_to_execute: f64, // Cost to buy/sell up to this level
}
impl OrderBookAnalytics {
pub fn calculate(snapshot: &OrderBookSnapshot) -> Option<Self> {
if snapshot.bids.is_empty() || snapshot.asks.is_empty() {
return None;
}
let best_bid = snapshot.bids[0].price;
let best_ask = snapshot.asks[0].price;
let spread = best_ask - best_bid;
let mid_price = (best_bid + best_ask) / 2.0;
let spread_bps = (spread / mid_price) * 10000.0;
// Calculate micro price (size-weighted)
let bid_size = snapshot.bids[0].size;
let ask_size = snapshot.asks[0].size;
let micro_price = (best_bid * ask_size + best_ask * bid_size) / (bid_size + ask_size);
// Calculate imbalance
let imbalance = (bid_size - ask_size) / (bid_size + ask_size);
// Calculate depth imbalance at different levels
let depth_imbalance = Self::calculate_depth_imbalance(snapshot);
// Calculate liquidity score
let liquidity_score = Self::calculate_liquidity_score(snapshot);
// Effective spread (considers depth)
let effective_spread = Self::calculate_effective_spread(snapshot, 1000.0); // $1000 order
Some(OrderBookAnalytics {
spread,
spread_bps,
mid_price,
micro_price,
imbalance,
depth_imbalance,
liquidity_score,
effective_spread,
})
}
fn calculate_depth_imbalance(snapshot: &OrderBookSnapshot) -> OrderBookImbalance {
let calc_imbalance = |depth: usize| -> f64 {
let bid_depth: f64 = snapshot.bids.iter()
.take(depth)
.map(|l| l.size)
.sum();
let ask_depth: f64 = snapshot.asks.iter()
.take(depth)
.map(|l| l.size)
.sum();
if bid_depth + ask_depth > 0.0 {
(bid_depth - ask_depth) / (bid_depth + ask_depth)
} else {
0.0
}
};
// Weighted imbalance (more weight on top levels)
let mut weighted_bid = 0.0;
let mut weighted_ask = 0.0;
let mut weight_sum = 0.0;
for (i, (bid, ask)) in snapshot.bids.iter().zip(snapshot.asks.iter()).enumerate().take(10) {
let weight = 1.0 / (i + 1) as f64;
weighted_bid += bid.size * weight;
weighted_ask += ask.size * weight;
weight_sum += weight;
}
let weighted = if weighted_bid + weighted_ask > 0.0 {
(weighted_bid - weighted_ask) / (weighted_bid + weighted_ask)
} else {
0.0
};
OrderBookImbalance {
level_1: calc_imbalance(1),
level_5: calc_imbalance(5),
level_10: calc_imbalance(10),
weighted,
}
}
fn calculate_liquidity_score(snapshot: &OrderBookSnapshot) -> f64 {
// Liquidity score based on depth and tightness
let depth_score = (snapshot.bids.len() + snapshot.asks.len()) as f64 / 20.0; // Normalize by 10 levels each side
let volume_score = {
let bid_volume: f64 = snapshot.bids.iter().take(5).map(|l| l.size).sum();
let ask_volume: f64 = snapshot.asks.iter().take(5).map(|l| l.size).sum();
((bid_volume + ask_volume) / 10000.0).min(1.0) // Normalize by $10k
};
let spread_score = if let (Some(bid), Some(ask)) = (snapshot.bids.first(), snapshot.asks.first()) {
let spread_bps = ((ask.price - bid.price) / ((ask.price + bid.price) / 2.0)) * 10000.0;
(50.0 / (spread_bps + 1.0)).min(1.0) // Lower spread = higher score
} else {
0.0
};
(depth_score * 0.3 + volume_score * 0.4 + spread_score * 0.3).min(1.0)
}
fn calculate_effective_spread(snapshot: &OrderBookSnapshot, order_size_usd: f64) -> f64 {
let avg_execution_price = |levels: &[PriceLevel], size_usd: f64, is_buy: bool| -> Option<f64> {
let mut remaining = size_usd;
let mut total_cost = 0.0;
let mut total_shares = 0.0;
for level in levels {
let level_value = level.price * level.size;
if remaining <= level_value {
let shares = remaining / level.price;
total_cost += remaining;
total_shares += shares;
break;
} else {
total_cost += level_value;
total_shares += level.size;
remaining -= level_value;
}
}
if total_shares > 0.0 {
Some(total_cost / total_shares)
} else {
None
}
};
if let (Some(bid_exec), Some(ask_exec)) = (
avg_execution_price(&snapshot.bids, order_size_usd, false),
avg_execution_price(&snapshot.asks, order_size_usd, true)
) {
ask_exec - bid_exec
} else if let (Some(bid), Some(ask)) = (snapshot.bids.first(), snapshot.asks.first()) {
ask.price - bid.price
} else {
0.0
}
}
}
impl LiquidityProfile {
pub fn from_snapshot(snapshot: &OrderBookSnapshot) -> Self {
let mut bid_liquidity = Vec::new();
let mut ask_liquidity = Vec::new();
let mut cumulative_bid_size = 0.0;
let mut cumulative_bid_cost = 0.0;
for bid in &snapshot.bids {
cumulative_bid_size += bid.size;
cumulative_bid_cost += bid.price * bid.size;
bid_liquidity.push(LiquidityLevel {
price: bid.price,
size: bid.size,
cumulative_size: cumulative_bid_size,
cost_to_execute: cumulative_bid_cost,
});
}
let mut cumulative_ask_size = 0.0;
let mut cumulative_ask_cost = 0.0;
for ask in &snapshot.asks {
cumulative_ask_size += ask.size;
cumulative_ask_cost += ask.price * ask.size;
ask_liquidity.push(LiquidityLevel {
price: ask.price,
size: ask.size,
cumulative_size: cumulative_ask_size,
cost_to_execute: cumulative_ask_cost,
});
}
let total_bid_depth = cumulative_bid_cost;
let total_ask_depth = cumulative_ask_cost;
let bid_depth_weighted_price = if cumulative_bid_size > 0.0 {
cumulative_bid_cost / cumulative_bid_size
} else {
0.0
};
let ask_depth_weighted_price = if cumulative_ask_size > 0.0 {
cumulative_ask_cost / cumulative_ask_size
} else {
0.0
};
Self {
bid_liquidity,
ask_liquidity,
total_bid_depth,
total_ask_depth,
bid_depth_weighted_price,
ask_depth_weighted_price,
}
}
/// Calculate the market impact of executing a given size
pub fn calculate_market_impact(&self, size_usd: f64, is_buy: bool) -> MarketImpact {
let levels = if is_buy { &self.ask_liquidity } else { &self.bid_liquidity };
if levels.is_empty() {
return MarketImpact::default();
}
let reference_price = levels[0].price;
let mut remaining = size_usd;
let mut total_cost = 0.0;
let mut total_shares = 0.0;
let mut levels_consumed = 0;
for (i, level) in levels.iter().enumerate() {
let level_value = level.price * level.size;
if remaining <= level_value {
let shares = remaining / level.price;
total_cost += shares * level.price;
total_shares += shares;
levels_consumed = i + 1;
break;
} else {
total_cost += level_value;
total_shares += level.size;
remaining -= level_value;
levels_consumed = i + 1;
}
}
let avg_execution_price = if total_shares > 0.0 {
total_cost / total_shares
} else {
reference_price
};
let price_impact = if is_buy {
(avg_execution_price - reference_price) / reference_price
} else {
(reference_price - avg_execution_price) / reference_price
};
let slippage = (avg_execution_price - reference_price).abs();
MarketImpact {
avg_execution_price,
price_impact,
slippage,
levels_consumed,
total_shares,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct MarketImpact {
pub avg_execution_price: f64,
pub price_impact: f64, // As percentage
pub slippage: f64, // In price units
pub levels_consumed: usize,
pub total_shares: f64,
}
/// Track orderbook dynamics over time
pub struct OrderBookDynamics {
snapshots: Vec<(chrono::DateTime<chrono::Utc>, OrderBookAnalytics)>,
max_history: usize,
}
impl OrderBookDynamics {
pub fn new(max_history: usize) -> Self {
Self {
snapshots: Vec::new(),
max_history,
}
}
pub fn add_snapshot(&mut self, timestamp: chrono::DateTime<chrono::Utc>, analytics: OrderBookAnalytics) {
self.snapshots.push((timestamp, analytics));
if self.snapshots.len() > self.max_history {
self.snapshots.remove(0);
}
}
pub fn get_volatility(&self, window: usize) -> Option<f64> {
if self.snapshots.len() < window {
return None;
}
let recent = &self.snapshots[self.snapshots.len() - window..];
let mid_prices: Vec<f64> = recent.iter().map(|(_, a)| a.mid_price).collect();
let mean = mid_prices.iter().sum::<f64>() / mid_prices.len() as f64;
let variance = mid_prices.iter()
.map(|p| (p - mean).powi(2))
.sum::<f64>() / mid_prices.len() as f64;
Some(variance.sqrt())
}
pub fn get_average_spread(&self, window: usize) -> Option<f64> {
if self.snapshots.len() < window {
return None;
}
let recent = &self.snapshots[self.snapshots.len() - window..];
let total_spread: f64 = recent.iter().map(|(_, a)| a.spread).sum();
Some(total_spread / window as f64)
}
pub fn detect_momentum(&self, window: usize) -> Option<f64> {
if self.snapshots.len() < window {
return None;
}
let recent = &self.snapshots[self.snapshots.len() - window..];
let imbalances: Vec<f64> = recent.iter()
.map(|(_, a)| a.depth_imbalance.weighted)
.collect();
// Average imbalance indicates momentum direction
Some(imbalances.iter().sum::<f64>() / imbalances.len() as f64)
}
}

View file

@ -0,0 +1,313 @@
pub mod analytics;
use crate::{Quote, Trade, Side, OrderBookSnapshot, PriceLevel};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use parking_lot::RwLock;
use std::collections::BTreeMap;
use std::sync::Arc;
pub use analytics::{OrderBookAnalytics, LiquidityProfile, OrderBookImbalance, MarketImpact};
// Manages order books for all symbols
pub struct OrderBookManager {
books: DashMap<String, Arc<RwLock<OrderBook>>>,
}
impl OrderBookManager {
pub fn new() -> Self {
Self {
books: DashMap::new(),
}
}
pub fn get_or_create(&self, symbol: &str) -> Arc<RwLock<OrderBook>> {
self.books
.entry(symbol.to_string())
.or_insert_with(|| Arc::new(RwLock::new(OrderBook::new(symbol.to_string()))))
.clone()
}
pub fn update_quote(&self, symbol: &str, quote: Quote, timestamp: DateTime<Utc>) {
let book = self.get_or_create(symbol);
let mut book_guard = book.write();
book_guard.update_quote(quote, timestamp);
}
pub fn update_trade(&self, symbol: &str, trade: Trade, timestamp: DateTime<Utc>) {
let book = self.get_or_create(symbol);
let mut book_guard = book.write();
book_guard.update_trade(trade, timestamp);
}
pub fn get_snapshot(&self, symbol: &str, depth: usize) -> Option<OrderBookSnapshot> {
self.books.get(symbol).map(|book| {
let book_guard = book.read();
book_guard.get_snapshot(depth)
})
}
pub fn get_best_bid_ask(&self, symbol: &str) -> Option<(f64, f64)> {
self.books.get(symbol).and_then(|book| {
let book_guard = book.read();
book_guard.get_best_bid_ask()
})
}
pub fn get_analytics(&self, symbol: &str, depth: usize) -> Option<OrderBookAnalytics> {
self.get_snapshot(symbol, depth)
.and_then(|snapshot| OrderBookAnalytics::calculate(&snapshot))
}
pub fn get_liquidity_profile(&self, symbol: &str, depth: usize) -> Option<LiquidityProfile> {
self.get_snapshot(symbol, depth)
.map(|snapshot| LiquidityProfile::from_snapshot(&snapshot))
}
}
// Individual order book for a symbol
pub struct OrderBook {
symbol: String,
bids: BTreeMap<OrderedFloat, Level>,
asks: BTreeMap<OrderedFloat, Level>,
last_update: DateTime<Utc>,
last_trade_price: Option<f64>,
last_trade_size: Option<f64>,
}
#[derive(Clone, Debug)]
struct Level {
price: f64,
size: f64,
order_count: u32,
last_update: DateTime<Utc>,
}
// Wrapper for f64 to allow BTreeMap ordering
#[derive(Clone, Copy, Debug, PartialEq)]
struct OrderedFloat(f64);
impl Eq for OrderedFloat {}
impl PartialOrd for OrderedFloat {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(&other.0)
}
}
impl Ord for OrderedFloat {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap_or(std::cmp::Ordering::Equal)
}
}
impl OrderBook {
pub fn new(symbol: String) -> Self {
Self {
symbol,
bids: BTreeMap::new(),
asks: BTreeMap::new(),
last_update: Utc::now(),
last_trade_price: None,
last_trade_size: None,
}
}
pub fn update_quote(&mut self, quote: Quote, timestamp: DateTime<Utc>) {
// Update bid
if quote.bid > 0.0 && quote.bid_size > 0.0 {
self.bids.insert(
OrderedFloat(-quote.bid), // Negative for reverse ordering
Level {
price: quote.bid,
size: quote.bid_size,
order_count: 1,
last_update: timestamp,
},
);
}
// Update ask
if quote.ask > 0.0 && quote.ask_size > 0.0 {
self.asks.insert(
OrderedFloat(quote.ask),
Level {
price: quote.ask,
size: quote.ask_size,
order_count: 1,
last_update: timestamp,
},
);
}
self.last_update = timestamp;
self.clean_stale_levels(timestamp);
}
pub fn update_trade(&mut self, trade: Trade, timestamp: DateTime<Utc>) {
self.last_trade_price = Some(trade.price);
self.last_trade_size = Some(trade.size);
self.last_update = timestamp;
// Optionally update order book based on trade
// Remove liquidity that was likely consumed
match trade.side {
Side::Buy => {
// Trade hit the ask, remove liquidity
self.remove_liquidity_up_to_asks(trade.price, trade.size);
}
Side::Sell => {
// Trade hit the bid, remove liquidity
self.remove_liquidity_up_to_bids(trade.price, trade.size);
}
}
}
pub fn get_snapshot(&self, depth: usize) -> OrderBookSnapshot {
let bids: Vec<PriceLevel> = self.bids
.values()
.take(depth)
.map(|level| PriceLevel {
price: level.price,
size: level.size,
order_count: Some(level.order_count),
})
.collect();
let asks: Vec<PriceLevel> = self.asks
.values()
.take(depth)
.map(|level| PriceLevel {
price: level.price,
size: level.size,
order_count: Some(level.order_count),
})
.collect();
OrderBookSnapshot {
symbol: self.symbol.clone(),
timestamp: self.last_update,
bids,
asks,
}
}
pub fn get_best_bid_ask(&self) -> Option<(f64, f64)> {
let best_bid = self.bids.values().next()?.price;
let best_ask = self.asks.values().next()?.price;
Some((best_bid, best_ask))
}
pub fn get_mid_price(&self) -> Option<f64> {
self.get_best_bid_ask()
.map(|(bid, ask)| (bid + ask) / 2.0)
}
pub fn get_spread(&self) -> Option<f64> {
self.get_best_bid_ask()
.map(|(bid, ask)| ask - bid)
}
pub fn get_depth_at_price(&self, price: f64, side: Side) -> f64 {
match side {
Side::Buy => {
self.bids.values()
.filter(|level| level.price >= price)
.map(|level| level.size)
.sum()
}
Side::Sell => {
self.asks.values()
.filter(|level| level.price <= price)
.map(|level| level.size)
.sum()
}
}
}
pub fn get_volume_weighted_price(&self, size: f64, side: Side) -> Option<f64> {
let levels: Vec<&Level> = match side {
Side::Buy => self.asks.values().collect(),
Side::Sell => self.bids.values().collect(),
};
let mut remaining_size = size;
let mut total_cost = 0.0;
let mut total_shares = 0.0;
for level in levels {
if remaining_size <= 0.0 {
break;
}
let fill_size = remaining_size.min(level.size);
total_cost += fill_size * level.price;
total_shares += fill_size;
remaining_size -= fill_size;
}
if total_shares > 0.0 {
Some(total_cost / total_shares)
} else {
None
}
}
fn clean_stale_levels(&mut self, current_time: DateTime<Utc>) {
let stale_threshold = chrono::Duration::seconds(60); // 60 seconds
self.bids.retain(|_, level| {
current_time - level.last_update < stale_threshold
});
self.asks.retain(|_, level| {
current_time - level.last_update < stale_threshold
});
}
fn remove_liquidity_up_to_asks(&mut self, price: f64, size: f64) {
let mut remaining_size = size;
let mut to_remove = Vec::new();
for (key, level) in self.asks.iter_mut() {
if level.price <= price {
if level.size <= remaining_size {
remaining_size -= level.size;
to_remove.push(*key);
} else {
level.size -= remaining_size;
break;
}
} else {
break;
}
}
for key in to_remove {
self.asks.remove(&key);
}
}
fn remove_liquidity_up_to_bids(&mut self, price: f64, size: f64) {
let mut remaining_size = size;
let mut to_remove = Vec::new();
for (key, level) in self.bids.iter_mut() {
if level.price >= price {
if level.size <= remaining_size {
remaining_size -= level.size;
to_remove.push(*key);
} else {
level.size -= remaining_size;
break;
}
} else {
break;
}
}
for key in to_remove {
self.bids.remove(&key);
}
}
}