Files
wickra/crates/wickra-core/src/indicators/rocr.rs
T
kingchencandGitHub 9eb46f144a feat: TA-Lib parity — 19 standalone indicators (DM components, price transforms, ROC/LinReg/MACD/SAR variants, Hilbert outputs) (#148)
Closes the remaining TA-Lib function-name gap by shipping each missing or
bundled-only function as a real, standalone, fully-covered indicator. 19 new
indicators across 5 families; mod-count 295 -> 314.

### Trend & Directional — Directional Movement components
- `PlusDm` (`PLUS_DM`), `MinusDm` (`MINUS_DM`) — Wilder-smoothed ±DM.
- `PlusDi` (`PLUS_DI`), `MinusDi` (`MINUS_DI`) — `100·smoothed(±DM)/ATR`.
- `Dx` (`DX`) — `100·|+DI−−DI|/(+DI+−DI)`.

### Price Statistics
- `AvgPrice` (`AVGPRICE`) — `(O+H+L+C)/4`.
- `MidPoint` (`MIDPOINT`) — `(max+min)/2` of a scalar series over N.
- `MidPrice` (`MIDPRICE`) — `(highestHigh+lowestLow)/2` over N.
- `LinRegIntercept` (`LINEARREG_INTERCEPT`) — OLS intercept.
- `Tsf` (`TSF`) — time series forecast `a + b·period`.

### Momentum Oscillators
- `Rocp` (`ROCP`), `Rocr` (`ROCR`), `Rocr100` (`ROCR100`) — ROC ratio forms.

### Trailing Stops
- `SarExt` (`SAREXT`) — Parabolic SAR with start value, reversal offset,
  separate long/short acceleration, signed output.

### Trend & Directional — MACD variants
- `MacdFix` (`MACDFIX`) — MACD fixed 12/26.
- `MacdExt` (`MACDEXT`) — MACD with a selectable moving-average type per line
  (new public `MaType` enum: SMA/EMA/WMA/DEMA/TEMA/TRIMA).

### Ehlers / Cycle (DSP) — Hilbert transform outputs
- `HtPhasor` (`HT_PHASOR`) — in-phase / quadrature components.
- `HtDcPhase` (`HT_DCPHASE`) — dominant-cycle phase (degrees).
- `HtTrendMode` (`HT_TRENDMODE`) — trend (1) vs cycle (0) classification.

Each indicator ships the full chain: core + every-branch unit tests, Python /
Node / WASM bindings, fuzz coverage, README counter + family rows, CHANGELOG.
`cargo test`, doctests, `clippy -D warnings`, `npm test` and pytest all green
locally; mod-count == lib-block == README counter (314), FAMILIES total 309.
2026-06-03 02:26:38 +02:00

158 lines
4.0 KiB
Rust

//! Rate of Change Ratio (ROCR).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Rate of Change Ratio (`ROCR`): `close / close[period]`.
///
/// The momentum ratio relative to the price `period` bars ago: `1.0` means no
/// change, `> 1` an advance, `< 1` a decline. It is [`Rocp`](crate::Rocp) plus
/// one. Where the reference price is zero the result is reported as `0`.
///
/// Non-finite inputs are ignored and leave the window untouched; the last
/// computed value is returned instead, matching the SMA / EMA convention.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, Rocr};
///
/// let mut indicator = Rocr::new(3).unwrap();
/// let mut last = None;
/// for i in 0..80 {
/// last = indicator.update(100.0 + f64::from(i));
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct Rocr {
period: usize,
window: VecDeque<f64>,
last: Option<f64>,
}
impl Rocr {
/// # Errors
/// Returns [`Error::PeriodZero`] if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
window: VecDeque::with_capacity(period + 1),
last: None,
})
}
/// Configured period.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for Rocr {
type Input = f64;
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return self.last;
}
if self.window.len() == self.period + 1 {
self.window.pop_front();
}
self.window.push_back(input);
if self.window.len() < self.period + 1 {
return None;
}
let prev = *self.window.front().expect("non-empty");
let rocr = if prev == 0.0 { 0.0 } else { input / prev };
self.last = Some(rocr);
Some(rocr)
}
fn reset(&mut self) {
self.window.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period + 1
}
fn is_ready(&self) -> bool {
self.window.len() == self.period + 1
}
fn name(&self) -> &'static str {
"ROCR"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_zero_period() {
assert!(matches!(Rocr::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_report_config() {
let r = Rocr::new(3).unwrap();
assert_eq!(r.period(), 3);
assert_eq!(r.name(), "ROCR");
assert_eq!(r.warmup_period(), 4);
assert!(!r.is_ready());
}
#[test]
fn known_value_is_a_ratio() {
// period 1 over [10, 11]: 11 / 10 = 1.1.
let mut r = Rocr::new(1).unwrap();
let out: Vec<Option<f64>> = r.batch(&[10.0, 11.0]);
assert_eq!(out[0], None);
assert_relative_eq!(out[1].unwrap(), 1.1, epsilon = 1e-12);
assert!(r.is_ready());
}
#[test]
fn constant_series_yields_one() {
let mut r = Rocr::new(3).unwrap();
for v in r.batch(&[10.0_f64; 12]).iter().skip(4).flatten() {
assert_relative_eq!(*v, 1.0, epsilon = 1e-12);
}
}
#[test]
fn zero_reference_price_reports_zero() {
let mut r = Rocr::new(1).unwrap();
let out: Vec<Option<f64>> = r.batch(&[0.0, 5.0]);
assert_relative_eq!(out[1].unwrap(), 0.0, epsilon = 1e-12);
}
#[test]
fn non_finite_input_holds_last() {
let mut r = Rocr::new(1).unwrap();
assert_eq!(r.update(10.0), None);
let v = r.update(11.0).unwrap();
assert_eq!(r.update(f64::INFINITY), Some(v));
}
#[test]
fn reset_clears_state() {
let mut r = Rocr::new(1).unwrap();
let _ = r.batch(&[10.0, 11.0]);
assert!(r.is_ready());
r.reset();
assert!(!r.is_ready());
assert_eq!(r.update(10.0), None);
}
}