kaccy-core 0.2.0

Core business logic for Kaccy Protocol - batching, fee optimization, and transaction management
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
//! Order Flow Toxicity Detection
//!
//! Implements advanced metrics to detect toxic order flow that may indicate
//! informed trading, adverse selection, or market manipulation. Market makers
//! can use these metrics to adjust spreads and manage inventory risk.

use crate::error::Result;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use uuid::Uuid;

/// Trade classification
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TradeDirection {
    /// Buyer-initiated trade.
    Buy,
    /// Seller-initiated trade.
    Sell,
}

/// Trade for toxicity analysis
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToxicityTrade {
    /// Unix timestamp of the trade in milliseconds.
    pub timestamp: i64,
    /// Execution price of the trade.
    pub price: Decimal,
    /// Trade volume.
    pub volume: Decimal,
    /// Buyer- or seller-initiated direction.
    pub direction: TradeDirection,
    /// Whether the order was aggressive (crossed the spread).
    pub is_aggressive: bool,
}

/// Toxicity metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToxicityMetrics {
    /// Volume-synchronized Probability of Informed Trading (VPIN)
    pub vpin: Decimal,

    /// Order imbalance ratio (-1 to 1)
    pub order_imbalance: Decimal,

    /// Adverse selection score (0 to 1)
    pub adverse_selection: Decimal,

    /// Flow toxicity index (0 to 1)
    pub toxicity_index: Decimal,

    /// Recommended spread adjustment (multiplier)
    pub spread_adjustment: Decimal,
}

/// Order flow toxicity detector
#[derive(Debug)]
pub struct OrderFlowToxicityDetector {
    /// Bucket size for VPIN calculation (in volume)
    pub bucket_size: Decimal,

    /// Number of buckets to analyze
    pub num_buckets: usize,

    /// Trade history
    trades: VecDeque<ToxicityTrade>,

    /// Maximum trades to keep in memory
    max_trades: usize,

    /// Toxicity threshold for spread adjustment
    pub toxicity_threshold: Decimal,
}

impl OrderFlowToxicityDetector {
    /// Create a new toxicity detector
    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), // 50% threshold
        }
    }

    /// Add a trade to the history
    pub fn add_trade(&mut self, trade: ToxicityTrade) {
        self.trades.push_back(trade);

        // Keep only recent trades
        while self.trades.len() > self.max_trades {
            self.trades.pop_front();
        }
    }

    /// Calculate Volume-synchronized Probability of Informed Trading (VPIN)
    /// VPIN measures the probability that the next trade is informed
    /// Formula: VPIN = |V_buy - V_sell| / V_total over volume buckets
    pub fn calculate_vpin(&self) -> Result<Decimal> {
        if self.trades.is_empty() {
            return Ok(Decimal::ZERO);
        }

        // Organize trades into volume buckets
        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 {
                // Trade fits in current bucket
                match trade.direction {
                    TradeDirection::Buy => current_bucket_buy += trade.volume,
                    TradeDirection::Sell => current_bucket_sell += trade.volume,
                }
                current_bucket_volume += trade.volume;

                // Check if bucket is complete
                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 {
                // Trade spans multiple buckets
                let mut remaining_volume = trade.volume;

                // Fill current bucket
                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;
                }

                // Create additional buckets as needed
                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;
                }

                // Start new bucket with remainder
                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,
                }
            }
        }

        // Calculate VPIN over the last N buckets
        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)
    }

    /// Calculate order imbalance ratio
    /// Positive = more buying pressure, Negative = more selling pressure
    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)
    }

    /// Calculate adverse selection score
    /// Measures how much price moves against the market maker after a trade
    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;

            // Check if price moved against the liquidity provider
            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)
    }

    /// Identify informed traders based on trade patterns
    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();

        // Count profitable trades for each user
        for (i, (user_id, trade)) in user_trades.iter().enumerate() {
            // Look ahead to see if trade was profitable
            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; // Total trades
                if is_profitable {
                    stats.0 += 1; // Profitable trades
                }
            }
        }

        // Identify traders with high win rate
        let mut informed_traders = Vec::new();
        for (user_id, (profitable, total)) in trader_metrics {
            if total >= 5 {
                // Minimum 5 trades
                let win_rate = Decimal::from(profitable) / Decimal::from(total);
                if win_rate > dec!(0.7) {
                    // >70% win rate
                    informed_traders.push(user_id);
                }
            }
        }

        informed_traders
    }

    /// Calculate comprehensive toxicity metrics
    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)?;

        // Calculate composite toxicity index
        // Weighted average of different metrics
        let toxicity_index = (vpin * dec!(0.4))
            + (order_imbalance.abs() * dec!(0.3))
            + (adverse_selection * dec!(0.3));

        // Calculate recommended spread adjustment
        // Higher toxicity = wider spread
        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,
        })
    }

    /// Adjust quote based on toxicity
    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);

        // Widen spread based on toxicity
        let adjusted_spread = spread * metrics.spread_adjustment;

        // Also adjust mid price slightly based on order imbalance
        let mid_adjustment = mid * metrics.order_imbalance * dec!(0.001); // 0.1% max adjustment

        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), // 1000 units per bucket
            50,         // Analyze 50 buckets
            10000,      // Keep 10k trades
        )
    }
}

#[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();

        // Add more buy trades
        for i in 0..7 {
            detector.add_trade(ToxicityTrade {
                timestamp: 1000 + i,
                price: dec!(100),
                volume: dec!(10),
                direction: TradeDirection::Buy,
                is_aggressive: true,
            });
        }

        // Add fewer sell trades
        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();

        // Should be positive (more buying)
        assert!(imbalance > Decimal::ZERO);
        // 70 buy - 30 sell = 40 / 100 = 0.4
        assert_eq!(imbalance, dec!(0.4));
    }

    #[test]
    fn test_adverse_selection() {
        let mut detector = OrderFlowToxicityDetector::default();

        // Add trades where price moves against liquidity provider
        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), // Price went up after buy (adverse)
            volume: dec!(10),
            direction: TradeDirection::Sell,
            is_aggressive: true,
        });

        detector.add_trade(ToxicityTrade {
            timestamp: 1002,
            price: dec!(103), // Price went down after sell (adverse)
            volume: dec!(10),
            direction: TradeDirection::Buy,
            is_aggressive: true,
        });

        let adverse_selection = detector.calculate_adverse_selection(100).unwrap();

        // Should be 100% adverse selection
        assert_eq!(adverse_selection, dec!(1.0));
    }

    #[test]
    fn test_vpin_calculation() {
        let mut detector = OrderFlowToxicityDetector::new(dec!(100), 5, 1000);

        // Add trades to create imbalance
        for i in 0..10 {
            detector.add_trade(ToxicityTrade {
                timestamp: 1000 + i,
                price: dec!(100),
                volume: dec!(15), // Will create multiple buckets
                direction: if i < 7 {
                    TradeDirection::Buy
                } else {
                    TradeDirection::Sell
                },
                is_aggressive: true,
            });
        }

        let vpin = detector.calculate_vpin().unwrap();

        // VPIN should be positive indicating order flow imbalance
        assert!(vpin > Decimal::ZERO);
        assert!(vpin <= dec!(1.0));
    }

    #[test]
    fn test_toxicity_metrics() {
        let mut detector = OrderFlowToxicityDetector::default();

        // Add some trades
        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();

        // Spread should be wider than original
        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();

        // User1: High win rate (informed)
        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));
        }

        // User2: Low win rate (not informed)
        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);

        // User1 should be identified as informed (consistent profitable pattern)
        assert!(informed.contains(&user1));
    }

    #[test]
    fn test_max_trades_limit() {
        let mut detector = OrderFlowToxicityDetector::new(dec!(100), 5, 100);

        // Add more than max trades
        for i in 0..150 {
            detector.add_trade(ToxicityTrade {
                timestamp: 1000 + i,
                price: dec!(100),
                volume: dec!(10),
                direction: TradeDirection::Buy,
                is_aggressive: true,
            });
        }

        // Should only keep max_trades
        assert_eq!(detector.trades.len(), 100);
    }
}