Skip to main content

quant_indicators/
rsi.rs

1//! Relative Strength Index (RSI) indicator.
2
3use quant_primitives::Candle;
4use rust_decimal::Decimal;
5
6use crate::error::IndicatorError;
7use crate::indicator::Indicator;
8use crate::series::Series;
9
10/// Relative Strength Index indicator.
11///
12/// Measures the magnitude of recent price changes to evaluate overbought
13/// or oversold conditions. Values range from 0 to 100.
14///
15/// # Formula
16///
17/// RSI = 100 - (100 / (1 + RS))
18/// RS = Average Gain / Average Loss
19///
20/// Traditional interpretation:
21/// - RSI > 70: Overbought
22/// - RSI < 30: Oversold
23///
24/// # Example
25///
26/// ```
27/// use quant_indicators::{Indicator, Rsi};
28/// use quant_primitives::Candle;
29/// use chrono::Utc;
30/// use rust_decimal_macros::dec;
31///
32/// let ts = Utc::now();
33/// let candles: Vec<Candle> = (0..20).map(|i| {
34///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
35/// }).collect();
36/// let rsi = Rsi::new(14).unwrap();
37/// let series = rsi.compute(&candles).unwrap();
38/// ```
39#[derive(Debug, Clone)]
40pub struct Rsi {
41    period: usize,
42    name: String,
43}
44
45impl Rsi {
46    /// Create a new RSI indicator with the specified period.
47    ///
48    /// Standard period is 14.
49    ///
50    /// # Errors
51    ///
52    /// Returns `InvalidParameter` if period is 0.
53    pub fn new(period: usize) -> Result<Self, IndicatorError> {
54        if period == 0 {
55            return Err(IndicatorError::InvalidParameter {
56                message: "RSI period must be > 0".to_string(),
57            });
58        }
59        Ok(Self {
60            period,
61            name: format!("RSI({})", period),
62        })
63    }
64}
65
66impl Indicator for Rsi {
67    fn name(&self) -> &str {
68        &self.name
69    }
70
71    fn warmup_period(&self) -> usize {
72        // Need period + 1 candles to compute first RSI
73        // (period changes = period + 1 prices)
74        self.period + 1
75    }
76
77    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
78        let required = self.period + 1;
79        if candles.len() < required {
80            return Err(IndicatorError::InsufficientData {
81                required,
82                actual: candles.len(),
83            });
84        }
85
86        // Calculate price changes
87        let changes: Vec<Decimal> = candles
88            .windows(2)
89            .map(|w| w[1].close() - w[0].close())
90            .collect();
91
92        let mut values = Vec::with_capacity(candles.len() - required + 1);
93        let period_dec = Decimal::from(self.period as u64);
94
95        // First RSI uses simple average of gains/losses
96        let mut avg_gain = Decimal::ZERO;
97        let mut avg_loss = Decimal::ZERO;
98
99        for change in changes.iter().take(self.period) {
100            if *change > Decimal::ZERO {
101                avg_gain += *change;
102            } else {
103                avg_loss += change.abs();
104            }
105        }
106        avg_gain /= period_dec;
107        avg_loss /= period_dec;
108
109        // Calculate first RSI
110        let rsi = calculate_rsi(avg_gain, avg_loss);
111        let ts = candles[self.period].timestamp();
112        values.push((ts, rsi));
113
114        // Subsequent RSI values use smoothed averages
115        for (i, change) in changes.iter().enumerate().skip(self.period) {
116            let (gain, loss) = if *change > Decimal::ZERO {
117                (*change, Decimal::ZERO)
118            } else {
119                (Decimal::ZERO, change.abs())
120            };
121
122            // Smoothed average: (prev_avg * (period - 1) + current) / period
123            avg_gain = (avg_gain * (period_dec - Decimal::ONE) + gain) / period_dec;
124            avg_loss = (avg_loss * (period_dec - Decimal::ONE) + loss) / period_dec;
125
126            let rsi = calculate_rsi(avg_gain, avg_loss);
127            let ts = candles[i + 1].timestamp();
128            values.push((ts, rsi));
129        }
130
131        Ok(Series::new(values))
132    }
133}
134
135/// Calculate RSI from average gain and loss.
136fn calculate_rsi(avg_gain: Decimal, avg_loss: Decimal) -> Decimal {
137    if avg_loss == Decimal::ZERO {
138        if avg_gain == Decimal::ZERO {
139            // No movement - neutral
140            Decimal::from(50)
141        } else {
142            // All gains, no losses
143            Decimal::from(100)
144        }
145    } else {
146        let rs = avg_gain / avg_loss;
147        Decimal::from(100) - (Decimal::from(100) / (Decimal::ONE + rs))
148    }
149}
150
151#[cfg(test)]
152#[path = "rsi_tests.rs"]
153mod tests;