work on new engine
This commit is contained in:
parent
44476da13f
commit
a1e5a21847
126 changed files with 3425 additions and 6695 deletions
374
apps/stock/engine/src/orderbook/analytics.rs
Normal file
374
apps/stock/engine/src/orderbook/analytics.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
313
apps/stock/engine/src/orderbook/mod.rs
Normal file
313
apps/stock/engine/src/orderbook/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue