//! Technical Indicator Features //! //! Dual-API implementations of core technical indicators: //! - RSI (Relative Strength Index) //! - EMA (Exponential Moving Average) //! - MACD (Moving Average Convergence Divergence) //! - Bollinger Bands //! - ATR (Average True Range) //! - ADX (Average Directional Index) //! //! Each indicator provides: //! 1. Streaming API: Stateful struct with update() method (O(1) per update) //! 2. Batch API: Process vector of prices (convenience wrapper) use std::collections::VecDeque; // ===== RSI (Relative Strength Index) ===== /// Relative Strength Index calculator (streaming API) #[derive(Debug, Clone)] pub struct RSI { period: usize, gains: VecDeque, losses: VecDeque, prev_close: Option, } impl RSI { /// Create a new RSI calculator /// /// ## Arguments /// - `period`: RSI period (typical: 14) pub fn new(period: usize) -> Self { Self { period, gains: VecDeque::with_capacity(period), losses: VecDeque::with_capacity(period), prev_close: None, } } /// Update RSI with a new price /// /// ## Returns /// Current RSI value (0-100 scale) pub fn update(&mut self, price: f64) -> f64 { if let Some(prev) = self.prev_close { let change = price - prev; let gain = if change > 0.0 { change } else { 0.0 }; let loss = if change < 0.0 { -change } else { 0.0 }; self.gains.push_back(gain); self.losses.push_back(loss); if self.gains.len() > self.period { self.gains.pop_front(); self.losses.pop_front(); } if self.gains.len() == self.period { let avg_gain: f64 = self.gains.iter().sum::() / self.period as f64; let avg_loss: f64 = self.losses.iter().sum::() / self.period as f64; if avg_loss > 0.0 { let rs = avg_gain / avg_loss; self.prev_close = Some(price); return 100.0 - (100.0 / (1.0 + rs)); } } } self.prev_close = Some(price); 50.0 // Default neutral RSI } } /// RSI batch API: Calculate RSI for a vector of prices pub fn rsi_batch(prices: &[f64], period: usize) -> Vec { let mut rsi = RSI::new(period); prices.iter().map(|&p| rsi.update(p)).collect() } // ===== EMA (Exponential Moving Average) ===== /// Exponential Moving Average calculator (streaming API) #[derive(Debug, Clone)] pub struct EMA { _period: usize, multiplier: f64, ema: Option, } impl EMA { /// Create a new EMA calculator /// /// ## Arguments /// - `period`: EMA period (typical: 12, 26) pub fn new(period: usize) -> Self { Self { _period: period, multiplier: 2.0 / (period as f64 + 1.0), ema: None, } } /// Update EMA with a new price /// /// ## Returns /// Current EMA value pub fn update(&mut self, price: f64) -> f64 { match self.ema { None => { self.ema = Some(price); price } Some(prev_ema) => { let new_ema = price * self.multiplier + prev_ema * (1.0 - self.multiplier); self.ema = Some(new_ema); new_ema } } } } /// EMA batch API: Calculate EMA for a vector of prices pub fn ema_batch(prices: &[f64], period: usize) -> Vec { let mut ema = EMA::new(period); prices.iter().map(|&p| ema.update(p)).collect() } // ===== MACD (Moving Average Convergence Divergence) ===== /// MACD calculator (streaming API) #[derive(Debug, Clone)] pub struct MACD { ema_fast: EMA, ema_slow: EMA, signal: EMA, } impl MACD { /// Create a new MACD calculator /// /// ## Arguments /// - `fast_period`: Fast EMA period (typical: 12) /// - `slow_period`: Slow EMA period (typical: 26) /// - `signal_period`: Signal line period (typical: 9) pub fn new(fast_period: usize, slow_period: usize, signal_period: usize) -> Self { Self { ema_fast: EMA::new(fast_period), ema_slow: EMA::new(slow_period), signal: EMA::new(signal_period), } } /// Update MACD with a new price /// /// ## Returns /// Tuple of (MACD line, Signal line, Histogram) pub fn update(&mut self, price: f64) -> (f64, f64, f64) { let fast = self.ema_fast.update(price); let slow = self.ema_slow.update(price); let macd_line = fast - slow; let signal_line = self.signal.update(macd_line); let histogram = macd_line - signal_line; (macd_line, signal_line, histogram) } } /// MACD batch API: Calculate MACD for a vector of prices pub fn macd_batch( prices: &[f64], fast_period: usize, slow_period: usize, signal_period: usize, ) -> Vec<(f64, f64, f64)> { let mut macd = MACD::new(fast_period, slow_period, signal_period); prices.iter().map(|&p| macd.update(p)).collect() } // ===== Bollinger Bands ===== /// Bollinger Bands calculator (streaming API) #[derive(Debug, Clone)] pub struct BollingerBands { period: usize, std_multiplier: f64, prices: VecDeque, } impl BollingerBands { /// Create a new Bollinger Bands calculator /// /// ## Arguments /// - `period`: Period for SMA and standard deviation (typical: 20) /// - `std_multiplier`: Number of standard deviations (typical: 2.0) pub fn new(period: usize, std_multiplier: f64) -> Self { Self { period, std_multiplier, prices: VecDeque::with_capacity(period), } } /// Update Bollinger Bands with a new price /// /// ## Returns /// Tuple of (Middle band, Upper band, Lower band) pub fn update(&mut self, price: f64) -> (f64, f64, f64) { self.prices.push_back(price); if self.prices.len() > self.period { self.prices.pop_front(); } if self.prices.len() == self.period { let sum: f64 = self.prices.iter().sum(); let middle = sum / self.period as f64; let variance: f64 = self .prices .iter() .map(|p| (p - middle).powi(2)) .sum::() / self.period as f64; let std = variance.sqrt(); let upper = middle + self.std_multiplier * std; let lower = middle - self.std_multiplier * std; (middle, upper, lower) } else { (price, price, price) } } } /// Bollinger Bands batch API pub fn bollinger_batch(prices: &[f64], period: usize, std_multiplier: f64) -> Vec<(f64, f64, f64)> { let mut bb = BollingerBands::new(period, std_multiplier); prices.iter().map(|&p| bb.update(p)).collect() } // ===== ATR (Average True Range) ===== /// ATR calculator (streaming API) #[derive(Debug, Clone)] pub struct ATR { period: usize, true_ranges: VecDeque, prev_close: Option, } impl ATR { /// Create a new ATR calculator /// /// ## Arguments /// - `period`: ATR period (typical: 14) pub fn new(period: usize) -> Self { Self { period, true_ranges: VecDeque::with_capacity(period), prev_close: None, } } /// Update ATR with a new OHLC bar /// /// ## Arguments /// - `high`: High price /// - `low`: Low price /// - `close`: Close price /// /// ## Returns /// Current ATR value pub fn update(&mut self, high: f64, low: f64, close: f64) -> f64 { if let Some(prev) = self.prev_close { let tr = (high - low) .max((high - prev).abs()) .max((low - prev).abs()); self.true_ranges.push_back(tr); if self.true_ranges.len() > self.period { self.true_ranges.pop_front(); } if self.true_ranges.len() == self.period { self.prev_close = Some(close); return self.true_ranges.iter().sum::() / self.period as f64; } } self.prev_close = Some(close); high - low // Fallback to simple range } } /// ATR batch API: Calculate ATR for a vector of (high, low, close) tuples pub fn atr_batch(bars: &[(f64, f64, f64)], period: usize) -> Vec { let mut atr = ATR::new(period); bars.iter() .map(|&(high, low, close)| atr.update(high, low, close)) .collect() } // ===== ADX (Average Directional Index) ===== /// ADX calculator (streaming API) /// /// Based on ml/src/regime/trending.rs and ml/src/features/regime_adx.rs #[derive(Debug, Clone)] pub struct ADX { _period: usize, _alpha: f64, alpha_wilder: f64, atr: Option, plus_dm_smooth: Option, minus_dm_smooth: Option, adx: Option, prev_high: Option, prev_low: Option, prev_close: Option, bar_count: usize, } impl ADX { /// Create a new ADX calculator /// /// ## Arguments /// - `period`: ADX period (typical: 14) pub fn new(period: usize) -> Self { Self { _period: period, _alpha: 1.0 / period as f64, alpha_wilder: 1.0 / period as f64, atr: None, plus_dm_smooth: None, minus_dm_smooth: None, adx: None, prev_high: None, prev_low: None, prev_close: None, bar_count: 0, } } /// Update ADX with a new OHLC bar /// /// ## Arguments /// - `high`: High price /// - `low`: Low price /// - `close`: Close price /// /// ## Returns /// Tuple of (ADX, +DI, -DI) pub fn update(&mut self, high: f64, low: f64, close: f64) -> (f64, f64, f64) { self.bar_count += 1; // Need at least 2 bars for calculation let (Some(prev_high), Some(prev_low), Some(prev_close)) = (self.prev_high, self.prev_low, self.prev_close) else { self.prev_high = Some(high); self.prev_low = Some(low); self.prev_close = Some(close); return (0.0, 0.0, 0.0); }; // 1. Calculate True Range let tr = (high - low) .max((high - prev_close).abs()) .max((low - prev_close).abs()); // 2. Calculate Directional Movement let plus_dm = if high > prev_high { (high - prev_high).max(0.0) } else { 0.0 }; let minus_dm = if low < prev_low { (prev_low - low).max(0.0) } else { 0.0 }; // 3. Smooth using Wilder's method self.atr = Some(match self.atr { None => tr, Some(prev_atr) => prev_atr * (1.0 - self.alpha_wilder) + tr * self.alpha_wilder, }); self.plus_dm_smooth = Some(match self.plus_dm_smooth { None => plus_dm, Some(prev) => prev * (1.0 - self.alpha_wilder) + plus_dm * self.alpha_wilder, }); self.minus_dm_smooth = Some(match self.minus_dm_smooth { None => minus_dm, Some(prev) => prev * (1.0 - self.alpha_wilder) + minus_dm * self.alpha_wilder, }); // 4. Calculate +DI and -DI let atr_val = self.atr.unwrap_or(1.0).max(1e-8); let plus_di = 100.0 * self.plus_dm_smooth.unwrap_or(0.0) / atr_val; let minus_di = 100.0 * self.minus_dm_smooth.unwrap_or(0.0) / atr_val; // 5. Calculate DX let di_sum = plus_di + minus_di; let dx = if di_sum > 1e-8 { 100.0 * (plus_di - minus_di).abs() / di_sum } else { 0.0 }; // 6. Smooth DX to get ADX self.adx = Some(match self.adx { None => dx, Some(prev_adx) => prev_adx * (1.0 - self.alpha_wilder) + dx * self.alpha_wilder, }); // Update state self.prev_high = Some(high); self.prev_low = Some(low); self.prev_close = Some(close); let adx_val = self.adx.unwrap_or(0.0).clamp(0.0, 100.0); (adx_val, plus_di.clamp(0.0, 100.0), minus_di.clamp(0.0, 100.0)) } } /// ADX batch API: Calculate ADX for a vector of (high, low, close) tuples pub fn adx_batch(bars: &[(f64, f64, f64)], period: usize) -> Vec<(f64, f64, f64)> { let mut adx = ADX::new(period); bars.iter() .map(|&(high, low, close)| adx.update(high, low, close)) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn test_rsi() { let prices = vec![ 44.0, 44.25, 44.5, 43.75, 44.0, 44.5, 44.75, 44.25, 44.0, 43.5, 43.75, 44.0, 44.5, 45.0, 45.5, ]; let rsi_values = rsi_batch(&prices, 14); assert_eq!(rsi_values.len(), 15); // RSI should be between 0 and 100 for &rsi in &rsi_values { assert!(rsi >= 0.0 && rsi <= 100.0); } } #[test] fn test_ema() { let prices = vec![10.0, 11.0, 12.0, 11.5, 12.5, 13.0]; let ema_values = ema_batch(&prices, 3); assert_eq!(ema_values.len(), 6); // EMA should track prices assert!(ema_values[5] > 10.0); } #[test] fn test_macd() { let prices = vec![100.0; 30]; let macd_values = macd_batch(&prices, 12, 26, 9); assert_eq!(macd_values.len(), 30); // For constant prices, MACD should approach 0 let (macd, signal, hist) = macd_values[29]; assert!(macd.abs() < 1.0); assert!(signal.abs() < 1.0); assert!(hist.abs() < 1.0); } #[test] fn test_bollinger_bands() { let prices = vec![100.0; 25]; let bb_values = bollinger_batch(&prices, 20, 2.0); assert_eq!(bb_values.len(), 25); // For constant prices, all bands should be equal let (middle, upper, lower) = bb_values[24]; assert!((middle - 100.0).abs() < 1e-6); assert!((upper - middle).abs() < 1e-6); assert!((lower - middle).abs() < 1e-6); } #[test] fn test_atr() { let bars = vec![ (102.0, 98.0, 100.0), (103.0, 99.0, 101.0), (104.0, 100.0, 102.0), ]; let atr_values = atr_batch(&bars, 2); assert_eq!(atr_values.len(), 3); // ATR should be positive for &atr in &atr_values { assert!(atr > 0.0); } } #[test] fn test_adx() { let bars = vec![ (102.0, 98.0, 100.0), (103.0, 99.0, 101.0), (104.0, 100.0, 102.0), (105.0, 101.0, 103.0), (106.0, 102.0, 104.0), ]; let adx_values = adx_batch(&bars, 3); assert_eq!(adx_values.len(), 5); // ADX and DI values should be in [0, 100] for &(adx, plus_di, minus_di) in &adx_values { assert!(adx >= 0.0 && adx <= 100.0); assert!(plus_di >= 0.0 && plus_di <= 100.0); assert!(minus_di >= 0.0 && minus_di <= 100.0); } } }