Skip to content

v2.0.5 — NaN poisoning in the indicator kernels

Latest

Choose a tag to compare

@marketcalls marketcalls released this 11 Sep 06:42

Bug-fix release. Chaining one indicator into another silently produced nothing: every indicator emits warm-up NaNs, and the moving-average kernels let a single NaN into their running accumulator, which stayed poisoned for the rest of the series.

df["RSI_wma"] = ta.rsi(ta.wma(df["close"], 55), 14)
df["RSI_avg"] = ta.wma(df["RSI_wma"], 9)             # was: NaN on every row
ta.crossover(df["RSI_wma"], df["RSI_avg"]).sum()     # was: 0 across 1600 bars

Reported in marketcalls/openalgo#2029. ta.crossover / ta.crossunder were never at fault and are unchanged.

Silent failures

Three kernels returned plausible numbers rather than NaN, which is worse than the reported symptom:

  • stdev returned 0.0 for the rest of the series after a NaN. f64::max yields the other operand against NaN, so (NaN).max(0.0).sqrt() is 0.0 — Bollinger width collapsed instead of failing visibly.
  • rsi returned a flat 100 over an upstream indicator's warm-up. A NaN delta is neither a gain nor a loss, so avg_loss seeded to 0 and took the "no losses" branch. This one predates the Rust core, which is why pinning back to 1.0.50 did not help.
  • median panicked through the PyO3 boundary (partial_cmp().unwrap()) on any window containing a NaN — a PanicException, so it escapes a normal except Exception.

ta.vi and ta.ulcerindex returned all-NaN even on clean OHLCV; both now match their textbook definitions.

The NaN contract is now stated and enforced

It was previously unstated, and the Rust core and the pure-NumPy fallback had silently drifted apart — the same script gave different answers from a wheel and from a source checkout.

  • Rolling-window kernels are window-local. out[i] is NaN iff i < period-1 or the window contains a NaN, and the series recovers as soon as the NaN slides out. Same rule as pandas .rolling(period).
  • Recursive kernels (the EMA family) skip leading NaNs, seed at the first finite value, and carry state across an interior NaN.

Affected kernels: sma, wma, ema, hma, zlema, t3, vwma, stdev, rolling_sum, rolling_variance, ema_sma, ema_wilder, ema_first_valid, true_range, atr_wilder, and the rolling extremes behind stochastic, williams_r, aroon, donchian, midprice and midpoint.

Compatibility and performance

For NaN-free input every other indicator is unchanged, bit for bit — verified across 155 output series against 2.0.4.

The branchless slide (NaN contributes 0.0 to the accumulator, a running count decides whether the window is publishable) keeps the hot kernels at or faster than 2.0.4 on 2M rows:

kernel 2.0.4 2.0.5
sma 3.36 ms 2.43 ms
wma 3.38 ms 2.62 ms
stdev 3.53 ms 2.96 ms
atr 9.30 ms 8.87 ms
hma 8.13 ms 7.73 ms
vwma 2.31 ms 3.20 ms

13 new Rust unit tests cover the contract, and benchmark/ci_smoke.py now checks the Rust core against the NumPy fallback and against pandas on every CI run.

Install

pip install --upgrade openalgo

Full notes: CHANGELOG.md