Skip to content

v2 Rust RelativeStrengthIndex ignores ma_type and regresses the last_value fix from #2703 (flat 1.0) #1

Description

@bebop23

Summary

The Rust-core RelativeStrengthIndex in 2.0.0rc1 has two numerical defects that the Cython
implementation does not (the Cython impl being the stated source of truth per nautechsystems#2507):

  1. ma_type is ignored. The constructor always builds Exponential inner moving averages
    regardless of the ma_type argument, so Wilder, Simple, and Exponential produce identical
    output. The Cython version plumbs ma_type into MovingAverageFactory.create(...).
  2. last_value is not advanced on zero-loss bars, so RSI gets pinned at rsi_max (1.0) and
    never recovers — even after real down-moves. This is the same bug reported in Relative Strength Index (RSI) Indicator always equal to 1.0 when inputs greater than initial nautechsystems/nautilus_trader#2673 and fixed
    for the Cython indicator in Fix last value updating for RSI indicator nautechsystems/nautilus_trader#2703; the fix was not carried into the Rust port.

Repro (only nautilus_trader required):

from nautilus_trader.indicators import RelativeStrengthIndex, MovingAverageType

def run(values, period, ma_type):
    ind = RelativeStrengthIndex(period, ma_type)
    return [round(ind.update_raw(float(v)) or ind.value * 100, 4) for v in values]

# 1) ma_type ignored — all three lines are identical
s = [44.34,44.09,44.15,43.61,44.33,44.83,45.10,45.42,45.84,46.08,45.89,46.03,45.61,46.28,46.28]
for mt in (MovingAverageType.Wilder, MovingAverageType.Simple, MovingAverageType.Exponential):
    print(mt, run(s, 14, mt)[-4:])

# 2) flat 100 after real losses (up 1..15 then down 14,12,9,5,2)
print(run([float(i) for i in range(1,16)] + [14,12,9,5,2], 14, MovingAverageType.Wilder))

Expected vs actual

Up 1..15 then down 14,12,9,5,2, period 14, ma_type=Wilder (Wilder RSI ×100):

idx 15 16 17 18 19
expected (Wilder) 89.35 72.69 55.86 41.92 34.89
actual (v2) 100.00 100.00 100.00 100.00 100.00

ma_type on the 15-close series, last value ×100:

ma_type expected actual (v2)
Wilder 72.23 72.23
Simple distinct (≠ Wilder) 72.23
Exponential distinct (≠ Wilder) 72.23

Root cause (crates/indicators/src/momentum/rsi.rs)

  • Defect 1 — constructor (RelativeStrengthIndex::new): inner MAs are built with the literal
    MovingAverageType::Exponential instead of the ma_type argument (which is only stored on the
    struct and used by Display).
    average_gain: MovingAverageFactory::create(MovingAverageType::Exponential, period), // should be ma_type
    average_loss: MovingAverageFactory::create(MovingAverageType::Exponential, period),
  • Defect 2 — update_raw: self.last_value = value is placed after the early return taken when
    average_loss.value() == 0.0, so last_value is never advanced on zero-loss bars. This is the
    exact case Fix last value updating for RSI indicator nautechsystems/nautilus_trader#2703 fixed in Cython (self._last_value = value before the return).

Proposed fix

  1. Pass the resolved ma_type into the factory for both averages:
    let ma = ma_type.unwrap_or(MovingAverageType::Exponential);
    // ...
    average_gain: MovingAverageFactory::create(ma, period),
    average_loss: MovingAverageFactory::create(ma, period),
    (and set self.ma_type = ma from the same resolved value).
  2. Advance last_value in the zero-loss branch, mirroring Fix last value updating for RSI indicator nautechsystems/nautilus_trader#2703:
    if self.average_loss.value() == 0.0 {
        self.value = self.rsi_max;
        self.last_value = value;   // add this
        return;
    }
  3. Add regression tests: (a) Wilder/Simple/Exponential produce distinct output on a fixed
    series; (b) a series that rises then falls returns < rsi_max once losses appear; (c) a Wilder
    golden series vs Wilder's published reference values. The existing Rust unit tests only exercise
    the default (Exponential) path and monotonic/constant inputs, which is why both defects slipped
    the Confirm parity between Python and Rust indicators nautechsystems/nautilus_trader#2713 parity pass.

Filed on this fork per the ToTheMoon Nautilus-v2 migration fork policy (tracked liability; see bebop23/ToTheMoon#83). Intended to be fixed here on a branch off develop and upstreamed as a PR to nautechsystems/nautilus_trader.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions