// 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;
///
/// STOCHRSI: Stochastic RSI Oscillator
///
///
/// 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.
///
[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;
/// Current %K value (SMA-smoothed raw stochastic of RSI).
public double K => _s.K;
/// Current %D value (SMA of %K signal line).
public double D => _s.D;
public override bool IsHot => _s.Count >= _rsi.WarmupPeriod + _stochLength - 1 + _kSmooth - 1;
///
/// Creates StochRSI with specified parameters.
///
/// Period for RSI calculation (default: 14).
/// Stochastic lookback over RSI values (default: 14).
/// SMA smoothing for %K (default: 3).
/// SMA smoothing for %D (default: 3).
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;
}
///
/// Creates StochRSI subscribed to a source publisher.
///
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;
}
///
/// Updates the indicator with a full series, returning K values.
/// Use the K and D properties or Batch method for both outputs.
///
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List(len);
var v = new List(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
// 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);
}
///
/// Returns both K and D series from source.
///
public (TSeries K, TSeries D) UpdateKD(TSeries source)
{
if (source.Count == 0)
{
return ([], []);
}
int len = source.Count;
var tK = new List(len);
var vK = new List(len);
var tD = new List(len);
var vD = new List(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 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;
}
///
/// Computes StochRSI %K for an entire series using a new instance.
///
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);
}
///
/// High-performance span-based StochRSI %K calculation.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan source, Span 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;
}
}
///
/// Runs batch calculation and returns a hot indicator ready for streaming.
///
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);
}
}