Add B10 Ehlers / Cycle deepening (10 indicators) (#199)

Deepens the **Ehlers / Cycle (DSP)** family (B10) with ten indicators (452 -> 462):

- **HighpassFilter**, **Reflex**, **Trendflex**, **CorrelationTrendIndicator**, **AdaptiveRsi**, **UniversalOscillator** — scalar (f64) Ehlers filters/oscillators.
- **AdaptiveCci** — efficiency-ratio-adaptive CCI on typical price (Candle input).
- **BandpassFilter**, **EvenBetterSinewave**, **AutocorrelationPeriodogram** — multi-arg scalar (hand-written bindings; the wasm variadic scalar macro covers wasm).

Verified locally: 3755 core lib + 420 doc tests, clippy clean, 537 node tests, 881 pytest, counter 462.
This commit is contained in:
kingchenc
2026-06-07 04:25:16 +02:00
committed by GitHub
parent 707f29e8e4
commit 80850c81f7
25 changed files with 3603 additions and 71 deletions
@@ -28,6 +28,15 @@ function num(v) {
// --- Scalar indicators: update(value) vs batch(prices) ---
const scalarFactories = {
AUTOCORRPGRAM: () => new wickra.AUTOCORRPGRAM(10, 48),
EVENBETTERSINE: () => new wickra.EVENBETTERSINE(40, 10),
BANDPASS: () => new wickra.BANDPASS(20, 0.3),
UNIVERSALOSC: () => new wickra.UNIVERSALOSC(20),
ADAPTIVERSI: () => new wickra.ADAPTIVERSI(14),
CTI: () => new wickra.CTI(20),
TRENDFLEX: () => new wickra.TRENDFLEX(20),
REFLEX: () => new wickra.REFLEX(20),
HIGHPASS: () => new wickra.HIGHPASS(48),
SAMPLEENT: () => new wickra.SAMPLEENT(20, 2, 0.2),
SHANNONENT: () => new wickra.SHANNONENT(20, 8),
ROLLINGMINMAX: () => new wickra.ROLLINGMINMAX(20),
@@ -367,6 +376,7 @@ const candleScalar = {
TradeVolumeIndex: { make: () => new wickra.TradeVolumeIndex(0.25), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
IntradayIntensity: { make: () => new wickra.IntradayIntensity(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
BetterVolume: { make: () => new wickra.BetterVolume(14), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
ADAPTIVECCI: { make: () => new wickra.ADAPTIVECCI(20), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
+90
View File
@@ -1070,6 +1070,87 @@ export declare class ROLLINGMINMAX {
isReady(): boolean
warmupPeriod(): number
}
export type HighpassFilterNode = HIGHPASS
export declare class HIGHPASS {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ReflexNode = REFLEX
export declare class REFLEX {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TrendflexNode = TRENDFLEX
export declare class TRENDFLEX {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type CorrelationTrendIndicatorNode = CTI
export declare class CTI {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type AdaptiveRsiNode = ADAPTIVERSI
export declare class ADAPTIVERSI {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type UniversalOscillatorNode = UNIVERSALOSC
export declare class UNIVERSALOSC {
constructor(period: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type BandpassFilterNode = BANDPASS
export declare class BANDPASS {
constructor(period: number, bandwidth: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type EvenBetterSinewaveNode = EVENBETTERSINE
export declare class EVENBETTERSINE {
constructor(hpPeriod: number, ssfLength: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type AutocorrelationPeriodogramNode = AUTOCORRPGRAM
export declare class AUTOCORRPGRAM {
constructor(minPeriod: number, maxPeriod: number)
update(value: number): number | null
batch(prices: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ShannonEntropyNode = SHANNONENT
export declare class SHANNONENT {
constructor(period: number, bins: number)
@@ -1751,6 +1832,15 @@ export declare class TimeBasedStop {
isReady(): boolean
warmupPeriod(): number
}
export type AdaptiveCciNode = ADAPTIVECCI
export declare class ADAPTIVECCI {
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 StochNode = Stochastic
export declare class Stochastic {
constructor(kPeriod: number, dPeriod: number)
+11 -1
View File
File diff suppressed because one or more lines are too long
+176
View File
@@ -231,6 +231,129 @@ node_scalar_indicator!(
"ROLLINGMINMAX",
wc::RollingMinMaxScaler
);
node_scalar_indicator!(HighpassFilterNode, "HIGHPASS", wc::HighpassFilter);
node_scalar_indicator!(ReflexNode, "REFLEX", wc::Reflex);
node_scalar_indicator!(TrendflexNode, "TRENDFLEX", wc::Trendflex);
node_scalar_indicator!(
CorrelationTrendIndicatorNode,
"CTI",
wc::CorrelationTrendIndicator
);
node_scalar_indicator!(AdaptiveRsiNode, "ADAPTIVERSI", wc::AdaptiveRsi);
node_scalar_indicator!(
UniversalOscillatorNode,
"UNIVERSALOSC",
wc::UniversalOscillator
);
// Multi-arg Ehlers scalars: hand-written (node_scalar_indicator! is single-period).
#[napi(js_name = "BANDPASS")]
pub struct BandpassFilterNode {
inner: wc::BandpassFilter,
}
#[napi]
impl BandpassFilterNode {
#[napi(constructor)]
pub fn new(period: u32, bandwidth: f64) -> napi::Result<Self> {
Ok(Self {
inner: wc::BandpassFilter::new(period as usize, bandwidth).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
}
}
#[napi(js_name = "EVENBETTERSINE")]
pub struct EvenBetterSinewaveNode {
inner: wc::EvenBetterSinewave,
}
#[napi]
impl EvenBetterSinewaveNode {
#[napi(constructor)]
pub fn new(hp_period: u32, ssf_length: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::EvenBetterSinewave::new(hp_period as usize, ssf_length 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
}
}
#[napi(js_name = "AUTOCORRPGRAM")]
pub struct AutocorrelationPeriodogramNode {
inner: wc::AutocorrelationPeriodogram,
}
#[napi]
impl AutocorrelationPeriodogramNode {
#[napi(constructor)]
pub fn new(min_period: u32, max_period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::AutocorrelationPeriodogram::new(min_period as usize, max_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
}
}
// Shannon Entropy / Sample Entropy: multi-arg scalar ctors, hand-written
// (node_scalar_indicator! only generates a single-period constructor).
@@ -3056,6 +3179,59 @@ impl TimeBasedStopNode {
}
}
#[napi(js_name = "ADAPTIVECCI")]
pub struct AdaptiveCciNode {
inner: wc::AdaptiveCci,
}
#[napi]
impl AdaptiveCciNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::AdaptiveCci::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(object)]
pub struct StochValue {
pub k: f64,