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;
}
}
}