using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// Computes the Price Volume Rank (PVR) that categorizes price-volume relationships into /// discrete states (0-4) based on price and volume direction changes. /// /// /// PVR Categories: /// 1: Price up, Volume up (strong bullish); /// 2: Price up, Volume down (weak bullish); /// 3: Price down, Volume down (weak bearish); /// 4: Price down, Volume up (strong bearish); /// 0: Price unchanged. /// /// Useful for filtering trade signals based on price-volume confirmation. /// This implementation is optimized for streaming updates with O(1) per bar using direction comparison. /// 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 Pvr : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State { public double PrevPrice; public double PrevVolume; public double LastValidPrice; public double LastValidVolume; public bool HasPrevious; } private State _s; private State _ps; public string Name { get; } public int WarmupPeriod { get; } = 1; public TValue Last { get; private set; } public bool IsHot { get; private set; } public event TValuePublishedHandler? Pub; /// /// Initializes a new instance of the Pvr class. /// public Pvr() { Name = "Pvr"; _s = new State { LastValidPrice = 0.0, LastValidVolume = 0.0 }; _ps = _s; } /// /// Updates the indicator with a new bar. /// /// The bar data containing Close and Volume /// Whether this is a new bar or an update to the current bar /// The PVR rank (0-4) [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return Update(bar.Close, bar.Volume, bar.Time, isNew); } /// /// Updates the indicator with price and volume values. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(double price, double volume, long time, bool isNew = true) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Handle NaN/Infinity double currentPrice = double.IsFinite(price) ? price : s.LastValidPrice; double currentVolume = double.IsFinite(volume) ? Math.Max(volume, 0.0) : s.LastValidVolume; if (double.IsFinite(price)) { s.LastValidPrice = price; } if (double.IsFinite(volume)) { s.LastValidVolume = Math.Max(volume, 0.0); } double pvrValue; if (!s.HasPrevious) { // First bar - no previous to compare pvrValue = 0.0; s.HasPrevious = true; IsHot = false; } else { // Calculate PVR based on price and volume direction double prevPrice = s.PrevPrice; double prevVolume = s.PrevVolume; if (currentPrice > prevPrice) { pvrValue = currentVolume > prevVolume ? 1.0 : 2.0; } else if (currentPrice < prevPrice) { pvrValue = currentVolume < prevVolume ? 3.0 : 4.0; } else { pvrValue = 0.0; } IsHot = true; } // Store current values for next comparison s.PrevPrice = currentPrice; s.PrevVolume = currentVolume; _s = s; Last = new TValue(time, pvrValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates PVR with a bar 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); } /// /// Resets the indicator to its initial state. /// public void Reset() { _s = new State { LastValidPrice = 0.0, LastValidVolume = 0.0 }; _ps = _s; Last = default; IsHot = false; } /// /// 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 PVR for a series of bars. /// public static TSeries Batch(TBarSeries bars) { if (bars.Count == 0) { return []; } var t = bars.Open.Times.ToArray(); var v = new double[bars.Count]; Batch(bars.Close.Values, bars.Volume.Values, v); return new TSeries(t, v); } /// /// Calculates PVR values using span-based processing. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan price, ReadOnlySpan volume, Span output) { if (price.Length != output.Length) { throw new ArgumentException("Output span must have the same length as price input", nameof(output)); } if (price.Length != volume.Length) { throw new ArgumentException("Volume span must have the same length as price input", nameof(volume)); } int length = price.Length; if (length == 0) { return; } // First bar - no previous to compare, output 0 (mirror instance Update behavior) output[0] = 0.0; double prevPrice = double.IsFinite(price[0]) ? price[0] : 0.0; double prevVolume = double.IsFinite(volume[0]) ? Math.Max(volume[0], 0.0) : 0.0; for (int i = 1; i < length; i++) { double currentPrice = price[i]; double currentVolume = volume[i]; // Handle NaN if (!double.IsFinite(currentPrice)) { currentPrice = prevPrice; } if (!double.IsFinite(currentVolume)) { currentVolume = prevVolume; } currentVolume = Math.Max(currentVolume, 0.0); // Calculate PVR if (currentPrice > prevPrice) { output[i] = currentVolume > prevVolume ? 1.0 : 2.0; } else if (currentPrice < prevPrice) { output[i] = currentVolume < prevVolume ? 3.0 : 4.0; } else { output[i] = 0.0; } prevPrice = currentPrice; prevVolume = currentVolume; } } public static (TSeries Results, Pvr Indicator) Calculate(TBarSeries bars) { var indicator = new Pvr(); TSeries results = indicator.Update(bars); return (results, indicator); } }