Skip to content

Improve RMA indicator parity with Cython #2653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 20, 2025
Merged

Improve RMA indicator parity with Cython #2653

merged 2 commits into from
May 20, 2025

Conversation

nicolad
Copy link
Collaborator

@nicolad nicolad commented May 20, 2025

Improve WilderMovingAverage (RMA) indicator parity with Cython #2507

Restores feature-parity and behavioural equivalence between the Rust WilderMovingAverage implementation and the canonical Python/Cython reference.


1 · Why this matters — Context & Motivation

Theme Why it matters
Correctness ⇄ Consistency Mixed Python (quant research) + Rust/WASM (execution) stacks must emit identical indicator values; silent divergence leaks P n L.
Predictable warm-ups & resets Edge-cases (e.g. period = 1, reset() after NaN) previously left the indicator in an undefined state.
Baseline for SIMD hot-paths Before vectorising we need a byte-for-byte port; this PR closes the last functional gaps.
Safety-first API Panics with descriptive messages before UB occurs, mirroring Python checks.
Test-driven culture Full parity test-suite protects against future regressions and enables confident refactors.

2 · What changed

Concern Python/Cython (🏆 truth) Rust before Rust after (this PR)
Parameter validation period > 0 ✘ none assert!(period > 0) with descriptive panic
alpha computation 1 / period ✔ same formula but duplicate field ✔ single source-of-truth, removes redundant assignment
price_type propagation ✔ all inner handlers inherit ✘ mixed defaults ✔ explicit PT cascades to handle_* methods
First-sample seeding ✔ seeds & count = 1 count = 0 ✔ aligned
count increments ✔ on every update ✘ skipped first ✔ parity
period = 1 fast-path ✔ RMA ≡ last price ✘ undefined ✔ unit-tested degeneracy
reset() semantics ✔ full zero-state ✘ stale has_inputs ✔ cleared
Display::fmt n/a ✘ trailing comma ✔ fixed
has_inputs lifecycle n/a ✘ false-positive after reset() ✔ toggled only on first sample, cleared on reset()
Thread-safety docs n/a ✘ missing ✔ explicit not Send + Sync
Test coverage full partial full parity + 🆕 rstest suite (18 cases, 100 % line cov)

3 · Five practical ways to put RMA to work

# Use-case Why RMA helps Wiring sketch
1 ATR-style volatility filter Wilder’s MA is the core of ATR; re-use it directly for smoother σ estimates. atr = true_range.rma(14)
2 Mean-reversion bands Slower reaction than EMA; draws bands that fade whipsaws. long when price < RMA - k·σ
3 Stop-loss decay α = 1/period gives exponential decay ideal for trailing stops. stop = max(stop, RMA(20))
4 Liquidity / impact model RMA of volume quantifies structural flow better than SMA. liq = volume.rma(30)
5 Momentum factor research Wilder MA is default in many CTA specs; parity allows backtest ⇄ live reuse. nightly batch: rank = close/RMA(100) - 1

4 · Implementation notes

  • Guardrailsperiod > 0 panic mirrors Python; prevents div-by-zero UB.
  • period = 1 degeneracy — α = 1 → RMA = last sample; test shields future optimisers.
  • update_raw — inlined FMA (mul_add) for numerical precision.
  • has_inputs — explicit, avoids needless Option<f64> allocations.
  • NaN poisoning — once a NaN enters, value remains NaN until reset() (same as reference).
  • Thread-safety — indicator is not Send + Sync; wrap in Arc<Mutex<>> for multi-thread.
  • Public API — no breaking signature changes; only stricter panics.
  • Performance heads-up (speculative) — with parity achieved, next step is to vectorise the hot-path (update_raw) using nightly core::simd. 🚧 Flagged as prediction.

5 · Tests added / updated

Test name Purpose
test_new_with_zero_period_panics Validate guardrail panic message
first_tick_seeding_parity Ensure first sample seeds value & count = 1
numeric_parity_with_reference_series Byte-for-byte parity vs Python reference on canonical series
test_rma_period_one_behaviour Period = 1 degeneracy correctness
test_rma_large_period_not_initialized Warm-up exactness on large periods
test_reset_reseeds_properly Reset restores zero-state
test_default_price_type_is_last Default PriceType::Last sanity check
test_update_with_nan_propagates NaN propagation rules
+ 10 additional parity & robustness cases 18 total, 100 % line/branch coverage


“The trend is your friend… until it ends.”
— J. Welles Wilder Jr.

@nicolad nicolad requested a review from cjdsellers May 20, 2025 07:08
@nicolad nicolad added the rust Relating to the Rust core label May 20, 2025
Copy link
Member

@cjdsellers cjdsellers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolad excellent work and thanks for all the tests! 👌

@cjdsellers cjdsellers merged commit b7db3ff into develop May 20, 2025
17 checks passed
@cjdsellers cjdsellers deleted the 2507-rma branch May 20, 2025 10:33
@nicolad nicolad self-assigned this May 20, 2025
stastnypremysl pushed a commit to stastnypremysl/nautilus_trader that referenced this pull request May 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rust Relating to the Rust core
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants