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:
@@ -219,6 +219,7 @@ node_scalar_indicator!(
|
||||
"TREND_STRENGTH_INDEX",
|
||||
wc::TrendStrengthIndex
|
||||
);
|
||||
node_scalar_indicator!(TsfOscillatorNode, "TsfOscillator", wc::TsfOscillator);
|
||||
#[napi(js_name = "JumpIndicator")]
|
||||
pub struct JumpIndicatorNode {
|
||||
inner: wc::JumpIndicator,
|
||||
@@ -4533,6 +4534,82 @@ impl DerivativeOscillatorNode {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== MacdHistogram ==============================
|
||||
|
||||
#[napi(js_name = "MacdHistogram")]
|
||||
pub struct MacdHistogramNode {
|
||||
inner: wc::MacdHistogram,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl MacdHistogramNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::MacdHistogram::new(fast as usize, slow as usize, signal as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== PpoHistogram ==============================
|
||||
|
||||
#[napi(js_name = "PpoHistogram")]
|
||||
pub struct PpoHistogramNode {
|
||||
inner: wc::PpoHistogram,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl PpoHistogramNode {
|
||||
#[napi(constructor)]
|
||||
pub fn new(fast: u32, slow: u32, signal: u32) -> napi::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: wc::PpoHistogram::new(fast as usize, slow as usize, signal as usize)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
#[napi]
|
||||
pub fn update(&mut self, value: f64) -> Option<f64> {
|
||||
self.inner.update(value)
|
||||
}
|
||||
#[napi]
|
||||
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
|
||||
flatten(self.inner.batch(&prices))
|
||||
}
|
||||
#[napi]
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[napi(js_name = "isReady")]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[napi(js_name = "warmupPeriod")]
|
||||
pub fn warmup_period(&self) -> u32 {
|
||||
self.inner.warmup_period() as u32
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== TSI ==============================
|
||||
|
||||
#[napi(js_name = "TSI")]
|
||||
|
||||
Reference in New Issue
Block a user