rusty_backtest/features/
volatility.rs

1//! Volatility Analysis - High-frequency volatility estimation
2//!
3//! Implements various volatility measures optimized for HFT timeframes.
4
5use super::TradeTick;
6use rust_decimal::prelude::ToPrimitive;
7
8/// Calculate realized volatility from trade ticks
9///
10/// Uses high-frequency returns to estimate short-term volatility.
11#[must_use]
12pub fn calculate_realized_volatility(trades: &[TradeTick]) -> f64 {
13    if trades.len() < 2 {
14        return 0.0;
15    }
16
17    // Calculate log returns
18    let mut returns = Vec::with_capacity(trades.len() - 1);
19    for i in 1..trades.len() {
20        if let (Some(prev_price), Some(curr_price)) =
21            (trades[i - 1].price.to_f64(), trades[i].price.to_f64())
22            && prev_price > 0.0
23            && curr_price > 0.0
24        {
25            returns.push((curr_price / prev_price).ln());
26        }
27    }
28
29    if returns.is_empty() {
30        return 0.0;
31    }
32
33    // Calculate sample standard deviation
34    let mean = returns.iter().sum::<f64>() / returns.len() as f64;
35
36    // Handle single return case
37    if returns.len() == 1 {
38        return returns[0].abs(); // Return absolute value of the single return
39    }
40
41    let variance =
42        returns.iter().map(|&r| (r - mean).powi(2)).sum::<f64>() / (returns.len() - 1) as f64;
43
44    variance.sqrt()
45}
46
47/// Volatility estimator with exponential decay
48pub struct VolatilityEstimator {
49    decay_factor: f64,
50    current_volatility: f64,
51    is_initialized: bool,
52}
53
54impl VolatilityEstimator {
55    /// Create new volatility estimator
56    #[must_use]
57    pub const fn new(decay_factor: f64) -> Self {
58        Self {
59            decay_factor,
60            current_volatility: 0.0,
61            is_initialized: false,
62        }
63    }
64
65    /// Update with new return and get current volatility estimate
66    #[must_use]
67    pub fn update(&mut self, return_value: f64) -> f64 {
68        if !self.is_initialized {
69            self.current_volatility = return_value.abs();
70            self.is_initialized = true;
71        } else {
72            // Exponentially weighted moving average of absolute returns
73            self.current_volatility = self.decay_factor * return_value.abs()
74                + (1.0 - self.decay_factor) * self.current_volatility;
75        }
76
77        self.current_volatility
78    }
79
80    /// Get current volatility estimate
81    pub const fn get_volatility(&self) -> f64 {
82        self.current_volatility
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use crate::features::TradeSide;
90    use rust_decimal_macros::dec;
91
92    #[test]
93    fn test_realized_volatility_zero_for_single_trade() {
94        let trades = vec![TradeTick {
95            timestamp_ns: 1000000000,
96            symbol: "BTC-USD".into(),
97            side: TradeSide::Buy,
98            price: dec!(50000),
99            quantity: dec!(1.0),
100        }];
101
102        let volatility = calculate_realized_volatility(&trades);
103        assert_eq!(volatility, 0.0);
104    }
105
106    #[test]
107    fn test_volatility_estimator() {
108        let mut estimator = VolatilityEstimator::new(0.1);
109
110        // First update should set initial volatility
111        let vol1 = estimator.update(0.01);
112        assert_eq!(vol1, 0.01);
113
114        // Second update should apply exponential decay
115        let vol2 = estimator.update(0.02);
116        assert!((vol2 - (0.1 * 0.02 + 0.9 * 0.01)).abs() < 1e-10);
117    }
118}