// 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;
}