use crate::error::Result;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TradeDirection {
Buy,
Sell,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToxicityTrade {
pub timestamp: i64,
pub price: Decimal,
pub volume: Decimal,
pub direction: TradeDirection,
pub is_aggressive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToxicityMetrics {
pub vpin: Decimal,
pub order_imbalance: Decimal,
pub adverse_selection: Decimal,
pub toxicity_index: Decimal,
pub spread_adjustment: Decimal,
}
#[derive(Debug)]
pub struct OrderFlowToxicityDetector {
pub bucket_size: Decimal,
pub num_buckets: usize,
trades: VecDeque<ToxicityTrade>,
max_trades: usize,
pub toxicity_threshold: Decimal,
}
impl OrderFlowToxicityDetector {
pub fn new(bucket_size: Decimal, num_buckets: usize, max_trades: usize) -> Self {
Self {
bucket_size,
num_buckets,
trades: VecDeque::new(),
max_trades,
toxicity_threshold: dec!(0.5), }
}
pub fn add_trade(&mut self, trade: ToxicityTrade) {
self.trades.push_back(trade);
while self.trades.len() > self.max_trades {
self.trades.pop_front();
}
}
pub fn calculate_vpin(&self) -> Result<Decimal> {
if self.trades.is_empty() {
return Ok(Decimal::ZERO);
}
let mut buckets = Vec::new();
let mut current_bucket_buy = Decimal::ZERO;
let mut current_bucket_sell = Decimal::ZERO;
let mut current_bucket_volume = Decimal::ZERO;
for trade in &self.trades {
let remaining = self.bucket_size - current_bucket_volume;
if trade.volume <= remaining {
match trade.direction {
TradeDirection::Buy => current_bucket_buy += trade.volume,
TradeDirection::Sell => current_bucket_sell += trade.volume,
}
current_bucket_volume += trade.volume;
if current_bucket_volume >= self.bucket_size {
buckets.push((current_bucket_buy, current_bucket_sell));
current_bucket_buy = Decimal::ZERO;
current_bucket_sell = Decimal::ZERO;
current_bucket_volume = Decimal::ZERO;
}
} else {
let mut remaining_volume = trade.volume;
if remaining > Decimal::ZERO {
match trade.direction {
TradeDirection::Buy => current_bucket_buy += remaining,
TradeDirection::Sell => current_bucket_sell += remaining,
}
buckets.push((current_bucket_buy, current_bucket_sell));
remaining_volume -= remaining;
}
while remaining_volume >= self.bucket_size {
match trade.direction {
TradeDirection::Buy => {
buckets.push((self.bucket_size, Decimal::ZERO));
}
TradeDirection::Sell => {
buckets.push((Decimal::ZERO, self.bucket_size));
}
}
remaining_volume -= self.bucket_size;
}
current_bucket_buy = Decimal::ZERO;
current_bucket_sell = Decimal::ZERO;
current_bucket_volume = remaining_volume;
match trade.direction {
TradeDirection::Buy => current_bucket_buy = remaining_volume,
TradeDirection::Sell => current_bucket_sell = remaining_volume,
}
}
}
let buckets_to_analyze = buckets.len().min(self.num_buckets);
if buckets_to_analyze == 0 {
return Ok(Decimal::ZERO);
}
let start_idx = buckets.len().saturating_sub(buckets_to_analyze);
let mut total_imbalance = Decimal::ZERO;
let mut total_volume = Decimal::ZERO;
for (buy_vol, sell_vol) in &buckets[start_idx..] {
let imbalance = (buy_vol - sell_vol).abs();
total_imbalance += imbalance;
total_volume += buy_vol + sell_vol;
}
let vpin = if total_volume > Decimal::ZERO {
total_imbalance / total_volume
} else {
Decimal::ZERO
};
Ok(vpin)
}
pub fn calculate_order_imbalance(&self, window_trades: usize) -> Result<Decimal> {
if self.trades.is_empty() {
return Ok(Decimal::ZERO);
}
let trades_to_analyze = self.trades.len().min(window_trades);
let start_idx = self.trades.len() - trades_to_analyze;
let mut buy_volume = Decimal::ZERO;
let mut sell_volume = Decimal::ZERO;
for trade in self.trades.iter().skip(start_idx) {
match trade.direction {
TradeDirection::Buy => buy_volume += trade.volume,
TradeDirection::Sell => sell_volume += trade.volume,
}
}
let total_volume = buy_volume + sell_volume;
let imbalance = if total_volume > Decimal::ZERO {
(buy_volume - sell_volume) / total_volume
} else {
Decimal::ZERO
};
Ok(imbalance)
}
pub fn calculate_adverse_selection(&self, window_trades: usize) -> Result<Decimal> {
if self.trades.len() < 2 {
return Ok(Decimal::ZERO);
}
let trades_to_analyze = self.trades.len().min(window_trades);
let start_idx = self.trades.len() - trades_to_analyze;
let mut adverse_count = 0;
let mut total_count = 0;
for i in start_idx..self.trades.len() - 1 {
let current_trade = &self.trades[i];
let next_price = self.trades[i + 1].price;
let is_adverse = match current_trade.direction {
TradeDirection::Buy => next_price > current_trade.price,
TradeDirection::Sell => next_price < current_trade.price,
};
if is_adverse {
adverse_count += 1;
}
total_count += 1;
}
let adverse_selection = if total_count > 0 {
Decimal::from(adverse_count) / Decimal::from(total_count)
} else {
Decimal::ZERO
};
Ok(adverse_selection)
}
pub fn identify_informed_traders(&self, user_trades: &[(Uuid, &ToxicityTrade)]) -> Vec<Uuid> {
let mut trader_metrics: std::collections::HashMap<Uuid, (usize, usize)> =
std::collections::HashMap::new();
for (i, (user_id, trade)) in user_trades.iter().enumerate() {
if i + 1 < user_trades.len() {
let next_price = user_trades[i + 1].1.price;
let is_profitable = match trade.direction {
TradeDirection::Buy => next_price > trade.price,
TradeDirection::Sell => next_price < trade.price,
};
let stats = trader_metrics.entry(*user_id).or_insert((0, 0));
stats.1 += 1; if is_profitable {
stats.0 += 1; }
}
}
let mut informed_traders = Vec::new();
for (user_id, (profitable, total)) in trader_metrics {
if total >= 5 {
let win_rate = Decimal::from(profitable) / Decimal::from(total);
if win_rate > dec!(0.7) {
informed_traders.push(user_id);
}
}
}
informed_traders
}
pub fn calculate_toxicity_metrics(&self, window_trades: usize) -> Result<ToxicityMetrics> {
let vpin = self.calculate_vpin()?;
let order_imbalance = self.calculate_order_imbalance(window_trades)?;
let adverse_selection = self.calculate_adverse_selection(window_trades)?;
let toxicity_index = (vpin * dec!(0.4))
+ (order_imbalance.abs() * dec!(0.3))
+ (adverse_selection * dec!(0.3));
let spread_adjustment = if toxicity_index > self.toxicity_threshold {
dec!(1.0) + (toxicity_index - self.toxicity_threshold) * dec!(2.0)
} else {
dec!(1.0)
};
Ok(ToxicityMetrics {
vpin,
order_imbalance,
adverse_selection,
toxicity_index,
spread_adjustment,
})
}
pub fn adjust_quote_for_toxicity(
&self,
base_bid: Decimal,
base_ask: Decimal,
metrics: &ToxicityMetrics,
) -> Result<(Decimal, Decimal)> {
let spread = base_ask - base_bid;
let mid = (base_bid + base_ask) / dec!(2);
let adjusted_spread = spread * metrics.spread_adjustment;
let mid_adjustment = mid * metrics.order_imbalance * dec!(0.001);
let adjusted_mid = mid + mid_adjustment;
let half_spread = adjusted_spread / dec!(2);
let adjusted_bid = adjusted_mid - half_spread;
let adjusted_ask = adjusted_mid + half_spread;
Ok((adjusted_bid, adjusted_ask))
}
}
impl Default for OrderFlowToxicityDetector {
fn default() -> Self {
Self::new(
dec!(1000), 50, 10000, )
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_trade() {
let mut detector = OrderFlowToxicityDetector::default();
detector.add_trade(ToxicityTrade {
timestamp: 1000,
price: dec!(100),
volume: dec!(10),
direction: TradeDirection::Buy,
is_aggressive: true,
});
assert_eq!(detector.trades.len(), 1);
}
#[test]
fn test_order_imbalance() {
let mut detector = OrderFlowToxicityDetector::default();
for i in 0..7 {
detector.add_trade(ToxicityTrade {
timestamp: 1000 + i,
price: dec!(100),
volume: dec!(10),
direction: TradeDirection::Buy,
is_aggressive: true,
});
}
for i in 0..3 {
detector.add_trade(ToxicityTrade {
timestamp: 2000 + i,
price: dec!(100),
volume: dec!(10),
direction: TradeDirection::Sell,
is_aggressive: true,
});
}
let imbalance = detector.calculate_order_imbalance(100).unwrap();
assert!(imbalance > Decimal::ZERO);
assert_eq!(imbalance, dec!(0.4));
}
#[test]
fn test_adverse_selection() {
let mut detector = OrderFlowToxicityDetector::default();
detector.add_trade(ToxicityTrade {
timestamp: 1000,
price: dec!(100),
volume: dec!(10),
direction: TradeDirection::Buy,
is_aggressive: true,
});
detector.add_trade(ToxicityTrade {
timestamp: 1001,
price: dec!(105), volume: dec!(10),
direction: TradeDirection::Sell,
is_aggressive: true,
});
detector.add_trade(ToxicityTrade {
timestamp: 1002,
price: dec!(103), volume: dec!(10),
direction: TradeDirection::Buy,
is_aggressive: true,
});
let adverse_selection = detector.calculate_adverse_selection(100).unwrap();
assert_eq!(adverse_selection, dec!(1.0));
}
#[test]
fn test_vpin_calculation() {
let mut detector = OrderFlowToxicityDetector::new(dec!(100), 5, 1000);
for i in 0..10 {
detector.add_trade(ToxicityTrade {
timestamp: 1000 + i,
price: dec!(100),
volume: dec!(15), direction: if i < 7 {
TradeDirection::Buy
} else {
TradeDirection::Sell
},
is_aggressive: true,
});
}
let vpin = detector.calculate_vpin().unwrap();
assert!(vpin > Decimal::ZERO);
assert!(vpin <= dec!(1.0));
}
#[test]
fn test_toxicity_metrics() {
let mut detector = OrderFlowToxicityDetector::default();
for i in 0..20 {
detector.add_trade(ToxicityTrade {
timestamp: 1000 + i,
price: dec!(100) + Decimal::from(i % 5),
volume: dec!(10),
direction: if i % 3 == 0 {
TradeDirection::Buy
} else {
TradeDirection::Sell
},
is_aggressive: i % 2 == 0,
});
}
let metrics = detector.calculate_toxicity_metrics(20).unwrap();
assert!(metrics.vpin >= Decimal::ZERO);
assert!(metrics.vpin <= dec!(1.0));
assert!(metrics.toxicity_index >= Decimal::ZERO);
assert!(metrics.toxicity_index <= dec!(1.0));
assert!(metrics.spread_adjustment >= dec!(1.0));
}
#[test]
fn test_quote_adjustment() {
let detector = OrderFlowToxicityDetector::default();
let metrics = ToxicityMetrics {
vpin: dec!(0.7),
order_imbalance: dec!(0.3),
adverse_selection: dec!(0.5),
toxicity_index: dec!(0.6),
spread_adjustment: dec!(1.5),
};
let (adjusted_bid, adjusted_ask) = detector
.adjust_quote_for_toxicity(dec!(99), dec!(101), &metrics)
.unwrap();
let original_spread = dec!(2);
let adjusted_spread = adjusted_ask - adjusted_bid;
assert!(adjusted_spread > original_spread);
}
#[test]
fn test_identify_informed_traders() {
let detector = OrderFlowToxicityDetector::default();
let user1 = Uuid::new_v4();
let user2 = Uuid::new_v4();
let mut user_trades = Vec::new();
for i in 0..10 {
let trade = ToxicityTrade {
timestamp: 1000 + i,
price: dec!(100) + Decimal::from(i),
volume: dec!(10),
direction: TradeDirection::Buy,
is_aggressive: true,
};
user_trades.push((user1, trade));
}
for i in 0..5 {
let trade = ToxicityTrade {
timestamp: 2000 + i,
price: dec!(100) - Decimal::from(i),
volume: dec!(10),
direction: TradeDirection::Sell,
is_aggressive: true,
};
user_trades.push((user2, trade));
}
let user_trades_ref: Vec<(Uuid, &ToxicityTrade)> =
user_trades.iter().map(|(id, trade)| (*id, trade)).collect();
let informed = detector.identify_informed_traders(&user_trades_ref);
assert!(informed.contains(&user1));
}
#[test]
fn test_max_trades_limit() {
let mut detector = OrderFlowToxicityDetector::new(dec!(100), 5, 100);
for i in 0..150 {
detector.add_trade(ToxicityTrade {
timestamp: 1000 + i,
price: dec!(100),
volume: dec!(10),
direction: TradeDirection::Buy,
is_aggressive: true,
});
}
assert_eq!(detector.trades.len(), 100);
}
}