using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// VWAPSD: Volume Weighted Average Price with Standard Deviation Bands /// A volatility channel indicator using VWAP as the center line with configurable /// standard deviation bands. /// /// /// The VWAPSD calculation process: /// 1. Calculate cumulative price×volume sum (sum_pv) /// 2. Calculate cumulative volume sum (sum_vol) /// 3. Calculate cumulative price²×volume sum (sum_pv2) /// 4. VWAP = sum_pv / sum_vol /// 5. Variance = (sum_pv2 / sum_vol) - VWAP² /// 6. StdDev = √Variance /// 7. Bands = VWAP ± (numDevs × StdDev) /// /// Key characteristics: /// - Volume-weighted price average as center line /// - Configurable number of standard deviations for bands /// - Bands adapt to volume-weighted price dispersion /// - Can reset on session boundaries or run continuously /// /// Sources: /// Standard VWAP calculation with configurable deviation bands /// Common in institutional trading for intraday analysis /// [SkipLocalsInit] public sealed class Vwapsd : AbstractBase { private readonly double _numDevs; private const double DefaultNumDevs = 2.0; private const double MinNumDevs = 0.1; private const double MaxNumDevs = 5.0; // State for streaming with bar correction [StructLayout(LayoutKind.Auto)] private record struct State( double SumPV, // Cumulative price × volume double SumVol, // Cumulative volume double SumPV2, // Cumulative price² × volume int Count, // Bar count since reset double LastValidPrice, double LastValidVolume, bool IsInitialized); private State _state; private State _p_state; private int _index; public override bool IsHot => _index >= WarmupPeriod; /// /// Upper band (VWAP + numDevs × StdDev) /// public TValue Upper { get; private set; } /// /// Lower band (VWAP - numDevs × StdDev) /// public TValue Lower { get; private set; } /// /// VWAP value (center line) /// public TValue Vwap { get; private set; } /// /// Standard deviation of volume-weighted prices /// public TValue StdDev { get; private set; } /// /// Band width (Upper - Lower = 2 × numDevs × StdDev) /// public TValue Width { get; private set; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vwapsd(double numDevs = DefaultNumDevs) { if (numDevs < MinNumDevs) { throw new ArgumentOutOfRangeException(nameof(numDevs), $"Number of deviations must be at least {MinNumDevs}."); } if (numDevs > MaxNumDevs) { throw new ArgumentOutOfRangeException(nameof(numDevs), $"Number of deviations must not exceed {MaxNumDevs}."); } _numDevs = numDevs; WarmupPeriod = 2; // Need at least 2 bars for variance Name = $"Vwapsd({numDevs:F1})"; Init(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Init() { _index = 0; _state = new State(0, 0, 0, 0, double.NaN, double.NaN, false); _p_state = _state; Vwap = new TValue(DateTime.UtcNow, 0); Upper = new TValue(DateTime.UtcNow, 0); Lower = new TValue(DateTime.UtcNow, 0); StdDev = new TValue(DateTime.UtcNow, 0); Width = new TValue(DateTime.UtcNow, 0); } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private static double GetFiniteValue(double value, ref double lastValid) { if (double.IsFinite(value)) { lastValid = value; return value; } return double.IsFinite(lastValid) ? lastValid : 0; } /// /// Updates the indicator with a new bar. Uses HLC3 as price and bar volume. /// /// The input bar with OHLCV data /// True for new bar, false for bar correction /// True to reset VWAP calculation (e.g., new session) /// The VWAP value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true, bool reset = false) { return Update(new TValue(bar.Time, bar.HLC3), bar.Volume, isNew, reset); } /// /// Updates the indicator with price and volume values. /// /// Price value (typically HLC3) /// Volume value /// True for new bar, false for bar correction /// True to reset VWAP calculation (e.g., new session) /// The VWAP value [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TValue input, double volume, bool isNew = true, bool reset = false) { // State management for bar correction if (isNew) { _p_state = _state; _index++; } else { // Restore previous state _state = _p_state; } double lastValidPrice = _state.LastValidPrice; double lastValidVolume = _state.LastValidVolume; double price = GetFiniteValue(input.Value, ref lastValidPrice); double vol = GetFiniteValue(volume, ref lastValidVolume); _state = _state with { LastValidPrice = lastValidPrice, LastValidVolume = lastValidVolume }; // Handle reset if (reset || !_state.IsInitialized) { // Reset warmup tracking for proper IsHot gating after session reset _index = 1; if (vol > 0) { _state = _state with { SumPV = price * vol, SumVol = vol, SumPV2 = price * price * vol, Count = 1, IsInitialized = true }; } else { _state = _state with { SumPV = 0, SumVol = 0, SumPV2 = 0, Count = 0, IsInitialized = true }; } } else { // Accumulate values if (vol > 0) { _state = _state with { SumPV = _state.SumPV + price * vol, SumVol = _state.SumVol + vol, SumPV2 = _state.SumPV2 + price * price * vol, Count = _state.Count + 1 }; } } // Calculate VWAP double vwap = _state.SumVol > 0 ? _state.SumPV / _state.SumVol : price; // Calculate variance and standard deviation double variance = 0; if (_state.SumVol > 0 && _state.Count > 1) { double meanP2 = _state.SumPV2 / _state.SumVol; double vwapSquared = vwap * vwap; variance = Math.Max(0, meanP2 - vwapSquared); } double stdev = Math.Sqrt(variance); // Calculate bands double upper = vwap + _numDevs * stdev; double lower = vwap - _numDevs * stdev; // Update output values Vwap = new TValue(input.Time, vwap); Upper = new TValue(input.Time, upper); Lower = new TValue(input.Time, lower); StdDev = new TValue(input.Time, stdev); Width = new TValue(input.Time, upper - lower); Last = Vwap; return Last; } /// /// Updates the indicator with a TValue. Assumes volume of 1.0 for each update. /// For proper VWAP calculation, use Update(TBar) or Update(TValue, double volume). /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { return Update(input, 1.0, isNew, false); } /// /// Updates the indicator with a bar series. /// public TSeries Update(TBarSeries source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } int len = source.Count; TSeries result = new(capacity: len); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); result.Add(Last.Time, Last.Value, isNew: true); } return result; } /// /// Updates the indicator with a price series (uses volume=1 for each bar). /// public override TSeries Update(TSeries source) { if (source == null) { throw new ArgumentNullException(nameof(source)); } int len = source.Count; TSeries result = new(capacity: len); for (int i = 0; i < len; i++) { Update(source[i], isNew: true); result.Add(Last.Time, Last.Value, isNew: true); } return result; } public override void Reset() { Init(); } public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { step ??= TimeSpan.FromSeconds(1); DateTime startTime = DateTime.UtcNow; for (int i = 0; i < source.Length; i++) { Update(new TValue(startTime + i * step.Value, source[i]), 1.0, isNew: true, reset: false); } } /// /// Calculates VWAP SD Bands for a bar series. /// /// Tuple of (Upper, Lower, Vwap, StdDev) public static (TSeries Upper, TSeries Lower, TSeries Vwap, TSeries StdDev) Calculate( TBarSeries source, double numDevs = DefaultNumDevs) { Vwapsd vwapsd = new(numDevs); int len = source.Count; TSeries upper = new(capacity: len); TSeries lower = new(capacity: len); TSeries vwap = new(capacity: len); TSeries stdev = new(capacity: len); for (int i = 0; i < len; i++) { vwapsd.Update(source[i], isNew: true); upper.Add(vwapsd.Upper.Time, vwapsd.Upper.Value, isNew: true); lower.Add(vwapsd.Lower.Time, vwapsd.Lower.Value, isNew: true); vwap.Add(vwapsd.Vwap.Time, vwapsd.Vwap.Value, isNew: true); stdev.Add(vwapsd.StdDev.Time, vwapsd.StdDev.Value, isNew: true); } return (upper, lower, vwap, stdev); } /// /// Calculates VWAP SD Bands using span arrays. /// /// Source price values (typically HLC3) /// Volume values /// Output span for upper band /// Output span for lower band /// Output span for VWAP values /// Output span for standard deviation values /// Number of standard deviations for bands (default 2.0) public static void Batch( ReadOnlySpan price, ReadOnlySpan volume, Span upper, Span lower, Span vwap, Span stdDev, double numDevs = DefaultNumDevs) { int len = price.Length; if (len != volume.Length || len != upper.Length || len != lower.Length || len != vwap.Length || len != stdDev.Length) { throw new ArgumentException("All spans must have the same length.", nameof(price)); } if (numDevs < MinNumDevs) { throw new ArgumentOutOfRangeException(nameof(numDevs), $"Number of deviations must be at least {MinNumDevs}."); } if (numDevs > MaxNumDevs) { throw new ArgumentOutOfRangeException(nameof(numDevs), $"Number of deviations must not exceed {MaxNumDevs}."); } if (len == 0) { return; } double sumPV = 0, sumVol = 0, sumPV2 = 0; int count = 0; double lastValidPrice = double.NaN; double lastValidVolume = double.NaN; for (int i = 0; i < len; i++) { double p = GetFiniteValue(price[i], ref lastValidPrice); double v = GetFiniteValue(volume[i], ref lastValidVolume); if (v > 0) { sumPV += p * v; sumVol += v; sumPV2 += p * p * v; count++; } double vwapVal = sumVol > 0 ? sumPV / sumVol : p; double variance = 0; if (sumVol > 0 && count > 1) { double meanP2 = sumPV2 / sumVol; double vwapSquared = vwapVal * vwapVal; variance = Math.Max(0, meanP2 - vwapSquared); } double stdev = Math.Sqrt(variance); vwap[i] = vwapVal; stdDev[i] = stdev; upper[i] = vwapVal + numDevs * stdev; lower[i] = vwapVal - numDevs * stdev; } } }