1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
//! 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;