mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 20:48:04 +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,261 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VwapsdIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal(2.0, indicator.NumDevs);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("VWAPSD - Volume Weighted Average Price with Configurable Standard Deviation Bands", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShortName_IncludesNumDevs()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.5 };
|
||||
|
||||
Assert.Contains("VWAPSD", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_CreatesFourLineSeries()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (VWAP, Upper, Lower, Width)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.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 VwapsdIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.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 VwapsdIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.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 VwapsdIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.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 VwapsdIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
|
||||
Assert.Equal(1.5, indicator.NumDevs);
|
||||
|
||||
indicator.NumDevs = 2.5;
|
||||
Assert.Equal(2.5, indicator.NumDevs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_AllBandsUpdate_Correctly()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.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 4 line series have values (VWAP, Upper, Lower, Width)
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
foreach (var series in indicator.LinesSeries)
|
||||
{
|
||||
Assert.Equal(5, series.Count);
|
||||
Assert.True(double.IsFinite(series.GetValue(0)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_BandRelationships_AreCorrect()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.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, Upper=1, Lower=2, Width=3
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
double upper = indicator.LinesSeries[1].GetValue(0);
|
||||
double lower = indicator.LinesSeries[2].GetValue(0);
|
||||
double width = indicator.LinesSeries[3].GetValue(0);
|
||||
|
||||
// Band relationships: Upper > VWAP > Lower
|
||||
Assert.True(upper >= vwap, $"Upper ({upper}) should be >= VWAP ({vwap})");
|
||||
Assert.True(vwap >= lower, $"VWAP ({vwap}) should be >= Lower ({lower})");
|
||||
|
||||
// Width = Upper - Lower (2 × numDevs × StdDev)
|
||||
Assert.True(Math.Abs(width - (upper - lower)) < 0.0001,
|
||||
$"Width ({width}) should equal Upper - Lower ({upper - lower})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_VolumeWeighting_AffectsVwap()
|
||||
{
|
||||
var indicator1 = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
var indicator2 = new VwapsdIndicator { NumDevs = 2.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");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_NumDevs_AffectsBandWidth()
|
||||
{
|
||||
var indicator1 = new VwapsdIndicator { NumDevs = 1.0 };
|
||||
var indicator2 = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 105, 95, 110, 90 };
|
||||
double[] volumes = { 1000, 1500, 2000, 1200, 1800 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double close = closes[i];
|
||||
indicator1.HistoricalData.AddBar(now, close, close + 3, close - 3, close, volumes[i]);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.HistoricalData.AddBar(now, close, close + 3, close - 3, close, volumes[i]);
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// Width should be proportional to numDevs
|
||||
double width1 = indicator1.LinesSeries[3].GetValue(0);
|
||||
double width2 = indicator2.LinesSeries[3].GetValue(0);
|
||||
|
||||
// Width2 should be approximately 2x Width1
|
||||
Assert.True(Math.Abs(width2 - 2 * width1) < 0.0001,
|
||||
$"Width2 ({width2}) should be ~2x Width1 ({width1})");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class VwapsdIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Number of Deviations", sortIndex: 1, minimum: 0.1, maximum: 5.0, increment: 0.1, decimalPlaces: 1)]
|
||||
public double NumDevs { get; set; } = 2.0;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Vwapsd? vwapsd;
|
||||
protected LineSeries? VwapSeries;
|
||||
protected LineSeries? UpperSeries;
|
||||
protected LineSeries? LowerSeries;
|
||||
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 => $"VWAPSD ({NumDevs:F1})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/channels/vwapsd/Vwapsd.cs";
|
||||
|
||||
public VwapsdIndicator()
|
||||
{
|
||||
Name = "VWAPSD - Volume Weighted Average Price with Configurable Standard Deviation Bands";
|
||||
Description = "Volume weighted average price with configurable standard deviation bands";
|
||||
|
||||
VwapSeries = new("VWAP", Color.Blue, 2, LineStyle.Solid);
|
||||
UpperSeries = new($"Upper (+{NumDevs}σ)", Color.Red, 1, LineStyle.Solid);
|
||||
LowerSeries = new($"Lower (-{NumDevs}σ)", Color.Green, 1, LineStyle.Solid);
|
||||
WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot);
|
||||
|
||||
AddLineSeries(VwapSeries);
|
||||
AddLineSeries(UpperSeries);
|
||||
AddLineSeries(LowerSeries);
|
||||
AddLineSeries(WidthSeries);
|
||||
|
||||
SeparateWindow = false;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
vwapsd = new(NumDevs);
|
||||
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 = vwapsd!.Update(bar, args.IsNewBar());
|
||||
|
||||
VwapSeries!.SetValue(result.Value, vwapsd.IsHot, ShowColdValues);
|
||||
UpperSeries!.SetValue(vwapsd.Upper.Value, vwapsd.IsHot, ShowColdValues);
|
||||
LowerSeries!.SetValue(vwapsd.Lower.Value, vwapsd.IsHot, ShowColdValues);
|
||||
WidthSeries!.SetValue(vwapsd.Width.Value, vwapsd.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,666 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VwapsdTests
|
||||
{
|
||||
[Fact]
|
||||
public void Vwapsd_Constructor_ValidatesNumDevs()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapsd(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapsd(-1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapsd(0.05)); // Below MinNumDevs (0.1)
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapsd(5.1)); // Above MaxNumDevs (5.0)
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Vwapsd(10)); // Above MaxNumDevs (5.0)
|
||||
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
Assert.NotNull(vwapsd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Constructor_AcceptsValidRange()
|
||||
{
|
||||
// Test boundary values
|
||||
var vwapsdMin = new Vwapsd(0.1);
|
||||
Assert.NotNull(vwapsdMin);
|
||||
|
||||
var vwapsdMax = new Vwapsd(5.0);
|
||||
Assert.NotNull(vwapsdMax);
|
||||
|
||||
var vwapsdMid = new Vwapsd(2.5);
|
||||
Assert.NotNull(vwapsdMid);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_DefaultConstructor_UsesDefaultNumDevs()
|
||||
{
|
||||
var vwapsd = new Vwapsd();
|
||||
Assert.NotNull(vwapsd);
|
||||
Assert.Contains("Vwapsd", vwapsd.Name, StringComparison.Ordinal);
|
||||
Assert.Contains("2.0", vwapsd.Name, StringComparison.Ordinal); // Default is 2.0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Update_ReturnsValue()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
||||
var result = vwapsd.Update(bar);
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.Upper.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.Lower.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.StdDev.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.Width.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_FirstBar_InitializesCorrectly()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 100, 1000);
|
||||
_ = vwapsd.Update(bar);
|
||||
|
||||
// First bar: VWAP = HLC3 = (105+95+100)/3 = 100
|
||||
double expectedVwap = (105 + 95 + 100) / 3.0;
|
||||
Assert.Equal(expectedVwap, vwapsd.Vwap.Value, precision: 10);
|
||||
|
||||
// First bar has zero variance (only 1 point)
|
||||
Assert.Equal(0, vwapsd.StdDev.Value, precision: 10);
|
||||
Assert.Equal(expectedVwap, vwapsd.Upper.Value, precision: 10);
|
||||
Assert.Equal(expectedVwap, vwapsd.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Properties_Accessible()
|
||||
{
|
||||
var vwapsd = new Vwapsd(2.0);
|
||||
|
||||
Assert.False(vwapsd.IsHot);
|
||||
Assert.Contains("Vwapsd", vwapsd.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(2, vwapsd.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Update_IsNew_AcceptsParameter()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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 = vwapsd.Update(bar1, isNew: true);
|
||||
var result2 = vwapsd.Update(bar2, isNew: false);
|
||||
|
||||
Assert.True(double.IsFinite(result1.Value));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Update_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i], isNew: true);
|
||||
}
|
||||
|
||||
double beforeCorrection = vwapsd.Vwap.Value;
|
||||
|
||||
// Correct last bar with different value
|
||||
var correctionBar = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 5000);
|
||||
vwapsd.Update(correctionBar, isNew: false);
|
||||
double afterCorrection = vwapsd.Vwap.Value;
|
||||
|
||||
Assert.NotEqual(beforeCorrection, afterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
double originalVwap = vwapsd.Vwap.Value;
|
||||
double originalUpper = vwapsd.Upper.Value;
|
||||
double originalLower = vwapsd.Lower.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);
|
||||
vwapsd.Update(correctionBar, isNew: false);
|
||||
}
|
||||
|
||||
// Restore original
|
||||
vwapsd.Update(bars[^1], isNew: false);
|
||||
double restoredVwap = vwapsd.Vwap.Value;
|
||||
double restoredUpper = vwapsd.Upper.Value;
|
||||
double restoredLower = vwapsd.Lower.Value;
|
||||
|
||||
Assert.Equal(originalVwap, restoredVwap, precision: 8);
|
||||
Assert.Equal(originalUpper, restoredUpper, precision: 8);
|
||||
Assert.Equal(originalLower, restoredLower, precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Reset_ClearsState()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(vwapsd.IsHot);
|
||||
|
||||
vwapsd.Reset();
|
||||
|
||||
Assert.False(vwapsd.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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
|
||||
vwapsd.Update(bars[0]);
|
||||
Assert.False(vwapsd.IsHot);
|
||||
|
||||
vwapsd.Update(bars[1]);
|
||||
Assert.True(vwapsd.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_WarmupPeriod_IsSetCorrectly()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
Assert.Equal(2, vwapsd.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_NaN_Price_UsesLastValidValue()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
vwapsd.Update(new TValue(DateTime.UtcNow, double.NaN), 1000, isNew: true);
|
||||
double afterNaN = vwapsd.Vwap.Value;
|
||||
|
||||
Assert.True(double.IsFinite(afterNaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_NaN_Volume_UsesLastValidValue()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
vwapsd.Update(new TValue(DateTime.UtcNow, 100), double.NaN, isNew: true);
|
||||
double afterNaN = vwapsd.Vwap.Value;
|
||||
|
||||
Assert.True(double.IsFinite(afterNaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
vwapsd.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity), 1000, isNew: true);
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value));
|
||||
|
||||
vwapsd.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity), 1000, isNew: true);
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_BandRelationship_UpperGreaterThanVwapGreaterThanLower()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
|
||||
// Skip first bar where StdDev is 0
|
||||
if (i > 0)
|
||||
{
|
||||
Assert.True(vwapsd.Upper.Value >= vwapsd.Vwap.Value,
|
||||
$"Upper ({vwapsd.Upper.Value}) should be >= Vwap ({vwapsd.Vwap.Value})");
|
||||
Assert.True(vwapsd.Vwap.Value >= vwapsd.Lower.Value,
|
||||
$"Vwap ({vwapsd.Vwap.Value}) should be >= Lower ({vwapsd.Lower.Value})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_VwapBetweenBands()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
Assert.True(vwapsd.Vwap.Value <= vwapsd.Upper.Value,
|
||||
$"Vwap ({vwapsd.Vwap.Value}) should be <= Upper ({vwapsd.Upper.Value})");
|
||||
Assert.True(vwapsd.Vwap.Value >= vwapsd.Lower.Value,
|
||||
$"Vwap ({vwapsd.Vwap.Value}) should be >= Lower ({vwapsd.Lower.Value})");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Width_EqualsUpperMinusLower()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.5);
|
||||
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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
double expectedWidth = vwapsd.Upper.Value - vwapsd.Lower.Value;
|
||||
Assert.Equal(expectedWidth, vwapsd.Width.Value, precision: 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_SessionReset_ResetsVwapCalculation()
|
||||
{
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
double vwapBeforeReset = vwapsd.Vwap.Value;
|
||||
|
||||
// Reset and process next bar - should start fresh
|
||||
var resetBar = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 1000);
|
||||
vwapsd.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, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.NotEqual(vwapBeforeReset, vwapsd.Vwap.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_VwapFormula_MatchesExpected()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
|
||||
// Bar 1: price=100, volume=1000
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vwapsd.Update(bar1);
|
||||
Assert.Equal(100.0, vwapsd.Vwap.Value, precision: 10);
|
||||
|
||||
// Bar 2: price=110, volume=2000
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 110, 110, 110, 110, 2000);
|
||||
vwapsd.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, vwapsd.Vwap.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_StdDevFormula_MatchesExpected()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
|
||||
// Bar 1: price=100, volume=1
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
vwapsd.Update(bar1);
|
||||
|
||||
// Bar 2: price=200, volume=1
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
vwapsd.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, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd.StdDev.Value, precision: 10);
|
||||
Assert.Equal(200.0, vwapsd.Upper.Value, precision: 10); // 150 + 1*50
|
||||
Assert.Equal(100.0, vwapsd.Lower.Value, precision: 10); // 150 - 1*50
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_NumDevs_AffectsBands()
|
||||
{
|
||||
// Test with 2 standard deviations
|
||||
var vwapsd2 = new Vwapsd(2.0);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
vwapsd2.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
vwapsd2.Update(bar2);
|
||||
|
||||
// VWAP = 150, StdDev = 50
|
||||
// With numDevs=2: Upper = 150 + 2*50 = 250, Lower = 150 - 2*50 = 50
|
||||
Assert.Equal(150.0, vwapsd2.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd2.StdDev.Value, precision: 10);
|
||||
Assert.Equal(250.0, vwapsd2.Upper.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd2.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var vwapsdIterative = new Vwapsd(1.5);
|
||||
var vwapsdBatch = new Vwapsd(1.5);
|
||||
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++)
|
||||
{
|
||||
vwapsdIterative.Update(bars[i]);
|
||||
iterativeVwap.Add(vwapsdIterative.Vwap.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResult = vwapsdBatch.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 Vwapsd_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 (upper, lower, vwap, stdev) = Vwapsd.Calculate(bars, 1.5);
|
||||
|
||||
Assert.Equal(50, upper.Count);
|
||||
Assert.Equal(50, lower.Count);
|
||||
Assert.Equal(50, vwap.Count);
|
||||
Assert.Equal(50, stdev.Count);
|
||||
Assert.True(double.IsFinite(vwap.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_SpanCalculate_ValidatesInput()
|
||||
{
|
||||
double[] price = [100, 101, 102, 103, 104];
|
||||
double[] volume = [1000, 1100, 1200, 1300, 1400];
|
||||
double[] upper = new double[5];
|
||||
double[] lower = new double[5];
|
||||
double[] vwap = new double[5];
|
||||
double[] wrongSize = new double[3];
|
||||
|
||||
// NumDevs must be >= MinNumDevs
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 0));
|
||||
|
||||
// NumDevs must be <= MaxNumDevs
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 6.0));
|
||||
|
||||
// All arrays must be same length
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_SpanCalculate_HandlesNaN()
|
||||
{
|
||||
double[] price = [100, 101, double.NaN, 103, 104];
|
||||
double[] volume = [1000, 1100, 1200, 1300, 1400];
|
||||
double[] upper = new double[5];
|
||||
double[] lower = new double[5];
|
||||
double[] vwap = new double[5];
|
||||
|
||||
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 1.0);
|
||||
|
||||
foreach (var val in vwap)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"VWAP should be finite, got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_FlatLine_ReturnsSameValueForVwap()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
vwapsd.Update(bar);
|
||||
}
|
||||
|
||||
// With constant price, VWAP should equal the price
|
||||
Assert.Equal(100.0, vwapsd.Vwap.Value, precision: 6);
|
||||
// StdDev of zero variance = 0, so upper = lower = vwap
|
||||
Assert.Equal(vwapsd.Vwap.Value, vwapsd.Upper.Value, precision: 6);
|
||||
Assert.Equal(vwapsd.Vwap.Value, vwapsd.Lower.Value, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_HigherNumDevs_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 vwapsd1 = new Vwapsd(1.0);
|
||||
var vwapsd2 = new Vwapsd(2.0);
|
||||
var vwapsd3 = new Vwapsd(3.0);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapsd1.Update(bars[i]);
|
||||
vwapsd2.Update(bars[i]);
|
||||
vwapsd3.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Same VWAP for all
|
||||
Assert.Equal(vwapsd1.Vwap.Value, vwapsd2.Vwap.Value, precision: 10);
|
||||
Assert.Equal(vwapsd2.Vwap.Value, vwapsd3.Vwap.Value, precision: 10);
|
||||
|
||||
// Higher numDevs = wider bands
|
||||
Assert.True(vwapsd2.Width.Value > vwapsd1.Width.Value,
|
||||
$"Width with numDevs=2 ({vwapsd2.Width.Value}) should be > width with numDevs=1 ({vwapsd1.Width.Value})");
|
||||
Assert.True(vwapsd3.Width.Value > vwapsd2.Width.Value,
|
||||
$"Width with numDevs=3 ({vwapsd3.Width.Value}) should be > width with numDevs=2 ({vwapsd2.Width.Value})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_ZeroVolume_DoesNotAffectVwap()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vwapsd.Update(bar1);
|
||||
double vwapAfterBar1 = vwapsd.Vwap.Value;
|
||||
|
||||
// Zero volume bar should not change VWAP
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 0);
|
||||
vwapsd.Update(bar2);
|
||||
double vwapAfterBar2 = vwapsd.Vwap.Value;
|
||||
|
||||
Assert.Equal(vwapAfterBar1, vwapAfterBar2, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_Prime_SetsStateCorrectly()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
double[] history = [100, 101, 102, 103, 104, 105, 106];
|
||||
|
||||
vwapsd.Prime(history);
|
||||
|
||||
Assert.True(vwapsd.IsHot);
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_UpdateTValue_UsesVolumeOfOne()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
|
||||
// Using Update(TValue) should use volume=1
|
||||
vwapsd.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
vwapsd.Update(new TValue(DateTime.UtcNow.AddMinutes(1), 200.0));
|
||||
|
||||
// With equal volume (1 each), VWAP = (100+200)/2 = 150
|
||||
Assert.Equal(150.0, vwapsd.Vwap.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_UpdateTSeries_Works()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.5);
|
||||
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 = vwapsd.Update(bars);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.True(double.IsFinite(result.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_UpdateTSeries_PriceOnly_Works()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.5);
|
||||
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 = vwapsd.Update(priceSeries);
|
||||
|
||||
Assert.Equal(50, result.Count);
|
||||
Assert.True(double.IsFinite(result.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_VolumeWeighting_AffectsVwap()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
|
||||
// High volume at low price
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 10000);
|
||||
vwapsd.Update(bar1);
|
||||
|
||||
// Low volume at high price
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 100);
|
||||
vwapsd.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, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.True(vwapsd.Vwap.Value < 110, "VWAP should be heavily weighted toward 100");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_FractionalNumDevs_Works()
|
||||
{
|
||||
var vwapsd = new Vwapsd(1.5);
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
vwapsd.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
vwapsd.Update(bar2);
|
||||
|
||||
// VWAP = 150, StdDev = 50
|
||||
// With numDevs=1.5: Upper = 150 + 1.5*50 = 225, Lower = 150 - 1.5*50 = 75
|
||||
Assert.Equal(150.0, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd.StdDev.Value, precision: 10);
|
||||
Assert.Equal(225.0, vwapsd.Upper.Value, precision: 10);
|
||||
Assert.Equal(75.0, vwapsd.Lower.Value, precision: 10);
|
||||
Assert.Equal(150.0, vwapsd.Width.Value, precision: 10); // 225 - 75
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_BoundaryNumDevs_Min_Works()
|
||||
{
|
||||
var vwapsd = new Vwapsd(0.1); // Minimum allowed
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
vwapsd.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
vwapsd.Update(bar2);
|
||||
|
||||
// VWAP = 150, StdDev = 50
|
||||
// With numDevs=0.1: Upper = 150 + 0.1*50 = 155, Lower = 150 - 0.1*50 = 145
|
||||
Assert.Equal(150.0, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd.StdDev.Value, precision: 10);
|
||||
Assert.Equal(155.0, vwapsd.Upper.Value, precision: 10);
|
||||
Assert.Equal(145.0, vwapsd.Lower.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Vwapsd_BoundaryNumDevs_Max_Works()
|
||||
{
|
||||
var vwapsd = new Vwapsd(5.0); // Maximum allowed
|
||||
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
vwapsd.Update(bar1);
|
||||
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
vwapsd.Update(bar2);
|
||||
|
||||
// VWAP = 150, StdDev = 50
|
||||
// With numDevs=5.0: Upper = 150 + 5.0*50 = 400, Lower = 150 - 5.0*50 = -100
|
||||
Assert.Equal(150.0, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd.StdDev.Value, precision: 10);
|
||||
Assert.Equal(400.0, vwapsd.Upper.Value, precision: 10);
|
||||
Assert.Equal(-100.0, vwapsd.Lower.Value, precision: 10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,613 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for VWAPSD (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 with configurable numDevs
|
||||
/// 4. Volume weighting behavior
|
||||
/// </summary>
|
||||
public sealed class VwapsdValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public VwapsdValidationTests(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[] numDevsValues = { 0.5, 1.0, 2.0, 3.0 };
|
||||
|
||||
foreach (var numDevs in numDevsValues)
|
||||
{
|
||||
// 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 streamingVwapsd = new Vwapsd(numDevs);
|
||||
var streamingVwap = new List<double>();
|
||||
var streamingUpper = new List<double>();
|
||||
var streamingLower = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVwapsd.Update(bars[i]);
|
||||
streamingVwap.Add(streamingVwapsd.Vwap.Value);
|
||||
streamingUpper.Add(streamingVwapsd.Upper.Value);
|
||||
streamingLower.Add(streamingVwapsd.Lower.Value);
|
||||
}
|
||||
|
||||
// Batch mode
|
||||
var batchVwapsd = new Vwapsd(numDevs);
|
||||
var batchResult = batchVwapsd.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("VWAPSD Streaming vs Batch consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Streaming_Span_Consistency()
|
||||
{
|
||||
double[] numDevsValues = { 0.5, 1.0, 2.0, 3.0 };
|
||||
|
||||
foreach (var numDevs in numDevsValues)
|
||||
{
|
||||
// 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 streamingVwapsd = new Vwapsd(numDevs);
|
||||
var streamingVwap = new List<double>();
|
||||
var streamingUpper = new List<double>();
|
||||
var streamingLower = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVwapsd.Update(bars[i]);
|
||||
streamingVwap.Add(streamingVwapsd.Vwap.Value);
|
||||
streamingUpper.Add(streamingVwapsd.Upper.Value);
|
||||
streamingLower.Add(streamingVwapsd.Lower.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[] spanUpper = new double[bars.Count];
|
||||
double[] spanLower = new double[bars.Count];
|
||||
|
||||
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
|
||||
spanUpper.AsSpan(), spanLower.AsSpan(),
|
||||
spanVwap.AsSpan(), numDevs);
|
||||
|
||||
// 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(streamingUpper[i], spanUpper[i], precision: 10);
|
||||
Assert.Equal(streamingLower[i], spanLower[i], precision: 10);
|
||||
}
|
||||
}
|
||||
_output.WriteLine("VWAPSD Streaming vs Span consistency validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_VwapFormula_ManualCalculation()
|
||||
{
|
||||
// Manually verify VWAP calculation: sum(price × volume) / sum(volume)
|
||||
var vwapsd = new Vwapsd(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);
|
||||
vwapsd.Update(bar);
|
||||
|
||||
Assert.Equal(expectedVwap, vwapsd.Vwap.Value, precision: 10);
|
||||
_output.WriteLine($"Bar {i + 1}: Price={price}, Vol={vol}, Expected VWAP={expectedVwap:F4}, Actual={vwapsd.Vwap.Value:F4}");
|
||||
}
|
||||
|
||||
_output.WriteLine("VWAPSD formula validation completed successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_StdDevFormula_ManualCalculation()
|
||||
{
|
||||
// Manually verify variance calculation: (sum(price² × vol) / sum(vol)) - VWAP²
|
||||
var vwapsd = new Vwapsd(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);
|
||||
vwapsd.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, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd.StdDev.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPSD StdDev formula validation completed successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NumDevsEffect_BandWidth()
|
||||
{
|
||||
// Verify that numDevs properly scales the band width
|
||||
var vwapsd1 = new Vwapsd(1.0);
|
||||
var vwapsd2 = new Vwapsd(2.0);
|
||||
var vwapsd3 = new Vwapsd(3.0);
|
||||
|
||||
// Create test data with known variance
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1);
|
||||
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 200, 200, 200, 200, 1);
|
||||
|
||||
vwapsd1.Update(bar1);
|
||||
vwapsd1.Update(bar2);
|
||||
vwapsd2.Update(bar1);
|
||||
vwapsd2.Update(bar2);
|
||||
vwapsd3.Update(bar1);
|
||||
vwapsd3.Update(bar2);
|
||||
|
||||
// VWAP = 150, StdDev = 50 for all
|
||||
Assert.Equal(150.0, vwapsd1.Vwap.Value, precision: 10);
|
||||
Assert.Equal(150.0, vwapsd2.Vwap.Value, precision: 10);
|
||||
Assert.Equal(150.0, vwapsd3.Vwap.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd1.StdDev.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd2.StdDev.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd3.StdDev.Value, precision: 10);
|
||||
|
||||
// With numDevs=1: Upper = 200, Lower = 100, Width = 100
|
||||
// With numDevs=2: Upper = 250, Lower = 50, Width = 200
|
||||
// With numDevs=3: Upper = 300, Lower = 0, Width = 300
|
||||
Assert.Equal(200.0, vwapsd1.Upper.Value, precision: 10);
|
||||
Assert.Equal(100.0, vwapsd1.Lower.Value, precision: 10);
|
||||
Assert.Equal(100.0, vwapsd1.Width.Value, precision: 10);
|
||||
|
||||
Assert.Equal(250.0, vwapsd2.Upper.Value, precision: 10);
|
||||
Assert.Equal(50.0, vwapsd2.Lower.Value, precision: 10);
|
||||
Assert.Equal(200.0, vwapsd2.Width.Value, precision: 10);
|
||||
|
||||
Assert.Equal(300.0, vwapsd3.Upper.Value, precision: 10);
|
||||
Assert.Equal(0.0, vwapsd3.Lower.Value, precision: 10);
|
||||
Assert.Equal(300.0, vwapsd3.Width.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPSD numDevs effect validation completed successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BandCharacteristics()
|
||||
{
|
||||
// Verify core VWAPSD characteristics:
|
||||
// 1. Upper >= VWAP >= Lower
|
||||
// 2. Bands are symmetric around VWAP
|
||||
// 3. Band width is proportional to numDevs × StdDev
|
||||
|
||||
double numDevs = 1.5;
|
||||
|
||||
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 vwapsd = new Vwapsd(numDevs);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
|
||||
// Skip first bar where StdDev is 0
|
||||
if (i > 0)
|
||||
{
|
||||
// Upper >= VWAP >= Lower
|
||||
Assert.True(vwapsd.Upper.Value >= vwapsd.Vwap.Value,
|
||||
$"Upper ({vwapsd.Upper.Value}) should be >= VWAP ({vwapsd.Vwap.Value})");
|
||||
Assert.True(vwapsd.Vwap.Value >= vwapsd.Lower.Value,
|
||||
$"VWAP ({vwapsd.Vwap.Value}) should be >= Lower ({vwapsd.Lower.Value})");
|
||||
|
||||
// Symmetry: Upper - VWAP == VWAP - Lower
|
||||
double upperOffset = vwapsd.Upper.Value - vwapsd.Vwap.Value;
|
||||
double lowerOffset = vwapsd.Vwap.Value - vwapsd.Lower.Value;
|
||||
Assert.Equal(upperOffset, lowerOffset, precision: 9);
|
||||
|
||||
// Width = 2 × numDevs × StdDev
|
||||
double expectedWidth = 2.0 * numDevs * vwapsd.StdDev.Value;
|
||||
Assert.Equal(expectedWidth, vwapsd.Width.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("VWAPSD band characteristics validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_VolumeWeighting()
|
||||
{
|
||||
// Verify that VWAP is properly volume-weighted
|
||||
var vwapsd = new Vwapsd(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);
|
||||
|
||||
vwapsd.Update(bar1);
|
||||
vwapsd.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, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.True(vwapsd.Vwap.Value < 110, "VWAP should be heavily weighted toward 100");
|
||||
|
||||
_output.WriteLine($"Volume weighting verified: VWAP = {vwapsd.Vwap.Value:F4} (expected ≈ 100.99)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NaN_Handling()
|
||||
{
|
||||
double numDevs = 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 vwapsd = new Vwapsd(numDevs);
|
||||
int nanCount = 0;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
if (i == 50 || i == 51)
|
||||
{
|
||||
// Inject NaN price
|
||||
vwapsd.Update(new TValue(bars[i].Time, double.NaN), bars[i].Volume, isNew: true);
|
||||
nanCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value),
|
||||
$"VWAP should be finite after NaN at index {i}");
|
||||
Assert.True(double.IsFinite(vwapsd.Upper.Value),
|
||||
$"Upper should be finite after NaN at index {i}");
|
||||
Assert.True(double.IsFinite(vwapsd.Lower.Value),
|
||||
$"Lower should be finite after NaN at index {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine($"VWAPSD NaN handling validated ({nanCount} NaN values handled)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BarCorrection()
|
||||
{
|
||||
double numDevs = 1.5;
|
||||
|
||||
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 vwapsd = new Vwapsd(numDevs);
|
||||
|
||||
// Process all bars
|
||||
for (int i = 0; i < bars.Count - 1; i++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
// Record state before last bar
|
||||
vwapsd.Update(bars[^1]);
|
||||
double originalVwap = vwapsd.Vwap.Value;
|
||||
double originalUpper = vwapsd.Upper.Value;
|
||||
|
||||
// Correct last bar with different value
|
||||
var correctedBar = new TBar(bars[^1].Time, 200, 210, 190, 200, 5000);
|
||||
vwapsd.Update(correctedBar, isNew: false);
|
||||
double correctedVwap = vwapsd.Vwap.Value;
|
||||
|
||||
// Should be different
|
||||
Assert.NotEqual(originalVwap, correctedVwap);
|
||||
|
||||
// Restore original bar
|
||||
vwapsd.Update(bars[^1], isNew: false);
|
||||
double restoredVwap = vwapsd.Vwap.Value;
|
||||
double restoredUpper = vwapsd.Upper.Value;
|
||||
|
||||
// Should match original
|
||||
Assert.Equal(originalVwap, restoredVwap, precision: 10);
|
||||
Assert.Equal(originalUpper, restoredUpper, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPSD bar correction validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_SessionReset()
|
||||
{
|
||||
// Verify that session reset properly clears VWAP accumulation
|
||||
var vwapsd = new Vwapsd(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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
double session1Vwap = vwapsd.Vwap.Value;
|
||||
|
||||
// Reset for new session
|
||||
var resetBar = new TBar(DateTime.UtcNow, 200, 200, 200, 200, 1000);
|
||||
vwapsd.Update(resetBar, isNew: true, reset: true);
|
||||
|
||||
// After reset, VWAP should be just the reset bar's price
|
||||
Assert.Equal(200.0, vwapsd.Vwap.Value, precision: 10);
|
||||
Assert.NotEqual(session1Vwap, vwapsd.Vwap.Value);
|
||||
|
||||
_output.WriteLine("VWAPSD session reset validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_DifferentNumDevs()
|
||||
{
|
||||
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[] numDevsValues = { 0.5, 1.0, 1.5, 2.0, 3.0 };
|
||||
var avgWidths = new List<double>();
|
||||
|
||||
foreach (var numDevs in numDevsValues)
|
||||
{
|
||||
var vwapsd = new Vwapsd(numDevs);
|
||||
double sumWidth = 0;
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
if (vwapsd.IsHot)
|
||||
{
|
||||
sumWidth += vwapsd.Width.Value;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
double avgWidth = count > 0 ? sumWidth / count : 0;
|
||||
avgWidths.Add(avgWidth);
|
||||
_output.WriteLine($"NumDevs {numDevs}: Average width = {avgWidth:F4}");
|
||||
}
|
||||
|
||||
// Higher numDevs should give wider bands
|
||||
for (int i = 1; i < avgWidths.Count; i++)
|
||||
{
|
||||
Assert.True(avgWidths[i] > avgWidths[i - 1],
|
||||
$"Higher numDevs should produce wider bands");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ZeroVolumeBars()
|
||||
{
|
||||
// Zero volume bars should not affect VWAP
|
||||
var vwapsd = new Vwapsd(1.0);
|
||||
|
||||
// First bar with volume
|
||||
var bar1 = new TBar(DateTime.UtcNow, 100, 100, 100, 100, 1000);
|
||||
vwapsd.Update(bar1);
|
||||
double vwapAfterBar1 = vwapsd.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);
|
||||
vwapsd.Update(zeroVolBar);
|
||||
}
|
||||
|
||||
// VWAP should remain unchanged
|
||||
Assert.Equal(vwapAfterBar1, vwapsd.Vwap.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPSD 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 vwapsd = new Vwapsd(2.0);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
|
||||
vwapsd.Update(bar);
|
||||
}
|
||||
|
||||
Assert.Equal(100.0, vwapsd.Vwap.Value, precision: 6);
|
||||
Assert.Equal(0.0, vwapsd.StdDev.Value, precision: 6);
|
||||
Assert.Equal(100.0, vwapsd.Upper.Value, precision: 6);
|
||||
Assert.Equal(100.0, vwapsd.Lower.Value, precision: 6);
|
||||
|
||||
_output.WriteLine("VWAPSD constant price validation completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_LargeDataset_Performance()
|
||||
{
|
||||
// Process large dataset to verify stability
|
||||
var vwapsd = new Vwapsd(1.5);
|
||||
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++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
|
||||
// Verify all values remain finite
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value), $"VWAP not finite at index {i}");
|
||||
Assert.True(double.IsFinite(vwapsd.StdDev.Value), $"StdDev not finite at index {i}");
|
||||
Assert.True(double.IsFinite(vwapsd.Upper.Value), $"Upper not finite at index {i}");
|
||||
Assert.True(double.IsFinite(vwapsd.Lower.Value), $"Lower 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 (upper, lower, vwap, stdev) = Vwapsd.Calculate(bars, 1.5);
|
||||
|
||||
Assert.Equal(bars.Count, upper.Count);
|
||||
Assert.Equal(bars.Count, lower.Count);
|
||||
Assert.Equal(bars.Count, vwap.Count);
|
||||
Assert.Equal(bars.Count, stdev.Count);
|
||||
|
||||
// Verify streaming matches static
|
||||
var streamingVwapsd = new Vwapsd(1.5);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingVwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(streamingVwapsd.Vwap.Value, vwap.Last.Value, precision: 10);
|
||||
Assert.Equal(streamingVwapsd.Upper.Value, upper.Last.Value, precision: 10);
|
||||
Assert.Equal(streamingVwapsd.Lower.Value, lower.Last.Value, precision: 10);
|
||||
|
||||
_output.WriteLine("VWAPSD static Calculate validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_FractionalNumDevs()
|
||||
{
|
||||
// Test fractional numDevs values within valid range
|
||||
double[] fractionalValues = { 0.1, 0.25, 0.5, 0.75, 1.25, 1.5, 2.5, 4.5 };
|
||||
|
||||
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));
|
||||
|
||||
foreach (var numDevs in fractionalValues)
|
||||
{
|
||||
var vwapsd = new Vwapsd(numDevs);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(vwapsd.Vwap.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.Upper.Value));
|
||||
Assert.True(double.IsFinite(vwapsd.Lower.Value));
|
||||
Assert.True(vwapsd.Width.Value >= 0);
|
||||
|
||||
_output.WriteLine($"NumDevs {numDevs:F2}: VWAP={vwapsd.Vwap.Value:F4}, Width={vwapsd.Width.Value:F4}");
|
||||
}
|
||||
|
||||
_output.WriteLine("VWAPSD fractional numDevs validation completed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_BoundaryNumDevs()
|
||||
{
|
||||
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));
|
||||
|
||||
// Test minimum boundary (0.1)
|
||||
var vwapsdMin = new Vwapsd(0.1);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapsdMin.Update(bars[i]);
|
||||
}
|
||||
Assert.True(vwapsdMin.Width.Value > 0 || vwapsdMin.StdDev.Value == 0);
|
||||
_output.WriteLine($"Min numDevs (0.1): Width={vwapsdMin.Width.Value:F6}");
|
||||
|
||||
// Test maximum boundary (5.0)
|
||||
var vwapsdMax = new Vwapsd(5.0);
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
vwapsdMax.Update(bars[i]);
|
||||
}
|
||||
Assert.True(vwapsdMax.Width.Value >= vwapsdMin.Width.Value);
|
||||
_output.WriteLine($"Max numDevs (5.0): Width={vwapsdMax.Width.Value:F6}");
|
||||
|
||||
// Verify width ratio matches numDevs ratio
|
||||
if (vwapsdMin.StdDev.Value > 0)
|
||||
{
|
||||
double expectedRatio = 5.0 / 0.1; // 50x
|
||||
double actualRatio = vwapsdMax.Width.Value / vwapsdMin.Width.Value;
|
||||
Assert.Equal(expectedRatio, actualRatio, precision: 8);
|
||||
}
|
||||
|
||||
_output.WriteLine("VWAPSD boundary numDevs validation completed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// VWAPSD: Volume Weighted Average Price with Standard Deviation Bands
|
||||
/// A volatility channel indicator using VWAP as the center line with configurable
|
||||
/// standard deviation bands.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The VWAPSD calculation process:
|
||||
/// 1. Calculate cumulative price×volume sum (sum_pv)
|
||||
/// 2. Calculate cumulative volume sum (sum_vol)
|
||||
/// 3. Calculate cumulative price²×volume sum (sum_pv2)
|
||||
/// 4. VWAP = sum_pv / sum_vol
|
||||
/// 5. Variance = (sum_pv2 / sum_vol) - VWAP²
|
||||
/// 6. StdDev = √Variance
|
||||
/// 7. Bands = VWAP ± (numDevs × StdDev)
|
||||
///
|
||||
/// Key characteristics:
|
||||
/// - Volume-weighted price average as center line
|
||||
/// - Configurable number of standard deviations for bands
|
||||
/// - Bands adapt to volume-weighted price dispersion
|
||||
/// - Can reset on session boundaries or run continuously
|
||||
///
|
||||
/// Sources:
|
||||
/// Standard VWAP calculation with configurable deviation bands
|
||||
/// Common in institutional trading for intraday analysis
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Vwapsd : AbstractBase
|
||||
{
|
||||
private readonly double _numDevs;
|
||||
private const double DefaultNumDevs = 2.0;
|
||||
private const double MinNumDevs = 0.1;
|
||||
private const double MaxNumDevs = 5.0;
|
||||
|
||||
// State for streaming with bar correction
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double SumPV, // Cumulative price × volume
|
||||
double SumVol, // Cumulative volume
|
||||
double SumPV2, // Cumulative price² × volume
|
||||
int Count, // Bar count since reset
|
||||
double LastValidPrice,
|
||||
double LastValidVolume,
|
||||
bool IsInitialized);
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private int _index;
|
||||
|
||||
public override bool IsHot => _index >= WarmupPeriod;
|
||||
|
||||
/// <summary>
|
||||
/// Upper band (VWAP + numDevs × StdDev)
|
||||
/// </summary>
|
||||
public TValue Upper { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Lower band (VWAP - numDevs × StdDev)
|
||||
/// </summary>
|
||||
public TValue Lower { 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 (Upper - Lower = 2 × numDevs × StdDev)
|
||||
/// </summary>
|
||||
public TValue Width { get; private set; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vwapsd(double numDevs = DefaultNumDevs)
|
||||
{
|
||||
if (numDevs < MinNumDevs)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(numDevs),
|
||||
$"Number of deviations must be at least {MinNumDevs}.");
|
||||
}
|
||||
if (numDevs > MaxNumDevs)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(numDevs),
|
||||
$"Number of deviations must not exceed {MaxNumDevs}.");
|
||||
}
|
||||
|
||||
_numDevs = numDevs;
|
||||
WarmupPeriod = 2; // Need at least 2 bars for variance
|
||||
Name = $"Vwapsd({numDevs:F1})";
|
||||
Init();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Init()
|
||||
{
|
||||
_index = 0;
|
||||
_state = new State(0, 0, 0, 0, double.NaN, double.NaN, false);
|
||||
_p_state = _state;
|
||||
Vwap = new TValue(DateTime.UtcNow, 0);
|
||||
Upper = new TValue(DateTime.UtcNow, 0);
|
||||
Lower = new TValue(DateTime.UtcNow, 0);
|
||||
StdDev = new TValue(DateTime.UtcNow, 0);
|
||||
Width = new TValue(DateTime.UtcNow, 0);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static double GetFiniteValue(double value, ref double lastValid)
|
||||
{
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
lastValid = value;
|
||||
return value;
|
||||
}
|
||||
return double.IsFinite(lastValid) ? lastValid : 0;
|
||||
}
|
||||
|
||||
/// <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 upper = vwap + _numDevs * stdev;
|
||||
double lower = vwap - _numDevs * stdev;
|
||||
|
||||
// Update output values
|
||||
Vwap = new TValue(input.Time, vwap);
|
||||
Upper = new TValue(input.Time, upper);
|
||||
Lower = new TValue(input.Time, lower);
|
||||
StdDev = new TValue(input.Time, stdev);
|
||||
Width = new TValue(input.Time, upper - lower);
|
||||
|
||||
Last = Vwap;
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <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 SD Bands for a bar series.
|
||||
/// </summary>
|
||||
/// <returns>Tuple of (Upper, Lower, Vwap, StdDev)</returns>
|
||||
public static (TSeries Upper, TSeries Lower, TSeries Vwap, TSeries StdDev) Calculate(
|
||||
TBarSeries source,
|
||||
double numDevs = DefaultNumDevs)
|
||||
{
|
||||
Vwapsd vwapsd = new(numDevs);
|
||||
int len = source.Count;
|
||||
|
||||
TSeries upper = new(capacity: len);
|
||||
TSeries lower = new(capacity: len);
|
||||
TSeries vwap = new(capacity: len);
|
||||
TSeries stdev = new(capacity: len);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
vwapsd.Update(source[i], isNew: true);
|
||||
upper.Add(vwapsd.Upper.Time, vwapsd.Upper.Value, isNew: true);
|
||||
lower.Add(vwapsd.Lower.Time, vwapsd.Lower.Value, isNew: true);
|
||||
vwap.Add(vwapsd.Vwap.Time, vwapsd.Vwap.Value, isNew: true);
|
||||
stdev.Add(vwapsd.StdDev.Time, vwapsd.StdDev.Value, isNew: true);
|
||||
}
|
||||
|
||||
return (upper, lower, vwap, stdev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates VWAP SD Bands using span arrays.
|
||||
/// </summary>
|
||||
public static void Calculate(
|
||||
ReadOnlySpan<double> price,
|
||||
ReadOnlySpan<double> volume,
|
||||
Span<double> upper,
|
||||
Span<double> lower,
|
||||
Span<double> vwap,
|
||||
double numDevs = DefaultNumDevs)
|
||||
{
|
||||
int len = price.Length;
|
||||
if (len != volume.Length || len != upper.Length || len != lower.Length || len != vwap.Length)
|
||||
{
|
||||
throw new ArgumentException("All spans must have the same length.", nameof(price));
|
||||
}
|
||||
if (numDevs < MinNumDevs)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(numDevs),
|
||||
$"Number of deviations must be at least {MinNumDevs}.");
|
||||
}
|
||||
if (numDevs > MaxNumDevs)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(numDevs),
|
||||
$"Number of deviations must not exceed {MaxNumDevs}.");
|
||||
}
|
||||
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double sumPV = 0, sumVol = 0, sumPV2 = 0;
|
||||
int count = 0;
|
||||
double lastValidPrice = double.NaN;
|
||||
double lastValidVolume = double.NaN;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double p = GetFiniteValue(price[i], ref lastValidPrice);
|
||||
double v = GetFiniteValue(volume[i], ref lastValidVolume);
|
||||
|
||||
if (v > 0)
|
||||
{
|
||||
sumPV += p * v;
|
||||
sumVol += v;
|
||||
sumPV2 += p * p * v;
|
||||
count++;
|
||||
}
|
||||
|
||||
double vwapVal = sumVol > 0 ? sumPV / sumVol : p;
|
||||
|
||||
double variance = 0;
|
||||
if (sumVol > 0 && count > 1)
|
||||
{
|
||||
double meanP2 = sumPV2 / sumVol;
|
||||
double vwapSquared = vwapVal * vwapVal;
|
||||
variance = Math.Max(0, meanP2 - vwapSquared);
|
||||
}
|
||||
double stdev = Math.Sqrt(variance);
|
||||
|
||||
vwap[i] = vwapVal;
|
||||
upper[i] = vwapVal + numDevs * stdev;
|
||||
lower[i] = vwapVal - numDevs * stdev;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user