using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// Computes the Volume Accumulation (VA) indicator that measures cumulative volume flow /// relative to each bar's range midpoint, indicating buying or selling pressure. /// /// /// VA Formula: /// Midpoint = (High + Low) / 2, /// VA_period = Volume × (Close - Midpoint), /// VA = Σ(VA_period). /// /// Positive values indicate buying pressure (close above midpoint); negative indicates selling pressure. /// This implementation is optimized for streaming updates with O(1) per bar using cumulative summation. /// Non-finite inputs (NaN/±Inf) are sanitized by substituting the last finite value observed /// for each OHLCV component independently. /// /// 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 Va : ITValuePublisher { [StructLayout(LayoutKind.Auto)] private record struct State( double VaValue, double LastValidHigh, double LastValidLow, double LastValidClose, double LastValidVolume, int Index); private State _s; private State _ps; public TValue Last { get; private set; } public bool IsHot => _s.Index >= 1; public static int WarmupPeriod => 1; public string Name { get; } public event TValuePublishedHandler? Pub; /// /// Initializes a new instance of the VA indicator. /// public Va() { Name = "Va"; Reset(); } /// /// Resets the indicator to its initial state. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Reset() { _s = new State(VaValue: 0, LastValidHigh: 0, LastValidLow: 0, LastValidClose: 0, LastValidVolume: 0, Index: 0); _ps = _s; Last = default; } /// /// Updates the VA with a new bar. /// /// The bar data. /// True if this is a new bar, false if updating current bar. /// The current VA value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar input, bool isNew = true) { if (isNew) { _ps = _s; } else { _s = _ps; } var s = _s; // Handle NaN/Infinity - substitute with last valid values double high = double.IsFinite(input.High) ? input.High : s.LastValidHigh; double low = double.IsFinite(input.Low) ? input.Low : s.LastValidLow; double close = double.IsFinite(input.Close) ? input.Close : s.LastValidClose; double volume = double.IsFinite(input.Volume) ? input.Volume : s.LastValidVolume; // Update last valid values if (double.IsFinite(input.High) && input.High > 0) { s.LastValidHigh = input.High; } if (double.IsFinite(input.Low) && input.Low > 0) { s.LastValidLow = input.Low; } 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 VA for this period double midpoint = (high + low) / 2.0; double vaPeriod = volume * (close - midpoint); // Accumulate s.VaValue += vaPeriod; if (isNew) { s.Index++; } _s = s; Last = new TValue(input.Time, s.VaValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the VA with a TValue input. /// /// /// VA requires OHLCV data for proper calculation. Using TValue without full bar data /// will keep VA unchanged. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, bool isNew = true) { // VA requires OHLCV; without it, we can't compute if (isNew) { _ps = _s; } else { _s = _ps; } Last = new TValue(input.Time, _s.VaValue); Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew }); return Last; } /// /// Updates the VA 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 VA for a series of bars (static batch mode). /// /// The bar series. /// The result series. public static TSeries Batch(TBarSeries source) { if (source.Count == 0) { return []; } var t = source.Open.Times.ToArray(); var v = new double[source.Count]; Batch(source.High.Values, source.Low.Values, source.Close.Values, source.Volume.Values, v); return new TSeries(t, v); } /// /// Calculates VA for spans of OHLCV data (high-performance span mode). /// /// The high price span. /// The low price span. /// The close price span. /// The volume span. /// The output VA span. /// Thrown when span lengths don't match. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close, ReadOnlySpan volume, Span output) { if (high.Length != low.Length || high.Length != close.Length || high.Length != volume.Length) { throw new ArgumentException("All input spans must be of the same length", nameof(volume)); } if (high.Length != output.Length) { throw new ArgumentException("Output span must be of the same length as input", nameof(output)); } int len = high.Length; if (len == 0) { return; } double va = 0; double lastValidHigh = high[0]; double lastValidLow = low[0]; double lastValidClose = close[0]; double lastValidVolume = volume[0]; for (int i = 0; i < len; i++) { // Get valid values double h = double.IsFinite(high[i]) ? high[i] : lastValidHigh; double l = double.IsFinite(low[i]) ? low[i] : lastValidLow; double c = double.IsFinite(close[i]) ? close[i] : lastValidClose; double v = double.IsFinite(volume[i]) ? volume[i] : lastValidVolume; // Update last valid values if (double.IsFinite(high[i]) && high[i] > 0) { lastValidHigh = high[i]; } if (double.IsFinite(low[i]) && low[i] > 0) { lastValidLow = low[i]; } if (double.IsFinite(close[i]) && close[i] > 0) { lastValidClose = close[i]; } if (double.IsFinite(volume[i]) && volume[i] >= 0) { lastValidVolume = volume[i]; } // Calculate VA double midpoint = (h + l) / 2.0; double vaPeriod = v * (c - midpoint); va += vaPeriod; output[i] = va; } } public static (TSeries Results, Va Indicator) Calculate(TBarSeries source) { var indicator = new Va(); TSeries results = indicator.Update(source); return (results, indicator); } }