Deepen Momentum Oscillators family with ten additions (#179)

Deepens the **Momentum Oscillators** family with ten widely-used oscillators
(403 → 413 indicators), the second batch of Part B (family deepening).

| Indicator | Binding | Input → Output |
|-----------|---------|----------------|
| `DisparityIndex` | `DisparityIndex` | scalar → scalar |
| `FisherRsi` | `FisherRSI` | scalar → scalar |
| `Rmi` | `RMI` | scalar (period, momentum) → scalar |
| `DerivativeOscillator` | `DerivativeOscillator` | scalar (4 periods) → scalar |
| `Rsx` | `RSX` | scalar → scalar |
| `DynamicMomentumIndex` | `DynamicMomentumIndex` | scalar → scalar |
| `IntradayMomentumIndex` | `IMI` | candle (open+close) → scalar |
| `StochasticCci` | `StochasticCCI` | candle → scalar |
| `ElderRay` | `ElderRay` | candle → struct (bull/bear) |
| `Qqe` | `QQE` | scalar → struct (rsi_ma/trailing) |

LSMA was dropped from the planned set: it already ships as `LinearRegression`.

The single-period scalars use generated macro bindings; `Rmi` /
`DerivativeOscillator` use hand node/python bindings with the typed wasm macro;
`ElderRay`/`Qqe` use custom struct bindings; `IntradayMomentumIndex` uses custom
candle bindings carrying the open. Full coverage: core modules with per-branch
unit tests, mod/lib catalogue, FAMILIES + assert, README + docs counters,
CHANGELOG, all three bindings (regenerated `index.d.ts`/`index.js`), fuzz
drivers, and the python/node test registries.

Local verification: `cargo test -p wickra-core` (lib 3335 + doc 371),
`cargo clippy --workspace --all-targets --all-features -D warnings` clean,
node `npm run build && npm test` (488), python `pytest` (802).
This commit is contained in:
kingchenc
2026-06-04 15:26:17 +02:00
committed by GitHub
parent 0d2acad28d
commit 4f81222aed
25 changed files with 3855 additions and 49 deletions
@@ -28,6 +28,12 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
DerivativeOscillator: () => new wickra.DerivativeOscillator(14, 5, 3, 9),
RMI: () => new wickra.RMI(14, 5),
DynamicMomentumIndex: () => new wickra.DynamicMomentumIndex(14),
RSX: () => new wickra.RSX(14),
FisherRSI: () => new wickra.FisherRSI(14),
DisparityIndex: () => new wickra.DisparityIndex(14),
HoltWinters: () => new wickra.HoltWinters(0.2, 0.1),
GD: () => new wickra.GD(5, 0.7),
AdaptiveLaguerre: () => new wickra.AdaptiveLaguerre(13),
@@ -334,6 +340,8 @@ const candleScalar = {
BodySizePct: { make: () => new wickra.BodySizePct(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
WickRatio: { make: () => new wickra.WickRatio(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HighLowRange: { make: () => new wickra.HighLowRange(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
StochasticCCI: { make: () => new wickra.StochasticCCI(14), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
IMI: { make: () => new wickra.IMI(14), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
@@ -416,6 +424,8 @@ const multi = {
FibArcs: { make: () => new wickra.FibArcs(), fields: ['arc382', 'arc500', 'arc618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibChannel: { make: () => new wickra.FibChannel(), fields: ['base', 'level618', 'level1000', 'level1618'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
FibTimeZones: { make: () => new wickra.FibTimeZones(), fields: ['onZone', 'barsToNext'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
ElderRay: { make: () => new wickra.ElderRay(13), fields: ['bullPower', 'bearPower'], step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
QQE: { make: () => new wickra.QQE(14, 5, 4.236), fields: ['rsiMa', 'trailingLine'], step: (ind, i) => ind.update(close[i]), batch: (ind) => ind.batch(close) },
};
for (const [name, d] of Object.entries(multi)) {
+98
View File
@@ -69,6 +69,14 @@ export interface HtPhasorValue {
inphase: number
quadrature: number
}
export interface QqeValue {
rsiMa: number
trailingLine: number
}
export interface ElderRayValue {
bullPower: number
bearPower: number
}
export interface StochValue {
k: number
d: number
@@ -917,6 +925,42 @@ export declare class AdaptiveLaguerre {
isReady(): boolean
warmupPeriod(): number
}
export type DisparityIndexNode = DisparityIndex
export declare class DisparityIndex {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FisherRsiNode = FisherRSI
export declare class FisherRSI {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type RsxNode = RSX
export declare class RSX {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type DynamicMomentumIndexNode = DynamicMomentumIndex
export declare class DynamicMomentumIndex {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type JumpIndicatorNode = JumpIndicator
export declare class JumpIndicator {
constructor(period: number, threshold: number)
@@ -1414,6 +1458,42 @@ export declare class HighLowRange {
isReady(): boolean
warmupPeriod(): number
}
export type StochasticCciNode = StochasticCCI
export declare class StochasticCCI {
constructor(period: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ImiNode = IMI
export declare class IMI {
constructor(period: number)
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type QqeNode = QQE
export declare class QQE {
constructor(rsiPeriod: number, smoothing: number, factor: number)
update(value: number): QqeValue | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ElderRayNode = ElderRay
export declare class ElderRay {
constructor(period: number)
update(high: number, low: number, close: number): ElderRayValue | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type StochNode = Stochastic
export declare class Stochastic {
constructor(kPeriod: number, dPeriod: number)
@@ -1740,6 +1820,24 @@ export declare class HoltWinters {
isReady(): boolean
warmupPeriod(): number
}
export type RmiNode = RMI
export declare class RMI {
constructor(period: number, momentum: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type DerivativeOscillatorNode = DerivativeOscillator
export declare class DerivativeOscillator {
constructor(rsiPeriod: number, smooth1: number, smooth2: number, signalPeriod: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TsiNode = TSI
export declare class TSI {
constructor(long: number, short: number)
+11 -1
View File
File diff suppressed because one or more lines are too long
+329
View File
@@ -206,6 +206,14 @@ node_scalar_indicator!(
"AdaptiveLaguerre",
wc::AdaptiveLaguerreFilter
);
node_scalar_indicator!(DisparityIndexNode, "DisparityIndex", wc::DisparityIndex);
node_scalar_indicator!(FisherRsiNode, "FisherRSI", wc::FisherRsi);
node_scalar_indicator!(RsxNode, "RSX", wc::Rsx);
node_scalar_indicator!(
DynamicMomentumIndexNode,
"DynamicMomentumIndex",
wc::DynamicMomentumIndex
);
#[napi(js_name = "JumpIndicator")]
pub struct JumpIndicatorNode {
inner: wc::JumpIndicator,
@@ -2051,6 +2059,242 @@ impl HighLowRangeNode {
}
}
#[napi(js_name = "StochasticCCI")]
pub struct StochasticCciNode {
inner: wc::StochasticCci,
}
#[napi]
impl StochasticCciNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::StochasticCci::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[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
}
}
#[napi(js_name = "IMI")]
pub struct ImiNode {
inner: wc::IntradayMomentumIndex,
}
#[napi]
impl ImiNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::IntradayMomentumIndex::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd4(open, high, low, close)?))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let n = open.len();
let mut out = Vec::with_capacity(n);
for i in 0..n {
out.push(
self.inner
.update(cnd4(open[i], high[i], low[i], close[i])?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[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
}
}
#[napi(object)]
pub struct QqeValue {
pub rsi_ma: f64,
pub trailing_line: f64,
}
#[napi(js_name = "QQE")]
pub struct QqeNode {
inner: wc::Qqe,
}
#[napi]
impl QqeNode {
#[napi(constructor)]
pub fn new(rsi_period: u32, smoothing: u32, factor: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::Qqe::new(rsi_period as usize, smoothing as usize, factor)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, value: f64) -> Option<QqeValue> {
self.inner.update(value).map(|o| QqeValue {
rsi_ma: o.rsi_ma,
trailing_line: o.trailing_line,
})
}
#[napi]
pub fn batch(&mut self, prices: Vec<f64>) -> Vec<f64> {
let mut out = vec![f64::NAN; prices.len() * 2];
for (i, p) in prices.iter().enumerate() {
if let Some(o) = self.inner.update(*p) {
out[i * 2] = o.rsi_ma;
out[i * 2 + 1] = o.trailing_line;
}
}
out
}
#[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
}
}
#[napi(object)]
pub struct ElderRayValue {
pub bull_power: f64,
pub bear_power: f64,
}
#[napi(js_name = "ElderRay")]
pub struct ElderRayNode {
inner: wc::ElderRay,
}
#[napi]
impl ElderRayNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::ElderRay::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<ElderRayValue>> {
Ok(self
.inner
.update(cnd(high, low, close, 0.0)?)
.map(|o| ElderRayValue {
bull_power: o.bull_power,
bear_power: o.bear_power,
}))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
if let Some(o) = self.inner.update(cnd(high[i], low[i], close[i], 0.0)?) {
out[i * 2] = o.bull_power;
out[i * 2 + 1] = o.bear_power;
}
}
Ok(out)
}
#[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
}
}
#[napi(object)]
pub struct StochValue {
pub k: f64,
@@ -3877,6 +4121,91 @@ impl HoltWintersNode {
}
}
// ============================== RMI ==============================
#[napi(js_name = "RMI")]
pub struct RmiNode {
inner: wc::Rmi,
}
#[napi]
impl RmiNode {
#[napi(constructor)]
pub fn new(period: u32, momentum: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Rmi::new(period as usize, momentum 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
}
}
// ============================== DerivativeOscillator ==============================
#[napi(js_name = "DerivativeOscillator")]
pub struct DerivativeOscillatorNode {
inner: wc::DerivativeOscillator,
}
#[napi]
impl DerivativeOscillatorNode {
#[napi(constructor)]
pub fn new(
rsi_period: u32,
smooth1: u32,
smooth2: u32,
signal_period: u32,
) -> napi::Result<Self> {
Ok(Self {
inner: wc::DerivativeOscillator::new(
rsi_period as usize,
smooth1 as usize,
smooth2 as usize,
signal_period 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")]