indexes_rs/v1/roc/
main.rs

1//! # Rate of Change (ROC) Module
2//!
3//! This module implements a Rate of Change (ROC) indicator.
4//! The ROC is calculated as the percentage change between the current price and the price
5//! from a specified number of periods ago. Additionally, it calculates a normalized momentum,
6//! an acceleration (change in ROC from the previous value), and generates a trading signal.
7//!
8//! Typical usage example:
9//!
10//! ```rust
11//! use indexes_rs::v1::roc::main::ROC;
12//! use indexes_rs::v1::types::TradingSignal;
13//!
14//! let mut roc = ROC::new(12);
15//! let prices = vec![100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0, 111.0, 112.0];
16//! let mut result = None;
17//! for price in prices {
18//!     result = roc.calculate(price);
19//! }
20//! if let Some(res) = result {
21//!     println!("ROC value: {:.2}%", res.value);
22//!     println!("Momentum: {:.2}%", res.momentum);
23//!     println!("Acceleration: {:?}", res.acceleration);
24//! }
25//! ```
26
27use super::types::ROCResult;
28use crate::v1::types::TradingSignal;
29use std::collections::VecDeque;
30
31/// A Rate of Change (ROC) indicator.
32pub struct ROC {
33    period: usize,
34    values: VecDeque<f64>,
35    prev_roc: Option<f64>,
36}
37
38impl ROC {
39    /// The default period for ROC calculation.
40    pub const DEFAULT_PERIOD: usize = 12;
41    /// The threshold used to generate trading signals.
42    pub const SIGNAL_THRESHOLD: f64 = 2.0;
43
44    /// Creates a new ROC indicator with the given period.
45    ///
46    /// # Arguments
47    ///
48    /// * `period` - The number of periods over which to calculate the ROC.
49    ///
50    /// # Example
51    ///
52    /// ```rust
53    /// use indexes_rs::v1::roc::main::ROC;
54    ///
55    /// let roc = ROC::new(12);
56    /// ```
57    pub fn new(period: usize) -> Self {
58        ROC {
59            period,
60            values: VecDeque::with_capacity(period + 1),
61            prev_roc: None,
62        }
63    }
64
65    /// Calculates the current ROC value based on the latest price.
66    ///
67    /// This method updates the sliding window of prices and computes:
68    /// - The ROC value (percentage change between the current price and the price from `period` periods ago).
69    /// - A normalized momentum value.
70    /// - The acceleration (difference between the current and previous ROC).
71    /// - A trading signal based on the ROC.
72    ///
73    /// # Arguments
74    ///
75    /// * `price` - The latest price.
76    ///
77    /// # Returns
78    ///
79    /// * `Some(ROCResult)` if there is enough data; otherwise, `None`.
80    pub fn calculate(&mut self, price: f64) -> Option<ROCResult> {
81        // Update the sliding window.
82        self.values.push_back(price);
83        if self.values.len() > self.period + 1 {
84            self.values.pop_front();
85        }
86
87        // Not enough data to calculate ROC.
88        if self.values.len() <= self.period {
89            return None;
90        }
91
92        let old_price = *self.values.front()?;
93        // Avoid division by zero: if the old price is 0, return None.
94        if old_price == 0.0 {
95            return None;
96        }
97
98        let current_roc = ((price - old_price) / old_price) * 100.0;
99
100        // Calculate acceleration if previous ROC exists.
101        let acceleration = self.prev_roc.map(|prev| current_roc - prev);
102        self.prev_roc = Some(current_roc);
103
104        Some(ROCResult {
105            value: current_roc,
106            momentum: self.normalize_momentum(current_roc),
107            acceleration,
108            signal: self.get_signal(current_roc),
109        })
110    }
111
112    /// Normalizes the ROC value to a momentum value between -100 and 100.
113    ///
114    /// This function assumes typical ROC values range approximately from -10 to +10.
115    fn normalize_momentum(&self, roc: f64) -> f64 {
116        // Normalize so that a value of 10 becomes 100%, and -10 becomes -100%.
117        let normalized = (roc / 10.0) * 100.0;
118        normalized.clamp(-100.0, 100.0)
119    }
120
121    /// Generates a trading signal based on the ROC value.
122    ///
123    /// If ROC > SIGNAL_THRESHOLD, returns `Buy`.
124    /// If ROC < -SIGNAL_THRESHOLD, returns `Sell`.
125    /// Otherwise, returns `Hold`.
126    fn get_signal(&self, roc: f64) -> TradingSignal {
127        if roc > Self::SIGNAL_THRESHOLD {
128            TradingSignal::Buy
129        } else if roc < -Self::SIGNAL_THRESHOLD {
130            TradingSignal::Sell
131        } else {
132            TradingSignal::Hold
133        }
134    }
135}