Agent Results Summary: ✅ Agent 1: Added Default trait to CheckpointMetadata ✅ Agent 2: Verified no E0382 moved value errors ✅ Agent 3: Fixed 2 type conversion errors (duplicate imports/From impl) ✅ Agent 4: Verified no ambiguous numeric type errors ✅ Agent 5: Verified OrderSide/OrderStatus already public ✅ Agent 6: Fixed 2 Duration import errors in E2E tests ✅ Agent 7: Implemented PartialEq<&str> for Symbol (21+ tests fixed) ✅ Agent 8: Fixed ServiceManager API usage in tests ✅ Agent 9: Fixed 13 ML test compilation errors ✅ Agent 10: Fixed 6 integration tests (data crate) ✅ Agent 11: Fixed workspace errors - main libs compile clean ✅ Agent 12: Generated comprehensive completion report Production Status: ✅ ALL LIBRARY CODE COMPILES Files Modified: 17 files Error Reduction: 57 errors in benchmarks/tests only Critical Achievement: - common, config, data, ml, risk, trading_engine, tli: ALL COMPILE ✅ - All production library code: 0 errors ✅ - Service binaries: Ready to build ✅ - Remaining issues: Non-production code (benchmarks/tests) Remaining Work: - 57 errors in TLI benchmarks (47) + ML tests (10) - Mostly missing protobuf types and trait implementations - Does NOT block production deployment Documentation: - WAVE35_COMPLETION_REPORT.md (comprehensive analysis) Next: Wave 36 to fix remaining benchmark/test errors
557 lines
16 KiB
Rust
557 lines
16 KiB
Rust
//! # Complete VPIN Calculator Implementation
|
|
//!
|
|
//! Volume-Synchronized Probability of Informed Trading implementation
|
|
//! for detecting order flow toxicity and informed trading activity.
|
|
//!
|
|
//! ## Performance Targets
|
|
//! - Target latency: <25μs per calculation
|
|
//! - Memory: Ring buffer with zero allocations in hot path
|
|
//! - Precision: Integer arithmetic with 10,000x scaling
|
|
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
use std::time::Instant;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::MLError;
|
|
|
|
/// Precision factor for integer arithmetic (10,000x scaling)
|
|
const VPIN_PRECISION_FACTOR: i64 = 10_000;
|
|
|
|
/// Maximum latency target in microseconds
|
|
const MAX_CALCULATION_LATENCY_US: u64 = 25;
|
|
|
|
/// Trade direction classification
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub enum TradeDirection {
|
|
/// Buyer-initiated trade (aggressive buy)
|
|
Buy,
|
|
/// Seller-initiated trade (aggressive sell)
|
|
Sell,
|
|
/// Unknown direction (at mid-price or insufficient data)
|
|
Unknown,
|
|
}
|
|
|
|
impl TradeDirection {
|
|
/// Classify trade using Lee-Ready algorithm
|
|
pub fn classify_lee_ready(trade_price: i64, bid: i64, ask: i64, prev_price: i64) -> Self {
|
|
let mid_price = (bid + ask) / 2;
|
|
|
|
if trade_price > mid_price {
|
|
TradeDirection::Buy
|
|
} else if trade_price < mid_price {
|
|
TradeDirection::Sell
|
|
} else {
|
|
// At midpoint - use tick rule
|
|
Self::classify_tick_rule(trade_price, prev_price)
|
|
}
|
|
}
|
|
|
|
/// Classify trade using tick rule
|
|
pub fn classify_tick_rule(trade_price: i64, prev_price: i64) -> Self {
|
|
if trade_price > prev_price {
|
|
TradeDirection::Buy
|
|
} else if trade_price < prev_price {
|
|
TradeDirection::Sell
|
|
} else {
|
|
TradeDirection::Unknown
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Market data update for VPIN calculation
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MarketDataUpdate {
|
|
/// Timestamp in microseconds
|
|
pub timestamp: u64,
|
|
/// Symbol identifier
|
|
pub symbol: String,
|
|
/// Trade price (scaled by PRECISION_FACTOR)
|
|
pub price: i64,
|
|
/// Trade volume
|
|
pub volume: u64,
|
|
/// Best bid price (scaled)
|
|
pub bid: i64,
|
|
/// Best ask price (scaled)
|
|
pub ask: i64,
|
|
/// Bid size
|
|
pub bid_size: u64,
|
|
/// Ask size
|
|
pub ask_size: u64,
|
|
/// Trade direction (if known)
|
|
pub direction: Option<TradeDirection>,
|
|
}
|
|
|
|
/// VPIN calculation configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VPINConfig {
|
|
/// Volume per bucket
|
|
pub bucket_volume: u64,
|
|
/// Number of buckets for rolling calculation
|
|
pub bucket_count: usize,
|
|
/// Toxicity threshold (scaled by PRECISION_FACTOR)
|
|
pub toxicity_threshold: i64,
|
|
/// Maximum age for data points (microseconds)
|
|
pub max_age_us: u64,
|
|
}
|
|
|
|
impl Default for VPINConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
bucket_volume: 10_000,
|
|
bucket_count: 50,
|
|
toxicity_threshold: 3_000, // 0.3 scaled
|
|
max_age_us: 300_000_000, // 5 minutes
|
|
}
|
|
}
|
|
}
|
|
|
|
/// VPIN calculation metrics
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VPINMetrics {
|
|
/// Current VPIN value (0.0 to 1.0)
|
|
pub vpin: f64,
|
|
/// Order flow imbalance (-1.0 to 1.0)
|
|
pub order_flow_imbalance: f64,
|
|
/// Toxicity score (0.0 to 1.0)
|
|
pub toxicity_score: f64,
|
|
/// Whether market is currently toxic
|
|
pub is_toxic: bool,
|
|
/// Number of completed buckets
|
|
pub bucket_count: usize,
|
|
/// Current bucket fill percentage (0.0 to 1.0)
|
|
pub current_bucket_fill: f64,
|
|
}
|
|
|
|
impl Default for VPINMetrics {
|
|
fn default() -> Self {
|
|
Self {
|
|
vpin: 0.0,
|
|
order_flow_imbalance: 0.0,
|
|
toxicity_score: 0.0,
|
|
is_toxic: false,
|
|
bucket_count: 0,
|
|
current_bucket_fill: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performance metrics for VPIN calculator
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VPINPerformanceMetrics {
|
|
/// Total number of calculations performed
|
|
pub total_calculations: u64,
|
|
/// Average latency in microseconds
|
|
pub avg_latency_us: f64,
|
|
/// Maximum latency in microseconds
|
|
pub max_latency_us: u64,
|
|
/// Number of calculations exceeding latency target
|
|
pub over_latency_count: u64,
|
|
}
|
|
|
|
impl Default for VPINPerformanceMetrics {
|
|
fn default() -> Self {
|
|
Self {
|
|
total_calculations: 0,
|
|
avg_latency_us: 0.0,
|
|
max_latency_us: 0,
|
|
over_latency_count: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Volume bucket for VPIN calculation
|
|
#[derive(Debug, Clone)]
|
|
struct VolumeBucket {
|
|
/// Bucket index
|
|
index: usize,
|
|
/// Buy volume (scaled)
|
|
buy_volume: u64,
|
|
/// Sell volume (scaled)
|
|
sell_volume: u64,
|
|
/// Total volume
|
|
total_volume: u64,
|
|
/// First trade timestamp
|
|
start_time: u64,
|
|
/// Last trade timestamp
|
|
end_time: u64,
|
|
}
|
|
|
|
impl VolumeBucket {
|
|
fn new(index: usize, timestamp: u64) -> Self {
|
|
Self {
|
|
index,
|
|
buy_volume: 0,
|
|
sell_volume: 0,
|
|
total_volume: 0,
|
|
start_time: timestamp,
|
|
end_time: timestamp,
|
|
}
|
|
}
|
|
|
|
fn add_trade(&mut self, volume: u64, direction: TradeDirection, timestamp: u64) {
|
|
match direction {
|
|
TradeDirection::Buy => self.buy_volume += volume,
|
|
TradeDirection::Sell => self.sell_volume += volume,
|
|
TradeDirection::Unknown => {
|
|
// Split unknown trades equally
|
|
self.buy_volume += volume / 2;
|
|
self.sell_volume += volume / 2;
|
|
},
|
|
}
|
|
self.total_volume += volume;
|
|
self.end_time = timestamp;
|
|
}
|
|
|
|
fn is_full(&self, target_volume: u64) -> bool {
|
|
self.total_volume >= target_volume
|
|
}
|
|
|
|
fn calculate_imbalance(&self) -> i64 {
|
|
if self.total_volume == 0 {
|
|
return 0;
|
|
}
|
|
|
|
let imbalance = (self.buy_volume as i64 - self.sell_volume as i64) * VPIN_PRECISION_FACTOR;
|
|
imbalance / self.total_volume as i64
|
|
}
|
|
}
|
|
|
|
/// Ring buffer for efficient bucket storage
|
|
#[derive(Debug, Clone)]
|
|
pub struct RingBuffer<T> {
|
|
data: Vec<T>,
|
|
head: usize,
|
|
len: usize,
|
|
capacity: usize,
|
|
}
|
|
|
|
impl<T: Clone> RingBuffer<T> {
|
|
pub fn new(capacity: usize) -> Self {
|
|
Self {
|
|
data: Vec::with_capacity(capacity),
|
|
head: 0,
|
|
len: 0,
|
|
capacity,
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, item: T) {
|
|
if self.len < self.capacity {
|
|
self.data.push(item);
|
|
self.len += 1;
|
|
} else {
|
|
self.data[self.head] = item;
|
|
self.head = (self.head + 1) % self.capacity;
|
|
}
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.len
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.len == 0
|
|
}
|
|
|
|
pub fn get(&self, index: usize) -> Option<&T> {
|
|
if index >= self.len {
|
|
return None;
|
|
}
|
|
|
|
if self.len < self.capacity {
|
|
self.data.get(index)
|
|
} else {
|
|
let real_index = (self.head + index) % self.capacity;
|
|
self.data.get(real_index)
|
|
}
|
|
}
|
|
|
|
fn iter(&self) -> RingBufferIterator<'_, T> {
|
|
RingBufferIterator {
|
|
buffer: self,
|
|
index: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RingBufferIterator<'a, T> {
|
|
buffer: &'a RingBuffer<T>,
|
|
index: usize,
|
|
}
|
|
|
|
impl<'a, T: Clone> Iterator for RingBufferIterator<'a, T> {
|
|
type Item = &'a T;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if self.index >= self.buffer.len() {
|
|
None
|
|
} else {
|
|
let item = self.buffer.get(self.index);
|
|
self.index += 1;
|
|
item
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Main VPIN calculator with high-performance implementation
|
|
#[derive(Debug)]
|
|
pub struct VPINCalculator {
|
|
/// Configuration
|
|
config: VPINConfig,
|
|
/// Completed volume buckets (ring buffer)
|
|
buckets: RingBuffer<VolumeBucket>,
|
|
/// Current active bucket
|
|
current_bucket: Option<VolumeBucket>,
|
|
/// Last trade price for tick rule
|
|
last_price: i64,
|
|
/// Performance metrics
|
|
performance: VPINPerformanceMetrics,
|
|
/// Calculation counter
|
|
calculation_count: AtomicU64,
|
|
/// Latency accumulator
|
|
latency_accumulator: AtomicU64,
|
|
}
|
|
|
|
impl VPINCalculator {
|
|
/// Create new VPIN calculator with configuration
|
|
pub fn new(config: VPINConfig) -> Self {
|
|
Self {
|
|
buckets: RingBuffer::new(config.bucket_count),
|
|
current_bucket: None,
|
|
last_price: 0,
|
|
performance: VPINPerformanceMetrics::default(),
|
|
calculation_count: AtomicU64::new(0),
|
|
latency_accumulator: AtomicU64::new(0),
|
|
config,
|
|
}
|
|
}
|
|
|
|
/// Update VPIN with new market data
|
|
pub fn update(&mut self, update: &MarketDataUpdate) -> Result<(), MLError> {
|
|
let start_time = Instant::now();
|
|
|
|
// Classify trade direction if not provided
|
|
let direction = update
|
|
.direction
|
|
.unwrap_or_else(|| self.classify_trade(update));
|
|
|
|
// Create new bucket if needed
|
|
if self.current_bucket.is_none() {
|
|
self.current_bucket = Some(VolumeBucket::new(self.buckets.len(), update.timestamp));
|
|
}
|
|
|
|
// Add trade to current bucket
|
|
if let Some(ref mut bucket) = self.current_bucket {
|
|
bucket.add_trade(update.volume, direction, update.timestamp);
|
|
|
|
// Check if bucket is full
|
|
if bucket.is_full(self.config.bucket_volume) {
|
|
// Calculate remaining volume before moving bucket
|
|
let remaining_volume = bucket.total_volume - self.config.bucket_volume;
|
|
|
|
// Move to completed buckets
|
|
let completed_bucket = self.current_bucket.take().unwrap();
|
|
self.buckets.push(completed_bucket);
|
|
|
|
// Start new bucket with remaining volume
|
|
if remaining_volume > 0 {
|
|
let mut new_bucket = VolumeBucket::new(self.buckets.len(), update.timestamp);
|
|
new_bucket.add_trade(remaining_volume, direction, update.timestamp);
|
|
self.current_bucket = Some(new_bucket);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update last price for tick rule
|
|
self.last_price = update.price;
|
|
|
|
// Record performance metrics
|
|
let latency_us = start_time.elapsed().as_micros() as u64;
|
|
self.update_performance_metrics(latency_us);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Classify trade direction using available information
|
|
pub fn classify_trade(&self, update: &MarketDataUpdate) -> TradeDirection {
|
|
// Use Lee-Ready algorithm if we have bid/ask
|
|
if update.bid > 0 && update.ask > 0 {
|
|
TradeDirection::classify_lee_ready(
|
|
update.price,
|
|
update.bid,
|
|
update.ask,
|
|
self.last_price,
|
|
)
|
|
} else if self.last_price > 0 {
|
|
// Fall back to tick rule
|
|
TradeDirection::classify_tick_rule(update.price, self.last_price)
|
|
} else {
|
|
TradeDirection::Unknown
|
|
}
|
|
}
|
|
|
|
/// Get current VPIN value
|
|
pub fn get_vpin(&self) -> f64 {
|
|
if self.buckets.len() == 0 {
|
|
return 0.0;
|
|
}
|
|
|
|
let total_imbalance: i64 = self
|
|
.buckets
|
|
.iter()
|
|
.map(|bucket| bucket.calculate_imbalance().abs())
|
|
.sum();
|
|
|
|
let avg_imbalance = total_imbalance / (self.buckets.len() as i64);
|
|
(avg_imbalance as f64) / (VPIN_PRECISION_FACTOR as f64)
|
|
}
|
|
|
|
/// Get number of completed buckets
|
|
pub fn get_bucket_count(&self) -> usize {
|
|
self.buckets.len()
|
|
}
|
|
|
|
/// Check if market is currently toxic
|
|
pub fn is_toxic(&self) -> bool {
|
|
let vpin_scaled = (self.get_vpin() * VPIN_PRECISION_FACTOR as f64) as i64;
|
|
vpin_scaled > self.config.toxicity_threshold
|
|
}
|
|
|
|
/// Get comprehensive VPIN result
|
|
pub fn get_result(&self) -> VPINMetrics {
|
|
let vpin = self.get_vpin();
|
|
let order_flow_imbalance = self.calculate_order_flow_imbalance();
|
|
let toxicity_score = vpin; // Simple mapping for now
|
|
let current_bucket_fill = self.get_current_bucket_fill();
|
|
|
|
VPINMetrics {
|
|
vpin,
|
|
order_flow_imbalance,
|
|
toxicity_score,
|
|
is_toxic: self.is_toxic(),
|
|
bucket_count: self.buckets.len(),
|
|
current_bucket_fill,
|
|
}
|
|
}
|
|
|
|
/// Get performance metrics
|
|
pub fn get_metrics(&self) -> VPINPerformanceMetrics {
|
|
let total_calcs = self.calculation_count.load(Ordering::Relaxed);
|
|
let total_latency = self.latency_accumulator.load(Ordering::Relaxed);
|
|
|
|
VPINPerformanceMetrics {
|
|
total_calculations: total_calcs,
|
|
avg_latency_us: if total_calcs > 0 {
|
|
total_latency as f64 / total_calcs as f64
|
|
} else {
|
|
0.0
|
|
},
|
|
max_latency_us: self.performance.max_latency_us,
|
|
over_latency_count: self.performance.over_latency_count,
|
|
}
|
|
}
|
|
|
|
/// Get current bucket fill percentage
|
|
pub fn get_current_bucket_fill(&self) -> f64 {
|
|
if let Some(ref bucket) = self.current_bucket {
|
|
bucket.total_volume as f64 / self.config.bucket_volume as f64
|
|
} else {
|
|
0.0
|
|
}
|
|
}
|
|
|
|
/// Calculate order flow imbalance
|
|
fn calculate_order_flow_imbalance(&self) -> f64 {
|
|
if self.buckets.len() == 0 {
|
|
return 0.0;
|
|
}
|
|
|
|
let total_buy: u64 = self.buckets.iter().map(|b| b.buy_volume).sum();
|
|
let total_sell: u64 = self.buckets.iter().map(|b| b.sell_volume).sum();
|
|
let total_volume = total_buy + total_sell;
|
|
|
|
if total_volume == 0 {
|
|
0.0
|
|
} else {
|
|
(total_buy as f64 - total_sell as f64) / total_volume as f64
|
|
}
|
|
}
|
|
|
|
/// Update performance metrics
|
|
fn update_performance_metrics(&mut self, latency_us: u64) {
|
|
self.calculation_count.fetch_add(1, Ordering::Relaxed);
|
|
self.latency_accumulator
|
|
.fetch_add(latency_us, Ordering::Relaxed);
|
|
|
|
if latency_us > self.performance.max_latency_us {
|
|
self.performance.max_latency_us = latency_us;
|
|
}
|
|
|
|
if latency_us > MAX_CALCULATION_LATENCY_US {
|
|
self.performance.over_latency_count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for VPINCalculator {
|
|
fn default() -> Self {
|
|
Self::new(VPINConfig::default())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_trade_direction_classification() {
|
|
// Test Lee-Ready algorithm
|
|
let direction = TradeDirection::classify_lee_ready(
|
|
105000, // trade price (10.50)
|
|
104000, // bid (10.40)
|
|
106000, // ask (10.60)
|
|
104500, // prev price (10.45)
|
|
);
|
|
assert_eq!(direction, TradeDirection::Buy);
|
|
|
|
// Test tick rule
|
|
let direction = TradeDirection::classify_tick_rule(105000, 104000);
|
|
assert_eq!(direction, TradeDirection::Buy);
|
|
}
|
|
|
|
#[test]
|
|
fn test_volume_bucket() {
|
|
let mut bucket = VolumeBucket::new(0, 1000000);
|
|
bucket.add_trade(500, TradeDirection::Buy, 1000001);
|
|
bucket.add_trade(300, TradeDirection::Sell, 1000002);
|
|
|
|
assert_eq!(bucket.total_volume, 800);
|
|
assert!(!bucket.is_full(1000));
|
|
|
|
let imbalance = bucket.calculate_imbalance();
|
|
// (500 - 300) * 10000 / 800 = 2500
|
|
assert_eq!(imbalance, 2500);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ring_buffer() {
|
|
let mut buffer = RingBuffer::new(3);
|
|
|
|
buffer.push(1);
|
|
buffer.push(2);
|
|
buffer.push(3);
|
|
|
|
assert_eq!(buffer.len(), 3);
|
|
assert_eq!(buffer.get(0), Some(&1));
|
|
assert_eq!(buffer.get(1), Some(&2));
|
|
assert_eq!(buffer.get(2), Some(&3));
|
|
|
|
buffer.push(4);
|
|
assert_eq!(buffer.len(), 3);
|
|
assert_eq!(buffer.get(0), Some(&2));
|
|
assert_eq!(buffer.get(1), Some(&3));
|
|
assert_eq!(buffer.get(2), Some(&4));
|
|
}
|
|
}
|