// LRSI: Laguerre RSI // John Ehlers, "Cybernetic Analysis for Stocks and Futures" (2004), Chapter 14. // A modified RSI that uses a 4-element Laguerre filter as its core moving average. // The gamma (damping) parameter trades responsiveness against smoothness. // Output is dimensionless [0, 1]; no period parameter required. using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// LRSI: Laguerre RSI /// /// /// Ehlers' Laguerre RSI replaces the standard RSI's gain/loss smoothing with /// a 4-stage cascaded Laguerre filter. The four outputs (L0–L3) represent /// successively delayed and damped versions of the input; the RSI-style /// numerator/denominator is computed over the stage-to-stage differences. /// /// Filter stages (γ = gamma): /// /// L0 = (1−γ)·price + γ·L0[1] /// L1 = −γ·L0 + L0[1] + γ·L1[1] /// L2 = −γ·L1 + L1[1] + γ·L2[1] /// L3 = −γ·L2 + L2[1] + γ·L3[1] /// /// /// RSI computation: /// /// cu = Σ max(L(k)−L(k+1), 0) for k = 0..2 /// cd = Σ max(L(k+1)−L(k), 0) for k = 0..2 /// LRSI = cu / (cu + cd) [or 0.5 when cu + cd == 0] /// /// /// Properties: /// /// Output is always in [0, 1] /// WarmupPeriod = 4 (four filter stages) /// Lower γ = faster response; higher γ = smoother output /// Recursive filter — no SIMD possible in streaming path /// /// /// References: /// Ehlers, J.F. (2004). Cybernetic Analysis for Stocks and Futures. Wiley. Ch. 14. /// PineScript reference: lrsi.pine /// [SkipLocalsInit] public sealed class Lrsi : ITValuePublisher { private readonly double _gamma; private readonly double _oneMinusGamma; [StructLayout(LayoutKind.Auto)] private record struct State( double L0, double L1, double L2, double L3, double LastValid); private State _s; private State _ps; /// Display name for the indicator. public string Name { get; } /// Bars required before output is considered reliable. public int WarmupPeriod { get; } /// True after 4 bars have been processed through all filter stages. public bool IsHot => _s.L0 != 0.0 || _s.L1 != 0.0 || _s.L2 != 0.0 || _s.L3 != 0.0; /// Current LRSI value in [0, 1]. public TValue Last { get; private set; } public event TValuePublishedHandler? Pub; private int _count; private int _pcount; /// /// Creates LRSI with the specified gamma damping factor. /// /// Laguerre damping factor in [0.0, 1.0] (default 0.5). /// Lower values produce faster response; higher values produce smoother output. public Lrsi(double gamma = 0.5) { if (gamma < 0.0 || gamma > 1.0) { throw new ArgumentException("gamma must be in [0.0, 1.0]", nameof(gamma)); } _gamma = gamma; _oneMinusGamma = 1.0 - gamma; _s = new State(0.0, 0.0, 0.0, 0.0, 0.5); _ps = _s; WarmupPeriod = 4; Name = $"Lrsi({gamma:F2})"; } /// /// Creates LRSI chained to an ITValuePublisher source. /// public Lrsi(ITValuePublisher source, double gamma = 0.5) : this(gamma) { source.Pub += Handle; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PubEvent(TValue value, bool isNew) => Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew }); /// Resets all state to initial conditions. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(0.0, 0.0, 0.0, 0.0, 0.5); _ps = _s; _count = 0; _pcount = 0; Last = default; } /// /// Updates LRSI with a new price value. /// /// Price input (typically close) /// True to advance state; false to rewrite the latest bar (bar correction) /// Current LRSI value as TValue in [0, 1] [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { double value = input.Value; // Sanitize input — substitute last-valid on NaN/Infinity if (!double.IsFinite(value)) { value = _s.LastValid; } if (isNew) { _ps = _s; _pcount = _count; _count++; } else { _s = _ps; _count = _pcount; } // State local copy — enables JIT struct promotion to registers var s = _s; // Update LastValid after rollback so we capture the sanitised value if (double.IsFinite(input.Value)) { s.LastValid = value; } double g = _gamma; double omg = _oneMinusGamma; // Stage 0: first-order IIR lowpass // L0 = (1−γ)·price + γ·L0[1] ≡ FMA(g, prevL0, omg·price) double prevL0 = s.L0; double prevL1 = s.L1; double prevL2 = s.L2; double prevL3 = s.L3; s.L0 = Math.FusedMultiplyAdd(g, prevL0, omg * value); // Stage 1: −γ·L0 + L0[1] + γ·L1[1] ≡ FMA(g, prevL1, prevL0 − g·s.L0) s.L1 = Math.FusedMultiplyAdd(g, prevL1, Math.FusedMultiplyAdd(-g, s.L0, prevL0)); // Stage 2: −γ·L1 + L1[1] + γ·L2[1] s.L2 = Math.FusedMultiplyAdd(g, prevL2, Math.FusedMultiplyAdd(-g, s.L1, prevL1)); // Stage 3: −γ·L2 + L2[1] + γ·L3[1] s.L3 = Math.FusedMultiplyAdd(g, prevL3, Math.FusedMultiplyAdd(-g, s.L2, prevL2)); // RSI-style: sum up/down stage differences double l0 = s.L0; double l1 = s.L1; double l2 = s.L2; double l3 = s.L3; double cu = (l0 > l1 ? l0 - l1 : 0.0) + (l1 > l2 ? l1 - l2 : 0.0) + (l2 > l3 ? l2 - l3 : 0.0); double cd = (l0 < l1 ? l1 - l0 : 0.0) + (l1 < l2 ? l2 - l1 : 0.0) + (l2 < l3 ? l3 - l2 : 0.0); double total = cu + cd; double lrsi = total != 0.0 ? cu / total : 0.5; _s = s; Last = new TValue(input.Time, lrsi); PubEvent(Last, isNew); return Last; } /// /// Batch-computes LRSI over a TSeries source. /// public static TSeries Calculate(TSeries source, double gamma = 0.5) { var lrsi = new Lrsi(gamma); int len = source.Count; var t = new List(len); var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); for (int i = 0; i < len; i++) { vSpan[i] = lrsi.Update(source[i], isNew: true).Value; tSpan[i] = source.Times[i]; } return new TSeries(t, v); } /// /// Batch static: span → span. Uses StackallocThreshold pattern (§2.6). /// No internal buffers needed beyond scalar state — no heap allocation for any input size. /// /// Input price span /// Output LRSI span (must match source length) /// Laguerre damping factor in [0.0, 1.0] public static void Calculate(ReadOnlySpan source, Span output, double gamma = 0.5) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (gamma < 0.0 || gamma > 1.0) { throw new ArgumentException("gamma must be in [0.0, 1.0]", nameof(gamma)); } int len = source.Length; if (len == 0) { return; } double g = gamma; double omg = 1.0 - gamma; double l0 = 0.0, l1 = 0.0, l2 = 0.0, l3 = 0.0; double lastValid = 0.5; for (int i = 0; i < len; i++) { double val = source[i]; if (!double.IsFinite(val)) { val = lastValid; } else { lastValid = val; } double pL0 = l0; double pL1 = l1; double pL2 = l2; double pL3 = l3; l0 = Math.FusedMultiplyAdd(g, pL0, omg * val); l1 = Math.FusedMultiplyAdd(g, pL1, Math.FusedMultiplyAdd(-g, l0, pL0)); l2 = Math.FusedMultiplyAdd(g, pL2, Math.FusedMultiplyAdd(-g, l1, pL1)); l3 = Math.FusedMultiplyAdd(g, pL3, Math.FusedMultiplyAdd(-g, l2, pL2)); double cu = (l0 > l1 ? l0 - l1 : 0.0) + (l1 > l2 ? l1 - l2 : 0.0) + (l2 > l3 ? l2 - l3 : 0.0); double cd = (l0 < l1 ? l1 - l0 : 0.0) + (l1 < l2 ? l2 - l1 : 0.0) + (l2 < l3 ? l3 - l2 : 0.0); double total = cu + cd; output[i] = total != 0.0 ? cu / total : 0.5; } } /// Gamma damping factor used by this instance. public double Gamma => _gamma; }