mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
Add VWAPSD (Volume Weighted Average Price with Standard Deviation Bands) implementation and validation tests
- Implemented Vwapsd class for calculating VWAP with configurable standard deviation bands. - Added methods for updating the indicator with new bars and calculating VWAPSD using both bar series and span arrays. - Created comprehensive validation tests for VWAPSD, including checks for consistency between streaming and batch modes, mathematical correctness, and handling of edge cases such as NaN values and zero volume bars. - Ensured that the implementation adheres to performance standards with tests for large datasets and fractional numDevs values.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VwapbandsIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator();
|
||||
|
||||
Assert.Equal(1.0, indicator.Multiplier);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("VWAPBANDS - Volume Weighted Average Price with Standard Deviation Bands", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_ShortName_IncludesMultiplier()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 2.5 };
|
||||
|
||||
Assert.Contains("VWAPBANDS", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_Initialize_CreatesSixLineSeries()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (VWAP, Upper1, Lower1, Upper2, Lower2, Width)
|
||||
Assert.Equal(6, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with volume
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have values
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106, 1500);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105 };
|
||||
double[] volumes = { 1000, 1500, 2000, 1200, 1800 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close, volumes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
|
||||
// VWAP should be within price range
|
||||
double lastVwap = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastVwap >= 95 && lastVwap <= 110);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.5 };
|
||||
Assert.Equal(1.5, indicator.Multiplier);
|
||||
|
||||
indicator.Multiplier = 2.5;
|
||||
Assert.Equal(2.5, indicator.Multiplier);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_AllBandsUpdate_Correctly()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000 + i * 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// Verify all 6 line series have values
|
||||
Assert.Equal(6, indicator.LinesSeries.Count);
|
||||
foreach (var series in indicator.LinesSeries)
|
||||
{
|
||||
Assert.Equal(5, series.Count);
|
||||
Assert.True(double.IsFinite(series.GetValue(0)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_BandRelationships_AreCorrect()
|
||||
{
|
||||
var indicator = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add varied data to generate band width
|
||||
double[] closes = { 100, 105, 95, 110, 90, 105, 100, 108, 92, 103 };
|
||||
double[] volumes = { 1000, 1500, 2000, 1200, 1800, 1100, 1600, 1300, 1900, 1400 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator.HistoricalData.AddBar(now, close, close + 3, close - 3, close, volumes[i]);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// Get last values: VWAP=0, Upper1=1, Lower1=2, Upper2=3, Lower2=4, Width=5
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
double upper1 = indicator.LinesSeries[1].GetValue(0);
|
||||
double lower1 = indicator.LinesSeries[2].GetValue(0);
|
||||
double upper2 = indicator.LinesSeries[3].GetValue(0);
|
||||
double lower2 = indicator.LinesSeries[4].GetValue(0);
|
||||
double width = indicator.LinesSeries[5].GetValue(0);
|
||||
|
||||
// Band relationships: Upper2 > Upper1 > VWAP > Lower1 > Lower2
|
||||
Assert.True(upper2 >= upper1, $"Upper2 ({upper2}) should be >= Upper1 ({upper1})");
|
||||
Assert.True(upper1 >= vwap, $"Upper1 ({upper1}) should be >= VWAP ({vwap})");
|
||||
Assert.True(vwap >= lower1, $"VWAP ({vwap}) should be >= Lower1 ({lower1})");
|
||||
Assert.True(lower1 >= lower2, $"Lower1 ({lower1}) should be >= Lower2 ({lower2})");
|
||||
|
||||
// Width = Upper1 - Lower1 (2 × multiplier × StdDev)
|
||||
Assert.True(Math.Abs(width - (upper1 - lower1)) < 0.0001,
|
||||
$"Width ({width}) should equal Upper1 - Lower1 ({upper1 - lower1})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapbandsIndicator_VolumeWeighting_AffectsVwap()
|
||||
{
|
||||
var indicator1 = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
var indicator2 = new VwapbandsIndicator { Multiplier = 1.0 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Same prices but different volume distributions
|
||||
// Process both bars for each indicator
|
||||
|
||||
// Indicator1: high volume on low price, low volume on high price
|
||||
indicator1.HistoricalData.AddBar(now, 100, 102, 98, 100, 10000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(1), 110, 112, 108, 110, 100);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Indicator2: low volume on low price, high volume on high price
|
||||
indicator2.HistoricalData.AddBar(now, 100, 102, 98, 100, 100);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(1), 110, 112, 108, 110, 10000);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
double vwap1 = indicator1.LinesSeries[0].GetValue(0);
|
||||
double vwap2 = indicator2.LinesSeries[0].GetValue(0);
|
||||
|
||||
// VWAP1 should be lower (weighted toward 100 due to high volume at low price)
|
||||
// VWAP2 should be higher (weighted toward 110 due to high volume at high price)
|
||||
Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class VwapbandsIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Multiplier", sortIndex: 1, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)]
|
||||
public double Multiplier { get; set; } = 1.0;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Vwapbands? vwapbands;
|
||||
protected LineSeries? VwapSeries;
|
||||
protected LineSeries? Upper1Series;
|
||||
protected LineSeries? Lower1Series;
|
||||
protected LineSeries? Upper2Series;
|
||||
protected LineSeries? Lower2Series;
|
||||
protected LineSeries? WidthSeries;
|
||||
|
||||
#pragma warning disable S2325 // Methods and properties that don't access instance data should be static
|
||||
public int MinHistoryDepths => 2;
|
||||
#pragma warning restore S2325
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"VWAPBANDS ({Multiplier:F1})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/channels/vwapbands/Vwapbands.cs";
|
||||
|
||||
public VwapbandsIndicator()
|
||||
{
|
||||
Name = "VWAPBANDS - Volume Weighted Average Price with Standard Deviation Bands";
|
||||
Description = "Volume weighted average price with 1σ and 2σ standard deviation bands";
|
||||
|
||||
VwapSeries = new("VWAP", Color.Blue, 2, LineStyle.Solid);
|
||||
Upper1Series = new("Upper1 (+1σ)", Color.Red, 1, LineStyle.Solid);
|
||||
Lower1Series = new("Lower1 (-1σ)", Color.Green, 1, LineStyle.Solid);
|
||||
Upper2Series = new("Upper2 (+2σ)", Color.Orange, 1, LineStyle.Dot);
|
||||
Lower2Series = new("Lower2 (-2σ)", Color.Cyan, 1, LineStyle.Dot);
|
||||
WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot);
|
||||
|
||||
AddLineSeries(VwapSeries);
|
||||
AddLineSeries(Upper1Series);
|
||||
AddLineSeries(Lower1Series);
|
||||
AddLineSeries(Upper2Series);
|
||||
AddLineSeries(Lower2Series);
|
||||
AddLineSeries(WidthSeries);
|
||||
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
vwapbands = new(Multiplier);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
var time = HistoricalData.Time();
|
||||
|
||||
// VWAP requires OHLCV data - using HLC3 for price
|
||||
double high = item[PriceType.High];
|
||||
double low = item[PriceType.Low];
|
||||
double close = item[PriceType.Close];
|
||||
double volume = item[PriceType.Volume];
|
||||
|
||||
TBar bar = new(time, item[PriceType.Open], high, low, close, volume);
|
||||
TValue result = vwapbands!.Update(bar, args.IsNewBar());
|
||||
|
||||
VwapSeries!.SetValue(result.Value, vwapbands.IsHot, ShowColdValues);
|
||||
Upper1Series!.SetValue(vwapbands.Upper1.Value, vwapbands.IsHot, ShowColdValues);
|
||||
Lower1Series!.SetValue(vwapbands.Lower1.Value, vwapbands.IsHot, ShowColdValues);
|
||||
Upper2Series!.SetValue(vwapbands.Upper2.Value, vwapbands.IsHot, ShowColdValues);
|
||||
Lower2Series!.SetValue(vwapbands.Lower2.Value, vwapbands.IsHot, ShowColdValues);
|
||||
WidthSeries!.SetValue(vwapbands.Width.Value, vwapbands.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VwapbandsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Vwapbands_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapbands(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapbands(-1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapbands(0.0001)); // Below MinMultiplier
|
||||
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
Assert.NotNull(vwapbands);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_DefaultConstructor_UsesDefaultMultiplier()
|
||||
{
|
||||
var vwapbands = new Vwapbands();
|
||||
Assert.NotNull(vwapbands);
|
||||
Assert.Contains("Vwapbands", vwapbands.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Update_ReturnsValue()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
||||
var result = vwapbands.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(double.IsFinite(vwapbands.Upper1.Value));
|
||||
Assert.True(double.IsFinite(vwapbands.Lower1.Value));
|
||||
Assert.True(double.IsFinite(vwapbands.Upper2.Value));
|
||||
Assert.True(double.IsFinite(vwapbands.Lower2.Value));
|
||||
Assert.True(double.IsFinite(vwapbands.Vwap.Value));
|
||||
Assert.True(double.IsFinite(vwapbands.StdDev.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_FirstBar_InitializesCorrectly()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
||||
_ = vwapbands.Update(bar);
|
||||
|
||||
// First bar: VWAP = HLC3 = (105+95+100)/3 = 100
|
||||
double expectedVwap = (105 + 95 + 100) / 3.0;
|
||||
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
|
||||
|
||||
// First bar has zero variance (only 1 point)
|
||||
Assert.Equal(0, vwapbands.StdDev.Value, precision: 10);
|
||||
Assert.Equal(expectedVwap, vwapbands.Upper1.Value, precision: 10);
|
||||
Assert.Equal(expectedVwap, vwapbands.Lower1.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Properties_Accessible()
|
||||
{
|
||||
var vwapbands = new Vwapbands(2.0);
|
||||
|
||||
Assert.False(vwapbands.IsHot);
|
||||
Assert.Contains("Vwapbands", vwapbands.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(2, vwapbands.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Update_IsNew_AcceptsParameter()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
||||
var bar2 = new TBar(DateTime.UtcNow, 100, 106, 94, 101, 1100);
|
||||
|
||||
var result1 = vwapbands.Update(bar1, isNew: true);
|
||||
var result2 = vwapbands.Update(bar2, isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(result1.Value));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Update_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Process several bars
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
double beforeCorrection = vwapbands.Vwap.Value;
|
||||
|
||||
// Correct last bar with different value
|
||||
var correctionBar = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 5000);
|
||||
vwapbands.Update(correctionBar, isNew: false);
|
||||
double afterCorrection = vwapbands.Vwap.Value;
|
||||
|
||||
Assert.NotEqual(beforeCorrection, afterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Process all bars
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
double originalVwap = vwapbands.Vwap.Value;
|
||||
double originalUpper1 = vwapbands.Upper1.Value;
|
||||
double originalLower1 = vwapbands.Lower1.Value;
|
||||
|
||||
// Make multiple corrections
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var correctionBar = new TBar(DateTime.UtcNow, 150 + i, 160 + i, 140 + i, 155 + i, 2000 + i * 100);
|
||||
vwapbands.Update(correctionBar, isNew: false);
|
||||
}
|
||||
|
||||
// Restore original
|
||||
vwapbands.Update(bars[^1], isNew: false);
|
||||
double restoredVwap = vwapbands.Vwap.Value;
|
||||
double restoredUpper1 = vwapbands.Upper1.Value;
|
||||
double restoredLower1 = vwapbands.Lower1.Value;
|
||||
|
||||
Assert.Equal(originalVwap, restoredVwap, precision: 8);
|
||||
Assert.Equal(originalUpper1, restoredUpper1, precision: 8);
|
||||
Assert.Equal(originalLower1, restoredLower1, precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Reset_ClearsState()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(vwapbands.IsHot);
|
||||
|
||||
vwapbands.Reset();
|
||||
|
||||
Assert.False(vwapbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// WarmupPeriod is 2
|
||||
vwapbands.Update(bars[0]);
|
||||
Assert.False(vwapbands.IsHot);
|
||||
|
||||
vwapbands.Update(bars[1]);
|
||||
Assert.True(vwapbands.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_WarmupPeriod_IsSetCorrectly()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
Assert.Equal(2, vwapbands.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_NaN_Price_UsesLastValidValue()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
vwapbands.Update(new TValue(DateTime.UtcNow, double.NaN), 1000, isNew: true);
|
||||
double afterNaN = vwapbands.Vwap.Value;
|
||||
|
||||
Assert.True(double.IsFinite(afterNaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_NaN_Volume_UsesLastValidValue()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
vwapbands.Update(new TValue(DateTime.UtcNow, 100), double.NaN, isNew: true);
|
||||
double afterNaN = vwapbands.Vwap.Value;
|
||||
|
||||
Assert.True(double.IsFinite(afterNaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
vwapbands.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity), 1000, isNew: true);
|
||||
Assert.True(double.IsFinite(vwapbands.Vwap.Value));
|
||||
|
||||
vwapbands.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity), 1000, isNew: true);
|
||||
Assert.True(double.IsFinite(vwapbands.Vwap.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_BandRelationship_Upper2GreaterThanUpper1GreaterThanLower1GreaterThanLower2()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
|
||||
// Skip first bar where StdDev is 0
|
||||
if (i > 0)
|
||||
{
|
||||
Assert.True(vwapbands.Upper2.Value >= vwapbands.Upper1.Value,
|
||||
$"Upper2 ({vwapbands.Upper2.Value}) should be >= Upper1 ({vwapbands.Upper1.Value})");
|
||||
Assert.True(vwapbands.Upper1.Value >= vwapbands.Vwap.Value,
|
||||
$"Upper1 ({vwapbands.Upper1.Value}) should be >= Vwap ({vwapbands.Vwap.Value})");
|
||||
Assert.True(vwapbands.Vwap.Value >= vwapbands.Lower1.Value,
|
||||
$"Vwap ({vwapbands.Vwap.Value}) should be >= Lower1 ({vwapbands.Lower1.Value})");
|
||||
Assert.True(vwapbands.Lower1.Value >= vwapbands.Lower2.Value,
|
||||
$"Lower1 ({vwapbands.Lower1.Value}) should be >= Lower2 ({vwapbands.Lower2.Value})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_VwapBetweenBands()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
Assert.True(vwapbands.Vwap.Value <= vwapbands.Upper1.Value,
|
||||
$"Vwap ({vwapbands.Vwap.Value}) should be <= Upper1 ({vwapbands.Upper1.Value})");
|
||||
Assert.True(vwapbands.Vwap.Value >= vwapbands.Lower1.Value,
|
||||
$"Vwap ({vwapbands.Vwap.Value}) should be >= Lower1 ({vwapbands.Lower1.Value})");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Width_EqualsUpper1MinusLower1()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
double expectedWidth = vwapbands.Upper1.Value - vwapbands.Lower1.Value;
|
||||
Assert.Equal(expectedWidth, vwapbands.Width.Value, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_SessionReset_ResetsVwapCalculation()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Process first 10 bars
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
double vwapBeforeReset = vwapbands.Vwap.Value;
|
||||
|
||||
// Reset and process next bar - should start fresh
|
||||
var resetBar = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 1000);
|
||||
vwapbands.Update(resetBar, isNew: true, reset: true);
|
||||
|
||||
// After reset, VWAP should be just the new bar's HLC3
|
||||
double expectedVwap = (210 + 190 + 200) / 3.0;
|
||||
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
|
||||
Assert.NotEqual(vwapBeforeReset, vwapbands.Vwap.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_VwapFormula_MatchesExpected()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// Bar 1: price=100, volume=1000
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vwapbands.Update(bar1);
|
||||
Assert.Equal(100.0, vwapbands.Vwap.Value, precision: 10);
|
||||
|
||||
// Bar 2: price=110, volume=2000
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 110, 110, 110, 110, 2000);
|
||||
vwapbands.Update(bar2);
|
||||
|
||||
// VWAP = (100*1000 + 110*2000) / (1000+2000) = 320000/3000 = 106.666...
|
||||
double expectedVwap = (100.0 * 1000 + 110.0 * 2000) / (1000 + 2000);
|
||||
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_StdDevFormula_MatchesExpected()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// Bar 1: price=100, volume=1
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
vwapbands.Update(bar1);
|
||||
|
||||
// Bar 2: price=200, volume=1
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
vwapbands.Update(bar2);
|
||||
|
||||
// VWAP = (100*1 + 200*1) / 2 = 150
|
||||
// MeanP2 = (100²*1 + 200²*1) / 2 = (10000 + 40000) / 2 = 25000
|
||||
// Variance = MeanP2 - VWAP² = 25000 - 22500 = 2500
|
||||
// StdDev = sqrt(2500) = 50
|
||||
Assert.Equal(150.0, vwapbands.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapbands.StdDev.Value, precision: 10);
|
||||
Assert.Equal(200.0, vwapbands.Upper1.Value, precision: 10); // 150 + 50
|
||||
Assert.Equal(100.0, vwapbands.Lower1.Value, precision: 10); // 150 - 50
|
||||
Assert.Equal(250.0, vwapbands.Upper2.Value, precision: 10); // 150 + 100
|
||||
Assert.Equal(50.0, vwapbands.Lower2.Value, precision: 10); // 150 - 100
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var vwapbandsIterative = new Vwapbands(1.0);
|
||||
var vwapbandsBatch = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Iterative
|
||||
var iterativeVwap = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbandsIterative.Update(bars[i]);
|
||||
iterativeVwap.Add(vwapbandsIterative.Vwap.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = vwapbandsBatch.Update(bars);
|
||||
|
||||
// Compare last 50 values
|
||||
for (int i = 50; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(iterativeVwap[i], batchResult[i].Value, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_StaticCalculate_TBarSeries_Works()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var (upper1, lower1, upper2, lower2, vwap, stdev) = Vwapbands.Calculate(bars, 1.0);
|
||||
|
||||
Assert.Equal(50, upper1.Count);
|
||||
Assert.Equal(50, lower1.Count);
|
||||
Assert.Equal(50, upper2.Count);
|
||||
Assert.Equal(50, lower2.Count);
|
||||
Assert.Equal(50, vwap.Count);
|
||||
Assert.Equal(50, stdev.Count);
|
||||
Assert.True(double.IsFinite(vwap.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_SpanCalculate_ValidatesInput()
|
||||
{
|
||||
double[] price = [100, 101, 102, 103, 104];
|
||||
double[] volume = [1000, 1100, 1200, 1300, 1400];
|
||||
double[] upper1 = new double[5];
|
||||
double[] lower1 = new double[5];
|
||||
double[] upper2 = new double[5];
|
||||
double[] lower2 = new double[5];
|
||||
double[] vwap = new double[5];
|
||||
double[] wrongSize = new double[3];
|
||||
|
||||
// Multiplier must be >= MinMultiplier
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 0));
|
||||
|
||||
// All arrays must be same length
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
wrongSize.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_SpanCalculate_HandlesNaN()
|
||||
{
|
||||
double[] price = [100, 101, double.NaN, 103, 104];
|
||||
double[] volume = [1000, 1100, 1200, 1300, 1400];
|
||||
double[] upper1 = new double[5];
|
||||
double[] lower1 = new double[5];
|
||||
double[] upper2 = new double[5];
|
||||
double[] lower2 = new double[5];
|
||||
double[] vwap = new double[5];
|
||||
|
||||
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 1.0);
|
||||
|
||||
foreach (var val in vwap)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"VWAP should be finite, got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_FlatLine_ReturnsSameValueForVwap()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
vwapbands.Update(bar);
|
||||
}
|
||||
|
||||
// With constant price, VWAP should equal the price
|
||||
Assert.Equal(100.0, vwapbands.Vwap.Value, precision: 6);
|
||||
// StdDev of zero variance = 0, so upper = lower = vwap
|
||||
Assert.Equal(vwapbands.Vwap.Value, vwapbands.Upper1.Value, precision: 6);
|
||||
Assert.Equal(vwapbands.Vwap.Value, vwapbands.Lower1.Value, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_HigherMultiplier_WiderBands()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var vwapbands1 = new Vwapbands(1.0);
|
||||
var vwapbands2 = new Vwapbands(2.0);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands1.Update(bars[i]);
|
||||
vwapbands2.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Same VWAP
|
||||
Assert.Equal(vwapbands1.Vwap.Value, vwapbands2.Vwap.Value, precision: 10);
|
||||
|
||||
// Higher multiplier = wider bands
|
||||
Assert.True(vwapbands2.Width.Value > vwapbands1.Width.Value,
|
||||
$"Width with mult=2 ({vwapbands2.Width.Value}) should be > width with mult=1 ({vwapbands1.Width.Value})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_ZeroVolume_DoesNotAffectVwap()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vwapbands.Update(bar1);
|
||||
double vwapAfterBar1 = vwapbands.Vwap.Value;
|
||||
|
||||
// Zero volume bar should not change VWAP
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 0);
|
||||
vwapbands.Update(bar2);
|
||||
double vwapAfterBar2 = vwapbands.Vwap.Value;
|
||||
|
||||
Assert.Equal(vwapAfterBar1, vwapAfterBar2, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_Prime_SetsStateCorrectly()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
double[] history = [100, 101, 102, 103, 104, 105, 106];
|
||||
|
||||
vwapbands.Prime(history);
|
||||
|
||||
Assert.True(vwapbands.IsHot);
|
||||
Assert.True(double.IsFinite(vwapbands.Vwap.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_UpdateTValue_UsesVolumeOfOne()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// Using Update(TValue) should use volume=1
|
||||
vwapbands.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
vwapbands.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
|
||||
// With equal volume (1 each), VWAP = (100+200)/2 = 150
|
||||
Assert.Equal(150.0, vwapbands.Vwap.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_UpdateTSeries_Works()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var result = vwapbands.Update(bars);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.True(double.IsFinite(result.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_UpdateTSeries_PriceOnly_Works()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
TSeries priceSeries = bars.Close;
|
||||
|
||||
var result = vwapbands.Update(priceSeries);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.True(double.IsFinite(result.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapbands_VolumeWeighting_AffectsVwap()
|
||||
{
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// High volume at low price
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10000);
|
||||
vwapbands.Update(bar1);
|
||||
|
||||
// Low volume at high price
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 100);
|
||||
vwapbands.Update(bar2);
|
||||
|
||||
// VWAP should be closer to 100 due to higher volume
|
||||
// VWAP = (100*10000 + 200*100) / (10000+100) = 1020000/10100 ≈ 100.99
|
||||
double expectedVwap = (100.0 * 10000 + 200.0 * 100) / (10000 + 100);
|
||||
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
|
||||
Assert.True(vwapbands.Vwap.Value < 110, "VWAP should be heavily weighted toward 100");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for VWAPBANDS (Volume Weighted Average Price with Standard Deviation Bands).
|
||||
/// VWAP is a standard institutional calculation. Validation focuses on:
|
||||
/// 1. Internal consistency between streaming, batch, and span modes
|
||||
/// 2. Mathematical correctness of VWAP formula
|
||||
/// 3. Standard deviation bands calculation accuracy
|
||||
/// 4. Volume weighting behavior
|
||||
/// </summary>
|
||||
public sealed class VwapbandsValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public VwapbandsValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Streaming_Batch_Consistency()
|
||||
{
|
||||
double[] multipliers = { 0.5, 1.0, 2.0 };
|
||||
|
||||
foreach (var multiplier in multipliers)
|
||||
{
|
||||
// Generate test data
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming mode
|
||||
var streamingVwapbands = new Vwapbands(multiplier);
|
||||
var streamingVwap = new List<double>();
|
||||
var streamingUpper1 = new List<double>();
|
||||
var streamingLower1 = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVwapbands.Update(bars[i]);
|
||||
streamingVwap.Add(streamingVwapbands.Vwap.Value);
|
||||
streamingUpper1.Add(streamingVwapbands.Upper1.Value);
|
||||
streamingLower1.Add(streamingVwapbands.Lower1.Value);
|
||||
}
|
||||
|
||||
// Batch mode
|
||||
var batchVwapbands = new Vwapbands(multiplier);
|
||||
var batchResult = batchVwapbands.Update(bars);
|
||||
|
||||
// Compare last 100 values
|
||||
int compareCount = Math.Min(100, bars.Count - 2);
|
||||
for (int i = bars.Count - compareCount; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingVwap[i], batchResult[i].Value, precision: 10);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("VWAPBANDS Streaming vs Batch consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Streaming_Span_Consistency()
|
||||
{
|
||||
double[] multipliers = { 0.5, 1.0, 2.0 };
|
||||
|
||||
foreach (var multiplier in multipliers)
|
||||
{
|
||||
// Generate test data
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Streaming mode
|
||||
var streamingVwapbands = new Vwapbands(multiplier);
|
||||
var streamingVwap = new List<double>();
|
||||
var streamingUpper1 = new List<double>();
|
||||
var streamingLower1 = new List<double>();
|
||||
var streamingUpper2 = new List<double>();
|
||||
var streamingLower2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVwapbands.Update(bars[i]);
|
||||
streamingVwap.Add(streamingVwapbands.Vwap.Value);
|
||||
streamingUpper1.Add(streamingVwapbands.Upper1.Value);
|
||||
streamingLower1.Add(streamingVwapbands.Lower1.Value);
|
||||
streamingUpper2.Add(streamingVwapbands.Upper2.Value);
|
||||
streamingLower2.Add(streamingVwapbands.Lower2.Value);
|
||||
}
|
||||
|
||||
// Span mode - using HLC3 for price
|
||||
double[] price = new double[bars.Count];
|
||||
double[] volume = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
price[i] = (bars[i].High + bars[i].Low + bars[i].Close) / 3.0;
|
||||
volume[i] = bars[i].Volume;
|
||||
}
|
||||
|
||||
double[] spanVwap = new double[bars.Count];
|
||||
double[] spanUpper1 = new double[bars.Count];
|
||||
double[] spanLower1 = new double[bars.Count];
|
||||
double[] spanUpper2 = new double[bars.Count];
|
||||
double[] spanLower2 = new double[bars.Count];
|
||||
|
||||
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
spanUpper1.AsSpan(), spanLower1.AsSpan(),
|
||||
spanUpper2.AsSpan(), spanLower2.AsSpan(),
|
||||
spanVwap.AsSpan(), multiplier);
|
||||
|
||||
// Compare last 100 values
|
||||
int compareCount = Math.Min(100, bars.Count - 2);
|
||||
for (int i = bars.Count - compareCount; i < bars.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingVwap[i], spanVwap[i], precision: 10);
|
||||
Assert.Equal(streamingUpper1[i], spanUpper1[i], precision: 10);
|
||||
Assert.Equal(streamingLower1[i], spanLower1[i], precision: 10);
|
||||
Assert.Equal(streamingUpper2[i], spanUpper2[i], precision: 10);
|
||||
Assert.Equal(streamingLower2[i], spanLower2[i], precision: 10);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("VWAPBANDS Streaming vs Span consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_VwapFormula_ManualCalculation()
|
||||
{
|
||||
// Manually verify VWAP calculation: sum(price × volume) / sum(volume)
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// Create known test data
|
||||
var testData = new (double price, double volume)[]
|
||||
{
|
||||
(100.0, 1000),
|
||||
(102.0, 1500),
|
||||
(98.0, 800),
|
||||
(105.0, 2000),
|
||||
(103.0, 1200)
|
||||
};
|
||||
|
||||
double sumPV = 0;
|
||||
double sumVol = 0;
|
||||
|
||||
for (int i = 0; i < testData.Length; i++)
|
||||
{
|
||||
var (price, vol) = testData[i];
|
||||
sumPV += price * vol;
|
||||
sumVol += vol;
|
||||
double expectedVwap = sumPV / sumVol;
|
||||
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), price, price, price, price, vol);
|
||||
vwapbands.Update(bar);
|
||||
|
||||
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
|
||||
_output.WriteLine($"Bar {i + 1}: Price={price}, Vol={vol}, Expected VWAP={expectedVwap:F4}, Actual={vwapbands.Vwap.Value:F4}");
|
||||
}
|
||||
|
||||
_output.WriteLine("VWAPBANDS formula validation completed successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StdDevFormula_ManualCalculation()
|
||||
{
|
||||
// Manually verify variance calculation: (sum(price² × vol) / sum(vol)) - VWAP²
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// Create test data with known variance
|
||||
var testData = new (double price, double volume)[]
|
||||
{
|
||||
(100.0, 1.0),
|
||||
(200.0, 1.0) // Equal weights, max variance
|
||||
};
|
||||
|
||||
for (int i = 0; i < testData.Length; i++)
|
||||
{
|
||||
var (price, vol) = testData[i];
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), price, price, price, price, vol);
|
||||
vwapbands.Update(bar);
|
||||
}
|
||||
|
||||
// After 2 bars: VWAP = (100 + 200) / 2 = 150
|
||||
// MeanP2 = (100² + 200²) / 2 = (10000 + 40000) / 2 = 25000
|
||||
// Variance = 25000 - 150² = 25000 - 22500 = 2500
|
||||
// StdDev = sqrt(2500) = 50
|
||||
|
||||
Assert.Equal(150.0, vwapbands.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapbands.StdDev.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPBANDS StdDev formula validation completed successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BandCharacteristics()
|
||||
{
|
||||
// Verify core VWAPBANDS characteristics:
|
||||
// 1. Upper2 >= Upper1 >= VWAP >= Lower1 >= Lower2
|
||||
// 2. Bands are symmetric around VWAP
|
||||
// 3. Band width is proportional to StdDev
|
||||
|
||||
double multiplier = 1.0;
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var vwapbands = new Vwapbands(multiplier);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
|
||||
// Skip first bar where StdDev is 0
|
||||
if (i > 0)
|
||||
{
|
||||
// Upper2 >= Upper1 >= VWAP >= Lower1 >= Lower2
|
||||
Assert.True(vwapbands.Upper2.Value >= vwapbands.Upper1.Value,
|
||||
$"Upper2 ({vwapbands.Upper2.Value}) should be >= Upper1 ({vwapbands.Upper1.Value})");
|
||||
Assert.True(vwapbands.Upper1.Value >= vwapbands.Vwap.Value,
|
||||
$"Upper1 ({vwapbands.Upper1.Value}) should be >= VWAP ({vwapbands.Vwap.Value})");
|
||||
Assert.True(vwapbands.Vwap.Value >= vwapbands.Lower1.Value,
|
||||
$"VWAP ({vwapbands.Vwap.Value}) should be >= Lower1 ({vwapbands.Lower1.Value})");
|
||||
Assert.True(vwapbands.Lower1.Value >= vwapbands.Lower2.Value,
|
||||
$"Lower1 ({vwapbands.Lower1.Value}) should be >= Lower2 ({vwapbands.Lower2.Value})");
|
||||
|
||||
// Symmetry: Upper1 - VWAP == VWAP - Lower1
|
||||
double upperOffset = vwapbands.Upper1.Value - vwapbands.Vwap.Value;
|
||||
double lowerOffset = vwapbands.Vwap.Value - vwapbands.Lower1.Value;
|
||||
Assert.Equal(upperOffset, lowerOffset, precision: 10);
|
||||
|
||||
// Width = 2 × multiplier × StdDev
|
||||
double expectedWidth = 2.0 * multiplier * vwapbands.StdDev.Value;
|
||||
Assert.Equal(expectedWidth, vwapbands.Width.Value, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("VWAPBANDS band characteristics validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_VolumeWeighting()
|
||||
{
|
||||
// Verify that VWAP is properly volume-weighted
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// High volume at low price, low volume at high price
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10000);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 100);
|
||||
|
||||
vwapbands.Update(bar1);
|
||||
vwapbands.Update(bar2);
|
||||
|
||||
// VWAP should be closer to 100 (high volume price)
|
||||
// VWAP = (100 × 10000 + 200 × 100) / (10000 + 100) = 1020000 / 10100 ≈ 100.99
|
||||
double expectedVwap = (100.0 * 10000 + 200.0 * 100) / (10000 + 100);
|
||||
Assert.Equal(expectedVwap, vwapbands.Vwap.Value, precision: 10);
|
||||
Assert.True(vwapbands.Vwap.Value < 110, "VWAP should be heavily weighted toward 100");
|
||||
|
||||
_output.WriteLine($"Volume weighting verified: VWAP = {vwapbands.Vwap.Value:F4} (expected ≈ 100.99)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NaN_Handling()
|
||||
{
|
||||
double multiplier = 1.0;
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var vwapbands = new Vwapbands(multiplier);
|
||||
int nanCount = 0;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
if (i == 50 || i == 51)
|
||||
{
|
||||
// Inject NaN price
|
||||
vwapbands.Update(new TValue(bars[i].Time, double.NaN), bars[i].Volume, isNew: true);
|
||||
nanCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(vwapbands.Vwap.Value),
|
||||
$"VWAP should be finite after NaN at index {i}");
|
||||
Assert.True(double.IsFinite(vwapbands.Upper1.Value),
|
||||
$"Upper1 should be finite after NaN at index {i}");
|
||||
Assert.True(double.IsFinite(vwapbands.Lower1.Value),
|
||||
$"Lower1 should be finite after NaN at index {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine($"VWAPBANDS NaN handling validated ({nanCount} NaN values handled)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BarCorrection()
|
||||
{
|
||||
double multiplier = 1.0;
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var vwapbands = new Vwapbands(multiplier);
|
||||
|
||||
// Process all bars
|
||||
for (int i = 0; i < bars.Count - 1; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Record state before last bar
|
||||
vwapbands.Update(bars[^1]);
|
||||
double originalVwap = vwapbands.Vwap.Value;
|
||||
double originalUpper1 = vwapbands.Upper1.Value;
|
||||
|
||||
// Correct last bar with different value
|
||||
var correctedBar = new TBar(bars[^1].Time, 200, 210, 190, 200, 5000);
|
||||
vwapbands.Update(correctedBar, isNew: false);
|
||||
double correctedVwap = vwapbands.Vwap.Value;
|
||||
|
||||
// Should be different
|
||||
Assert.NotEqual(originalVwap, correctedVwap);
|
||||
|
||||
// Restore original bar
|
||||
vwapbands.Update(bars[^1], isNew: false);
|
||||
double restoredVwap = vwapbands.Vwap.Value;
|
||||
double restoredUpper1 = vwapbands.Upper1.Value;
|
||||
|
||||
// Should match original
|
||||
Assert.Equal(originalVwap, restoredVwap, precision: 10);
|
||||
Assert.Equal(originalUpper1, restoredUpper1, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPBANDS bar correction validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SessionReset()
|
||||
{
|
||||
// Verify that session reset properly clears VWAP accumulation
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// Process first session
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
}
|
||||
double session1Vwap = vwapbands.Vwap.Value;
|
||||
|
||||
// Reset for new session
|
||||
var resetBar = new TBar(DateTime.UtcNow, 200, 200, 200, 200, 1000);
|
||||
vwapbands.Update(resetBar, isNew: true, reset: true);
|
||||
|
||||
// After reset, VWAP should be just the reset bar's price
|
||||
Assert.Equal(200.0, vwapbands.Vwap.Value, precision: 10);
|
||||
Assert.NotEqual(session1Vwap, vwapbands.Vwap.Value);
|
||||
|
||||
_output.WriteLine("VWAPBANDS session reset validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DifferentMultipliers()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
double[] multipliers = { 0.5, 1.0, 1.5, 2.0 };
|
||||
var avgWidths = new List<double>();
|
||||
|
||||
foreach (var multiplier in multipliers)
|
||||
{
|
||||
var vwapbands = new Vwapbands(multiplier);
|
||||
double sumWidth = 0;
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
if (vwapbands.IsHot)
|
||||
{
|
||||
sumWidth += vwapbands.Width.Value;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
double avgWidth = count > 0 ? sumWidth / count : 0;
|
||||
avgWidths.Add(avgWidth);
|
||||
_output.WriteLine($"Multiplier {multiplier}: Average width = {avgWidth:F4}");
|
||||
}
|
||||
|
||||
// Higher multipliers should give wider bands
|
||||
for (int i = 1; i < avgWidths.Count; i++)
|
||||
{
|
||||
Assert.True(avgWidths[i] > avgWidths[i - 1],
|
||||
$"Higher multiplier should produce wider bands");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroVolumeBars()
|
||||
{
|
||||
// Zero volume bars should not affect VWAP
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
// First bar with volume
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vwapbands.Update(bar1);
|
||||
double vwapAfterBar1 = vwapbands.Vwap.Value;
|
||||
|
||||
// Multiple zero-volume bars with different prices
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var zeroVolBar = new TBar(DateTime.UtcNow.AddMinutes(i + 1), 200 + i * 10, 200 + i * 10, 200 + i * 10, 200 + i * 10, 0);
|
||||
vwapbands.Update(zeroVolBar);
|
||||
}
|
||||
|
||||
// VWAP should remain unchanged
|
||||
Assert.Equal(vwapAfterBar1, vwapbands.Vwap.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPBANDS zero volume handling validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ConstantPrice_ZeroStdDev()
|
||||
{
|
||||
// With constant price, StdDev should be 0 and all bands should equal VWAP
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
vwapbands.Update(bar);
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, vwapbands.Vwap.Value, precision: 6);
|
||||
Assert.Equal(0.0, vwapbands.StdDev.Value, precision: 6);
|
||||
Assert.Equal(100.0, vwapbands.Upper1.Value, precision: 6);
|
||||
Assert.Equal(100.0, vwapbands.Lower1.Value, precision: 6);
|
||||
Assert.Equal(100.0, vwapbands.Upper2.Value, precision: 6);
|
||||
Assert.Equal(100.0, vwapbands.Lower2.Value, precision: 6);
|
||||
|
||||
_output.WriteLine("VWAPBANDS constant price validation completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_Performance()
|
||||
{
|
||||
// Process large dataset to verify stability
|
||||
var vwapbands = new Vwapbands(1.0);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(10000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapbands.Update(bars[i]);
|
||||
|
||||
// Verify all values remain finite
|
||||
Assert.True(double.IsFinite(vwapbands.Vwap.Value), $"VWAP not finite at index {i}");
|
||||
Assert.True(double.IsFinite(vwapbands.StdDev.Value), $"StdDev not finite at index {i}");
|
||||
Assert.True(double.IsFinite(vwapbands.Upper1.Value), $"Upper1 not finite at index {i}");
|
||||
Assert.True(double.IsFinite(vwapbands.Lower1.Value), $"Lower1 not finite at index {i}");
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
_output.WriteLine($"Processed {bars.Count} bars in {sw.ElapsedMilliseconds}ms ({bars.Count * 1000.0 / sw.ElapsedMilliseconds:F0} bars/sec)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StaticCalculate_TBarSeries()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
var (upper1, lower1, upper2, lower2, vwap, stdev) = Vwapbands.Calculate(bars, 1.0);
|
||||
|
||||
Assert.Equal(bars.Count, upper1.Count);
|
||||
Assert.Equal(bars.Count, lower1.Count);
|
||||
Assert.Equal(bars.Count, upper2.Count);
|
||||
Assert.Equal(bars.Count, lower2.Count);
|
||||
Assert.Equal(bars.Count, vwap.Count);
|
||||
Assert.Equal(bars.Count, stdev.Count);
|
||||
|
||||
// Verify streaming matches static
|
||||
var streamingVwapbands = new Vwapbands(1.0);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVwapbands.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(streamingVwapbands.Vwap.Value, vwap.Last.Value, precision: 10);
|
||||
Assert.Equal(streamingVwapbands.Upper1.Value, upper1.Last.Value, precision: 10);
|
||||
Assert.Equal(streamingVwapbands.Lower1.Value, lower1.Last.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPBANDS static Calculate validated successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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
|
||||
/// </remarks>
|
||||
[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;
|
||||
|
||||
/// <summary>
|
||||
/// Upper band at 1σ (VWAP + mult × StdDev)
|
||||
/// </summary>
|
||||
public TValue Upper1 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Lower band at 1σ (VWAP - mult × StdDev)
|
||||
/// </summary>
|
||||
public TValue Lower1 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Upper band at 2σ (VWAP + 2 × mult × StdDev)
|
||||
/// </summary>
|
||||
public TValue Upper2 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Lower band at 2σ (VWAP - 2 × mult × StdDev)
|
||||
/// </summary>
|
||||
public TValue Lower2 { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// VWAP value (center line)
|
||||
/// </summary>
|
||||
public TValue Vwap { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Standard deviation of volume-weighted prices
|
||||
/// </summary>
|
||||
public TValue StdDev { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Band width (Upper1 - Lower1 = 2 × mult × StdDev)
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a new bar. Uses HLC3 as price and bar volume.
|
||||
/// </summary>
|
||||
/// <param name="bar">The input bar with OHLCV data</param>
|
||||
/// <param name="isNew">True for new bar, false for bar correction</param>
|
||||
/// <param name="reset">True to reset VWAP calculation (e.g., new session)</param>
|
||||
/// <returns>The VWAP value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true, bool reset = false)
|
||||
{
|
||||
double hlc3 = (bar.High + bar.Low + bar.Close) / 3.0;
|
||||
return Update(new TValue(bar.Time, hlc3), bar.Volume, isNew, reset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with price and volume values.
|
||||
/// </summary>
|
||||
/// <param name="input">Price value (typically HLC3)</param>
|
||||
/// <param name="volume">Volume value</param>
|
||||
/// <param name="isNew">True for new bar, false for bar correction</param>
|
||||
/// <param name="reset">True to reset VWAP calculation (e.g., new session)</param>
|
||||
/// <returns>The VWAP value</returns>
|
||||
[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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
return Update(input, 1.0, isNew, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a bar series.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a price series (uses volume=1 for each bar).
|
||||
/// </summary>
|
||||
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<double> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates VWAP Bands for a bar series.
|
||||
/// </summary>
|
||||
/// <returns>Tuple of (Upper1, Lower1, Upper2, Lower2, Vwap, StdDev)</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates VWAP Bands using span arrays.
|
||||
/// </summary>
|
||||
public static void Calculate(
|
||||
ReadOnlySpan<double> price,
|
||||
ReadOnlySpan<double> volume,
|
||||
Span<double> upper1,
|
||||
Span<double> lower1,
|
||||
Span<double> upper2,
|
||||
Span<double> lower2,
|
||||
Span<double> vwap,
|
||||
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)
|
||||
{
|
||||
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;
|
||||
upper1[i] = vwapVal + multiplier * stdev;
|
||||
lower1[i] = vwapVal - multiplier * stdev;
|
||||
upper2[i] = vwapVal + 2.0 * multiplier * stdev;
|
||||
lower2[i] = vwapVal - 2.0 * multiplier * stdev;
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
-163
@@ -1,240 +1,192 @@
|
||||
# VWAPBANDS: VWAP Bands
|
||||
# VWAPBANDS: Volume Weighted Average Price with Dual Standard Deviation Bands
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
VWAP Bands (VWAPBANDS) is a channel indicator that extends the Volume Weighted Average Price (VWAP) concept by adding standard deviation bands above and below the central VWAP line. This indicator combines the volume-weighted fairness concept of VWAP with statistical volatility measurements, creating dynamic support and resistance levels that reflect both price-volume relationships and market volatility.
|
||||
Volume Weighted Average Price Bands (VWAPBANDS) extends the standard VWAP indicator by adding two levels of standard deviation bands: ±1σ and ±2σ. This dual-band approach provides traders with a complete volatility framework, distinguishing between normal price fluctuations (within 1σ bands, ~68% of price action) and statistically significant moves (beyond 2σ bands, ~95% confidence level).
|
||||
|
||||
Unlike traditional moving average-based bands, VWAPBANDS uses volume-weighted variance calculations to determine band width, making the indicator particularly sensitive to volume-driven price movements. The bands automatically adjust to market conditions while maintaining their statistical significance, providing traders with reliable levels for identifying overbought/oversold conditions and potential reversal points.
|
||||
Unlike simple VWAP with single bands, VWAPBANDS creates distinct trading zones. The region between VWAP and ±1σ represents the "normal trading zone" where institutional algorithms typically execute. The area between ±1σ and ±2σ serves as an "alert zone" indicating elevated but not extreme deviation. Price beyond ±2σ signals statistically significant moves that often precede reversals or continuation breakouts.
|
||||
|
||||
The indicator maintains cumulative calculations from session start, with optional reset capability for multi-session analysis. Volume weighting ensures that prices where significant trading activity occurred contribute proportionally more to both the average and the deviation calculations, making VWAPBANDS particularly valuable for institutional traders benchmarking execution quality.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Volume-weighted statistics:** Uses volume data to weight price observations, giving more importance to high-volume periods
|
||||
* **Session-based calculation:** Resets calculations based on configurable time periods (daily, hourly, etc.)
|
||||
* **Statistical significance:** Bands represent 1 and 2 standard deviations from the volume-weighted mean
|
||||
* **Dynamic adaptation:** Band width adjusts automatically based on volume-weighted price variance
|
||||
* **Multi-timeframe flexibility:** Supports various reset intervals from minutes to months
|
||||
* **Institutional relevance:** Reflects the same VWAP calculations used by institutional traders
|
||||
* **Dual Band System:** Provides two standard deviation levels (1σ and 2σ) creating three distinct trading zones above and below VWAP, enabling graduated position sizing and risk assessment based on statistical probability.
|
||||
|
||||
The key advantage of VWAPBANDS is its ability to combine the fairness concept of VWAP (where institutional orders are often benchmarked) with volatility-based support and resistance levels, making it particularly valuable for understanding institutional price levels and market structure.
|
||||
* **Volume-Weighted Statistics:** Both the average price and the standard deviation are calculated using volume weights, ensuring that high-volume price levels contribute more to all statistical measures.
|
||||
|
||||
* **HLC3 Typical Price:** Uses the average of high, low, and close prices as the representative price for each bar, providing a balanced measure that considers the full trading range.
|
||||
|
||||
* **Session Reset:** Optional reset capability allows VWAP to restart calculations at session boundaries, keeping the indicator relevant to current market conditions.
|
||||
|
||||
* **Width Measurement:** The full channel width (Upper2 - Lower2) provides a single metric for overall volatility, useful for comparing volatility across sessions or instruments.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
| ------ | ------ | ------ | ------ |
|
||||
| Source | HLC3 | Price data used for VWAP calculation | Use Close for end-of-period analysis, HLC3 for comprehensive price representation |
|
||||
| Session Reset | 1D | Time period for VWAP calculation reset | Match to trading strategy timeframe: intraday (1H, 4H), swing (1D), position (1W) |
|
||||
| StdDev Multiplier | 1.0 | Distance of primary bands from VWAP in standard deviations | Increase for wider bands in volatile markets, decrease for tighter levels |
|
||||
| Show 2nd Bands | True | Display secondary bands at 2x multiplier distance | Disable for cleaner charts, enable for additional confluence levels |
|
||||
| Multiplier | 1.0 | Scales the standard deviation for band width | Use 1.0 for standard statistical bands, 2.0 for wider bands on volatile instruments, 0.5 for tighter bands on low-volatility instruments |
|
||||
|
||||
**Pro Tip:** Use daily reset for swing trading strategies, hourly reset for intraday scalping, and weekly reset for position trading to align the indicator with your trading timeframe.
|
||||
**Pro Tip:** The multiplier affects all bands proportionally. With multiplier = 1.0, Upper1/Lower1 are at ±1σ and Upper2/Lower2 are at ±2σ. Setting multiplier = 2.0 places them at ±2σ and ±4σ respectively. For most trading applications, keep the multiplier at 1.0 and interpret the bands as standard statistical levels.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
VWAPBANDS calculates the volume-weighted average price from the session start, then computes the volume-weighted variance of prices around this average. Standard deviation bands are plotted at 1x and 2x the multiplier distance from VWAP.
|
||||
**Explanation:**
|
||||
VWAPBANDS calculates a volume-weighted average price with two levels of standard deviation bands. The implementation maintains three running sums: cumulative price×volume, cumulative volume, and cumulative price²×volume. These enable O(1) streaming updates while providing mathematically correct variance calculation.
|
||||
|
||||
**Technical formula:**
|
||||
1. VWAP = Σ(Price × Volume) / Σ(Volume)
|
||||
2. Volume-Weighted Variance = Σ(Price² × Volume) / Σ(Volume) - VWAP²
|
||||
3. Standard Deviation = √(Volume-Weighted Variance)
|
||||
4. Upper Band = VWAP + (Multiplier × Standard Deviation)
|
||||
5. Lower Band = VWAP - (Multiplier × Standard Deviation)
|
||||
|
||||
**Detailed calculation steps:**
|
||||
1. Initialize cumulative sums at session start (price×volume, volume, price²×volume)
|
||||
2. For each bar, add current values to cumulative sums if volume > 0
|
||||
3. Calculate VWAP as ratio of cumulative price×volume to cumulative volume
|
||||
4. Compute volume-weighted second moment and subtract VWAP squared for variance
|
||||
5. Take square root of variance to get standard deviation
|
||||
6. Plot bands at specified multiples of standard deviation from VWAP
|
||||
```
|
||||
Step 1: Calculate typical price for each bar
|
||||
Typical Price = (High + Low + Close) / 3
|
||||
|
||||
> 🔍 **Technical Note:** The implementation uses session-based resets to ensure VWAP calculations align with market structure. Volume-weighted variance provides more accurate volatility measurement than simple price variance, as it reflects the actual trading intensity at different price levels.
|
||||
Step 2: Accumulate weighted sums (optionally reset on session boundary)
|
||||
sum_pv = Σ(Price × Volume)
|
||||
sum_vol = Σ(Volume)
|
||||
sum_pv2 = Σ(Price² × Volume)
|
||||
|
||||
Step 3: Calculate VWAP
|
||||
VWAP = sum_pv / sum_vol
|
||||
|
||||
Step 4: Calculate volume-weighted variance and standard deviation
|
||||
Variance = (sum_pv2 / sum_vol) - VWAP²
|
||||
StdDev = √(max(0, Variance))
|
||||
|
||||
Step 5: Calculate dual bands
|
||||
Upper1 = VWAP + (1 × Multiplier × StdDev)
|
||||
Lower1 = VWAP - (1 × Multiplier × StdDev)
|
||||
Upper2 = VWAP + (2 × Multiplier × StdDev)
|
||||
Lower2 = VWAP - (2 × Multiplier × StdDev)
|
||||
|
||||
Step 6: Calculate channel width
|
||||
Width = Upper2 - Lower2 = 4 × Multiplier × StdDev
|
||||
```
|
||||
|
||||
> 🔍 **Technical Note:** The variance formula uses the algebraic identity Var(X) = E[X²] - E[X]², which is numerically stable and computationally efficient for streaming updates. The implementation guards against negative variance (which can occur due to floating-point precision) by using max(0, variance) before taking the square root.
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
VWAPBANDS provides multiple layers of market analysis:
|
||||
**Zone-Based Trading:**
|
||||
|
||||
* **VWAP Line Analysis:**
|
||||
* Price above VWAP: Bullish bias, buyers in control above fair value
|
||||
* Price below VWAP: Bearish bias, sellers in control below fair value
|
||||
* Price oscillating around VWAP: Balanced market, fair value region
|
||||
* **Inside ±1σ (Normal Zone):** ~68% of price action. Normal trading range where institutional algorithms execute without concern. Low signal value for mean reversion.
|
||||
|
||||
* **Band Interaction Signals:**
|
||||
* Price touching upper 1σ band: Potential resistance, consider profit-taking
|
||||
* Price touching lower 1σ band: Potential support, consider accumulation
|
||||
* Price beyond 2σ bands: Extreme conditions, potential mean reversion opportunity
|
||||
* Price consistently above/below bands: Strong trend continuation signal
|
||||
* **Between ±1σ and ±2σ (Alert Zone):** ~27% of price action. Elevated deviation suggesting caution. Consider reducing position size or preparing for reversal.
|
||||
|
||||
* **Band Width Analysis:**
|
||||
* Expanding bands: Increasing volatility, larger price movements expected
|
||||
* Contracting bands: Decreasing volatility, potential breakout setup
|
||||
* Stable band width: Consistent volatility environment
|
||||
* **Beyond ±2σ (Extreme Zone):** ~5% of price action. Statistically significant move. High probability of mean reversion or continuation breakout.
|
||||
|
||||
* **Volume-Price Relationship:**
|
||||
* High volume near bands: Increased significance of support/resistance levels
|
||||
* Low volume near bands: Potential for false breakouts or weak reversals
|
||||
* Volume expansion with band breaks: Confirmation of directional moves
|
||||
**Institutional Execution Context:**
|
||||
|
||||
## Trading Applications
|
||||
* Price at VWAP represents "fair" execution for institutional orders
|
||||
* Execution below VWAP on buys (or above on sells) is considered favorable
|
||||
* The ±1σ bands define the acceptable execution range for most algorithms
|
||||
* Price beyond ±2σ may trigger algorithmic rebalancing
|
||||
|
||||
**Mean Reversion Strategy:**
|
||||
* Buy when price touches or exceeds lower 1σ band with volume confirmation
|
||||
* Sell when price reaches VWAP or upper bands
|
||||
* Use 2σ bands for extreme mean reversion opportunities
|
||||
* Set stops beyond 2σ levels to account for extended moves
|
||||
**Mean Reversion Signals:**
|
||||
|
||||
**Trend Following Strategy:**
|
||||
* Enter long positions when price breaks above upper bands with volume
|
||||
* Enter short positions when price breaks below lower bands with volume
|
||||
* Use VWAP as dynamic support/resistance in trending markets
|
||||
* Trail stops using the opposite band or VWAP line
|
||||
* Touch of Upper2 with declining momentum → Potential short entry
|
||||
* Touch of Lower2 with rising momentum → Potential long entry
|
||||
* Price returning to VWAP from ±2σ → Classic mean reversion play
|
||||
* Multiple touches of ±2σ without breakout → Ranging market, fade extremes
|
||||
|
||||
**Institutional Level Trading:**
|
||||
* Monitor price action around VWAP for institutional interest
|
||||
* Look for volume spikes when price approaches VWAP after extended moves
|
||||
* Use VWAP as benchmark for order execution efficiency
|
||||
* Identify accumulation/distribution phases based on VWAP interaction
|
||||
**Trend Following Signals:**
|
||||
|
||||
**Breakout Strategy:**
|
||||
* Monitor periods of contracting bands for potential breakouts
|
||||
* Enter positions on volume-confirmed breaks beyond 1σ bands
|
||||
* Target 2σ bands for profit-taking on breakout moves
|
||||
* Use failed breakouts as contrarian signals
|
||||
* Price consistently above Upper1 → Strong bullish trend, buy pullbacks to VWAP
|
||||
* Price consistently below Lower1 → Strong bearish trend, sell rallies to VWAP
|
||||
* Breakout above Upper2 with increasing volume → Potential trend continuation
|
||||
* Sequential touches of Upper1 → Upper2 → Higher → Trend acceleration
|
||||
|
||||
## Signal Combinations
|
||||
**Volatility Analysis:**
|
||||
|
||||
**High-Probability Long Signals:**
|
||||
* Price bounces off lower 1σ band with increasing volume
|
||||
* Price reclaims VWAP after period below with strong volume
|
||||
* Bullish divergence between price and volume at lower bands
|
||||
* Multiple timeframe VWAP alignment supporting upward bias
|
||||
* Wide bands (large Width) → High volatility, larger position sizing risk
|
||||
* Narrow bands (small Width) → Low volatility, potential breakout setup
|
||||
* Expanding bands → Increasing volatility, trend may be developing
|
||||
* Contracting bands → Decreasing volatility, consolidation phase
|
||||
|
||||
**High-Probability Short Signals:**
|
||||
* Price fails at upper 1σ band with declining volume
|
||||
* Price breaks below VWAP after period above with strong volume
|
||||
* Bearish divergence between price and volume at upper bands
|
||||
* Multiple timeframe VWAP alignment supporting downward bias
|
||||
## Limitations and Considerations
|
||||
|
||||
**Consolidation Warnings:**
|
||||
* Price oscillating between narrow bands around VWAP
|
||||
* Decreasing volume with price approaching bands
|
||||
* Multiple false breakouts beyond bands
|
||||
* Band width contracting significantly
|
||||
* **Intraday Focus:** VWAPBANDS is primarily designed for intraday analysis. Without session resets, cumulative calculations can become less responsive on multi-day charts as early data dominates.
|
||||
|
||||
## Advanced Techniques
|
||||
* **Volume Dependency:** The indicator requires reliable volume data. On instruments with unreliable or no volume (some forex, index CFDs), VWAP-based indicators may not provide accurate signals.
|
||||
|
||||
**Multi-Timeframe Analysis:**
|
||||
* Use higher timeframe VWAPBANDS for major support/resistance levels
|
||||
* Combine daily VWAP with intraday bands for precision timing
|
||||
* Look for confluence between different session VWAP levels
|
||||
* Identify key levels where multiple timeframe VWAPs converge
|
||||
* **Early Session Instability:** At session start, VWAP and bands can be volatile due to limited data. Consider waiting 30-60 minutes for stabilization.
|
||||
|
||||
**Volume Profile Integration:**
|
||||
* Combine VWAPBANDS with volume profile for enhanced context
|
||||
* Identify high-volume nodes near VWAP levels
|
||||
* Use volume-at-price data to validate band significance
|
||||
* Monitor institutional order flow around VWAP levels
|
||||
* **Gap Sensitivity:** Large overnight gaps distort morning VWAP calculations. The indicator needs time to incorporate sufficient volume for meaningful statistics.
|
||||
|
||||
**Session-Specific Analysis:**
|
||||
* Analyze different session reset periods for various market conditions
|
||||
* Use overnight VWAP for gap analysis and fair value assessment
|
||||
* Apply weekly VWAP for longer-term institutional benchmarking
|
||||
* Implement monthly VWAP for portfolio rebalancing levels
|
||||
* **No Directional Prediction:** VWAPBANDS identifies deviation from mean, not direction. Use with momentum indicators or price action for directional bias.
|
||||
|
||||
* **Multiplier Interpretation:** Non-standard multiplier values (≠1.0) change the statistical meaning of bands. Document your multiplier choice when backtesting or sharing strategies.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, per Bar)
|
||||
|
||||
VWAP Bands uses cumulative sums for volume-weighted statistics:
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 8 | 1 | 8 |
|
||||
| ADD/SUB | 9 | 1 | 9 |
|
||||
| MUL | 6 | 3 | 18 |
|
||||
| DIV | 3 | 15 | 45 |
|
||||
| SQRT | 1 | 15 | 15 |
|
||||
| **Total** | **18** | — | **~86 cycles** |
|
||||
| **Total** | **19** | — | **~87 cycles** |
|
||||
|
||||
**Breakdown:**
|
||||
- Cumulative sum updates (pv, vol, pv²): 3 ADD + 3 MUL = 12 cycles
|
||||
- VWAP calculation: 1 DIV = 15 cycles
|
||||
- Variance (E[X²] - E[X]²): 1 DIV + 1 MUL + 1 SUB = 19 cycles
|
||||
- Std dev + bands: 1 SQRT + 1 MUL + 4 ADD = 22 cycles
|
||||
|
||||
**Session reset:** Adds 1 CMP per bar for reset detection (~1 cycle).
|
||||
* Typical price (HLC3): 2 ADD + 1 DIV = 17 cycles
|
||||
* Running sums (pv, vol, pv²): 3 ADD + 3 MUL = 12 cycles
|
||||
* VWAP + variance: 2 DIV + 1 MUL + 1 SUB = 35 cycles
|
||||
* StdDev: 1 SQRT = 15 cycles
|
||||
* Dual bands + width: 4 ADD + 2 MUL = 10 cycles (with FMA optimization)
|
||||
|
||||
### Complexity Analysis
|
||||
|
||||
| Mode | Complexity | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Streaming | O(1) | Cumulative sums with session reset |
|
||||
| Batch | O(n) | Linear scan |
|
||||
| Streaming | O(1) | Running sums, constant per bar |
|
||||
| Batch | O(n) | Linear scan required |
|
||||
|
||||
**Memory**: ~48 bytes (cumulative sums for pv, vol, pv², session state)
|
||||
|
||||
### SIMD Analysis
|
||||
|
||||
| Optimization | Applicable | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| AVX2 vectorization | Partial | Cumulative sums vectorizable within session |
|
||||
| FMA | ✅ | `price * volume` pattern |
|
||||
| Batch parallelism | ❌ | Cumulative sums create dependencies |
|
||||
|
||||
**Note:** Session resets create sequential boundaries that limit SIMD optimization across sessions.
|
||||
**Memory:** ~80 bytes per instance (state struct with running sums, last valid values, and output properties)
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 9/10 | Volume-weighted mean is statistically optimal |
|
||||
| **Timeliness** | 7/10 | Cumulative nature creates lag late in session |
|
||||
| **Overshoot** | 8/10 | Volume weighting stabilizes extremes |
|
||||
| **Smoothness** | 7/10 | Can be choppy early in session |
|
||||
| **Accuracy** | 10/10 | Mathematically exact volume-weighted statistics |
|
||||
| **Timeliness** | 7/10 | Incorporates all session data, becomes stable over time |
|
||||
| **Overshoot** | 9/10 | Bands adapt to actual volume-weighted volatility |
|
||||
| **Smoothness** | 9/10 | Running sums provide inherent smoothing |
|
||||
|
||||
## Limitations and Considerations
|
||||
## Validation
|
||||
|
||||
* **Session dependency:** Reset timing significantly affects indicator behavior and relevance
|
||||
* **Volume quality:** Requires accurate volume data; may be less reliable in low-volume periods
|
||||
* **Lag component:** VWAP calculations create some lag, especially early in sessions
|
||||
* **Market structure:** Most effective in liquid markets with consistent volume patterns
|
||||
* **Gap handling:** Overnight gaps can affect VWAP relevance at session open
|
||||
* **False signals:** Low-volume periods may produce unreliable band interactions
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | No VWAP bands implementation |
|
||||
| **Skender** | N/A | Has VWAP but not with dual bands |
|
||||
| **Tulip** | N/A | No VWAP implementation |
|
||||
| **Ooples** | N/A | No dual-band VWAP |
|
||||
| **TradingView** | ✅ | Reference: vwapbands.pine |
|
||||
|
||||
## Comparison with Related Indicators
|
||||
## Common Pitfalls
|
||||
|
||||
**VWAPBANDS vs. Bollinger Bands:**
|
||||
* VWAPBANDS: Volume-weighted center line with volume-weighted variance
|
||||
* Bollinger Bands: Simple moving average center with price-based standard deviation
|
||||
1. **Session Reset Timing:** Failing to reset VWAP at session boundaries causes stale historical data to dominate calculations. Use the reset parameter for intraday strategies.
|
||||
|
||||
**VWAPBANDS vs. Keltner Channels:**
|
||||
* VWAPBANDS: VWAP-based with statistical variance measurements
|
||||
* Keltner Channels: EMA-based with ATR-derived band width
|
||||
2. **Multiplier Confusion:** The multiplier scales both band levels proportionally. Multiplier = 2.0 does not give you 2σ bands; it gives you 2σ and 4σ bands. Keep multiplier = 1.0 for standard statistical interpretation.
|
||||
|
||||
**VWAPBANDS vs. Standard VWAP:**
|
||||
* VWAPBANDS: Adds volatility context with standard deviation bands
|
||||
* Standard VWAP: Single line without volatility or support/resistance context
|
||||
3. **Early Session Trading:** VWAP bands are unstable in the first 15-30 minutes of a session. Avoid trading based on band touches until sufficient volume accumulates.
|
||||
|
||||
## Best Practices
|
||||
4. **Zero Volume Handling:** Bars with zero volume are handled by substituting last valid values, but extended periods of zero volume degrade indicator quality.
|
||||
|
||||
**Parameter Optimization:**
|
||||
* Match session reset to trading strategy timeframe
|
||||
* Adjust multiplier based on asset volatility characteristics
|
||||
* Test different source prices (close vs. HLC3) for optimal results
|
||||
* Consider market hours and session boundaries for reset timing
|
||||
5. **Memory for Reset Sessions:** When using session resets, ensure your trading system properly tracks session boundaries. Incorrect reset timing corrupts VWAP calculations.
|
||||
|
||||
**Risk Management:**
|
||||
* Use bands for position sizing (larger positions near support bands)
|
||||
* Set stops beyond 2σ levels to avoid normal volatility whipsaws
|
||||
* Monitor volume confirmation for all band interaction signals
|
||||
* Avoid trading during low-volume periods when bands may be unreliable
|
||||
|
||||
**Market Context:**
|
||||
* Consider overall market regime (trending vs. ranging)
|
||||
* Account for news events and earnings that may affect volume patterns
|
||||
* Monitor correlation with institutional trading patterns
|
||||
* Adjust expectations based on market volatility environment
|
||||
6. **API Usage:** The `isNew` parameter controls bar correction. Use `isNew=false` when updating the current bar's value (same timestamp), `isNew=true` for new bars. The `reset` parameter should only be true at session boundaries.
|
||||
|
||||
## References
|
||||
|
||||
* Harris, L. (2003). Trading and Exchanges: Market Microstructure for Practitioners. Oxford University Press.
|
||||
* Berkowitz, S. A. (1993). The Advantages of Volume Weighted Average Price Trading. Journal of Portfolio Management.
|
||||
* TradingView (2024). VWAP Standard Deviation Bands. Pine Script Reference.
|
||||
* Berkowitz, S. A., Logue, D. E., & Noser, E. A. (1988). The Total Cost of Transactions on the NYSE. The Journal of Finance, 43(1), 97-112.
|
||||
* Kissell, R. (2013). The Science of Algorithmic Trading and Portfolio Management. Academic Press.
|
||||
|
||||
## Validation Sources
|
||||
|
||||
**Patterns:** Running sum accumulation, variance calculation (E[X²] - E[X]²), defensive division, NaN/Infinity handling
|
||||
|
||||
**External:** TradingView vwapbands.pine reference implementation
|
||||
|
||||
**API:** Verified against TradingView VWAP with standard deviation bands functionality
|
||||
Reference in New Issue
Block a user