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)
|
||||
* [REVERSEEMA - Ehlers Reverse EMA](/lib/oscillators/reverseema/ReverseEma.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)
|
||||
* [SQUEEZE - Squeeze Momentum](/lib/oscillators/squeeze/Squeeze.md)
|
||||
* [SQUEEZE_PRO - Squeeze Pro](/lib/oscillators/squeeze_pro/squeeze_pro.md)
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
| [RV](volatility/rv/Rv.md) | Realized Volatility | Volatility |
|
||||
| [RVI](volatility/rvi/Rvi.md) | Relative Volatility Index | Volatility |
|
||||
| [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) |
|
||||
| [SAK](filters/sak/Sak.md) | Ehlers Swiss Army Knife | Filters |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
|
||||
@@ -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 |
|
||||
| reverseema | `ReverseEma` | A | period |
|
||||
| rvgi | `Rvgi` | C (OHLC) | period |
|
||||
| rrsi | `Rrsi` | A | smoothLength, rsiLength |
|
||||
| smi | `Smi` | I (HLC→2+) | period, smoothK, smoothD |
|
||||
| squeeze | `Squeeze` | I (HLC→multi) | bbPeriod, kcPeriod... |
|
||||
| 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_INERTIA = _bind("qtl_inertia", [_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_CTI = _bind("qtl_cti", [_dp, _ci, _dp, _ci])
|
||||
HAS_REFLEX = _bind("qtl_reflex", [_dp, _ci, _dp, _ci])
|
||||
|
||||
@@ -25,6 +25,7 @@ __all__ = [
|
||||
"qqe",
|
||||
"reverseema",
|
||||
"rvgi",
|
||||
"rrsi",
|
||||
"smi",
|
||||
"squeeze",
|
||||
"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)
|
||||
|
||||
|
||||
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:
|
||||
"""Stochastic Momentum Index."""
|
||||
kPeriod = int(kPeriod)
|
||||
|
||||
@@ -327,6 +327,17 @@ public static unsafe partial class Exports
|
||||
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
|
||||
[UnmanagedCallersOnly(EntryPoint = "qtl_er")]
|
||||
public static int QtlEr(double* src, int n, double* dst, int period)
|
||||
|
||||
Reference in New Issue
Block a user