feat: Family 06 Trend-Strength - 5 new directional/random-walk indicators (#44)
* feat(adxr): add Wilder Average Directional Movement Index Rating
ADXR is the trend-strength smoother Wilder published alongside ADX in
*New Concepts in Technical Trading Systems* (1978):
ADXR_t = (ADX_t + ADX_{t - (period - 1)}) / 2
The lookback length is the same period that feeds the underlying ADX.
Because the older ADX is period - 1 bars stale, ADXR responds more
slowly than ADX and is the canonical metric for comparing
trend-strength across instruments.
Implementation reuses the existing wickra_core::Adx engine plus a
period-length ring of past ADX values; warmup is 3 * period - 1
(41 for period = 14). Bindings: Python PyAdxr (PyArray1 batch),
Node AdxrNode (number scalar), WASM WasmAdxr. Fuzz target covers
the candle-input path. Python + Node streaming-vs-batch tests
parametrised, plus a pure-uptrend reference value (ADXR == 100
when ADX saturates at 100). Criterion bench added under crates/
wickra/benches/indicators.rs.
README family table and indicator counter updated (71 -> 72).
* feat(rwi): add Mike Poulos Random Walk Index
RWI compares actual price displacement to what a random walk would
produce over the same horizon: for each lookback i in [2, period],
RWI_High_t(i) = (high_t - low_{t-i+1}) / (ATR_i(t) * sqrt(i))
RWI_Low_t(i) = (high_{t-i+1} - low_t) / (ATR_i(t) * sqrt(i))
Per-bar output is the maximum across lookbacks for each direction;
a reading > 1 means the trend beats random-walk noise, > 2 is the
typical strong-trend threshold. Multi-output (high, low). period
must be >= 2 (the shortest meaningful lookback); period < 2 returns
InvalidPeriod. Warmup = period (e.g. 14 for the standard default).
Bindings: Python PyRwi (PyArray2 shape (n, 2)), Node RwiNode +
RwiValue struct, WASM WasmRwi (Object/Reflect for update,
Float64Array interleaved for batch). Fuzz target adds the candle
input case. Python parametric streaming-vs-batch test and pure
uptrend reference test (RWI_High dominates RWI_Low and exceeds 1).
Node parametric streaming-vs-interleaved-batch test. Criterion
bench under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (72 -> 73).
* feat(tii): add M.H. Pee Trend Intensity Index
TII is a [0, 100] oscillator that asks 'what fraction of the recent
SMA deviations are positive?'. The construction is
dev_t = close_t - SMA(close, sma_period)_t
SD_pos = sum of positive dev_t over the last dev_period bars
SD_neg = sum of |negative dev_t| over the last dev_period bars
TII = 100 * SD_pos / (SD_pos + SD_neg)
Saturates at 100 on a pure uptrend (every close above the lagging
SMA), at 0 on a pure downtrend, and returns the neutral mid-point 50
on a perfectly flat window. The output is clamped to [0, 100] as
the rolling-sum subtraction loop can accumulate a few ULP of error
on long histories. Canonical Pee parameters (sma_period=60,
dev_period=30) wired as Python defaults; warmup is
sma_period + dev_period - 1 (89 for the defaults).
Bindings: Python PyTii (PyArray1 batch), Node TiiNode (scalar
update + batch), WASM WasmTii via the two-arg wasm_scalar_indicator!
macro. Fuzz target adds the scalar path. Python parametric
streaming-vs-batch test plus pure-uptrend (TII == 100) and
flat-market (TII == 50) reference tests. Node parametric
streaming-vs-batch test. Criterion bench under crates/wickra/
benches/indicators.rs.
README family table and indicator counter updated (73 -> 74).
* feat(kst): add Pring Know Sure Thing oscillator
KST is Martin Pring's long-horizon momentum gauge: four smoothed
rate-of-change components combined with fixed weights (1, 2, 3, 4),
plus an SMA signal line.
RCMA_i = SMA(ROC(close, roc_i), sma_i) for i in 1..=4
KST = 1*RCMA_1 + 2*RCMA_2 + 3*RCMA_3 + 4*RCMA_4
Signal = SMA(KST, signal_period)
Kst::classic() exposes Pring's recommended parameter set
(roc = (10, 15, 20, 30), sma = (10, 10, 10, 15), signal = 9);
warmup = max(roc_i + sma_i) + signal_period - 1 (53 for the classic
parameters). All four parallel branches are fed unconditionally so
they warm in lock-step.
Bindings: Python PyKst (PyArray2 shape (n, 2)) with a KST.classic()
staticmethod, Node KstNode + KstValue with a KST.classic() factory,
WASM WasmKst with both new(...) and classic() constructors plus
Object/Reflect for update and Float64Array for batch. Fuzz target
adds the scalar multi-output path. Python tests gain a new
MULTI_SCALAR section parametric over scalar-input/multi-output
indicators, plus a classic-on-constant-series reference test. Node
tests gain a KST entry in the multi-output section. Criterion
benchmark added under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (74 -> 75).
* feat(wave-trend): add LazyBear Wave Trend Oscillator
Two-line mean-reverting momentum gauge built from the typical price
and three cascaded EMAs:
ap = (high + low + close) / 3
esa = EMA(ap, channel_period)
d = EMA(|ap - esa|, channel_period)
ci = (ap - esa) / (0.015 * d)
wt1 = EMA(ci, average_period)
wt2 = SMA(wt1, signal_period)
WaveTrend::classic() exposes LazyBear's defaults
(channel = 10, average = 21, signal = 4); warmup is
2 * channel_period + average_period + signal_period - 3 (42 for the
classic defaults). On a perfectly flat market the SMA-seeded EMA
introduces a single-ULP drift between ap and esa, which on a tiny d
would make the ratio explode to -1/0.015 = -66.67; a price-scaled
flat-tolerance guard (d <= 16 * EPSILON * max(|esa|, 1)) collapses
the channel index to 0 in that regime so both lines remain at zero.
Bindings: Python PyWaveTrend (PyArray2 shape (n, 2)) with a
WaveTrend.classic() staticmethod, Node WaveTrendNode + WaveTrendValue
with a WaveTrend.classic() factory, WASM WasmWaveTrend with both
new(...) and classic() constructors. Fuzz target adds the candle
multi-output path (sorted alphabetically). Python parametric
streaming-vs-batch test plus a flat-market reference test. Node
parametric streaming-vs-interleaved-batch test. Criterion bench
under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (75 -> 76).
* fix(family-06): re-add KST::classic() factory + drop dup fuzz block
Family-06 PR's tests call ta.KST.classic() / wickra.KST.classic() — main's
KST binding shipped without the static factory. Add classic() in Python
(staticmethod) and Node (napi factory); WASM already had it. Also drop the
duplicate Kst::classic().unwrap() block in fuzz/indicator_update.rs that
the merge left behind (main's API no longer returns Result).
* test(rwi): drop dead count==0 guard
The loop `for i in 2..=period` makes `count = tr_end - tr_start = i - 1`
which is always >= 1, so the `if count == 0 { continue; }` branch was
unreachable defensive code that codecov flagged on the family-06 PR.
This commit is contained in:
+226
-62
@@ -88,6 +88,75 @@ wasm_scalar_indicator!(WasmMom, "MOM", wc::Mom, period: usize);
|
||||
wasm_scalar_indicator!(WasmCmo, "CMO", wc::Cmo, period: usize);
|
||||
wasm_scalar_indicator!(WasmTsi, "TSI", wc::Tsi, long: usize, short: usize);
|
||||
wasm_scalar_indicator!(WasmPmo, "PMO", wc::Pmo, smoothing1: usize, smoothing2: usize);
|
||||
wasm_scalar_indicator!(WasmTii, "TII", wc::Tii, sma_period: usize, dev_period: usize);
|
||||
|
||||
#[wasm_bindgen(js_name = KST)]
|
||||
pub struct WasmKst {
|
||||
inner: wc::Kst,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = KST)]
|
||||
impl WasmKst {
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
roc1: usize,
|
||||
roc2: usize,
|
||||
roc3: usize,
|
||||
roc4: usize,
|
||||
sma1: usize,
|
||||
sma2: usize,
|
||||
sma3: usize,
|
||||
sma4: usize,
|
||||
signal_period: usize,
|
||||
) -> Result<WasmKst, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Kst::new(
|
||||
roc1,
|
||||
roc2,
|
||||
roc3,
|
||||
roc4,
|
||||
sma1,
|
||||
sma2,
|
||||
sma3,
|
||||
sma4,
|
||||
signal_period,
|
||||
)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn classic() -> WasmKst {
|
||||
Self {
|
||||
inner: wc::Kst::classic(),
|
||||
}
|
||||
}
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"kst".into(), &o.kst.into()).ok();
|
||||
Reflect::set(&obj, &"signal".into(), &o.signal.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
/// Returns `[kst0, signal0, kst1, signal1, ...]`, length `2 * n`. Warmup is NaN.
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for (i, &p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(p) {
|
||||
out[i * 2] = o.kst;
|
||||
out[i * 2 + 1] = o.signal;
|
||||
}
|
||||
}
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
wasm_scalar_indicator!(WasmStochRsi, "StochRSI", wc::StochRsi, rsi_period: usize, stoch_period: usize);
|
||||
wasm_scalar_indicator!(WasmDpo, "DPO", wc::Dpo, period: usize);
|
||||
wasm_scalar_indicator!(WasmPpo, "PPO", wc::Ppo, fast: usize, slow: usize);
|
||||
@@ -568,68 +637,6 @@ impl WasmSmi {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = KST)]
|
||||
pub struct WasmKst {
|
||||
inner: wc::Kst,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = KST)]
|
||||
impl WasmKst {
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
roc1: usize,
|
||||
roc2: usize,
|
||||
roc3: usize,
|
||||
roc4: usize,
|
||||
sma1: usize,
|
||||
sma2: usize,
|
||||
sma3: usize,
|
||||
sma4: usize,
|
||||
signal: usize,
|
||||
) -> Result<WasmKst, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Kst::new(roc1, roc2, roc3, roc4, sma1, sma2, sma3, sma4, signal)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
/// Returns `[kst0, signal0, kst1, signal1, ...]`, length `2n`.
|
||||
pub fn batch(&mut self, prices: &[f64]) -> Float64Array {
|
||||
let n = prices.len();
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for (i, p) in prices.iter().enumerate() {
|
||||
if let Some(o) = self.inner.update(*p) {
|
||||
out[i * 2] = o.kst;
|
||||
out[i * 2 + 1] = o.signal;
|
||||
}
|
||||
}
|
||||
Float64Array::from(out.as_slice())
|
||||
}
|
||||
/// Streaming update. Returns `{ kst, signal }` once warm, else `null`.
|
||||
pub fn update(&mut self, value: f64) -> JsValue {
|
||||
match self.inner.update(value) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"kst".into(), &o.kst.into()).ok();
|
||||
Reflect::set(&obj, &"signal".into(), &o.signal.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = PGO)]
|
||||
pub struct WasmPgo {
|
||||
inner: wc::Pgo,
|
||||
@@ -1889,6 +1896,117 @@ impl WasmVortex {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = WaveTrend)]
|
||||
pub struct WasmWaveTrend {
|
||||
inner: wc::WaveTrend,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = WaveTrend)]
|
||||
impl WasmWaveTrend {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
channel_period: usize,
|
||||
average_period: usize,
|
||||
signal_period: usize,
|
||||
) -> Result<WasmWaveTrend, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::WaveTrend::new(channel_period, average_period, signal_period)
|
||||
.map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn classic() -> Result<WasmWaveTrend, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::WaveTrend::classic().map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"wt1".into(), &o.wt1.into()).ok();
|
||||
Reflect::set(&obj, &"wt2".into(), &o.wt2.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.wt1;
|
||||
out[i * 2 + 1] = o.wt2;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = RWI)]
|
||||
pub struct WasmRwi {
|
||||
inner: wc::Rwi,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = RWI)]
|
||||
impl WasmRwi {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmRwi, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Rwi::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<JsValue, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(match self.inner.update(c) {
|
||||
Some(o) => {
|
||||
let obj = Object::new();
|
||||
Reflect::set(&obj, &"high".into(), &o.high.into()).ok();
|
||||
Reflect::set(&obj, &"low".into(), &o.low.into()).ok();
|
||||
obj.into()
|
||||
}
|
||||
None => JsValue::NULL,
|
||||
})
|
||||
}
|
||||
/// Returns `[high0, low0, high1, low1, ...]`, length `2 * n`. Warmup is NaN.
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
let n = high.len();
|
||||
if low.len() != n || close.len() != n {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = vec![f64::NAN; n * 2];
|
||||
for i in 0..n {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
if let Some(o) = self.inner.update(c) {
|
||||
out[i * 2] = o.high;
|
||||
out[i * 2 + 1] = o.low;
|
||||
}
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = MassIndex)]
|
||||
pub struct WasmMassIndex {
|
||||
inner: wc::MassIndex,
|
||||
@@ -2058,6 +2176,52 @@ impl WasmAdx {
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = ADXR)]
|
||||
pub struct WasmAdxr {
|
||||
inner: wc::Adxr,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_class = ADXR)]
|
||||
impl WasmAdxr {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(period: usize) -> Result<WasmAdxr, JsError> {
|
||||
Ok(Self {
|
||||
inner: wc::Adxr::new(period).map_err(map_err)?,
|
||||
})
|
||||
}
|
||||
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
|
||||
let c = make_candle(high, low, close, 0.0)?;
|
||||
Ok(self.inner.update(c))
|
||||
}
|
||||
pub fn batch(
|
||||
&mut self,
|
||||
high: &[f64],
|
||||
low: &[f64],
|
||||
close: &[f64],
|
||||
) -> Result<Float64Array, JsError> {
|
||||
if high.len() != low.len() || low.len() != close.len() {
|
||||
return Err(JsError::new("high, low, close must be equal length"));
|
||||
}
|
||||
let mut out = Vec::with_capacity(high.len());
|
||||
for i in 0..high.len() {
|
||||
let c = make_candle(high[i], low[i], close[i], 0.0)?;
|
||||
out.push(self.inner.update(c).unwrap_or(f64::NAN));
|
||||
}
|
||||
Ok(Float64Array::from(out.as_slice()))
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.inner.reset();
|
||||
}
|
||||
#[wasm_bindgen(js_name = isReady)]
|
||||
pub fn is_ready(&self) -> bool {
|
||||
self.inner.is_ready()
|
||||
}
|
||||
#[wasm_bindgen(js_name = warmupPeriod)]
|
||||
pub fn warmup_period(&self) -> usize {
|
||||
self.inner.warmup_period()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = WilliamsR)]
|
||||
pub struct WasmWilliamsR {
|
||||
inner: wc::WilliamsR,
|
||||
|
||||
Reference in New Issue
Block a user