use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use uuid::Uuid;
use crate::error::CoreError;
use crate::trading::OrderSide;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VolumeClass {
Retail,
MediumBulk,
LargeBulk,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassifiedTrade {
pub trade_id: Uuid,
pub token_id: Uuid,
pub side: OrderSide,
pub price: Decimal,
pub amount: Decimal,
pub timestamp: DateTime<Utc>,
pub volume_class: VolumeClass,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnhancedVPIN {
pub vpin: Decimal,
pub bulk_vpin: Decimal,
pub retail_vpin: Decimal,
pub bucket_count: usize,
pub total_volume: Decimal,
pub bulk_percentage: Decimal,
pub timestamp: DateTime<Utc>,
}
impl EnhancedVPIN {
pub fn is_toxic(&self) -> bool {
self.vpin > dec!(0.7) || self.bulk_vpin > dec!(0.75)
}
pub fn toxicity_level(&self) -> u8 {
let weighted_vpin = (self.vpin * dec!(0.4)) + (self.bulk_vpin * dec!(0.6));
let score = (weighted_vpin * dec!(10)).round();
score.to_string().parse::<u8>().unwrap_or(0).min(10)
}
pub fn fee_multiplier(&self) -> Decimal {
let base = dec!(1.0);
let toxicity_score = Decimal::from(self.toxicity_level()) / dec!(10);
base + (toxicity_score * toxicity_score * dec!(2.0))
}
pub fn has_informed_bulk_flow(&self) -> bool {
self.bulk_vpin > self.retail_vpin + dec!(0.2)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToxicityFeeAdjustment {
pub base_fee: Decimal,
pub toxicity_multiplier: Decimal,
pub adjusted_fee: Decimal,
pub vpin_score: Decimal,
pub reason: String,
}
impl ToxicityFeeAdjustment {
pub fn from_vpin(vpin: &EnhancedVPIN, base_fee: Decimal) -> Self {
let multiplier = vpin.fee_multiplier();
let adjusted = base_fee * multiplier;
let reason = if vpin.is_toxic() {
if vpin.has_informed_bulk_flow() {
"High informed institutional trading detected".to_string()
} else {
"High order flow toxicity detected".to_string()
}
} else if multiplier > dec!(1.5) {
"Elevated toxicity level".to_string()
} else {
"Normal market conditions".to_string()
};
Self {
base_fee,
toxicity_multiplier: multiplier,
adjusted_fee: adjusted,
vpin_score: vpin.vpin,
reason,
}
}
}
#[derive(Debug, Clone)]
pub struct VPINAnalyzer {
token_id: Uuid,
trades: VecDeque<ClassifiedTrade>,
max_history: usize,
medium_bulk_threshold: Decimal,
large_bulk_threshold: Decimal,
}
impl VPINAnalyzer {
pub fn new(token_id: Uuid) -> Self {
Self {
token_id,
trades: VecDeque::new(),
max_history: 5000,
medium_bulk_threshold: dec!(2.0), large_bulk_threshold: dec!(5.0), }
}
pub fn with_thresholds(
token_id: Uuid,
medium_bulk_threshold: Decimal,
large_bulk_threshold: Decimal,
) -> Self {
Self {
token_id,
trades: VecDeque::new(),
max_history: 5000,
medium_bulk_threshold,
large_bulk_threshold,
}
}
pub fn add_trade(
&mut self,
trade_id: Uuid,
token_id: Uuid,
side: OrderSide,
price: Decimal,
amount: Decimal,
timestamp: DateTime<Utc>,
) -> Result<(), CoreError> {
if token_id != self.token_id {
return Err(CoreError::Validation(
"Trade token ID does not match analyzer".to_string(),
));
}
let volume_class = self.classify_volume(amount);
let trade = ClassifiedTrade {
trade_id,
token_id,
side,
price,
amount,
timestamp,
volume_class,
};
self.trades.push_back(trade);
while self.trades.len() > self.max_history {
self.trades.pop_front();
}
Ok(())
}
fn classify_volume(&self, amount: Decimal) -> VolumeClass {
if self.trades.is_empty() {
return VolumeClass::Retail;
}
let sample_size = self.trades.len().min(100);
let total: Decimal = self
.trades
.iter()
.rev()
.take(sample_size)
.map(|t| t.amount)
.sum();
let avg = total / Decimal::from(sample_size);
if amount >= avg * self.large_bulk_threshold {
VolumeClass::LargeBulk
} else if amount >= avg * self.medium_bulk_threshold {
VolumeClass::MediumBulk
} else {
VolumeClass::Retail
}
}
pub fn calculate_vpin(
&self,
window_seconds: i64,
bucket_count: usize,
) -> Result<EnhancedVPIN, CoreError> {
if bucket_count == 0 {
return Err(CoreError::Validation(
"Bucket count must be greater than 0".to_string(),
));
}
let now = Utc::now();
let cutoff = now - chrono::Duration::seconds(window_seconds);
let mut total_volume = dec!(0);
let mut bulk_volume = dec!(0);
let mut retail_volume = dec!(0);
for trade in self.trades.iter().rev() {
if trade.timestamp < cutoff {
break;
}
total_volume += trade.amount;
match trade.volume_class {
VolumeClass::Retail => retail_volume += trade.amount,
VolumeClass::MediumBulk | VolumeClass::LargeBulk => bulk_volume += trade.amount,
}
}
if total_volume == dec!(0) {
return Ok(EnhancedVPIN {
vpin: dec!(0),
bulk_vpin: dec!(0),
retail_vpin: dec!(0),
bucket_count,
total_volume: dec!(0),
bulk_percentage: dec!(0),
timestamp: now,
});
}
let vpin = self.calculate_vpin_for_filter(
window_seconds,
bucket_count,
Some(|_: &ClassifiedTrade| true),
)?;
let bulk_vpin = self.calculate_vpin_for_filter(
window_seconds,
bucket_count,
Some(|t: &ClassifiedTrade| {
matches!(
t.volume_class,
VolumeClass::MediumBulk | VolumeClass::LargeBulk
)
}),
)?;
let retail_vpin = self.calculate_vpin_for_filter(
window_seconds,
bucket_count,
Some(|t: &ClassifiedTrade| matches!(t.volume_class, VolumeClass::Retail)),
)?;
let bulk_percentage = if total_volume > dec!(0) {
bulk_volume / total_volume
} else {
dec!(0)
};
Ok(EnhancedVPIN {
vpin,
bulk_vpin,
retail_vpin,
bucket_count,
total_volume,
bulk_percentage,
timestamp: now,
})
}
fn calculate_vpin_for_filter<F>(
&self,
window_seconds: i64,
bucket_count: usize,
filter: Option<F>,
) -> Result<Decimal, CoreError>
where
F: Fn(&ClassifiedTrade) -> bool,
{
let now = Utc::now();
let cutoff = now - chrono::Duration::seconds(window_seconds);
let mut buckets: Vec<(Decimal, Decimal)> = vec![(dec!(0), dec!(0)); bucket_count];
let mut total_volume = dec!(0);
for trade in self.trades.iter().rev() {
if trade.timestamp < cutoff {
break;
}
if let Some(ref f) = filter {
if !f(trade) {
continue;
}
}
total_volume += trade.amount;
}
if total_volume == dec!(0) {
return Ok(dec!(0));
}
let volume_per_bucket = total_volume / Decimal::from(bucket_count);
let mut current_bucket = 0;
let mut bucket_volume = dec!(0);
for trade in self.trades.iter().rev() {
if trade.timestamp < cutoff {
break;
}
if let Some(ref f) = filter {
if !f(trade) {
continue;
}
}
if current_bucket >= bucket_count {
break;
}
let remaining_in_bucket = volume_per_bucket - bucket_volume;
let trade_volume = trade.amount.min(remaining_in_bucket);
match trade.side {
OrderSide::Buy => buckets[current_bucket].0 += trade_volume,
OrderSide::Sell => buckets[current_bucket].1 += trade_volume,
}
bucket_volume += trade_volume;
if bucket_volume >= volume_per_bucket {
current_bucket += 1;
bucket_volume = dec!(0);
}
}
let mut vpin_sum = dec!(0);
let mut valid_buckets = 0;
for (buy_vol, sell_vol) in &buckets {
let bucket_total = buy_vol + sell_vol;
if bucket_total > dec!(0) {
let imbalance = (buy_vol - sell_vol).abs() / bucket_total;
vpin_sum += imbalance;
valid_buckets += 1;
}
}
let vpin = if valid_buckets > 0 {
vpin_sum / Decimal::from(valid_buckets)
} else {
dec!(0)
};
Ok(vpin)
}
pub fn get_volume_distribution(&self, window_seconds: i64) -> (Decimal, Decimal, Decimal) {
let now = Utc::now();
let cutoff = now - chrono::Duration::seconds(window_seconds);
let mut retail = dec!(0);
let mut medium = dec!(0);
let mut large = dec!(0);
for trade in self.trades.iter().rev() {
if trade.timestamp < cutoff {
break;
}
match trade.volume_class {
VolumeClass::Retail => retail += trade.amount,
VolumeClass::MediumBulk => medium += trade.amount,
VolumeClass::LargeBulk => large += trade.amount,
}
}
(retail, medium, large)
}
pub fn trade_count(&self) -> usize {
self.trades.len()
}
pub fn clear(&mut self) {
self.trades.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_volume_classification() {
let token_id = Uuid::new_v4();
let mut analyzer = VPINAnalyzer::new(token_id);
for i in 0..10 {
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
OrderSide::Buy,
dec!(100),
dec!(10),
Utc::now() - chrono::Duration::seconds(i * 10),
)
.unwrap();
}
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
OrderSide::Buy,
dec!(100),
dec!(60),
Utc::now(),
)
.unwrap();
let last_trade = analyzer.trades.back().unwrap();
assert_eq!(last_trade.volume_class, VolumeClass::LargeBulk);
}
#[test]
fn test_enhanced_vpin_calculation() {
let token_id = Uuid::new_v4();
let mut analyzer = VPINAnalyzer::new(token_id);
for i in 0..20 {
let side = if i < 14 {
OrderSide::Buy
} else {
OrderSide::Sell
};
let amount = if i < 2 { dec!(50) } else { dec!(10) };
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
side,
dec!(100),
amount,
Utc::now() - chrono::Duration::seconds((20 - i) * 2),
)
.unwrap();
}
let vpin = analyzer.calculate_vpin(60, 5).unwrap();
assert!(vpin.vpin >= dec!(0));
assert!(vpin.bulk_vpin >= dec!(0));
assert!(vpin.retail_vpin >= dec!(0));
assert!(vpin.total_volume > dec!(0));
}
#[test]
fn test_toxicity_detection() {
let vpin = EnhancedVPIN {
vpin: dec!(0.8),
bulk_vpin: dec!(0.85),
retail_vpin: dec!(0.4),
bucket_count: 5,
total_volume: dec!(1000),
bulk_percentage: dec!(0.3),
timestamp: Utc::now(),
};
assert!(vpin.is_toxic());
assert!(vpin.has_informed_bulk_flow());
assert!(vpin.toxicity_level() >= 7);
}
#[test]
fn test_fee_multiplier_calculation() {
let low_toxicity = EnhancedVPIN {
vpin: dec!(0.2),
bulk_vpin: dec!(0.15),
retail_vpin: dec!(0.25),
bucket_count: 5,
total_volume: dec!(1000),
bulk_percentage: dec!(0.2),
timestamp: Utc::now(),
};
let multiplier = low_toxicity.fee_multiplier();
assert!(multiplier >= dec!(1.0));
assert!(multiplier <= dec!(1.5));
let high_toxicity = EnhancedVPIN {
vpin: dec!(0.9),
bulk_vpin: dec!(0.95),
retail_vpin: dec!(0.5),
bucket_count: 5,
total_volume: dec!(1000),
bulk_percentage: dec!(0.4),
timestamp: Utc::now(),
};
let high_multiplier = high_toxicity.fee_multiplier();
assert!(high_multiplier > multiplier);
assert!(high_multiplier <= dec!(3.0));
}
#[test]
fn test_toxicity_fee_adjustment() {
let vpin = EnhancedVPIN {
vpin: dec!(0.75),
bulk_vpin: dec!(0.8),
retail_vpin: dec!(0.3),
bucket_count: 5,
total_volume: dec!(1000),
bulk_percentage: dec!(0.35),
timestamp: Utc::now(),
};
let adjustment = ToxicityFeeAdjustment::from_vpin(&vpin, dec!(0.0025));
assert!(adjustment.adjusted_fee > adjustment.base_fee);
assert!(adjustment.toxicity_multiplier >= dec!(1.0));
assert!(!adjustment.reason.is_empty());
}
#[test]
fn test_volume_distribution() {
let token_id = Uuid::new_v4();
let mut analyzer = VPINAnalyzer::new(token_id);
for _ in 0..5 {
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
OrderSide::Buy,
dec!(100),
dec!(10),
Utc::now(),
)
.unwrap();
}
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
OrderSide::Buy,
dec!(100),
dec!(25),
Utc::now(),
)
.unwrap();
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
OrderSide::Buy,
dec!(100),
dec!(100),
Utc::now(),
)
.unwrap();
let (retail, medium, large) = analyzer.get_volume_distribution(60);
assert!(retail > dec!(0));
assert!(medium > dec!(0));
assert!(large > dec!(0));
}
#[test]
fn test_invalid_token_id() {
let token_id = Uuid::new_v4();
let mut analyzer = VPINAnalyzer::new(token_id);
let result = analyzer.add_trade(
Uuid::new_v4(),
Uuid::new_v4(), OrderSide::Buy,
dec!(100),
dec!(10),
Utc::now(),
);
assert!(result.is_err());
}
#[test]
fn test_empty_vpin_calculation() {
let token_id = Uuid::new_v4();
let analyzer = VPINAnalyzer::new(token_id);
let vpin = analyzer.calculate_vpin(60, 5).unwrap();
assert_eq!(vpin.vpin, dec!(0));
assert_eq!(vpin.total_volume, dec!(0));
}
#[test]
fn test_max_history_limit() {
let token_id = Uuid::new_v4();
let mut analyzer = VPINAnalyzer::new(token_id);
analyzer.max_history = 10;
for _ in 0..20 {
analyzer
.add_trade(
Uuid::new_v4(),
token_id,
OrderSide::Buy,
dec!(100),
dec!(10),
Utc::now(),
)
.unwrap();
}
assert_eq!(analyzer.trade_count(), 10);
}
}