Skip to main content

quant_metrics/
rolling.rs

1//! Rolling window calculations.
2
3use rust_decimal::Decimal;
4
5use crate::MetricsError;
6
7/// Rolling window calculator for time-series metrics.
8///
9/// # Example
10/// ```
11/// use quant_metrics::RollingWindow;
12/// use rust_decimal_macros::dec;
13///
14/// let equity = vec![dec!(100), dec!(101), dec!(102), dec!(101), dec!(103), dec!(105)];
15/// let rolling = RollingWindow::new(3);
16///
17/// let returns = rolling.returns(&equity).unwrap();
18/// let volatility = rolling.volatility(&equity).unwrap();
19/// ```
20pub struct RollingWindow {
21    window_size: usize,
22}
23
24impl RollingWindow {
25    /// Create a new rolling window calculator.
26    ///
27    /// # Arguments
28    /// * `window_size` - Number of periods in the window
29    pub fn new(window_size: usize) -> Self {
30        Self { window_size }
31    }
32
33    /// Calculate rolling returns.
34    ///
35    /// Returns the period return for each window.
36    pub fn returns(&self, equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
37        if equity.len() < self.window_size {
38            return Err(MetricsError::InsufficientData {
39                required: self.window_size,
40                actual: equity.len(),
41            });
42        }
43
44        let mut results = Vec::with_capacity(equity.len() - self.window_size + 1);
45
46        for window in equity.windows(self.window_size) {
47            let start = window[0];
48            let end = window[self.window_size - 1];
49
50            if start == Decimal::ZERO {
51                results.push(Decimal::ZERO);
52            } else {
53                let ret = ((end - start) / start) * Decimal::from(100);
54                results.push(ret);
55            }
56        }
57
58        Ok(results)
59    }
60
61    /// Calculate rolling volatility (standard deviation of returns).
62    pub fn volatility(&self, equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
63        if equity.len() < self.window_size + 1 {
64            return Err(MetricsError::InsufficientData {
65                required: self.window_size + 1,
66                actual: equity.len(),
67            });
68        }
69
70        // First compute period returns
71        let returns: Vec<Decimal> = equity
72            .windows(2)
73            .filter_map(|w| {
74                if w[0] == Decimal::ZERO {
75                    None
76                } else {
77                    Some((w[1] - w[0]) / w[0])
78                }
79            })
80            .collect();
81
82        if returns.len() < self.window_size {
83            return Err(MetricsError::InsufficientData {
84                required: self.window_size,
85                actual: returns.len(),
86            });
87        }
88
89        let mut results = Vec::with_capacity(returns.len() - self.window_size + 1);
90
91        for window in returns.windows(self.window_size) {
92            let std = std_deviation(window);
93            results.push(std);
94        }
95
96        Ok(results)
97    }
98
99    /// Calculate rolling Sharpe ratio.
100    ///
101    /// # Arguments
102    /// * `equity` - Equity curve
103    /// * `risk_free_rate` - Annual risk-free rate
104    /// * `periods_per_year` - Trading periods per year
105    pub fn sharpe(
106        &self,
107        equity: &[Decimal],
108        risk_free_rate: Decimal,
109        periods_per_year: u32,
110    ) -> Result<Vec<Decimal>, MetricsError> {
111        if equity.len() < self.window_size + 1 {
112            return Err(MetricsError::InsufficientData {
113                required: self.window_size + 1,
114                actual: equity.len(),
115            });
116        }
117
118        let returns: Vec<Decimal> = equity
119            .windows(2)
120            .filter_map(|w| {
121                if w[0] == Decimal::ZERO {
122                    None
123                } else {
124                    Some((w[1] - w[0]) / w[0])
125                }
126            })
127            .collect();
128
129        if returns.len() < self.window_size {
130            return Err(MetricsError::InsufficientData {
131                required: self.window_size,
132                actual: returns.len(),
133            });
134        }
135
136        let period_rf = risk_free_rate / Decimal::from(periods_per_year);
137        let sqrt_periods = decimal_sqrt(Decimal::from(periods_per_year));
138
139        let mut results = Vec::with_capacity(returns.len() - self.window_size + 1);
140
141        for window in returns.windows(self.window_size) {
142            let mean_ret = mean(window);
143            let std = std_deviation(window);
144
145            if std == Decimal::ZERO {
146                results.push(Decimal::ZERO);
147            } else {
148                let sharpe = ((mean_ret - period_rf) / std) * sqrt_periods;
149                results.push(sharpe);
150            }
151        }
152
153        Ok(results)
154    }
155
156    /// Calculate rolling maximum drawdown.
157    pub fn max_drawdown(&self, equity: &[Decimal]) -> Result<Vec<Decimal>, MetricsError> {
158        if equity.len() < self.window_size {
159            return Err(MetricsError::InsufficientData {
160                required: self.window_size,
161                actual: equity.len(),
162            });
163        }
164
165        let mut results = Vec::with_capacity(equity.len() - self.window_size + 1);
166
167        for window in equity.windows(self.window_size) {
168            let max_dd = window_max_drawdown(window);
169            results.push(max_dd);
170        }
171
172        Ok(results)
173    }
174}
175
176/// Calculate max drawdown for a window.
177fn window_max_drawdown(equity: &[Decimal]) -> Decimal {
178    if equity.is_empty() {
179        return Decimal::ZERO;
180    }
181
182    let mut peak = equity[0];
183    let mut max_dd = Decimal::ZERO;
184
185    for &value in equity {
186        if value > peak {
187            peak = value;
188        }
189
190        if peak != Decimal::ZERO {
191            let dd = (value - peak) / peak;
192            if dd < max_dd {
193                max_dd = dd;
194            }
195        }
196    }
197
198    max_dd * Decimal::from(100)
199}
200
201use crate::math::{decimal_sqrt, mean, std_deviation};
202
203#[cfg(test)]
204#[path = "rolling_tests.rs"]
205mod tests;