using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace QuanTAlib; /// /// VWAPBANDS: Volume Weighted Average Price with Standard Deviation Bands /// A volatility channel indicator using VWAP as the center line with bands /// calculated from the volume-weighted standard deviation of prices. /// /// /// The VWAPBANDS 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 ± (multiplier × StdDev) /// /// Key characteristics: /// - Volume-weighted price average as center line /// - Bands adapt to volume-weighted price dispersion /// - Can reset on session boundaries or run continuously /// - Supports 1σ and 2σ standard deviation bands /// /// Sources: /// Standard VWAP calculation with Bollinger-style deviation bands /// Common in institutional trading for intraday analysis /// [SkipLocalsInit] public sealed class Vwapbands : AbstractBase { private readonly double _multiplier; private const double DefaultMultiplier = 1.0; private const double MinMultiplier = 0.001; // 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 at 1σ (VWAP + mult × StdDev) /// public TValue Upper1 { get; private set; } /// /// Lower band at 1σ (VWAP - mult × StdDev) /// public TValue Lower1 { get; private set; } /// /// Upper band at 2σ (VWAP + 2 × mult × StdDev) /// public TValue Upper2 { get; private set; } /// /// Lower band at 2σ (VWAP - 2 × mult × StdDev) /// public TValue Lower2 { 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 (Upper1 - Lower1 = 2 × mult × StdDev) /// public TValue Width { get; private set; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vwapbands(double multiplier = DefaultMultiplier) { if (multiplier < MinMultiplier) { throw new ArgumentOutOfRangeException(nameof(multiplier), $"Multiplier must be at least {MinMultiplier}."); } _multiplier = multiplier; WarmupPeriod = 2; // Need at least 2 bars for variance Name = $"Vwapbands({multiplier: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); Upper1 = new TValue(DateTime.UtcNow, 0); Lower1 = new TValue(DateTime.UtcNow, 0); Upper2 = new TValue(DateTime.UtcNow, 0); Lower2 = 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 upper1 = vwap + _multiplier * stdev; double lower1 = vwap - _multiplier * stdev; double upper2 = vwap + 2.0 * _multiplier * stdev; double lower2 = vwap - 2.0 * _multiplier * stdev; // Update output values Vwap = new TValue(input.Time, vwap); Upper1 = new TValue(input.Time, upper1); Lower1 = new TValue(input.Time, lower1); Upper2 = new TValue(input.Time, upper2); Lower2 = new TValue(input.Time, lower2); StdDev = new TValue(input.Time, stdev); Width = new TValue(input.Time, upper1 - lower1); 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)); } Reset(); 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)); } Reset(); 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 Bands for a bar series. /// /// Tuple of (Upper1, Lower1, Upper2, Lower2, Vwap, StdDev) public static (TSeries Upper1, TSeries Lower1, TSeries Upper2, TSeries Lower2, TSeries Vwap, TSeries StdDev) Calculate( TBarSeries source, double multiplier = DefaultMultiplier) { Vwapbands vwapbands = new(multiplier); int len = source.Count; TSeries upper1 = new(capacity: len); TSeries lower1 = new(capacity: len); TSeries upper2 = new(capacity: len); TSeries lower2 = new(capacity: len); TSeries vwap = new(capacity: len); TSeries stdev = new(capacity: len); for (int i = 0; i < len; i++) { vwapbands.Update(source[i], isNew: true); upper1.Add(vwapbands.Upper1.Time, vwapbands.Upper1.Value, isNew: true); lower1.Add(vwapbands.Lower1.Time, vwapbands.Lower1.Value, isNew: true); upper2.Add(vwapbands.Upper2.Time, vwapbands.Upper2.Value, isNew: true); lower2.Add(vwapbands.Lower2.Time, vwapbands.Lower2.Value, isNew: true); vwap.Add(vwapbands.Vwap.Time, vwapbands.Vwap.Value, isNew: true); stdev.Add(vwapbands.StdDev.Time, vwapbands.StdDev.Value, isNew: true); } return (upper1, lower1, upper2, lower2, vwap, stdev); } /// /// Calculates VWAP Bands using span arrays. /// /// Source price values (typically HLC3) /// Volume values /// Output span for upper band at 1σ /// Output span for lower band at 1σ /// Output span for upper band at 2σ /// Output span for lower band at 2σ /// Output span for VWAP values /// Output span for standard deviation values /// Band multiplier (default 1.0) public static void Batch( ReadOnlySpan price, ReadOnlySpan volume, Span upper1, Span lower1, Span upper2, Span lower2, Span vwap, Span stdDev, double multiplier = DefaultMultiplier) { int len = price.Length; if (len != volume.Length || len != upper1.Length || len != lower1.Length || len != upper2.Length || len != lower2.Length || len != vwap.Length || len != stdDev.Length) { throw new ArgumentException("All spans must have the same length.", nameof(price)); } if (multiplier < MinMultiplier) { throw new ArgumentOutOfRangeException(nameof(multiplier), $"Multiplier must be at least {MinMultiplier}."); } 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; upper1[i] = vwapVal + multiplier * stdev; lower1[i] = vwapVal - multiplier * stdev; upper2[i] = vwapVal + 2.0 * multiplier * stdev; lower2[i] = vwapVal - 2.0 * multiplier * stdev; } } }