A4: align ROC non-finite handling with SMA/EMA
ROC now stores its last emitted value and returns it on a non-finite input instead of None, leaving the window untouched. This matches the SMA / EMA convention. reset() clears the new field. Adds a non-finite-input test.
This commit is contained in:
@@ -6,10 +6,14 @@ use crate::error::{Error, Result};
|
|||||||
use crate::traits::Indicator;
|
use crate::traits::Indicator;
|
||||||
|
|
||||||
/// Rate of Change as a percentage: `(close - close[period]) / close[period] * 100`.
|
/// Rate of Change as a percentage: `(close - close[period]) / close[period] * 100`.
|
||||||
|
///
|
||||||
|
/// Non-finite inputs are ignored and leave the window untouched; the last
|
||||||
|
/// computed value is returned instead, matching the SMA / EMA convention.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Roc {
|
pub struct Roc {
|
||||||
period: usize,
|
period: usize,
|
||||||
window: VecDeque<f64>,
|
window: VecDeque<f64>,
|
||||||
|
last: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Roc {
|
impl Roc {
|
||||||
@@ -22,6 +26,7 @@ impl Roc {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
period,
|
period,
|
||||||
window: VecDeque::with_capacity(period + 1),
|
window: VecDeque::with_capacity(period + 1),
|
||||||
|
last: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +41,9 @@ impl Indicator for Roc {
|
|||||||
type Output = f64;
|
type Output = f64;
|
||||||
|
|
||||||
fn update(&mut self, input: f64) -> Option<f64> {
|
fn update(&mut self, input: f64) -> Option<f64> {
|
||||||
|
// Non-finite inputs are ignored: return the last value, leave state as is.
|
||||||
if !input.is_finite() {
|
if !input.is_finite() {
|
||||||
return None;
|
return self.last;
|
||||||
}
|
}
|
||||||
if self.window.len() == self.period + 1 {
|
if self.window.len() == self.period + 1 {
|
||||||
self.window.pop_front();
|
self.window.pop_front();
|
||||||
@@ -47,14 +53,18 @@ impl Indicator for Roc {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let prev = *self.window.front().expect("non-empty");
|
let prev = *self.window.front().expect("non-empty");
|
||||||
if prev == 0.0 {
|
let roc = if prev == 0.0 {
|
||||||
return Some(0.0);
|
0.0
|
||||||
}
|
} else {
|
||||||
Some((input - prev) / prev * 100.0)
|
(input - prev) / prev * 100.0
|
||||||
|
};
|
||||||
|
self.last = Some(roc);
|
||||||
|
Some(roc)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {
|
fn reset(&mut self) {
|
||||||
self.window.clear();
|
self.window.clear();
|
||||||
|
self.last = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn warmup_period(&self) -> usize {
|
fn warmup_period(&self) -> usize {
|
||||||
@@ -117,4 +127,20 @@ mod tests {
|
|||||||
fn rejects_zero_period() {
|
fn rejects_zero_period() {
|
||||||
assert!(Roc::new(0).is_err());
|
assert!(Roc::new(0).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_non_finite_input() {
|
||||||
|
let mut roc = Roc::new(3).unwrap();
|
||||||
|
let out = roc.batch(&[100.0, 105.0, 108.0, 110.0]);
|
||||||
|
let ready = out[3].expect("ROC(3) ready after four inputs");
|
||||||
|
// Non-finite inputs return the last value without sliding the window.
|
||||||
|
assert_eq!(roc.update(f64::NAN), Some(ready));
|
||||||
|
assert_eq!(roc.update(f64::INFINITY), Some(ready));
|
||||||
|
// Window untouched: the next finite input still references prev = 105.
|
||||||
|
assert_relative_eq!(
|
||||||
|
roc.update(115.0).unwrap(),
|
||||||
|
(115.0 - 105.0) / 105.0 * 100.0,
|
||||||
|
epsilon = 1e-12
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user