1pub mod asymmetry;
7pub mod liquidity;
8pub mod market_impact;
9pub mod order_flow;
10pub mod queue;
11pub mod tardis_advanced;
12pub mod tardis_features;
13pub mod volatility;
14
15#[cfg(target_arch = "x86_64")]
16pub mod order_flow_simd;
17
18#[cfg(target_arch = "x86_64")]
19pub mod volatility_simd;
20
21#[cfg(test)]
22mod proptest_tests;
23
24pub use asymmetry::{
26 AsymmetryIndexCalculator, AsymmetryMetrics, MultiTimeframeAsymmetry, calculate_asymmetry_index,
27};
28pub use liquidity::{LiquidityAnalyzer, calculate_liquidity_shocks};
29pub use market_impact::{KylesLambdaCalculator, MarketImpactAnalyzer, MarketImpactMetrics};
30pub use order_flow::{OrderFlowAnalyzer, calculate_ofi, calculate_vpin};
31pub use queue::{QueueAnalyzer, calculate_queue_imbalance};
32pub use tardis_advanced::{
33 HarmonicOscillator, HarmonicResult, OrderType, OrderTypeTracker, TardisAdvancedFeatures,
34 TardisConfig, TardisFeatureVector,
35};
36pub use tardis_features::{
37 AdvancedVPINCalculator, ExponentialDecayCalculator, PriceEntropyCalculator, PriceRunCalculator,
38 RelativeTickVolumeCalculator, RollingPriceImpactCalculator, TradingBurstDetector,
39 VolumePriceSensitivityCalculator, calculate_depth_weighted_ofi,
40 calculate_liquidity_shock_ratio, calculate_multi_level_ofi_detailed,
41 calculate_multi_level_queue_imbalance, calculate_order_book_depth, calculate_order_book_slope,
42 calculate_order_cancel_rate, calculate_relative_spread, calculate_volume_weighted_ofi,
43 calculate_weighted_order_imbalance,
44};
45pub use volatility::{VolatilityEstimator, calculate_realized_volatility};
46
47use rust_decimal::Decimal;
48use rust_decimal::prelude::ToPrimitive;
49use rusty_common::collections::FxHashMap;
50use smallvec::SmallVec;
51
52#[derive(Debug, Clone)]
54pub enum FeatureError {
55 DecimalConversion(&'static str),
57 InvalidInput(&'static str),
59 InsufficientData,
61}
62
63impl std::fmt::Display for FeatureError {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 match self {
66 FeatureError::DecimalConversion(msg) => write!(f, "Decimal conversion error: {msg}"),
67 FeatureError::InvalidInput(msg) => write!(f, "Invalid input: {msg}"),
68 FeatureError::InsufficientData => write!(f, "Insufficient data for calculation"),
69 }
70 }
71}
72
73impl std::error::Error for FeatureError {}
74
75pub type FeatureResult<T> = Result<T, FeatureError>;
77
78#[inline]
85pub(crate) fn decimal_to_f64_or_nan(d: Decimal) -> f64 {
86 d.to_f64().unwrap_or_else(|| {
87 #[cfg(debug_assertions)]
88 eprintln!("Warning: Decimal to f64 conversion failed for value: {d}");
89 f64::NAN
90 })
91}
92
93#[derive(Debug, Clone, Copy)]
95pub struct Level {
96 pub price: Decimal,
98 pub quantity: Decimal,
100 pub order_count: u32,
102}
103
104#[repr(align(64))]
108#[derive(Debug, Clone)]
109pub struct OrderBookSnapshot {
110 pub timestamp_ns: u64,
112 pub symbol: String,
114 pub bids: SmallVec<[Level; 25]>,
116 pub asks: SmallVec<[Level; 25]>,
118}
119
120impl OrderBookSnapshot {
121 #[must_use]
123 pub fn mid_price(&self) -> Decimal {
124 if self.bids.is_empty() || self.asks.is_empty() {
125 return Decimal::ZERO;
126 }
127 (self.bids[0].price + self.asks[0].price) / Decimal::TWO
128 }
129}
130
131#[repr(align(64))]
135#[derive(Debug, Clone)]
136pub struct TradeTick {
137 pub timestamp_ns: u64,
139 pub symbol: String,
141 pub side: TradeSide,
143 pub price: Decimal,
145 pub quantity: Decimal,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum TradeSide {
152 Buy = 1,
154 Sell = -1,
156}
157
158#[repr(align(64))]
162#[derive(Debug, Clone, Default)]
163pub struct MicrostructuralFeatures {
164 pub timestamp_ns: u64,
166
167 pub ofi_basic: f64,
170 pub ofi_weighted: f64,
172 pub vpin: f64,
174
175 pub queue_imbalance: f64,
178 pub weighted_queue_imbalance: f64,
180
181 pub bid_liquidity: Decimal,
184 pub ask_liquidity: Decimal,
186 pub liquidity_ratio: f64,
188
189 pub realized_volatility: f64,
192 pub price_impact: f64,
194
195 pub spread: Decimal,
198 pub mid_price: Decimal,
200 pub relative_spread: f64,
202}
203
204#[derive(Debug, Clone, Default)]
206pub struct FeatureSnapshot {
207 pub timestamp: u64,
209
210 pub vwap: f64,
213 pub trade_intensity: f64,
215 pub buy_sell_ratio: f64,
217 pub large_trade_ratio: f64,
219 pub order_flow_imbalance: f64,
221
222 pub bid_liquidity: f64,
225 pub ask_liquidity: f64,
227 pub spread_bps: f64,
229 pub mid_price: f64,
231 pub queue_imbalance: f64,
233 pub book_depth_ratio: f64,
235 pub bid_slope: f64,
237 pub ask_slope: f64,
239 pub orderbook_skew: f64,
241 pub orderbook_imbalance: f64,
243 pub weighted_mid_price: f64,
245
246 pub vpin: f64,
249 pub weighted_queue_imbalance: f64,
251 pub ofi: f64,
253 pub weighted_ofi: f64,
255 pub realized_volatility: f64,
257 pub kyles_lambda: f64,
259 pub asymmetry_index: f64,
261}
262
263#[derive(Debug, Clone, Default)]
268pub struct TradeFeatures {
269 pub vwap: f64,
271 pub trade_intensity: f64,
273 pub buy_sell_ratio: f64,
275 pub large_trade_ratio: f64,
277 pub order_flow_imbalance: f64,
279}
280
281#[derive(Debug, Clone, Default)]
287pub struct OrderBookFeatures {
288 pub bid_liquidity: f64,
290 pub ask_liquidity: f64,
292 pub spread_bps: f64,
294 pub mid_price: f64,
296 pub queue_imbalance: f64,
298 pub book_depth_ratio: f64,
300 pub bid_slope: f64,
302 pub ask_slope: f64,
304 pub orderbook_skew: f64,
306 pub orderbook_imbalance: f64,
308 pub weighted_mid_price: f64,
310 pub ofi_basic: f64,
312}
313
314pub struct FeatureCalculator {
316 window_size: usize,
317 trade_buffer: Vec<TradeTick>,
318 orderbook_buffer: Vec<OrderBookSnapshot>,
319 #[allow(dead_code)]
320 feature_cache: FxHashMap<u64, MicrostructuralFeatures>,
321}
322
323impl FeatureCalculator {
324 #[must_use]
326 pub fn new(window_size: usize) -> Self {
327 Self {
328 window_size,
329 trade_buffer: Vec::with_capacity(window_size * 2),
330 orderbook_buffer: Vec::with_capacity(window_size * 2),
331 feature_cache: FxHashMap::default(),
332 }
333 }
334
335 pub fn add_trade(&mut self, trade: &TradeTick) {
350 self.trade_buffer.push(trade.clone());
351
352 if self.trade_buffer.len() > self.window_size * 2 {
354 let drain_count = self.window_size;
355 self.trade_buffer.drain(0..drain_count);
356 }
357 }
358
359 pub fn add_orderbook(&mut self, snapshot: &OrderBookSnapshot) {
361 self.orderbook_buffer.push(snapshot.clone());
362
363 if self.orderbook_buffer.len() > self.window_size * 2 {
365 let drain_count = self.window_size;
366 self.orderbook_buffer.drain(0..drain_count);
367 }
368 }
369
370 fn calculate_trade_features(&self) -> TradeFeatures {
372 let recent_trades =
373 &self.trade_buffer[self.trade_buffer.len().saturating_sub(self.window_size)..];
374
375 let mut features = TradeFeatures::default();
376
377 if !recent_trades.is_empty() {
379 let total_value: f64 = recent_trades
380 .iter()
381 .map(|t| decimal_to_f64_or_nan(t.price * t.quantity))
382 .sum();
383 let total_volume: f64 = recent_trades
384 .iter()
385 .map(|t| decimal_to_f64_or_nan(t.quantity))
386 .sum();
387 features.vwap = if total_volume > 0.0 {
388 total_value / total_volume
389 } else {
390 0.0
391 };
392
393 features.trade_intensity = recent_trades.len() as f64;
395
396 let buy_count = recent_trades
398 .iter()
399 .filter(|t| t.side == TradeSide::Buy)
400 .count() as f64;
401 let sell_count = recent_trades
402 .iter()
403 .filter(|t| t.side == TradeSide::Sell)
404 .count() as f64;
405 features.buy_sell_ratio = if sell_count > 0.0 {
406 buy_count / sell_count
407 } else {
408 buy_count
409 };
410
411 features.large_trade_ratio = 0.0;
413
414 let buy_volume: f64 = recent_trades
416 .iter()
417 .filter(|t| t.side == TradeSide::Buy)
418 .map(|t| decimal_to_f64_or_nan(t.quantity))
419 .sum();
420 let sell_volume: f64 = recent_trades
421 .iter()
422 .filter(|t| t.side == TradeSide::Sell)
423 .map(|t| decimal_to_f64_or_nan(t.quantity))
424 .sum();
425 let total = buy_volume + sell_volume;
426 features.order_flow_imbalance = if total > 0.0 {
427 (buy_volume - sell_volume) / total
428 } else {
429 0.0
430 };
431 }
432
433 features
434 }
435
436 fn calculate_orderbook_features(&self) -> OrderBookFeatures {
438 let recent_books =
439 &self.orderbook_buffer[self.orderbook_buffer.len().saturating_sub(self.window_size)..];
440
441 let mut features = OrderBookFeatures::default();
442
443 if let Some(current_book) = recent_books.last() {
444 if let (Some(best_bid), Some(best_ask)) =
446 (current_book.bids.first(), current_book.asks.first())
447 {
448 let spread = best_ask.price - best_bid.price;
449 features.mid_price =
450 decimal_to_f64_or_nan((best_bid.price + best_ask.price) / Decimal::from(2));
451 features.spread_bps = decimal_to_f64_or_nan(
452 spread / ((best_bid.price + best_ask.price) / Decimal::from(2))
453 * Decimal::from(10000),
454 );
455 }
456
457 features.queue_imbalance =
459 calculate_queue_imbalance(¤t_book.bids, ¤t_book.asks);
460
461 if recent_books.len() >= 2 {
463 let prev_book = &recent_books[recent_books.len() - 2];
464 features.ofi_basic = calculate_ofi(prev_book, current_book);
465 }
466
467 features.bid_liquidity = current_book
469 .bids
470 .iter()
471 .take(10)
472 .map(|l| decimal_to_f64_or_nan(l.quantity))
473 .sum();
474 features.ask_liquidity = current_book
475 .asks
476 .iter()
477 .take(10)
478 .map(|l| decimal_to_f64_or_nan(l.quantity))
479 .sum();
480
481 features.book_depth_ratio = if features.ask_liquidity > 0.0 {
483 features.bid_liquidity / features.ask_liquidity
484 } else {
485 0.0
486 };
487
488 features.bid_slope = 0.0;
490 features.ask_slope = 0.0;
491
492 features.orderbook_skew = if features.bid_liquidity + features.ask_liquidity > 0.0 {
494 (features.bid_liquidity - features.ask_liquidity)
495 / (features.bid_liquidity + features.ask_liquidity)
496 } else {
497 0.0
498 };
499
500 features.orderbook_imbalance = features.orderbook_skew;
502
503 if let (Some(best_bid), Some(best_ask)) =
505 (current_book.bids.first(), current_book.asks.first())
506 {
507 let bid_weight = decimal_to_f64_or_nan(best_bid.quantity);
508 let ask_weight = decimal_to_f64_or_nan(best_ask.quantity);
509 let total_weight = bid_weight + ask_weight;
510 if total_weight > 0.0 {
511 features.weighted_mid_price = (decimal_to_f64_or_nan(best_bid.price)
512 * ask_weight
513 + decimal_to_f64_or_nan(best_ask.price) * bid_weight)
514 / total_weight;
515 } else {
516 features.weighted_mid_price = features.mid_price;
517 }
518 }
519 }
520
521 features
522 }
523
524 #[must_use]
526 pub fn on_orderbook(&mut self, snapshot: &OrderBookSnapshot) -> OrderBookFeatures {
527 self.add_orderbook(snapshot);
528 self.calculate_orderbook_features()
529 }
530
531 #[must_use]
533 pub fn on_trade(&mut self, trade: &TradeTick) -> Option<TradeFeatures> {
534 self.add_trade(trade);
535 if !self.trade_buffer.is_empty() {
536 Some(self.calculate_trade_features())
537 } else {
538 None
539 }
540 }
541
542 pub fn get_features(&self) -> FeatureSnapshot {
544 let trade_features = if !self.trade_buffer.is_empty() {
546 self.calculate_trade_features()
547 } else {
548 TradeFeatures::default()
549 };
550
551 let orderbook_features = if !self.orderbook_buffer.is_empty() {
552 self.calculate_orderbook_features()
553 } else {
554 OrderBookFeatures::default()
555 };
556
557 FeatureSnapshot {
558 timestamp: self
559 .orderbook_buffer
560 .last()
561 .map(|ob| ob.timestamp_ns)
562 .or_else(|| self.trade_buffer.last().map(|t| t.timestamp_ns))
563 .unwrap_or(0),
564 vwap: trade_features.vwap,
566 trade_intensity: trade_features.trade_intensity,
567 buy_sell_ratio: trade_features.buy_sell_ratio,
568 large_trade_ratio: trade_features.large_trade_ratio,
569 order_flow_imbalance: trade_features.order_flow_imbalance,
570 bid_liquidity: orderbook_features.bid_liquidity,
571 ask_liquidity: orderbook_features.ask_liquidity,
572 spread_bps: orderbook_features.spread_bps,
573 mid_price: orderbook_features.mid_price,
574 queue_imbalance: orderbook_features.queue_imbalance,
575 book_depth_ratio: orderbook_features.book_depth_ratio,
576 bid_slope: orderbook_features.bid_slope,
577 ask_slope: orderbook_features.ask_slope,
578 orderbook_skew: orderbook_features.orderbook_skew,
579 orderbook_imbalance: orderbook_features.orderbook_imbalance,
580 weighted_mid_price: orderbook_features.weighted_mid_price,
581
582 vpin: 0.0,
584 weighted_queue_imbalance: 0.0,
585 ofi: orderbook_features.ofi_basic,
586 weighted_ofi: 0.0,
587 realized_volatility: 0.0,
588 kyles_lambda: 0.0,
589 asymmetry_index: 0.0,
590 }
591 }
592}