quant-indicators 0.7.0

Pure indicator math library for trading — MA, RSI, Bollinger, MACD, ATR, HRP
Documentation
//! Relative Strength Index (RSI) indicator.

use quant_primitives::Candle;
use rust_decimal::Decimal;

use crate::error::IndicatorError;
use crate::indicator::Indicator;
use crate::series::Series;

/// Relative Strength Index indicator.
///
/// Measures the magnitude of recent price changes to evaluate overbought
/// or oversold conditions. Values range from 0 to 100.
///
/// # Formula
///
/// RSI = 100 - (100 / (1 + RS))
/// RS = Average Gain / Average Loss
///
/// Traditional interpretation:
/// - RSI > 70: Overbought
/// - RSI < 30: Oversold
///
/// # Example
///
/// ```
/// use quant_indicators::{Indicator, Rsi};
/// use quant_primitives::Candle;
/// use chrono::Utc;
/// use rust_decimal_macros::dec;
///
/// let ts = Utc::now();
/// let candles: Vec<Candle> = (0..20).map(|i| {
///     Candle::new(dec!(100), dec!(110), dec!(90), dec!(100) + rust_decimal::Decimal::from(i), dec!(1000), ts).unwrap()
/// }).collect();
/// let rsi = Rsi::new(14).unwrap();
/// let series = rsi.compute(&candles).unwrap();
/// ```
#[derive(Debug, Clone)]
pub struct Rsi {
    period: usize,
    name: String,
}

impl Rsi {
    /// Create a new RSI indicator with the specified period.
    ///
    /// Standard period is 14.
    ///
    /// # Errors
    ///
    /// Returns `InvalidParameter` if period is 0.
    pub fn new(period: usize) -> Result<Self, IndicatorError> {
        if period == 0 {
            return Err(IndicatorError::InvalidParameter {
                message: "RSI period must be > 0".to_string(),
            });
        }
        Ok(Self {
            period,
            name: format!("RSI({})", period),
        })
    }
}

impl Indicator for Rsi {
    fn name(&self) -> &str {
        &self.name
    }

    fn warmup_period(&self) -> usize {
        // Need period + 1 candles to compute first RSI
        // (period changes = period + 1 prices)
        self.period + 1
    }

    fn compute(&self, candles: &[Candle]) -> Result<Series, IndicatorError> {
        let required = self.period + 1;
        if candles.len() < required {
            return Err(IndicatorError::InsufficientData {
                required,
                actual: candles.len(),
            });
        }

        // Calculate price changes
        let changes: Vec<Decimal> = candles
            .windows(2)
            .map(|w| w[1].close() - w[0].close())
            .collect();

        let mut values = Vec::with_capacity(candles.len() - required + 1);
        let period_dec = Decimal::from(self.period as u64);

        // First RSI uses simple average of gains/losses
        let mut avg_gain = Decimal::ZERO;
        let mut avg_loss = Decimal::ZERO;

        for change in changes.iter().take(self.period) {
            if *change > Decimal::ZERO {
                avg_gain += *change;
            } else {
                avg_loss += change.abs();
            }
        }
        avg_gain /= period_dec;
        avg_loss /= period_dec;

        // Calculate first RSI
        let rsi = calculate_rsi(avg_gain, avg_loss);
        let ts = candles[self.period].timestamp();
        values.push((ts, rsi));

        // Subsequent RSI values use smoothed averages
        for (i, change) in changes.iter().enumerate().skip(self.period) {
            let (gain, loss) = if *change > Decimal::ZERO {
                (*change, Decimal::ZERO)
            } else {
                (Decimal::ZERO, change.abs())
            };

            // Smoothed average: (prev_avg * (period - 1) + current) / period
            avg_gain = (avg_gain * (period_dec - Decimal::ONE) + gain) / period_dec;
            avg_loss = (avg_loss * (period_dec - Decimal::ONE) + loss) / period_dec;

            let rsi = calculate_rsi(avg_gain, avg_loss);
            let ts = candles[i + 1].timestamp();
            values.push((ts, rsi));
        }

        Ok(Series::new(values))
    }
}

/// Calculate RSI from average gain and loss.
fn calculate_rsi(avg_gain: Decimal, avg_loss: Decimal) -> Decimal {
    if avg_loss == Decimal::ZERO {
        if avg_gain == Decimal::ZERO {
            // No movement - neutral
            Decimal::from(50)
        } else {
            // All gains, no losses
            Decimal::from(100)
        }
    } else {
        let rs = avg_gain / avg_loss;
        Decimal::from(100) - (Decimal::from(100) / (Decimal::ONE + rs))
    }
}

#[cfg(test)]
#[path = "rsi_tests.rs"]
mod tests;