using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// Computes the Volume Rate of Change (VROC) measuring volume momentum over a specified period. /// /// /// VROC measures volume change: VROC = ((Volume - Volume[period]) / Volume[period]) × 100 /// for percentage mode, or VROC = Volume - Volume[period] for point mode. /// /// This implementation is optimized for streaming updates with O(1) per bar using circular buffers. /// Non-finite inputs (NaN/±Inf) are sanitized by substituting the last finite value observed. /// /// For the authoritative algorithm reference, full rationale, and behavioral contracts, see the /// companion files in the same directory. /// /// Detailed documentation /// Reference Pine Script implementation [SkipLocalsInit] public sealed class Vroc : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State( int Head, int Count, double LastValidVolume, int Index); private State _s; private State _ps; private readonly int _period; private readonly bool _usePercent; private readonly double[] _buffer; private double[]? _pBuffer; public TValue Last { get; private set; } public bool IsHot => _s.Index > _period; public int WarmupPeriod => _period + 1; public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Initializes a new instance of the VROC indicator. /// /// The lookback period (default: 12). /// True for percentage mode, false for point change (default: true). /// Thrown when period is less than 1. public Vroc(int period = 12, bool usePercent = true) { if (period < 1) { throw new ArgumentException("Period must be at least 1", nameof(period)); } _period = period; _usePercent = usePercent; _buffer = new double[period + 1]; // Need period + 1 to store historical value Name = $"Vroc({period},{(usePercent ? "%" : "pt")})"; Reset(); } /// /// Resets the indicator to its initial state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(Head: 0, Count: 0, LastValidVolume: 0, Index: 0); _ps = _s; Array.Clear(_buffer); _pBuffer = null; Last = default; } /// /// Updates the VROC with a new bar. /// /// The bar data. /// True if this is a new bar, false if updating current bar. /// The current VROC value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _ps = _s; _pBuffer = (double[])_buffer.Clone(); } else { _s = _ps; if (_pBuffer != null) { Array.Copy(_pBuffer, _buffer, _buffer.Length); } } var s = _s; int bufLen = _period + 1; // Handle NaN/Infinity - substitute with last valid value double volume = double.IsFinite(input.Volume) && input.Volume >= 0 ? input.Volume : s.LastValidVolume; if (double.IsFinite(input.Volume) && input.Volume >= 0) { s.LastValidVolume = input.Volume; } double vrocResult; // Store current volume in buffer _buffer[s.Head] = volume; if (s.Count < _period) { // Still filling the buffer - not enough history yet s.Head = (s.Head + 1) % bufLen; s.Count++; vrocResult = 0; } else { // Get historical volume (the value 'period' positions back) // With ring buffer of size period+1, historical is at (head - period + bufLen) % bufLen // which simplifies to (head + 1) % bufLen when count >= period int histIdx = (s.Head + 1) % bufLen; double historicalVolume = _buffer[histIdx]; // Advance head for next iteration s.Head = (s.Head + 1) % bufLen; if (s.Count < bufLen) { s.Count++; } // Calculate VROC if (_usePercent) { vrocResult = historicalVolume > 0 ? ((volume - historicalVolume) / historicalVolume) * 100.0 : 0.0; } else { vrocResult = volume - historicalVolume; } } if (isNew) { s.Index++; } _s = s; Last = new TValue(input.Time, vrocResult); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the VROC with a TValue input. /// /// /// VROC requires volume data for proper calculation. Using TValue without volume data /// will keep VROC unchanged. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { // VROC requires volume; without it, we can't compute if (isNew) { _ps = _s; } else { _s = _ps; } Last = new TValue(input.Time, Last.Value); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the VROC with a series of bars (batch mode). /// /// The bar series. /// The result series. public TSeries Update(TBarSeries source) { var t = new List(source.Count); var v = new List(source.Count); Reset(); for (int i = 0; i < source.Count; i++) { var val = Update(source[i], isNew: true); t.Add(val.Time); v.Add(val.Value); } return new TSeries(t, v); } /// /// Initializes the indicator state using the provided bar series history. /// /// Historical bar data. public void Prime(TBarSeries source) { Reset(); if (source.Count == 0) { return; } for (int i = 0; i < source.Count; i++) { Update(source[i], isNew: true); } } /// /// Calculates VROC for a series of bars (static batch mode). /// /// The bar series. /// The lookback period (default: 12). /// True for percentage mode, false for point change (default: true). /// The result series. public static TSeries Batch(TBarSeries source, int period = 12, bool usePercent = true) { if (source.Count == 0) { return []; } var t = source.Open.Times.ToArray(); var v = new double[source.Count]; Batch(source.Volume.Values, v, period, usePercent); return new TSeries(t, v); } /// /// Calculates VROC for spans of volume data (high-performance span mode). /// /// The volume span. /// The output VROC span. /// The lookback period (default: 12). /// True for percentage mode, false for point change (default: true). /// Thrown when parameters are invalid. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan volume, Span output, int period = 12, bool usePercent = true) { if (period < 1) { throw new ArgumentException("Period must be at least 1", nameof(period)); } if (volume.Length != output.Length) { throw new ArgumentException("Output span must be of the same length as input", nameof(output)); } int len = volume.Length; if (len == 0) { return; } double lastValidVolume = 1.0; for (int i = 0; i < len; i++) { // Get valid volume double vol = double.IsFinite(volume[i]) && volume[i] >= 0 ? volume[i] : lastValidVolume; if (double.IsFinite(volume[i]) && volume[i] >= 0) { lastValidVolume = volume[i]; } if (i < period) { // Not enough history output[i] = 0; } else { // Get historical volume double histVol = double.IsFinite(volume[i - period]) && volume[i - period] >= 0 ? volume[i - period] : lastValidVolume; if (usePercent) { output[i] = histVol > 0 ? ((vol - histVol) / histVol) * 100.0 : 0.0; } else { output[i] = vol - histVol; } } } } public static (TSeries Results, Vroc Indicator) Calculate(TBarSeries source, int period = 12, bool usePercent = true) { var indicator = new Vroc(period, usePercent); TSeries results = indicator.Update(source); return (results, indicator); } }