feat(core): B4 price oscillators (TsfOscillator, MacdHistogram, PpoHistogram) (#184)
Adds three **Price Oscillators** family indicators (420 → 423). ## Indicators - **TsfOscillator** — `100·(close − TSF)/close`, the percentage gap of the close to the **one-bar-ahead** time-series forecast. Close-relative companion to `Cfo`, which measures the same gap against the regression value at the *current* bar; the two differ by exactly the slope term `100·b/close`. - **MacdHistogram** — the standalone `macd − signal` bar of MACD exposed as a plain `f64` series. - **PpoHistogram** — the Percentage Price Oscillator with its 9-period signal EMA and the resulting scale-free, zero-centered histogram (PPO itself only emits the line). All three are scalar `f64` indicators wrapping existing, already-tested building blocks (`MacdIndicator`, `Ppo` + `Ema`, `Tsf`). ## Scope notes (VORAB-CHECK) The B4 roadmap listed six items; three were dropped to avoid duplicates: - *Forecast Oscillator* already ships as `Cfo`. - *Derivative Oscillator* already ships (`DerivativeOscillator`, B2). - *Detrended Synthetic Price* deferred — no citable formula distinct from the existing `Apo`/`Dpo`. ## Touchpoints Core (`tsf_oscillator.rs`, `macd_histogram.rs`, `ppo_histogram.rs`) with full per-branch unit tests, `mod.rs`/`lib.rs`, python/node/wasm bindings (wasm via typed-arg macro, python/node hand-written for the multi-arg histograms), fuzz drivers, python reference + streaming-vs-batch tests, node factories, README family row + counter, CHANGELOG. Local verify: `cargo test --workspace` green, `clippy -D warnings` clean, node 498 tests, full python suite green.
This commit is contained in:
@@ -3295,6 +3295,144 @@ impl PyKasePermissionStochastic {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TsfOscillator ==============================
|
||||
|
||||
#[pyclass(name = "TsfOscillator", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyTsfOscillator {
|
||||
inner: wc::TsfOscillator,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyTsfOscillator {
|
||||
#[new]
|
||||
#[pyo3(signature = (period=14))]
|
||||
fn new(period: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::TsfOscillator::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
#[getter]
|
||||
fn period(&self) -> usize {
|
||||
self.inner.period()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("TsfOscillator(period={})", self.inner.period())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MacdHistogram ==============================
|
||||
|
||||
#[pyclass(name = "MacdHistogram", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyMacdHistogram {
|
||||
inner: wc::MacdHistogram,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyMacdHistogram {
|
||||
#[new]
|
||||
#[pyo3(signature = (fast=12, slow=26, signal=9))]
|
||||
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MacdHistogram::new(fast, slow, signal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (fast, slow, signal) = self.inner.periods();
|
||||
format!("MacdHistogram(fast={fast}, slow={slow}, signal={signal})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== PpoHistogram ==============================
|
||||
|
||||
#[pyclass(name = "PpoHistogram", module = "wickra._wickra", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
struct PyPpoHistogram {
|
||||
inner: wc::PpoHistogram,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyPpoHistogram {
|
||||
#[new]
|
||||
#[pyo3(signature = (fast=12, slow=26, signal=9))]
|
||||
fn new(fast: usize, slow: usize, signal: usize) -> PyResult<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PpoHistogram::new(fast, slow, signal).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
fn batch<'py>(
|
||||
&mut self,
|
||||
py: Python<'py>,
|
||||
prices: PyReadonlyArray1<'py, f64>,
|
||||
) -> PyResult<Bound<'py, PyArray1<f64>>> {
|
||||
let s = prices
|
||||
.as_slice()
|
||||
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
|
||||
Ok(flatten(self.inner.batch(s)).into_pyarray(py))
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
let (fast, slow, signal) = self.inner.periods();
|
||||
format!("PpoHistogram(fast={fast}, slow={slow}, signal={signal})")
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Stochastic ==============================
|
||||
|
||||
#[pyclass(name = "IMI", module = "wickra._wickra", skip_from_py_object)]
|
||||
@@ -21422,5 +21560,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<PyWavePm>()?;
|
||||
m.add_class::<PyGatorOscillator>()?;
|
||||
m.add_class::<PyKasePermissionStochastic>()?;
|
||||
m.add_class::<PyTsfOscillator>()?;
|
||||
m.add_class::<PyMacdHistogram>()?;
|
||||
m.add_class::<PyPpoHistogram>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user