using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// RRSI: Rocket RSI (Ehlers) /// /// /// Combines a Super Smoother–filtered RSI with the Fisher Transform /// to produce a zero-mean Gaussian-distributed oscillator with sharp /// turning-point signals. /// /// Pipeline: /// /// Half-cycle momentum: Mom = Close − Close[rsiLength−1] /// Super Smoother (2-pole Butterworth IIR) on (Mom + Mom[1])/2 /// Ehlers RSI: RSI = (CU − CD)/(CU + CD) over rsiLength /// bars of filtered momentum differences (result already in [−1, 1]) /// Fisher Transform: RocketRSI = arctanh(clamp(RSI, ±0.999)) /// /// /// Reference: John F. Ehlers, "Rocket RSI", TASC May 2018. /// [SkipLocalsInit] public sealed class Rrsi : AbstractBase { private readonly int _smoothLength; private readonly int _rsiLength; // Super Smoother coefficients (computed once) private readonly double _c1, _c2, _c3; // Close history for momentum lookback private readonly RingBuffer _closeBuf; // Filter history for RSI accumulation private readonly RingBuffer _filtBuf; [StructLayout(LayoutKind.Auto)] private record struct State( double Mom, double MomPrev, double Filt, double FiltPrev, double LastValid, int Count); private State _s; private State _ps; /// public override bool IsHot => _s.Count >= WarmupPeriod; /// Smooth filter length. public int SmoothLength => _smoothLength; /// RSI accumulation length. public int RsiLength => _rsiLength; /// /// Creates a Rocket RSI indicator. /// /// Super Smoother period (must be > 0, default 10). /// RSI accumulation period (must be > 0, default 10). public Rrsi(int smoothLength = 10, int rsiLength = 10) { if (smoothLength <= 0) { throw new ArgumentException("Smooth length must be greater than 0", nameof(smoothLength)); } if (rsiLength <= 0) { throw new ArgumentException("RSI length must be greater than 0", nameof(rsiLength)); } _smoothLength = smoothLength; _rsiLength = rsiLength; // Super Smoother coefficients (Ehlers 2-pole Butterworth) double a1 = Math.Exp(-1.414 * Math.PI / smoothLength); double b1 = 2.0 * a1 * Math.Cos(1.414 * Math.PI / smoothLength); _c2 = b1; _c3 = -(a1 * a1); _c1 = 1.0 - _c2 - _c3; _closeBuf = new RingBuffer(rsiLength); _filtBuf = new RingBuffer(rsiLength + 1); Name = $"Rrsi({smoothLength},{rsiLength})"; WarmupPeriod = smoothLength + rsiLength; } /// /// Creates a Rocket RSI with a source publisher. /// public Rrsi(ITValuePublisher source, int smoothLength = 10, int rsiLength = 10) : this(smoothLength, rsiLength) { source.Pub += Handle; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { double value = input.Value; // Sanitize NaN/Inf if (!double.IsFinite(value)) { value = double.IsFinite(_s.LastValid) ? _s.LastValid : 0.0; } else { _s.LastValid = value; } if (isNew) { _ps = _s; _closeBuf.Add(value); _s.Count++; } else { _s = _ps; _closeBuf.UpdateNewest(value); } // Step 1: Half-cycle momentum double mom; if (_closeBuf.Count >= _rsiLength) { // Close - Close[rsiLength - 1] // _closeBuf[0] is oldest, _closeBuf[Count-1] is newest // Close[rsiLength-1] ago = _closeBuf[_closeBuf.Count - _rsiLength] mom = value - _closeBuf[_closeBuf.Count - _rsiLength]; } else { mom = 0.0; } // Step 2: Super Smoother Filter on (Mom + MomPrev) / 2 double filt; if (_s.Count <= 2) { // Not enough history for IIR — pass through filt = mom; } else { filt = (_c1 * (mom + _s.Mom) * 0.5) + (_c2 * _s.Filt) + (_c3 * _s.FiltPrev); } // Update state for next bar _s.FiltPrev = _s.Filt; _s.Filt = filt; _s.MomPrev = _s.Mom; _s.Mom = mom; // Step 3: Store Filt for RSI accumulation if (isNew) { _filtBuf.Add(filt); } else { _filtBuf.UpdateNewest(filt); } // Step 4: Ehlers RSI — accumulate CU/CD over rsiLength Filt differences double cu = 0.0; double cd = 0.0; int filtCount = _filtBuf.Count; int lookback = Math.Min(_rsiLength, filtCount - 1); for (int i = 0; i < lookback; i++) { // Filt[i] and Filt[i+1] in Ehlers notation (0 = newest) // In our buffer: newest = filtCount-1, so Filt[i] = _filtBuf[filtCount - 1 - i] double filtNewer = _filtBuf[filtCount - 1 - i]; double filtOlder = _filtBuf[filtCount - 2 - i]; double diff = filtNewer - filtOlder; if (diff > 0.0) { cu += diff; } else if (diff < 0.0) { cd -= diff; // accumulate absolute value } } // Step 5: Compute RSI in [-1, 1] range double myRsi; double cuCd = cu + cd; if (cuCd > 1e-10) { myRsi = (cu - cd) / cuCd; } else { myRsi = 0.0; } // Clamp to avoid arctanh singularity if (myRsi > 0.999) { myRsi = 0.999; } else if (myRsi < -0.999) { myRsi = -0.999; } // Step 6: Fisher Transform (arctanh) double rocketRsi = 0.5 * Math.Log((1.0 + myRsi) / (1.0 - myRsi)); Last = new TValue(input.Time, rocketRsi); PubEvent(Last, isNew); return Last; } public override TSeries Update(TSeries source) { int len = source.Count; if (len == 0) { return []; } 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); Batch(source.Values, vSpan, _smoothLength, _rsiLength); source.Times.CopyTo(tSpan); // Replay for streaming state sync Reset(); for (int i = 0; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i])); } return new TSeries(t, v); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { foreach (double value in source) { Update(new TValue(DateTime.MinValue, value)); } } /// Batch-process a series. public static TSeries Batch(TSeries source, int smoothLength = 10, int rsiLength = 10) { var ind = new Rrsi(smoothLength, rsiLength); return ind.Update(source); } /// Batch-process span data. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int smoothLength = 10, int rsiLength = 10) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (smoothLength <= 0) { throw new ArgumentException("Smooth length must be greater than 0", nameof(smoothLength)); } if (rsiLength <= 0) { throw new ArgumentException("RSI length must be greater than 0", nameof(rsiLength)); } int len = source.Length; if (len == 0) { return; } // Super Smoother coefficients double a1 = Math.Exp(-1.414 * Math.PI / smoothLength); double b1 = 2.0 * a1 * Math.Cos(1.414 * Math.PI / smoothLength); double c2 = b1; double c3 = -(a1 * a1); double c1 = 1.0 - c2 - c3; // Allocate momentum and filter arrays double[] momRented = ArrayPool.Shared.Rent(len); double[] filtRented = ArrayPool.Shared.Rent(len); Span momArr = momRented.AsSpan(0, len); Span filtArr = filtRented.AsSpan(0, len); try { // Pass 1: Momentum for (int i = 0; i < len; i++) { momArr[i] = (i >= rsiLength - 1) ? source[i] - source[i - rsiLength + 1] : 0.0; } // Pass 2: Super Smoother filtArr[0] = momArr[0]; if (len > 1) { filtArr[1] = (c1 * (momArr[1] + momArr[0]) * 0.5) + (c2 * filtArr[0]); } for (int i = 2; i < len; i++) { filtArr[i] = (c1 * (momArr[i] + momArr[i - 1]) * 0.5) + (c2 * filtArr[i - 1]) + (c3 * filtArr[i - 2]); } // Pass 3: RSI + Fisher for (int i = 0; i < len; i++) { double cu = 0.0; double cd = 0.0; int lookback = Math.Min(rsiLength, i); for (int j = 0; j < lookback; j++) { double diff = filtArr[i - j] - filtArr[i - j - 1]; if (diff > 0.0) { cu += diff; } else if (diff < 0.0) { cd -= diff; } } double cuCd = cu + cd; double myRsi = (cuCd > 1e-10) ? (cu - cd) / cuCd : 0.0; // Clamp if (myRsi > 0.999) { myRsi = 0.999; } else if (myRsi < -0.999) { myRsi = -0.999; } output[i] = 0.5 * Math.Log((1.0 + myRsi) / (1.0 - myRsi)); } } finally { ArrayPool.Shared.Return(momRented); ArrayPool.Shared.Return(filtRented); } } /// Calculate and return both results and indicator. public static (TSeries Results, Rrsi Indicator) Calculate(TSeries source, int smoothLength = 10, int rsiLength = 10) { var ind = new Rrsi(smoothLength, rsiLength); return (ind.Update(source), ind); } public override void Reset() { _closeBuf.Clear(); _filtBuf.Clear(); _s = default; _ps = default; Last = default; } }