work on new engine
This commit is contained in:
parent
44476da13f
commit
a1e5a21847
126 changed files with 3425 additions and 6695 deletions
223
apps/stock/engine/src/indicators/rsi.rs
Normal file
223
apps/stock/engine/src/indicators/rsi.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
use super::{Indicator, IncrementalIndicator, IndicatorResult, IndicatorError, PriceData};
|
||||
use super::common::RollingWindow;
|
||||
|
||||
/// Relative Strength Index (RSI) Indicator
|
||||
///
|
||||
/// Measures momentum by comparing the magnitude of recent gains to recent losses
|
||||
/// RSI = 100 - (100 / (1 + RS))
|
||||
/// where RS = Average Gain / Average Loss
|
||||
pub struct RSI {
|
||||
period: usize,
|
||||
avg_gain: f64,
|
||||
avg_loss: f64,
|
||||
prev_value: Option<f64>,
|
||||
window: RollingWindow<f64>,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl RSI {
|
||||
pub fn new(period: usize) -> Result<Self, IndicatorError> {
|
||||
if period == 0 {
|
||||
return Err(IndicatorError::InvalidParameter(
|
||||
"Period must be greater than 0".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
period,
|
||||
avg_gain: 0.0,
|
||||
avg_loss: 0.0,
|
||||
prev_value: None,
|
||||
window: RollingWindow::new(period + 1),
|
||||
initialized: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate RSI for a series of values
|
||||
pub fn calculate_series(values: &[f64], period: usize) -> Result<Vec<f64>, IndicatorError> {
|
||||
if period == 0 {
|
||||
return Err(IndicatorError::InvalidParameter(
|
||||
"Period must be greater than 0".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
if values.len() <= period {
|
||||
return Err(IndicatorError::InsufficientData {
|
||||
required: period + 1,
|
||||
actual: values.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(values.len() - period);
|
||||
let mut gains = Vec::with_capacity(values.len() - 1);
|
||||
let mut losses = Vec::with_capacity(values.len() - 1);
|
||||
|
||||
// Calculate price changes
|
||||
for i in 1..values.len() {
|
||||
let change = values[i] - values[i - 1];
|
||||
if change > 0.0 {
|
||||
gains.push(change);
|
||||
losses.push(0.0);
|
||||
} else {
|
||||
gains.push(0.0);
|
||||
losses.push(-change);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate initial averages using SMA
|
||||
let initial_avg_gain: f64 = gains[0..period].iter().sum::<f64>() / period as f64;
|
||||
let initial_avg_loss: f64 = losses[0..period].iter().sum::<f64>() / period as f64;
|
||||
|
||||
// Calculate first RSI
|
||||
let rs = if initial_avg_loss > 0.0 {
|
||||
initial_avg_gain / initial_avg_loss
|
||||
} else {
|
||||
100.0 // If no losses, RSI is 100
|
||||
};
|
||||
result.push(100.0 - (100.0 / (1.0 + rs)));
|
||||
|
||||
// Calculate remaining RSIs using EMA smoothing
|
||||
let mut avg_gain = initial_avg_gain;
|
||||
let mut avg_loss = initial_avg_loss;
|
||||
let alpha = 1.0 / period as f64;
|
||||
|
||||
for i in period..gains.len() {
|
||||
// Wilder's smoothing method
|
||||
avg_gain = (avg_gain * (period - 1) as f64 + gains[i]) / period as f64;
|
||||
avg_loss = (avg_loss * (period - 1) as f64 + losses[i]) / period as f64;
|
||||
|
||||
let rs = if avg_loss > 0.0 {
|
||||
avg_gain / avg_loss
|
||||
} else {
|
||||
100.0
|
||||
};
|
||||
result.push(100.0 - (100.0 / (1.0 + rs)));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn calculate_rsi(&self) -> f64 {
|
||||
if self.avg_loss == 0.0 {
|
||||
100.0
|
||||
} else {
|
||||
let rs = self.avg_gain / self.avg_loss;
|
||||
100.0 - (100.0 / (1.0 + rs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Indicator for RSI {
|
||||
fn calculate(&mut self, data: &PriceData) -> Result<IndicatorResult, IndicatorError> {
|
||||
let values = &data.close;
|
||||
|
||||
if values.len() <= self.period {
|
||||
return Err(IndicatorError::InsufficientData {
|
||||
required: self.period + 1,
|
||||
actual: values.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let rsi_values = Self::calculate_series(values, self.period)?;
|
||||
Ok(IndicatorResult::Series(rsi_values))
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.avg_gain = 0.0;
|
||||
self.avg_loss = 0.0;
|
||||
self.prev_value = None;
|
||||
self.window.clear();
|
||||
self.initialized = false;
|
||||
}
|
||||
|
||||
fn is_ready(&self) -> bool {
|
||||
self.initialized
|
||||
}
|
||||
}
|
||||
|
||||
impl IncrementalIndicator for RSI {
|
||||
fn update(&mut self, value: f64) -> Result<Option<f64>, IndicatorError> {
|
||||
self.window.push(value);
|
||||
|
||||
if let Some(prev) = self.prev_value {
|
||||
let change = value - prev;
|
||||
let gain = if change > 0.0 { change } else { 0.0 };
|
||||
let loss = if change < 0.0 { -change } else { 0.0 };
|
||||
|
||||
if !self.initialized && self.window.len() > self.period {
|
||||
// Initialize using first period values
|
||||
let values = self.window.as_slice();
|
||||
let mut sum_gain = 0.0;
|
||||
let mut sum_loss = 0.0;
|
||||
|
||||
for i in 1..=self.period {
|
||||
let change = values[i] - values[i - 1];
|
||||
if change > 0.0 {
|
||||
sum_gain += change;
|
||||
} else {
|
||||
sum_loss += -change;
|
||||
}
|
||||
}
|
||||
|
||||
self.avg_gain = sum_gain / self.period as f64;
|
||||
self.avg_loss = sum_loss / self.period as f64;
|
||||
self.initialized = true;
|
||||
} else if self.initialized {
|
||||
// Update using Wilder's smoothing
|
||||
self.avg_gain = (self.avg_gain * (self.period - 1) as f64 + gain) / self.period as f64;
|
||||
self.avg_loss = (self.avg_loss * (self.period - 1) as f64 + loss) / self.period as f64;
|
||||
}
|
||||
}
|
||||
|
||||
self.prev_value = Some(value);
|
||||
|
||||
if self.initialized {
|
||||
Ok(Some(self.calculate_rsi()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn current(&self) -> Option<f64> {
|
||||
if self.initialized {
|
||||
Some(self.calculate_rsi())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rsi_calculation() {
|
||||
let values = vec![
|
||||
44.0, 44.25, 44.38, 44.38, 44.88, 45.05,
|
||||
45.25, 45.38, 45.75, 46.03, 46.23, 46.08,
|
||||
46.03, 45.85, 46.25, 46.38, 46.50
|
||||
];
|
||||
|
||||
let result = RSI::calculate_series(&values, 14).unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
|
||||
// RSI should be between 0 and 100
|
||||
for rsi in &result {
|
||||
assert!(*rsi >= 0.0 && *rsi <= 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rsi_extremes() {
|
||||
// All gains - RSI should be close to 100
|
||||
let increasing = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
|
||||
let result = RSI::calculate_series(&increasing, 5).unwrap();
|
||||
assert!(result.last().unwrap() > &95.0);
|
||||
|
||||
// All losses - RSI should be close to 0
|
||||
let decreasing = vec![8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0];
|
||||
let result = RSI::calculate_series(&decreasing, 5).unwrap();
|
||||
assert!(result.last().unwrap() < &5.0);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue