using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// PVT: Price Volume Trend /// /// /// Price Volume Trend is a cumulative volume-based indicator that measures buying /// and selling pressure by weighting volume by the relative price change. Unlike OBV /// which uses all-or-nothing volume assignment, PVT uses proportional volume based /// on how much price moved. /// /// Calculation: /// PVT = Previous PVT + Volume * ((Close - Previous Close) / Previous Close) /// /// Key differences from OBV: /// - OBV assigns entire volume to buyers or sellers /// - PVT assigns proportional volume based on price change magnitude /// - PVT is more sensitive to the size of price moves /// /// Sources: /// https://www.investopedia.com/terms/p/pvtrend.asp /// https://school.stockcharts.com/doku.php?id=technical_indicators:price_volume_trend_pvt /// [SkipLocalsInit] public sealed class Pvt : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State( double PvtValue, double PrevClose, double LastValidClose, double LastValidVolume, int Index); private State _s; private State _ps; /// /// Display name for the indicator. /// public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Current PVT value. /// public TValue Last { get; private set; } /// /// True if the indicator has processed at least 2 bars. /// public bool IsHot => _s.Index >= 2; /// /// Warmup period required before the indicator is considered hot. /// #pragma warning disable S2325 // Instance property required by indicator interface convention public int WarmupPeriod => 2; #pragma warning restore S2325 /// /// Creates a new PVT indicator. /// public Pvt() { _s = new State(PvtValue: 0, PrevClose: 0, LastValidClose: 0, LastValidVolume: 0, Index: 0); _ps = _s; Name = "Pvt"; } /// /// Resets the indicator state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(PvtValue: 0, PrevClose: 0, LastValidClose: 0, LastValidVolume: 0, Index: 0); _ps = _s; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Handle NaN/Infinity in close and volume double close = double.IsFinite(input.Close) ? input.Close : s.LastValidClose; double volume = double.IsFinite(input.Volume) ? input.Volume : s.LastValidVolume; if (double.IsFinite(input.Close) && input.Close > 0) { s.LastValidClose = input.Close; } if (double.IsFinite(input.Volume) && input.Volume > 0) { s.LastValidVolume = input.Volume; } // Calculate PVT: volume * (price_change / prev_price) if (s.Index > 0 && s.PrevClose > 0) { double priceChange = close - s.PrevClose; double priceChangeRatio = priceChange / s.PrevClose; double volumeAdjustment = volume * priceChangeRatio; s.PvtValue += volumeAdjustment; } // Store for next iteration s.PrevClose = close; if (isNew) { s.Index++; } _s = s; Last = new TValue(input.Time, s.PvtValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates PVT with price and volume directly. /// [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 close = double.IsFinite(price) ? price : s.LastValidClose; double vol = double.IsFinite(volume) ? volume : s.LastValidVolume; if (double.IsFinite(price) && price > 0) { s.LastValidClose = price; } if (double.IsFinite(volume) && volume > 0) { s.LastValidVolume = volume; } // Calculate PVT: volume * (price_change / prev_price) if (s.Index > 0 && s.PrevClose > 0) { double priceChange = close - s.PrevClose; double priceChangeRatio = priceChange / s.PrevClose; double volumeAdjustment = vol * priceChangeRatio; s.PvtValue += volumeAdjustment; } // Store for next iteration s.PrevClose = close; if (isNew) { s.Index++; } _s = s; Last = new TValue(time, s.PvtValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates PVT with a TValue input. /// /// /// PVT requires volume data to compute. Using TValue without volume data will /// keep PVT unchanged. For proper PVT calculation, use Update(TBar). /// #pragma warning disable S2325 // Method signature must match ITValuePublisher contract public TValue Update(TValue input, bool isNew = true) #pragma warning restore S2325 { // PVT requires volume; without it, we can't compute // Return current value unchanged if (isNew) { _ps = _s; } else { _s = _ps; } Last = new TValue(input.Time, _s.PvtValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } 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); } public static TSeries Calculate(TBarSeries source) { if (source.Count == 0) { return []; } var t = source.Open.Times.ToArray(); var v = new double[source.Count]; Calculate(source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Calculate(ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (close.Length != volume.Length) { throw new ArgumentException("Close and Volume spans must be of the same length", nameof(volume)); } if (close.Length != output.Length) { throw new ArgumentException("Output span must be of the same length as input", nameof(output)); } int len = close.Length; if (len == 0) { return; } // First value is zero (no comparison yet) output[0] = 0; double prevClose = close[0]; double pvt = 0; for (int i = 1; i < len; i++) { double currentClose = close[i]; double currentVolume = volume[i]; // Calculate PVT if inputs are finite and prevClose is positive (consistent with Update method) if (double.IsFinite(currentClose) && double.IsFinite(currentVolume) && double.IsFinite(prevClose) && prevClose > 0) { double priceChange = currentClose - prevClose; double priceChangeRatio = priceChange / prevClose; pvt += currentVolume * priceChangeRatio; } output[i] = pvt; // Update prevClose only if current is valid if (double.IsFinite(currentClose)) { prevClose = currentClose; } } } }