mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-27 17:27:43 +00:00
feat: add RRSI (Rocket RSI) — Ehlers TASC May 2018
Algorithm: SuperSmoother-filtered momentum → Ehlers RSI → Fisher Transform - 2-pole Butterworth IIR pre-filter removes noise - Ehlers RSI (raw summation, not Wilder) outputs [-1,1] - arctanh produces Gaussian-distributed zero-mean oscillator Files: Rrsi.cs, Rrsi.Quantower.cs, Rrsi.md, 31+7 tests Integration: sidebar, indices, Python bridge (Exports, _bridge, oscillators, SPEC) Build: 0 warnings, 0 errors | Tests: 15,963 passed, 0 failed
This commit is contained in:
@@ -161,6 +161,7 @@
|
|||||||
* [REFLEX - Ehlers Reflex](/lib/oscillators/reflex/Reflex.md)
|
* [REFLEX - Ehlers Reflex](/lib/oscillators/reflex/Reflex.md)
|
||||||
* [REVERSEEMA - Ehlers Reverse EMA](/lib/oscillators/reverseema/ReverseEma.md)
|
* [REVERSEEMA - Ehlers Reverse EMA](/lib/oscillators/reverseema/ReverseEma.md)
|
||||||
* [RVGI - Relative Vigor Index](/lib/oscillators/rvgi/Rvgi.md)
|
* [RVGI - Relative Vigor Index](/lib/oscillators/rvgi/Rvgi.md)
|
||||||
|
* [RRSI - Rocket RSI (Ehlers)](/lib/oscillators/rrsi/Rrsi.md)
|
||||||
* [SMI - Stochastic Momentum Index](/lib/oscillators/smi/Smi.md)
|
* [SMI - Stochastic Momentum Index](/lib/oscillators/smi/Smi.md)
|
||||||
* [SQUEEZE - Squeeze Momentum](/lib/oscillators/squeeze/Squeeze.md)
|
* [SQUEEZE - Squeeze Momentum](/lib/oscillators/squeeze/Squeeze.md)
|
||||||
* [SQUEEZE_PRO - Squeeze Pro](/lib/oscillators/squeeze_pro/squeeze_pro.md)
|
* [SQUEEZE_PRO - Squeeze Pro](/lib/oscillators/squeeze_pro/squeeze_pro.md)
|
||||||
|
|||||||
@@ -309,6 +309,7 @@
|
|||||||
| [RV](volatility/rv/Rv.md) | Realized Volatility | Volatility |
|
| [RV](volatility/rv/Rv.md) | Realized Volatility | Volatility |
|
||||||
| [RVI](volatility/rvi/Rvi.md) | Relative Volatility Index | Volatility |
|
| [RVI](volatility/rvi/Rvi.md) | Relative Volatility Index | Volatility |
|
||||||
| [RVGI](oscillators/rvgi/Rvgi.md) | Relative Vigor Index | Oscillators |
|
| [RVGI](oscillators/rvgi/Rvgi.md) | Relative Vigor Index | Oscillators |
|
||||||
|
| [RRSI](oscillators/rrsi/Rrsi.md) | Rocket RSI (Ehlers) | Oscillators |
|
||||||
| [RWMA](trends_FIR/rwma/Rwma.md) | Range Weighted MA | Trends (FIR) |
|
| [RWMA](trends_FIR/rwma/Rwma.md) | Range Weighted MA | Trends (FIR) |
|
||||||
| [SAK](filters/sak/Sak.md) | Ehlers Swiss Army Knife | Filters |
|
| [SAK](filters/sak/Sak.md) | Ehlers Swiss Army Knife | Filters |
|
||||||
| [SAM](momentum/sam/Sam.md) | Smoothed Adaptive Momentum | Momentum |
|
| [SAM](momentum/sam/Sam.md) | Smoothed Adaptive Momentum | Momentum |
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ Oscillators fluctuate above and below a centerline or within bounded ranges. Use
|
|||||||
| [REFLEX](reflex/Reflex.md) | Ehlers Reflex | Ehlers zero-centered reversal oscillator using super smoother with normalized sum-of-differences. |
|
| [REFLEX](reflex/Reflex.md) | Ehlers Reflex | Ehlers zero-centered reversal oscillator using super smoother with normalized sum-of-differences. |
|
||||||
| [REVERSEEMA](reverseema/ReverseEma.md) | Ehlers Reverse EMA | 8-stage cascaded Z-transform inversion subtracts EMA lag, producing zero-centered oscillator signal. |
|
| [REVERSEEMA](reverseema/ReverseEma.md) | Ehlers Reverse EMA | 8-stage cascaded Z-transform inversion subtracts EMA lag, producing zero-centered oscillator signal. |
|
||||||
| [RVGI](rvgi/Rvgi.md) | Relative Vigor Index | Open-close vs high-low ratio with SMA smoothing. Measures conviction. |
|
| [RVGI](rvgi/Rvgi.md) | Relative Vigor Index | Open-close vs high-low ratio with SMA smoothing. Measures conviction. |
|
||||||
|
| [RRSI](rrsi/Rrsi.md) | Rocket RSI | Fisher Transform of Super Smoother–filtered RSI. Sharp cyclic reversal signals. |
|
||||||
| [SMI](smi/Smi.md) | Stochastic Momentum Index | Distance from range midpoint. More sensitive than classic Stochastic. |
|
| [SMI](smi/Smi.md) | Stochastic Momentum Index | Distance from range midpoint. More sensitive than classic Stochastic. |
|
||||||
| [SQUEEZE](squeeze/Squeeze.md) | Squeeze | BB width < KC width indicates consolidation. Breakout imminent. |
|
| [SQUEEZE](squeeze/Squeeze.md) | Squeeze | BB width < KC width indicates consolidation. Breakout imminent. |
|
||||||
| [SQUEEZE_PRO](squeeze_pro/squeeze_pro.md) | Squeeze Pro | Multi-level BB vs KC squeeze (wide/normal/narrow) with MOM-smoothed momentum. LazyBear. |
|
| [SQUEEZE_PRO](squeeze_pro/squeeze_pro.md) | Squeeze Pro | Multi-level BB vs KC squeeze (wide/normal/narrow) with MOM-smoothed momentum. LazyBear. |
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using TradingPlatform.BusinessLayer;
|
||||||
|
|
||||||
|
namespace QuanTAlib;
|
||||||
|
|
||||||
|
[SkipLocalsInit]
|
||||||
|
public sealed class RrsiIndicator : Indicator, IWatchlistIndicator
|
||||||
|
{
|
||||||
|
[InputParameter("Smooth Length", sortIndex: 1, 1, 500, 1, 0)]
|
||||||
|
public int SmoothLength { get; set; } = 10;
|
||||||
|
|
||||||
|
[InputParameter("RSI Length", sortIndex: 2, 1, 500, 1, 0)]
|
||||||
|
public int RsiLength { get; set; } = 10;
|
||||||
|
|
||||||
|
[IndicatorExtensions.DataSourceInput(sortIndex: 3)]
|
||||||
|
public SourceType Source { get; set; } = SourceType.Close;
|
||||||
|
|
||||||
|
[InputParameter("Show cold values", sortIndex: 21)]
|
||||||
|
public bool ShowColdValues { get; set; } = true;
|
||||||
|
|
||||||
|
private Rrsi _rrsi = null!;
|
||||||
|
private readonly LineSeries _rrsiLine;
|
||||||
|
|
||||||
|
public static int MinHistoryDepths => 0;
|
||||||
|
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||||
|
|
||||||
|
public override string ShortName => $"RRSI ({SmoothLength},{RsiLength})";
|
||||||
|
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/rrsi/Rrsi.Quantower.cs";
|
||||||
|
|
||||||
|
public RrsiIndicator()
|
||||||
|
{
|
||||||
|
OnBackGround = true;
|
||||||
|
SeparateWindow = true;
|
||||||
|
Name = "RRSI - Rocket RSI (Ehlers)";
|
||||||
|
Description = "Fisher Transform of Super Smoother–filtered RSI for cyclic reversal signals";
|
||||||
|
|
||||||
|
_rrsiLine = new LineSeries("RocketRSI", Color.DodgerBlue, 2, LineStyle.Solid);
|
||||||
|
AddLineSeries(_rrsiLine);
|
||||||
|
|
||||||
|
AddLineLevel(0, "Zero", Color.Gray, 1, LineStyle.Dash);
|
||||||
|
AddLineLevel(2, "Overbought", Color.Red, 1, LineStyle.Dash);
|
||||||
|
AddLineLevel(-2, "Oversold", Color.Green, 1, LineStyle.Dash);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
_rrsi = new Rrsi(SmoothLength, RsiLength);
|
||||||
|
base.OnInit();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected override void OnUpdate(UpdateArgs args)
|
||||||
|
{
|
||||||
|
var priceSelector = Source.GetPriceSelector();
|
||||||
|
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||||
|
double price = priceSelector(item);
|
||||||
|
|
||||||
|
TValue input = new(item.TimeLeft, price);
|
||||||
|
TValue result = _rrsi.Update(input, args.IsNewBar());
|
||||||
|
|
||||||
|
if (!_rrsi.IsHot && !ShowColdValues)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rrsiLine.SetValue(result.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace QuanTAlib;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RRSI: Rocket RSI (Ehlers)
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Combines a Super Smoother–filtered RSI with the Fisher Transform
|
||||||
|
/// to produce a zero-mean Gaussian-distributed oscillator with sharp
|
||||||
|
/// turning-point signals.
|
||||||
|
///
|
||||||
|
/// Pipeline:
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>Half-cycle momentum: <c>Mom = Close − Close[rsiLength−1]</c></item>
|
||||||
|
/// <item>Super Smoother (2-pole Butterworth IIR) on <c>(Mom + Mom[1])/2</c></item>
|
||||||
|
/// <item>Ehlers RSI: <c>RSI = (CU − CD)/(CU + CD)</c> over <c>rsiLength</c>
|
||||||
|
/// bars of filtered momentum differences (result already in [−1, 1])</item>
|
||||||
|
/// <item>Fisher Transform: <c>RocketRSI = arctanh(clamp(RSI, ±0.999))</c></item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// Reference: John F. Ehlers, "Rocket RSI", TASC May 2018.
|
||||||
|
/// </remarks>
|
||||||
|
[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;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool IsHot => _s.Count >= WarmupPeriod;
|
||||||
|
|
||||||
|
/// <summary>Smooth filter length.</summary>
|
||||||
|
public int SmoothLength => _smoothLength;
|
||||||
|
|
||||||
|
/// <summary>RSI accumulation length.</summary>
|
||||||
|
public int RsiLength => _rsiLength;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Rocket RSI indicator.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="smoothLength">Super Smoother period (must be > 0, default 10).</param>
|
||||||
|
/// <param name="rsiLength">RSI accumulation period (must be > 0, default 10).</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Rocket RSI with a source publisher.
|
||||||
|
/// </summary>
|
||||||
|
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<long>(len);
|
||||||
|
var v = new List<double>(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<double> source, TimeSpan? step = null)
|
||||||
|
{
|
||||||
|
foreach (double value in source)
|
||||||
|
{
|
||||||
|
Update(new TValue(DateTime.MinValue, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Batch-process a series.</summary>
|
||||||
|
public static TSeries Batch(TSeries source, int smoothLength = 10, int rsiLength = 10)
|
||||||
|
{
|
||||||
|
var ind = new Rrsi(smoothLength, rsiLength);
|
||||||
|
return ind.Update(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Batch-process span data.</summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static void Batch(ReadOnlySpan<double> source, Span<double> 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<double>.Shared.Rent(len);
|
||||||
|
double[] filtRented = ArrayPool<double>.Shared.Rent(len);
|
||||||
|
Span<double> momArr = momRented.AsSpan(0, len);
|
||||||
|
Span<double> 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<double>.Shared.Return(momRented);
|
||||||
|
ArrayPool<double>.Shared.Return(filtRented);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Calculate and return both results and indicator.</summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Rocket RSI (RRSI)
|
||||||
|
|
||||||
|
**Category:** Oscillators
|
||||||
|
**Type:** Unbounded zero-mean oscillator
|
||||||
|
**Author:** John F. Ehlers, TASC May 2018
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Rocket RSI combines Ehlers' Super Smoother filter with a custom RSI calculation
|
||||||
|
and applies the Fisher Transform to produce a Gaussian-distributed oscillator
|
||||||
|
with sharp turning-point signals ideal for cyclic reversal detection.
|
||||||
|
|
||||||
|
## Mathematical Foundation
|
||||||
|
|
||||||
|
### Step 1: Half-Cycle Momentum
|
||||||
|
$$\text{Mom}_i = \text{Close}_i - \text{Close}_{i - (\text{rsiLength} - 1)}$$
|
||||||
|
|
||||||
|
### Step 2: Super Smoother Filter (2-Pole Butterworth)
|
||||||
|
Coefficients (computed once):
|
||||||
|
$$a_1 = e^{-1.414\pi / \text{smoothLength}}, \quad b_1 = 2 a_1 \cos(1.414\pi / \text{smoothLength})$$
|
||||||
|
$$c_2 = b_1, \quad c_3 = -a_1^2, \quad c_1 = 1 - c_2 - c_3$$
|
||||||
|
|
||||||
|
Filter:
|
||||||
|
$$\text{Filt}_i = c_1 \cdot \frac{\text{Mom}_i + \text{Mom}_{i-1}}{2} + c_2 \cdot \text{Filt}_{i-1} + c_3 \cdot \text{Filt}_{i-2}$$
|
||||||
|
|
||||||
|
### Step 3: Ehlers RSI (Normalized to ±1)
|
||||||
|
Over the last `rsiLength` bars of filter differences:
|
||||||
|
$$CU = \sum_{j=0}^{n-1} \max(\text{Filt}_{i-j} - \text{Filt}_{i-j-1},\ 0)$$
|
||||||
|
$$CD = \sum_{j=0}^{n-1} \max(\text{Filt}_{i-j-1} - \text{Filt}_{i-j},\ 0)$$
|
||||||
|
$$\text{RSI} = \frac{CU - CD}{CU + CD} \in [-1, 1]$$
|
||||||
|
|
||||||
|
### Step 4: Fisher Transform
|
||||||
|
$$\text{RocketRSI} = \frac{1}{2} \ln\left(\frac{1 + \text{clamp(RSI, \pm0.999)}}{1 - \text{clamp(RSI, \pm0.999)}}\right) = \text{arctanh}(\text{RSI})$$
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Default | Range | Description |
|
||||||
|
|-----------|---------|-------|-------------|
|
||||||
|
| smoothLength | 10 | > 0 | Super Smoother filter period |
|
||||||
|
| rsiLength | 10 | > 0 | RSI accumulation window |
|
||||||
|
|
||||||
|
## Interpretation
|
||||||
|
|
||||||
|
- **Values > +2**: Overbought — potential sell signal
|
||||||
|
- **Values < −2**: Oversold — potential buy signal
|
||||||
|
- **Zero crossings**: Momentum shift
|
||||||
|
- **Peaks/troughs**: Cyclic turning points
|
||||||
|
|
||||||
|
The Fisher Transform produces a nearly Gaussian distribution, meaning:
|
||||||
|
- ~68% of values fall within ±1 standard deviation
|
||||||
|
- Values beyond ±2 are statistically extreme (~5%)
|
||||||
|
- Values beyond ±3 are very rare (~0.3%)
|
||||||
|
|
||||||
|
## Key Differences from Standard RSI
|
||||||
|
|
||||||
|
1. **Super Smoother pre-filter** removes high-frequency noise
|
||||||
|
2. **Ehlers RSI** uses raw summation (not Wilder's exponential smoothing)
|
||||||
|
3. **RSI output is ±1** (not 0–100), already suited for Fisher Transform
|
||||||
|
4. **Fisher Transform** converts to Gaussian distribution with sharp reversals
|
||||||
|
|
||||||
|
## Warmup Period
|
||||||
|
|
||||||
|
`smoothLength + rsiLength` bars are needed for the IIR filter to stabilize
|
||||||
|
and the RSI accumulation window to fill.
|
||||||
|
|
||||||
|
## C# Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Streaming
|
||||||
|
var rrsi = new Rrsi(smoothLength: 10, rsiLength: 10);
|
||||||
|
foreach (var bar in series)
|
||||||
|
{
|
||||||
|
TValue result = rrsi.Update(bar);
|
||||||
|
// result.Value is the Rocket RSI
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch
|
||||||
|
TSeries results = Rrsi.Batch(series);
|
||||||
|
|
||||||
|
// Span
|
||||||
|
Rrsi.Batch(source, output, smoothLength: 10, rsiLength: 10);
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- Ehlers, J. F. (2018). "Rocket RSI." *Technical Analysis of Stocks & Commodities*, May 2018.
|
||||||
|
- Ehlers, J. F. (2004). *Cybernetic Analysis for Stocks and Futures*. Wiley.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using TradingPlatform.BusinessLayer;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace QuanTAlib.Tests;
|
||||||
|
|
||||||
|
public sealed class RrsiIndicatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_DefaultParams()
|
||||||
|
{
|
||||||
|
var indicator = new RrsiIndicator();
|
||||||
|
Assert.Equal(10, indicator.SmoothLength);
|
||||||
|
Assert.Equal(10, indicator.RsiLength);
|
||||||
|
Assert.True(indicator.ShowColdValues);
|
||||||
|
Assert.Contains("RRSI", indicator.Name, StringComparison.Ordinal);
|
||||||
|
Assert.True(indicator.SeparateWindow);
|
||||||
|
Assert.True(indicator.OnBackGround);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_CustomParams()
|
||||||
|
{
|
||||||
|
var indicator = new RrsiIndicator { SmoothLength = 8, RsiLength = 14 };
|
||||||
|
Assert.Equal(8, indicator.SmoothLength);
|
||||||
|
Assert.Equal(14, indicator.RsiLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_ShortName_Format()
|
||||||
|
{
|
||||||
|
var indicator = new RrsiIndicator { SmoothLength = 8, RsiLength = 14 };
|
||||||
|
Assert.Equal("RRSI (8,14)", indicator.ShortName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_SourceCodeLink_Valid()
|
||||||
|
{
|
||||||
|
var indicator = new RrsiIndicator();
|
||||||
|
Assert.Contains("Rrsi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_HasLineSeries()
|
||||||
|
{
|
||||||
|
var indicator = new RrsiIndicator();
|
||||||
|
Assert.Single(indicator.LinesSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_ImplementsIWatchlist()
|
||||||
|
{
|
||||||
|
var indicator = new RrsiIndicator();
|
||||||
|
Assert.IsAssignableFrom<IWatchlistIndicator>(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Indicator_MinHistoryDepths_IsZero()
|
||||||
|
{
|
||||||
|
Assert.Equal(0, RrsiIndicator.MinHistoryDepths);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,390 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace QuanTAlib.Tests;
|
||||||
|
|
||||||
|
public sealed class RrsiTests
|
||||||
|
{
|
||||||
|
private static TSeries GenerateSeries(int count, int seed = 42)
|
||||||
|
{
|
||||||
|
var rng = new Random(seed);
|
||||||
|
var series = new TSeries();
|
||||||
|
double price = 100.0;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
price += (rng.NextDouble() - 0.5) * 2.0;
|
||||||
|
series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), price));
|
||||||
|
}
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === A) Constructor ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_Default_ValidState()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
Assert.Equal(10, ind.SmoothLength);
|
||||||
|
Assert.Equal(10, ind.RsiLength);
|
||||||
|
Assert.False(ind.IsHot);
|
||||||
|
Assert.Contains("Rrsi(", ind.Name, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_CustomParams_ValidState()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi(smoothLength: 8, rsiLength: 14);
|
||||||
|
Assert.Equal(8, ind.SmoothLength);
|
||||||
|
Assert.Equal(14, ind.RsiLength);
|
||||||
|
Assert.Contains("Rrsi(8,14)", ind.Name, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0, 10)]
|
||||||
|
[InlineData(-1, 10)]
|
||||||
|
[InlineData(10, 0)]
|
||||||
|
[InlineData(10, -1)]
|
||||||
|
public void Constructor_InvalidParams_Throws(int smooth, int rsi)
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => new Rrsi(smooth, rsi));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === B) Basic calculation ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_SingleValue_ReturnsValue()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
var result = ind.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||||
|
Assert.True(double.IsFinite(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_EnoughBars_BecomesHot()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi(smoothLength: 5, rsiLength: 5);
|
||||||
|
var series = GenerateSeries(30);
|
||||||
|
foreach (var tv in series)
|
||||||
|
{
|
||||||
|
ind.Update(tv);
|
||||||
|
}
|
||||||
|
Assert.True(ind.IsHot);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Update_NotEnoughBars_NotHot()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi(smoothLength: 10, rsiLength: 10);
|
||||||
|
var series = GenerateSeries(5);
|
||||||
|
foreach (var tv in series)
|
||||||
|
{
|
||||||
|
ind.Update(tv);
|
||||||
|
}
|
||||||
|
Assert.False(ind.IsHot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === C) Output range ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Output_IsFinite_ForAll()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
var series = GenerateSeries(200);
|
||||||
|
int bar = 0;
|
||||||
|
foreach (var tv in series)
|
||||||
|
{
|
||||||
|
var result = ind.Update(tv);
|
||||||
|
Assert.True(double.IsFinite(result.Value),
|
||||||
|
$"Non-finite at bar {bar}: {result.Value}");
|
||||||
|
bar++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Output_OscillatesAroundZero()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
var series = GenerateSeries(500);
|
||||||
|
bool hasPositive = false;
|
||||||
|
bool hasNegative = false;
|
||||||
|
foreach (var tv in series)
|
||||||
|
{
|
||||||
|
double val = ind.Update(tv).Value;
|
||||||
|
if (val > 0.01) { hasPositive = true; }
|
||||||
|
if (val < -0.01) { hasNegative = true; }
|
||||||
|
}
|
||||||
|
Assert.True(hasPositive, "Should have positive values");
|
||||||
|
Assert.True(hasNegative, "Should have negative values");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Output_FlatPrice_NearZero()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 50.0));
|
||||||
|
}
|
||||||
|
Assert.True(Math.Abs(ind.Last.Value) < 0.01,
|
||||||
|
$"Flat price should yield ~0, got {ind.Last.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === D) Streaming vs Batch ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StreamingMatchesBatch_TSeries()
|
||||||
|
{
|
||||||
|
var source = GenerateSeries(100);
|
||||||
|
var batchResult = Rrsi.Batch(source, 10, 10);
|
||||||
|
|
||||||
|
var streaming = new Rrsi(10, 10);
|
||||||
|
for (int i = 0; i < source.Count; i++)
|
||||||
|
{
|
||||||
|
streaming.Update(source[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare last values
|
||||||
|
Assert.Equal(batchResult[^1].Value, streaming.Last.Value, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SpanBatch_MatchesTSeriesBatch()
|
||||||
|
{
|
||||||
|
var source = GenerateSeries(100);
|
||||||
|
var batchResult = Rrsi.Batch(source, 8, 12);
|
||||||
|
|
||||||
|
double[] output = new double[source.Count];
|
||||||
|
Rrsi.Batch(source.Values, output, 8, 12);
|
||||||
|
|
||||||
|
for (int i = 0; i < source.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(batchResult[i].Value, output[i], 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === E) Bar correction ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BarCorrection_IsNew_False_DoesNotAdvance()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
var series = GenerateSeries(30);
|
||||||
|
|
||||||
|
// Feed first 20 bars normally
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
ind.Update(series[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bar 20: first tick
|
||||||
|
_ = ind.Update(series[20], isNew: true);
|
||||||
|
|
||||||
|
// Bar 20: correction ticks (isNew=false)
|
||||||
|
var result2 = ind.Update(new TValue(series[20].Time, series[20].Value + 0.5), isNew: false);
|
||||||
|
var result3 = ind.Update(new TValue(series[20].Time, series[20].Value + 0.1), isNew: false);
|
||||||
|
|
||||||
|
// Final tick should give a different result from first
|
||||||
|
// but indicator should not have advanced count
|
||||||
|
Assert.True(double.IsFinite(result2.Value));
|
||||||
|
Assert.True(double.IsFinite(result3.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BarCorrection_Consistency()
|
||||||
|
{
|
||||||
|
var source = GenerateSeries(50);
|
||||||
|
var ind1 = new Rrsi(8, 10);
|
||||||
|
var ind2 = new Rrsi(8, 10);
|
||||||
|
|
||||||
|
// ind1: clean feed
|
||||||
|
foreach (var tv in source)
|
||||||
|
{
|
||||||
|
ind1.Update(tv);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ind2: feed with corrections on every other bar
|
||||||
|
for (int i = 0; i < source.Count; i++)
|
||||||
|
{
|
||||||
|
ind2.Update(source[i], isNew: true);
|
||||||
|
if (i % 2 == 0)
|
||||||
|
{
|
||||||
|
// Correct back to original value
|
||||||
|
ind2.Update(new TValue(source[i].Time, source[i].Value + 1.0), isNew: false);
|
||||||
|
ind2.Update(source[i], isNew: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(ind1.Last.Value, ind2.Last.Value, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === F) Reset ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reset_ClearsState()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
var series = GenerateSeries(50);
|
||||||
|
foreach (var tv in series)
|
||||||
|
{
|
||||||
|
ind.Update(tv);
|
||||||
|
}
|
||||||
|
Assert.True(ind.IsHot);
|
||||||
|
|
||||||
|
ind.Reset();
|
||||||
|
Assert.False(ind.IsHot);
|
||||||
|
Assert.Equal(0.0, ind.Last.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reset_ReplayProducesSameResult()
|
||||||
|
{
|
||||||
|
var source = GenerateSeries(100);
|
||||||
|
var ind = new Rrsi();
|
||||||
|
|
||||||
|
foreach (var tv in source) { ind.Update(tv); }
|
||||||
|
double firstRun = ind.Last.Value;
|
||||||
|
|
||||||
|
ind.Reset();
|
||||||
|
foreach (var tv in source) { ind.Update(tv); }
|
||||||
|
double secondRun = ind.Last.Value;
|
||||||
|
|
||||||
|
Assert.Equal(firstRun, secondRun, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === G) Dispose ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispose_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
var ex = Record.Exception(() => ind.Dispose());
|
||||||
|
Assert.Null(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === H) Edge cases ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NaN_Input_Handled()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
for (int i = 0; i < 30; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 50.0 + i));
|
||||||
|
}
|
||||||
|
// Feed NaN
|
||||||
|
var result = ind.Update(new TValue(DateTime.UtcNow.AddMinutes(30), double.NaN));
|
||||||
|
Assert.True(double.IsFinite(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Infinity_Input_Handled()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi();
|
||||||
|
for (int i = 0; i < 30; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 50.0 + i));
|
||||||
|
}
|
||||||
|
var result = ind.Update(new TValue(DateTime.UtcNow.AddMinutes(30), double.PositiveInfinity));
|
||||||
|
Assert.True(double.IsFinite(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Batch_EmptySeries_ReturnsEmpty()
|
||||||
|
{
|
||||||
|
var series = new TSeries();
|
||||||
|
var result = Rrsi.Batch(series);
|
||||||
|
Assert.Empty(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Batch_Span_LengthMismatch_Throws()
|
||||||
|
{
|
||||||
|
double[] src = new double[10];
|
||||||
|
double[] dst = new double[5];
|
||||||
|
Assert.Throws<ArgumentException>(() => Rrsi.Batch(src, dst));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Batch_Span_InvalidSmoothLength_Throws()
|
||||||
|
{
|
||||||
|
double[] src = new double[10];
|
||||||
|
double[] dst = new double[10];
|
||||||
|
Assert.Throws<ArgumentException>(() => Rrsi.Batch(src, dst, smoothLength: 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Batch_Span_InvalidRsiLength_Throws()
|
||||||
|
{
|
||||||
|
double[] src = new double[10];
|
||||||
|
double[] dst = new double[10];
|
||||||
|
Assert.Throws<ArgumentException>(() => Rrsi.Batch(src, dst, rsiLength: 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Batch_Span_Empty_NoException()
|
||||||
|
{
|
||||||
|
var ex = Record.Exception(() => Rrsi.Batch(ReadOnlySpan<double>.Empty, Span<double>.Empty));
|
||||||
|
Assert.Null(ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === I) Calculate factory ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Calculate_ReturnsResultsAndIndicator()
|
||||||
|
{
|
||||||
|
var source = GenerateSeries(50);
|
||||||
|
var (results, indicator) = Rrsi.Calculate(source, 10, 10);
|
||||||
|
Assert.Equal(source.Count, results.Count);
|
||||||
|
Assert.True(indicator.IsHot);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === J) Pub event ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PubEvent_FiresOnUpdate()
|
||||||
|
{
|
||||||
|
var source = new TSeries();
|
||||||
|
var ind = new Rrsi(source, 5, 5);
|
||||||
|
int count = 0;
|
||||||
|
ind.Pub += (object? sender, in TValueEventArgs e) => count++;
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
source.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 50.0 + i));
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(20, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === K) Trending input ===
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StrongUptrend_PositiveOutput()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi(smoothLength: 5, rsiLength: 5);
|
||||||
|
// Feed flat, then strong uptrend
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||||
|
}
|
||||||
|
for (int i = 20; i < 50; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 + (i - 20) * 2.0));
|
||||||
|
}
|
||||||
|
Assert.True(ind.Last.Value > 0, $"Strong uptrend should be positive, got {ind.Last.Value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StrongDowntrend_NegativeOutput()
|
||||||
|
{
|
||||||
|
var ind = new Rrsi(smoothLength: 5, rsiLength: 5);
|
||||||
|
for (int i = 0; i < 20; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0));
|
||||||
|
}
|
||||||
|
for (int i = 20; i < 50; i++)
|
||||||
|
{
|
||||||
|
ind.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0 - (i - 20) * 2.0));
|
||||||
|
}
|
||||||
|
Assert.True(ind.Last.Value < 0, $"Strong downtrend should be negative, got {ind.Last.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -671,6 +671,7 @@ packages = ["quantalib"]
|
|||||||
| reflex | `Reflex` | A | period |
|
| reflex | `Reflex` | A | period |
|
||||||
| reverseema | `ReverseEma` | A | period |
|
| reverseema | `ReverseEma` | A | period |
|
||||||
| rvgi | `Rvgi` | C (OHLC) | period |
|
| rvgi | `Rvgi` | C (OHLC) | period |
|
||||||
|
| rrsi | `Rrsi` | A | smoothLength, rsiLength |
|
||||||
| smi | `Smi` | I (HLC→2+) | period, smoothK, smoothD |
|
| smi | `Smi` | I (HLC→2+) | period, smoothK, smoothD |
|
||||||
| squeeze | `Squeeze` | I (HLC→multi) | bbPeriod, kcPeriod... |
|
| squeeze | `Squeeze` | I (HLC→multi) | bbPeriod, kcPeriod... |
|
||||||
| squeeze_pro | `SqueezePro` | I (HLC→multi) | period, bbMult, kcMultWide/Normal/Narrow |
|
| squeeze_pro | `SqueezePro` | I (HLC→multi) | period, bbMult, kcMultWide/Normal/Narrow |
|
||||||
|
|||||||
@@ -438,6 +438,7 @@ HAS_DPO = _bind("qtl_dpo", [_dp, _ci, _dp, _ci])
|
|||||||
HAS_TRIX = _bind("qtl_trix", [_dp, _ci, _dp, _ci])
|
HAS_TRIX = _bind("qtl_trix", [_dp, _ci, _dp, _ci])
|
||||||
HAS_INERTIA = _bind("qtl_inertia", [_dp, _ci, _dp, _ci])
|
HAS_INERTIA = _bind("qtl_inertia", [_dp, _ci, _dp, _ci])
|
||||||
HAS_RSX = _bind("qtl_rsx", [_dp, _ci, _dp, _ci])
|
HAS_RSX = _bind("qtl_rsx", [_dp, _ci, _dp, _ci])
|
||||||
|
HAS_RRSI = _bind("qtl_rrsi", [_dp, _ci, _dp, _ci, _ci])
|
||||||
HAS_ER = _bind("qtl_er", [_dp, _ci, _dp, _ci])
|
HAS_ER = _bind("qtl_er", [_dp, _ci, _dp, _ci])
|
||||||
HAS_CTI = _bind("qtl_cti", [_dp, _ci, _dp, _ci])
|
HAS_CTI = _bind("qtl_cti", [_dp, _ci, _dp, _ci])
|
||||||
HAS_REFLEX = _bind("qtl_reflex", [_dp, _ci, _dp, _ci])
|
HAS_REFLEX = _bind("qtl_reflex", [_dp, _ci, _dp, _ci])
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ __all__ = [
|
|||||||
"qqe",
|
"qqe",
|
||||||
"reverseema",
|
"reverseema",
|
||||||
"rvgi",
|
"rvgi",
|
||||||
|
"rrsi",
|
||||||
"smi",
|
"smi",
|
||||||
"squeeze",
|
"squeeze",
|
||||||
"stc",
|
"stc",
|
||||||
@@ -283,6 +284,17 @@ def rvgi(open: object, high: object, low: object, close: object, period: int = 1
|
|||||||
return _wrap_multi({"rvgiOutput": rvgiOutput, "signalOutput": signalOutput}, idx, "oscillators", offset)
|
return _wrap_multi({"rvgiOutput": rvgiOutput, "signalOutput": signalOutput}, idx, "oscillators", offset)
|
||||||
|
|
||||||
|
|
||||||
|
def rrsi(close: object, smoothLength: int = 10, rsiLength: int = 10, offset: int = 0, **kwargs) -> object:
|
||||||
|
"""Rocket RSI (Ehlers) — Fisher Transform of Super Smoother–filtered RSI."""
|
||||||
|
src = _to_np(close)
|
||||||
|
n = len(src)
|
||||||
|
out = _np.empty(n, dtype=_np.float64)
|
||||||
|
_bridge._check(_bridge._lib.qtl_rrsi(
|
||||||
|
src.ctypes.data_as(_bridge._dp), n,
|
||||||
|
out.ctypes.data_as(_bridge._dp), smoothLength, rsiLength))
|
||||||
|
return _shift(out, offset)
|
||||||
|
|
||||||
|
|
||||||
def smi(high: object, low: object, close: object, kPeriod: int = 14, kSmooth: int = 3, dSmooth: int = 3, blau: int = 3, offset: int = 0, **kwargs) -> object:
|
def smi(high: object, low: object, close: object, kPeriod: int = 14, kSmooth: int = 3, dSmooth: int = 3, blau: int = 3, offset: int = 0, **kwargs) -> object:
|
||||||
"""Stochastic Momentum Index."""
|
"""Stochastic Momentum Index."""
|
||||||
kPeriod = int(kPeriod)
|
kPeriod = int(kPeriod)
|
||||||
|
|||||||
@@ -327,6 +327,17 @@ public static unsafe partial class Exports
|
|||||||
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rrsi: Pattern A (dual period params)
|
||||||
|
[UnmanagedCallersOnly(EntryPoint = "qtl_rrsi")]
|
||||||
|
public static int QtlRrsi(double* src, int n, double* dst, int smoothLength, int rsiLength)
|
||||||
|
{
|
||||||
|
int v = Chk1(src, dst, n); if (v != 0) return v;
|
||||||
|
v = ChkPeriod(smoothLength); if (v != 0) return v;
|
||||||
|
v = ChkPeriod(rsiLength); if (v != 0) return v;
|
||||||
|
try { Rrsi.Batch(Src(src, n), Dst(dst, n), smoothLength, rsiLength); return StatusCodes.QTL_OK; }
|
||||||
|
catch { return StatusCodes.QTL_ERR_INTERNAL; }
|
||||||
|
}
|
||||||
|
|
||||||
// Er: Pattern A
|
// Er: Pattern A
|
||||||
[UnmanagedCallersOnly(EntryPoint = "qtl_er")]
|
[UnmanagedCallersOnly(EntryPoint = "qtl_er")]
|
||||||
public static int QtlEr(double* src, int n, double* dst, int period)
|
public static int QtlEr(double* src, int n, double* dst, int period)
|
||||||
|
|||||||
Reference in New Issue
Block a user