using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// RAVI: Chande Range Action Verification Index /// Measures trend strength by computing the absolute percentage divergence /// between a short-period SMA and a long-period SMA. /// /// /// Calculation steps: /// /// SMA_short = running sum of last shortPeriod closes / shortPeriod /// SMA_long = running sum of last longPeriod closes / longPeriod /// RAVI = |SMA_short - SMA_long| / |SMA_long| * 100 /// /// /// Sources: /// Tushar Chande, "Beyond Technical Analysis", Wiley, 2nd ed. (2001), pp. 66-70 /// /// Detailed documentation [SkipLocalsInit] public sealed class Ravi : AbstractBase { private readonly int _shortPeriod; private readonly int _longPeriod; private readonly RingBuffer _shortBuffer; private readonly RingBuffer _longBuffer; [StructLayout(LayoutKind.Auto)] private record struct State( double ShortSum, double LongSum, double ShortSumComp, double LongSumComp, double LastValidValue ); private State _s; private State _ps; /// /// Creates RAVI with specified short and long SMA periods. /// /// Short SMA period (must be > 0, default 7) /// Long SMA period (must be > shortPeriod, default 65) public Ravi(int shortPeriod = 7, int longPeriod = 65) { if (shortPeriod <= 0) { throw new ArgumentException("Short period must be greater than 0", nameof(shortPeriod)); } if (longPeriod <= 0) { throw new ArgumentException("Long period must be greater than 0", nameof(longPeriod)); } if (shortPeriod >= longPeriod) { throw new ArgumentException("Short period must be less than long period", nameof(shortPeriod)); } _shortPeriod = shortPeriod; _longPeriod = longPeriod; _shortBuffer = new RingBuffer(shortPeriod); _longBuffer = new RingBuffer(longPeriod); Name = $"Ravi({shortPeriod},{longPeriod})"; WarmupPeriod = longPeriod; _s = default; _ps = _s; } /// /// Creates RAVI with specified source and parameters. /// public Ravi(ITValuePublisher source, int shortPeriod = 7, int longPeriod = 65) : this(shortPeriod, longPeriod) { source.Pub += Handle; } private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew); /// /// True when both SMA buffers are full (long buffer determines warmup). /// public override bool IsHot => _longBuffer.IsFull; /// /// Updates the indicator with a single TValue input. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { if (isNew) { _ps = _s; } else { _s = _ps; // Restore buffer state for bar correction _shortBuffer.UpdateNewest(_shortBuffer.Newest); _longBuffer.UpdateNewest(_longBuffer.Newest); } var s = _s; // NaN/Infinity handling: last-valid substitution double val = input.Value; if (double.IsFinite(val)) { s.LastValidValue = val; } else { val = s.LastValidValue; } if (isNew) { // Short buffer — Kahan compensated double shortRemoved = _shortBuffer.Count == _shortBuffer.Capacity ? _shortBuffer.Oldest : 0.0; double sDelta = val - shortRemoved - s.ShortSumComp; double sNewSum = s.ShortSum + sDelta; s.ShortSumComp = (sNewSum - s.ShortSum) - sDelta; s.ShortSum = sNewSum; _shortBuffer.Add(val); // Long buffer — Kahan compensated double longRemoved = _longBuffer.Count == _longBuffer.Capacity ? _longBuffer.Oldest : 0.0; double lDelta = val - longRemoved - s.LongSumComp; double lNewSum = s.LongSum + lDelta; s.LongSumComp = (lNewSum - s.LongSum) - lDelta; s.LongSum = lNewSum; _longBuffer.Add(val); } else { // Bar correction: update newest value in both buffers _shortBuffer.UpdateNewest(val); s.ShortSum = _shortBuffer.Sum; s.ShortSumComp = 0; _longBuffer.UpdateNewest(val); s.LongSum = _longBuffer.Sum; s.LongSumComp = 0; } // Calculate RAVI double result; if (_longBuffer.IsFull && _shortBuffer.IsFull) { double smaShort = s.ShortSum / _shortPeriod; double smaLong = s.LongSum / _longPeriod; double absSmaLong = Math.Abs(smaLong); // Division-by-zero guard if (absSmaLong > 1e-10) { result = Math.Abs(smaShort - smaLong) / absSmaLong * 100.0; } else { result = 0.0; } } else { result = 0.0; } _s = s; Last = new TValue(input.Time, result); PubEvent(Last, isNew); return Last; } 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); Batch(source.Values, vSpan, _shortPeriod, _longPeriod); source.Times.CopyTo(tSpan); // Prime internal state by replaying last longPeriod bars Prime(source.Values); Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { if (source.Length == 0) { return; } _shortBuffer.Clear(); _longBuffer.Clear(); _s = default; _ps = default; int warmupLength = Math.Min(source.Length, WarmupPeriod); int startIndex = source.Length - warmupLength; // Seed LastValidValue _s.LastValidValue = 0; for (int i = startIndex - 1; i >= 0; i--) { if (double.IsFinite(source[i])) { _s.LastValidValue = source[i]; break; } } if (_s.LastValidValue == 0) { for (int i = startIndex; i < source.Length; i++) { if (double.IsFinite(source[i])) { _s.LastValidValue = source[i]; break; } } } for (int i = startIndex; i < source.Length; i++) { Update(new TValue(DateTime.MinValue, source[i]), isNew: true); } _ps = _s; } /// /// Calculates RAVI for the entire series using a new instance. /// public static TSeries Batch(TSeries source, int shortPeriod = 7, int longPeriod = 65) { var ravi = new Ravi(shortPeriod, longPeriod); return ravi.Update(source); } /// /// Span-based batch calculation for close price arrays. /// Zero-allocation method for maximum performance. /// /// Close prices. /// Output RAVI values. /// Short SMA period. /// Long SMA period. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan source, Span output, int shortPeriod = 7, int longPeriod = 65) { if (source.Length != output.Length) { throw new ArgumentException("Source and output must have the same length", nameof(output)); } if (shortPeriod <= 0) { throw new ArgumentException("Short period must be greater than 0", nameof(shortPeriod)); } if (longPeriod <= 0) { throw new ArgumentException("Long period must be greater than 0", nameof(longPeriod)); } if (shortPeriod >= longPeriod) { throw new ArgumentException("Short period must be less than long period", nameof(shortPeriod)); } int len = source.Length; if (len == 0) { return; } CalculateScalarCore(source, output, shortPeriod, longPeriod); } /// /// Calculates RAVI and returns both results and the indicator instance. /// public static (TSeries Results, Ravi Indicator) Calculate(TSeries source, int shortPeriod = 7, int longPeriod = 65) { var indicator = new Ravi(shortPeriod, longPeriod); TSeries results = indicator.Update(source); return (results, indicator); } // ---- Private implementation ---- [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int shortPeriod, int longPeriod) { int len = source.Length; const int StackAllocThreshold = 256; // Short buffer double[]? rentedShort = shortPeriod > StackAllocThreshold ? ArrayPool.Shared.Rent(shortPeriod) : null; Span shortBuf = rentedShort != null ? rentedShort.AsSpan(0, shortPeriod) : stackalloc double[shortPeriod]; // Long buffer double[]? rentedLong = longPeriod > StackAllocThreshold ? ArrayPool.Shared.Rent(longPeriod) : null; Span longBuf = rentedLong != null ? rentedLong.AsSpan(0, longPeriod) : stackalloc double[longPeriod]; try { double shortSum = 0; double shortSumComp = 0; double longSum = 0; double longSumComp = 0; double lastValid = 0; int shortIdx = 0; int longIdx = 0; int shortFilled = 0; int longFilled = 0; // Find first valid value to seed lastValid for (int k = 0; k < len; k++) { if (double.IsFinite(source[k])) { lastValid = source[k]; break; } } for (int i = 0; i < len; i++) { double val = source[i]; if (double.IsFinite(val)) { lastValid = val; } else { val = lastValid; } // Kahan-compensated update for short buffer { double deltaS = val - (shortFilled >= shortPeriod ? shortBuf[shortIdx] : 0); double yS = deltaS - shortSumComp; double tS = shortSum + yS; shortSumComp = (tS - shortSum) - yS; shortSum = tS; } shortBuf[shortIdx] = val; if (shortFilled < shortPeriod) { shortFilled++; } shortIdx++; if (shortIdx >= shortPeriod) { shortIdx = 0; } // Kahan-compensated update for long buffer { double deltaL = val - (longFilled >= longPeriod ? longBuf[longIdx] : 0); double yL = deltaL - longSumComp; double tL = longSum + yL; longSumComp = (tL - longSum) - yL; longSum = tL; } longBuf[longIdx] = val; if (longFilled < longPeriod) { longFilled++; } longIdx++; if (longIdx >= longPeriod) { longIdx = 0; } // Calculate RAVI if (shortFilled >= shortPeriod && longFilled >= longPeriod) { double smaShort = shortSum / shortPeriod; double smaLong = longSum / longPeriod; double absSmaLong = Math.Abs(smaLong); if (absSmaLong > 1e-10) { output[i] = Math.Abs(smaShort - smaLong) / absSmaLong * 100.0; } else { output[i] = 0.0; } } else { output[i] = 0.0; } } } finally { if (rentedShort != null) { ArrayPool.Shared.Return(rentedShort); } if (rentedLong != null) { ArrayPool.Shared.Return(rentedLong); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void Reset() { _shortBuffer.Clear(); _longBuffer.Clear(); _s = new State(0, 0, 0, 0, 0); _ps = _s; Last = default; } }