Skip to content

Commit

Permalink
Added RSI indicator & tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ohkthx committed Aug 17, 2023
1 parent 8c83b03 commit 2cba8b7
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 25 deletions.
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ name = "moving_average_convergence_divergence"
path = "examples/moving_average_convergence_divergence.rs"
required-features = ["test-data"]

[[example]]
name = "relative_strength_index"
path = "examples/relative_strength_index.rs"
required-features = ["test-data"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
27 changes: 27 additions & 0 deletions examples/relative_strength_index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Demonstrates how to initialize and use a RSI.
use tatk::indicators::RSI;
use tatk::test_data::TEST_DATA;

fn main() {
let period: usize = 14;

println!("Data: {:?}", TEST_DATA);
println!("Period: {}", period);

let mut rsi = match RSI::new(period, TEST_DATA) {
Ok(value) => value,
Err(error) => panic!("{}", error),
};

// Update the thresholds for both oversold and overbought.
rsi.set_oversold(30.0);
rsi.set_overbought(70.0);

println!("\nRSI: {}", rsi.value());
println!(
"Oversold?: {}, Overbought: {}",
rsi.is_oversold(),
rsi.is_overbought()
);
println!("Adding 107.00. New RSI: {}", rsi.next(107.000000));
}
2 changes: 1 addition & 1 deletion src/indicators/ema.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Exponential Moving Average
//! Exponential Moving Average (EMA)
//!
//! # Formula
//!
Expand Down
2 changes: 2 additions & 0 deletions src/indicators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
mod dema;
mod ema;
mod macd;
mod rsi;
mod sma;

pub use dema::DEMA;
pub use ema::EMA;
pub use macd::MACD;
pub use rsi::RSI;
pub use sma::SMA;
198 changes: 198 additions & 0 deletions src/indicators/rsi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
//! Relative Strength Index (RSI)
//!
//! # Formula
//!
//! RSI (step1) = 100 - [100 / (1 + (x / y))]
//!
//! RSI (step2) = 100 - [100 / (1 + ((x * z + x1) / (y * z + y2)))]
//!
//! where:
//!
//! * `x` = Average gain over period.
//! * `y` = Average loss over period.
//! * `z` = Period - 1.
//! * `x1` = Most recent gain.
//! * `y1` = Most recent loss.
use crate::error::TAError;

/// Relative Strength Index (RSI)
///
/// # Formula
///
/// RSI (step1) = 100 - [100 / (1 + (x / y))]
///
/// RSI (step2) = 100 - [100 / (1 + (( x * 13 + x1) / (y * 13 + y1)))]
///
/// where:
///
/// * `x` = Average gain over period.
/// * `y` = Average loss over period.
/// * `z` = Period - 1.
/// * `x1` = Most recent gain.
/// * `y1` = Most recent loss.
#[derive(Debug)]
pub struct RSI {
/// Size of the period (window) in which data is looked at.
period: usize,
/// RSI's current value.
value: f64,
/// Average gain percentage.
gain_avg: f64,
/// Average loss percentage.
loss_avg: f64,
/// Last value processed.
last: f64,
/// Oversold threshold.
oversold: f64,
/// Overbought threshold.
overbought: f64,
}

impl RSI {
/// Creates a new RSI with the supplied period and initial data.
///
/// # Arguments
///
/// * `period` - Size of the period / window used.
/// * `data` - Array of values to create the RSI from.
pub fn new(period: usize, data: &[f64]) -> Result<Self, TAError> {
if period + 1 > data.len() {
return Err(TAError::InvalidArray);
} else if period == 0 {
return Err(TAError::InvalidPeriod);
}

let mut gains: f64 = 0.0;
let mut losses: f64 = 0.0;
let mut last: f64 = data[0].clone();

// Generates the gains / losses for the first period of values. Unique and uses all gains /
// losses for the first period as a seed value.
for value in data[1..=period].iter() {
let change = value - last;
last = value.clone();
if change > 0.0 {
gains = gains + change;
} else {
losses = losses + change.abs();
}
}

// These values will be updated by calculate, used to calculate period + 1.
let mut last_gain: f64 = 0.0;
let mut last_loss: f64 = 0.0;
let mut value = Self::calculate(period, &mut last_gain, &mut last_loss, gains, losses);

// Calculate remaining values. This uses the average + next value. It's a slightly
// different calculation than the initial seed value for the RSI.
if period < data.len() {
for v in &data[(period + 1)..] {
let change = v - last;
let mut gain = 0.0;
let mut loss = 0.0;

if change > 0.0 {
gain = change;
} else {
loss = change.abs();
}

value = Self::calculate(period, &mut last_gain, &mut last_loss, gain, loss);
last = v.clone();
}
}

Ok(Self {
period,
value,
gain_avg: last_gain,
loss_avg: last_loss,
last,
oversold: 20.0,
overbought: 80.0,
})
}

/// Period (window) for the samples.
pub fn period(&self) -> usize {
self.period
}

/// Current and most recent value calculated.
pub fn value(&self) -> f64 {
self.value
}

/// Changes the Oversold Threshold from the default (20.0)
pub fn set_oversold(&mut self, oversold_value: f64) {
self.oversold = oversold_value;
}

/// Changes the Overbought Threshold from the default (80.0)
pub fn set_overbought(&mut self, overbought_value: f64) {
self.overbought = overbought_value;
}

/// Checks if the RSI is currently within the oversold threshold (default 20.0)
pub fn is_oversold(&self) -> bool {
self.value < self.oversold
}

/// Checks if the RSI is currently within the overbought threshold (default 80.0)
pub fn is_overbought(&self) -> bool {
self.value > self.overbought
}

/// Supply an additional value to recalculate a new RSI.
///
/// # Arguments
///
/// * `value` - New value to add to period.
pub fn next(&mut self, value: f64) -> f64 {
let mut gain = 0.0;
let mut loss = 0.0;
let change = value - self.last;

if change > 0.0 {
gain = change;
} else {
loss = change.abs();
}

// Calculate the new RSI.
self.last = value;
self.value = Self::calculate(
self.period,
&mut self.gain_avg,
&mut self.loss_avg,
gain,
loss,
);
self.value
}

/// Calculates a RSI with the given data between two indexes.
///
/// # Arguments
///
/// * `period` - Size of the period / window used.
/// * `gain_avg` - Past calculation gain average.
/// * `loss_avg` - Past calculation loss average.
/// * `gain` - Most recent gain (>= 0).
/// * `loss` - Most recent loss (>= 0).
pub(crate) fn calculate(
period: usize,
gain_avg: &mut f64,
loss_avg: &mut f64,
gain: f64,
loss: f64,
) -> f64 {
let period_value = (period as f64) - 1.0;

// Update the callers gain and loss averages.
*gain_avg = (*gain_avg * period_value + gain) / period as f64;
*loss_avg = (*loss_avg * period_value + loss) / period as f64;

100.0 - (100.0 / (1.0 + (*gain_avg / *loss_avg)))
}
}
2 changes: 1 addition & 1 deletion src/indicators/sma.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Simple Moving Average
//! Simple Moving Average (SMA)
//!
//! Average moves within a period.
use crate::buffer::Buffer;
Expand Down
23 changes: 0 additions & 23 deletions tests/moving_averages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,3 @@ fn next_dema() {
let mut dema = DEMA::new(14, TEST_DATA).unwrap();
assert_eq!(dema.next(107.000000), 108.91612556961066);
}

#[test]
#[cfg(feature = "test-data")]
/// Create and calculate a MACD using 252 data points with a short of 12, long of 26, and signal of 9.
fn create_macd() {
use tatk::indicators::MACD;
use tatk::test_data::TEST_DATA;

let macd = MACD::new(12, 26, 9, TEST_DATA).unwrap();
assert_eq!(macd.value(), 0.9040092995013111);
}

#[test]
#[cfg(feature = "test-data")]
/// Creates a MACD from 252 data points with short of 12, long of 26, and signal of 9, then adds an additional data point
/// to move the ensure the window of viewed is moving.
fn next_macd() {
use tatk::indicators::MACD;
use tatk::test_data::TEST_DATA;

let mut macd = MACD::new(12, 26, 9, TEST_DATA).unwrap();
assert_eq!(macd.next(107.000000), 0.6789823967962718);
}
45 changes: 45 additions & 0 deletions tests/oscillators.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#[test]
#[cfg(feature = "test-data")]
/// Create and calculate a MACD using 252 data points with a short of 12, long of 26, and signal of 9.
fn create_macd() {
use tatk::indicators::MACD;
use tatk::test_data::TEST_DATA;

let macd = MACD::new(12, 26, 9, TEST_DATA).unwrap();
assert_eq!(macd.value(), 0.9040092995013111);
}

#[test]
#[cfg(feature = "test-data")]
/// Creates a MACD from 252 data points with short of 12, long of 26, and signal of 9, then adds an additional data point
/// to move the ensure the window of viewed is moving.
fn next_macd() {
use tatk::indicators::MACD;
use tatk::test_data::TEST_DATA;

let mut macd = MACD::new(12, 26, 9, TEST_DATA).unwrap();
assert_eq!(macd.next(107.000000), 0.6789823967962718);
}

#[test]
#[cfg(feature = "test-data")]
/// Create and calculate a RSI using 252 data points with a short of 12, long of 26, and signal of 9.
fn create_rsi() {
use tatk::indicators::RSI;
use tatk::test_data::TEST_DATA;

let rsi = RSI::new(14, TEST_DATA).unwrap();
assert_eq!(rsi.value(), 49.63210207086755);
}

#[test]
#[cfg(feature = "test-data")]
/// Creates a RSI from 252 data points with short of 12, long of 26, and signal of 9, then adds an additional data point
/// to move the ensure the window of viewed is moving.
fn next_rsi() {
use tatk::indicators::RSI;
use tatk::test_data::TEST_DATA;

let mut rsi = RSI::new(14, TEST_DATA).unwrap();
assert_eq!(rsi.next(107.000000), 47.53209455563524);
}

0 comments on commit 2cba8b7

Please sign in to comment.