Files
QuanTAlib/lib/oscillators/stochrsi/Stochrsi.cs
T

412 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// STOCHRSI: Stochastic RSI Oscillator
// Applies the Stochastic formula to RSI values instead of price,
// producing a more sensitive overbought/oversold indicator.
// Tushar Chande & Stanley Kroll, 1994.
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// STOCHRSI: Stochastic RSI Oscillator
/// </summary>
/// <remarks>
/// Applies the Stochastic oscillator formula to RSI values.
/// K = SMA(100 × (RSI - minRSI) / (maxRSI - minRSI), kSmooth)
/// D = SMA(K, dSmooth)
/// Range: 0-100. More sensitive than RSI alone.
/// </remarks>
[SkipLocalsInit]
public sealed class Stochrsi : AbstractBase
{
private const int DefaultRsiLength = 14;
private const int DefaultStochLength = 14;
private const int DefaultKSmooth = 3;
private const int DefaultDSmooth = 3;
private readonly int _stochLength;
private readonly int _kSmooth;
private readonly int _dSmooth;
private readonly Rsi _rsi;
private readonly double[] _rsiBuf;
private readonly double[] _kBuf;
private readonly double[] _dBuf;
private readonly MonotonicDeque _maxDeque;
private readonly MonotonicDeque _minDeque;
[StructLayout(LayoutKind.Auto)]
private record struct State(
long Count,
double KSum,
int KHead,
double DSum,
int DHead,
double LastValidValue,
double K,
double D,
double PrevRsiBufVal,
double PrevKBufVal,
double PrevDBufVal);
private State _s;
private State _ps;
/// <summary>Current %K value (SMA-smoothed raw stochastic of RSI).</summary>
public double K => _s.K;
/// <summary>Current %D value (SMA of %K signal line).</summary>
public double D => _s.D;
public override bool IsHot => _s.Count >= _rsi.WarmupPeriod + _stochLength - 1 + _kSmooth - 1;
/// <summary>
/// Creates StochRSI with specified parameters.
/// </summary>
/// <param name="rsiLength">Period for RSI calculation (default: 14).</param>
/// <param name="stochLength">Stochastic lookback over RSI values (default: 14).</param>
/// <param name="kSmooth">SMA smoothing for %K (default: 3).</param>
/// <param name="dSmooth">SMA smoothing for %D (default: 3).</param>
public Stochrsi(int rsiLength = DefaultRsiLength, int stochLength = DefaultStochLength,
int kSmooth = DefaultKSmooth, int dSmooth = DefaultDSmooth)
{
if (rsiLength <= 0)
{
throw new ArgumentException("RSI length must be greater than 0", nameof(rsiLength));
}
if (stochLength <= 0)
{
throw new ArgumentException("Stochastic length must be greater than 0", nameof(stochLength));
}
if (kSmooth <= 0)
{
throw new ArgumentException("K smoothing must be greater than 0", nameof(kSmooth));
}
if (dSmooth <= 0)
{
throw new ArgumentException("D smoothing must be greater than 0", nameof(dSmooth));
}
_stochLength = stochLength;
_kSmooth = kSmooth;
_dSmooth = dSmooth;
_rsi = new Rsi(rsiLength);
_rsiBuf = new double[stochLength];
_kBuf = new double[kSmooth];
_dBuf = new double[dSmooth];
_maxDeque = new MonotonicDeque(stochLength);
_minDeque = new MonotonicDeque(stochLength);
_s = new State(0, 0, 0, 0, 0, double.NaN, double.NaN, double.NaN, 0, 0, 0);
_ps = _s;
Name = $"StochRsi({rsiLength},{stochLength},{kSmooth},{dSmooth})";
WarmupPeriod = _rsi.WarmupPeriod + stochLength - 1 + kSmooth - 1 + dSmooth - 1;
}
/// <summary>
/// Creates StochRSI subscribed to a source publisher.
/// </summary>
public Stochrsi(ITValuePublisher source, int rsiLength = DefaultRsiLength,
int stochLength = DefaultStochLength, int kSmooth = DefaultKSmooth,
int dSmooth = DefaultDSmooth)
: this(rsiLength, stochLength, kSmooth, dSmooth)
{
source.Pub += Handle;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
// Save buffer slot values that will be overwritten (for future rollback)
int idx = (int)(_s.Count % _stochLength);
_s.PrevRsiBufVal = _rsiBuf[idx];
if (_kSmooth > 1)
{
_s.PrevKBufVal = _kBuf[_s.KHead];
}
if (_dSmooth > 1)
{
_s.PrevDBufVal = _dBuf[_s.DHead];
}
_ps = _s;
}
else
{
// Restore buffer slots that were overwritten by previous call
int idx = (int)(_ps.Count % _stochLength);
_rsiBuf[idx] = _ps.PrevRsiBufVal;
if (_kSmooth > 1)
{
_kBuf[_ps.KHead] = _ps.PrevKBufVal;
}
if (_dSmooth > 1)
{
_dBuf[_ps.DHead] = _ps.PrevDBufVal;
}
_s = _ps;
}
var s = _s;
// NaN/Infinity guard
double val = input.Value;
if (!double.IsFinite(val))
{
val = double.IsFinite(s.LastValidValue) ? s.LastValidValue : 0;
}
else
{
s.LastValidValue = val;
}
// Step 1: Compute RSI (RSI handles its own bar correction via isNew)
double rsiVal = _rsi.Update(new TValue(input.Time, val), isNew).Value;
// Step 2: Store RSI in circular buffer, then update deques
int bufIdx = (int)(s.Count % _stochLength);
_rsiBuf[bufIdx] = rsiVal;
if (isNew)
{
_maxDeque.PushMax(s.Count, rsiVal, _rsiBuf);
_minDeque.PushMin(s.Count, rsiVal, _rsiBuf);
}
else
{
// Rebuild deques from buffer (buffer now has correct value at current index)
int bufCount = (int)Math.Min(s.Count + 1, _stochLength);
_maxDeque.RebuildMax(_rsiBuf, s.Count, bufCount);
_minDeque.RebuildMin(_rsiBuf, s.Count, bufCount);
}
double highestRsi = _maxDeque.GetExtremum(_rsiBuf);
double lowestRsi = _minDeque.GetExtremum(_rsiBuf);
double rsiRange = highestRsi - lowestRsi;
// Step 3: Raw stochastic of RSI
double kRaw = rsiRange > 1e-10 ? 100.0 * (rsiVal - lowestRsi) / rsiRange : 50.0;
// Step 4: SMA smooth kRaw → K
double kSmoothed;
if (_kSmooth <= 1)
{
kSmoothed = kRaw;
}
else
{
// Circular buffer SMA for K
s.KSum -= _kBuf[s.KHead];
_kBuf[s.KHead] = kRaw;
s.KSum += kRaw;
s.KHead = (s.KHead + 1) % _kSmooth;
long kCount = s.Count + 1 - (_rsi.WarmupPeriod + _stochLength - 1);
int kFilled = (int)Math.Min(Math.Max(kCount, 1), _kSmooth);
kSmoothed = s.KSum / kFilled;
}
// Step 5: SMA smooth K → D
double dSmoothed;
if (_dSmooth <= 1)
{
dSmoothed = kSmoothed;
}
else
{
s.DSum -= _dBuf[s.DHead];
_dBuf[s.DHead] = kSmoothed;
s.DSum += kSmoothed;
s.DHead = (s.DHead + 1) % _dSmooth;
long dCount = s.Count + 1 - (_rsi.WarmupPeriod + _stochLength - 1 + _kSmooth - 1);
int dFilled = (int)Math.Min(Math.Max(dCount, 1), _dSmooth);
dSmoothed = s.DSum / dFilled;
}
s.K = kSmoothed;
s.D = dSmoothed;
s.Count++;
_s = s;
Last = new TValue(input.Time, kSmoothed);
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// Updates the indicator with a full series, returning K values.
/// Use the K and D properties or Batch method for both outputs.
/// </summary>
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
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);
// Use streaming replay to ensure consistency with Update(TValue)
Reset();
for (int i = 0; i < len; i++)
{
var result = Update(new TValue(source.Times[i], source.Values[i]));
tSpan[i] = source.Times[i];
vSpan[i] = result.Value;
}
return new TSeries(t, v);
}
/// <summary>
/// Returns both K and D series from source.
/// </summary>
public (TSeries K, TSeries D) UpdateKD(TSeries source)
{
if (source.Count == 0)
{
return ([], []);
}
int len = source.Count;
var tK = new List<long>(len);
var vK = new List<double>(len);
var tD = new List<long>(len);
var vD = new List<double>(len);
CollectionsMarshal.SetCount(tK, len);
CollectionsMarshal.SetCount(vK, len);
CollectionsMarshal.SetCount(tD, len);
CollectionsMarshal.SetCount(vD, len);
Reset();
var tKSpan = CollectionsMarshal.AsSpan(tK);
var vKSpan = CollectionsMarshal.AsSpan(vK);
var tDSpan = CollectionsMarshal.AsSpan(tD);
var vDSpan = CollectionsMarshal.AsSpan(vD);
for (int i = 0; i < len; i++)
{
_ = Update(new TValue(source.Times[i], source.Values[i]));
long time = source.Times[i];
tKSpan[i] = time;
vKSpan[i] = _s.K;
tDSpan[i] = time;
vDSpan[i] = _s.D;
}
return (new TSeries(tK, vK), new TSeries(tD, vD));
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (double value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
public override void Reset()
{
_rsi.Reset();
_maxDeque.Reset();
_minDeque.Reset();
Array.Clear(_rsiBuf);
Array.Clear(_kBuf);
Array.Clear(_dBuf);
_s = new State(0, 0, 0, 0, 0, double.NaN, double.NaN, double.NaN, 0, 0, 0);
_ps = _s;
Last = default;
}
/// <summary>
/// Computes StochRSI %K for an entire series using a new instance.
/// </summary>
public static TSeries Batch(TSeries source, int rsiLength = DefaultRsiLength,
int stochLength = DefaultStochLength, int kSmooth = DefaultKSmooth,
int dSmooth = DefaultDSmooth)
{
var ind = new Stochrsi(rsiLength, stochLength, kSmooth, dSmooth);
return ind.Update(source);
}
/// <summary>
/// High-performance span-based StochRSI %K calculation.
/// </summary>
/// <remarks>
/// Computes only the %K line (SMA-smoothed Stochastic RSI). The %D signal line (<see cref="D"/>)
/// is not available from this API; use <see cref="Update(TValue, bool)"/> streaming to access both outputs.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan<double> source, Span<double> output,
int rsiLength, int stochLength, int kSmooth, int dSmooth)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (rsiLength <= 0)
{
throw new ArgumentException("RSI length must be greater than 0", nameof(rsiLength));
}
if (stochLength <= 0)
{
throw new ArgumentException("Stochastic length must be greater than 0", nameof(stochLength));
}
if (kSmooth <= 0)
{
throw new ArgumentException("K smoothing must be greater than 0", nameof(kSmooth));
}
if (dSmooth <= 0)
{
throw new ArgumentException("D smoothing must be greater than 0", nameof(dSmooth));
}
int len = source.Length;
if (len == 0)
{
return;
}
// Use streaming instance to guarantee consistency with Update(TValue)
var ind = new Stochrsi(rsiLength, stochLength, kSmooth, dSmooth);
for (int i = 0; i < len; i++)
{
output[i] = ind.Update(new TValue(DateTime.MinValue, source[i])).Value;
}
}
/// <summary>
/// Runs batch calculation and returns a hot indicator ready for streaming.
/// </summary>
public static (TSeries Results, Stochrsi Indicator) Calculate(TSeries source,
int rsiLength = DefaultRsiLength, int stochLength = DefaultStochLength,
int kSmooth = DefaultKSmooth, int dSmooth = DefaultDSmooth)
{
var indicator = new Stochrsi(rsiLength, stochLength, kSmooth, dSmooth);
TSeries results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
}