F3: add MOM, CMO, TSI and PMO momentum indicators

Completes the F3 family (Momentum) end to end:

- Rust core: mom.rs (raw price-difference momentum), cmo.rs (Chande
  Momentum Oscillator — unsmoothed gain/loss sum, bounded [-100,100]),
  tsi.rs (True Strength Index — double-EMA-smoothed momentum ratio),
  pmo.rs (DecisionPoint Price Momentum Oscillator — doubly-smoothed ROC
  with the 2/period custom smoothing). Each with a full Indicator impl,
  runnable doctest and reference-value / saturation / warmup / reset /
  batch==streaming / non-finite tests.
- Python: PyMom / PyCmo / PyTsi / PyPmo PyO3 classes + module
  registration + .pyi stubs (defaults MOM=10, CMO=14, TSI=(25,13),
  PMO=(35,20)).
- Node: MomNode / CmoNode via the scalar macro, explicit TsiNode and
  PmoNode; index.d.ts and index.js updated.
- WASM: WasmMom / WasmCmo / WasmTsi / WasmPmo via the scalar macro.
- Wiki: Indicator-Mom/Cmo/Tsi/Pmo.md plus rows in Indicators-Overview.md
  and entries in Home.md.

cargo fmt + clippy (core/wickra/data/wasm/node) clean; 262 core tests,
25 data tests and 37 doctests green.
This commit is contained in:
kingchenc
2026-05-22 17:53:46 +02:00
parent 780a176072
commit 7728151c87
17 changed files with 1826 additions and 5 deletions
+76
View File
@@ -105,6 +105,8 @@ node_scalar_indicator!(TrixNode, "TRIX", wc::Trix);
node_scalar_indicator!(SmmaNode, "SMMA", wc::Smma);
node_scalar_indicator!(TrimaNode, "TRIMA", wc::Trima);
node_scalar_indicator!(ZlemaNode, "ZLEMA", wc::Zlema);
node_scalar_indicator!(MomNode, "MOM", wc::Mom);
node_scalar_indicator!(CmoNode, "CMO", wc::Cmo);
// ============================== MACD ==============================
@@ -1066,6 +1068,80 @@ impl T3Node {
}
}
// ============================== TSI ==============================
#[napi(js_name = "TSI")]
pub struct TsiNode {
inner: wc::Tsi,
}
#[napi]
impl TsiNode {
#[napi(constructor)]
pub fn new(long: u32, short: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Tsi::new(long as usize, short 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
}
}
// ============================== PMO ==============================
#[napi(js_name = "PMO")]
pub struct PmoNode {
inner: wc::Pmo,
}
#[napi]
impl PmoNode {
#[napi(constructor)]
pub fn new(smoothing1: u32, smoothing2: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Pmo::new(smoothing1 as usize, smoothing2 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
}
}
// ============================== VWMA ==============================
#[napi(js_name = "VWMA")]