mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
This commit is contained in:
@@ -0,0 +1,460 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VwapsdIndicatorTests
|
||||
{
|
||||
// ── Constructor & Defaults ──────────────────────────────────────────
|
||||
|
||||
[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_Constructor_Description_IsNotEmpty()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.False(string.IsNullOrWhiteSpace(indicator.Description));
|
||||
Assert.Contains("volume", indicator.Description, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_CreatesFourLineSeries()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Constructor_LineSeriesNames_BeforeInit()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
// Before OnInit, series have their constructor names
|
||||
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Upper", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower", indicator.LinesSeries[2].Name);
|
||||
Assert.Equal("Width", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
|
||||
// ── MinHistoryDepths ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_MinHistoryDepths_EqualsTwo()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
Assert.Equal(2, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
// ── ShortName ───────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShortName_DefaultFormat()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Equal("VWAPSD (2.0)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// ── SourceCodeLink ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_SourceCodeLink_PointsToGitHub()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Contains("Vwapsd.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// ── OnInit σ Rename ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_RenamesSeriesWithSigmaNotation()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// After OnInit, Upper/Lower should have σ notation
|
||||
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_SigmaNotation_ReflectsNumDevs()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 1.5 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Upper (+1.5σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-1.5σ)", indicator.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_PreservesSeriesCount()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
|
||||
// After init, line series should exist (VWAP, Upper, Lower, Width)
|
||||
indicator.Initialize();
|
||||
Assert.Equal(4, indicator.LinesSeries.Count);
|
||||
}
|
||||
|
||||
// ── Parameters ──────────────────────────────────────────────────────
|
||||
|
||||
[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_ShowColdValues_CanBeChanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: HistoricalBar ────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: NewBar ───────────────────────────────────────────
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// ── ProcessUpdate: NewTick ──────────────────────────────────────────
|
||||
|
||||
[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));
|
||||
}
|
||||
|
||||
// ── MultipleUpdates ─────────────────────────────────────────────────
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// ── AllBandsUpdate ──────────────────────────────────────────────────
|
||||
|
||||
[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)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── BandRelationships ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_BandRelationships_AreCorrect()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Assert.True(upper >= vwap, $"Upper ({upper}) should be >= VWAP ({vwap})");
|
||||
Assert.True(vwap >= lower, $"VWAP ({vwap}) should be >= Lower ({lower})");
|
||||
Assert.True(Math.Abs(width - (upper - lower)) < 0.0001,
|
||||
$"Width ({width}) should equal Upper - Lower ({upper - lower})");
|
||||
}
|
||||
|
||||
// ── VolumeWeighting ─────────────────────────────────────────────────
|
||||
|
||||
[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;
|
||||
|
||||
// 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);
|
||||
|
||||
Assert.True(vwap1 < vwap2, $"VWAP1 ({vwap1}) should be less than VWAP2 ({vwap2}) due to volume weighting");
|
||||
}
|
||||
|
||||
// ── NumDevs Effect ──────────────────────────────────────────────────
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
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})");
|
||||
}
|
||||
|
||||
// ── Width Non-Negative ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Width_IsNonNegative()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 98, 105, 97, 103, 101, 99 };
|
||||
double[] volumes = { 1000, 1200, 800, 1500, 900, 1100, 1300, 700 };
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Width should be non-negative at every bar
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
double w = indicator.LinesSeries[3].GetValue(closes.Length - 1 - i);
|
||||
Assert.True(w >= 0.0, $"Width at bar {i} ({w}) should be >= 0");
|
||||
}
|
||||
}
|
||||
|
||||
// ── SingleBar Zero Width ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_SingleBar_ProducesZeroWidth()
|
||||
{
|
||||
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));
|
||||
|
||||
// With only one bar, stddev is 0 → width should be 0
|
||||
double width = indicator.LinesSeries[3].GetValue(0);
|
||||
Assert.Equal(0.0, width, 4);
|
||||
}
|
||||
|
||||
// ── ShowColdValues False ────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShowColdValues_False_SuppressesColdValues()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// With ShowColdValues=false, cold bars produce NaN
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
// Value is either NaN (suppressed) or finite (hot)
|
||||
Assert.True(double.IsNaN(vwap) || double.IsFinite(vwap));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ShowColdValues_True_ShowsAllValues()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0, ShowColdValues = true };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// With ShowColdValues=true, all values should be finite
|
||||
double vwap = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(vwap));
|
||||
}
|
||||
|
||||
// ── ReInitialize Updates Series Names ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_ReInitialize_UpdatesSigmaNotation()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Upper (+2.0σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-2.0σ)", indicator.LinesSeries[2].Name);
|
||||
|
||||
// Change NumDevs and re-init
|
||||
indicator.NumDevs = 3.0;
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal("Upper (+3.0σ)", indicator.LinesSeries[1].Name);
|
||||
Assert.Equal("Lower (-3.0σ)", indicator.LinesSeries[2].Name);
|
||||
}
|
||||
|
||||
// ── VWAP Series Name Unchanged After Init ───────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void VwapsdIndicator_Initialize_VwapAndWidthNames_Unchanged()
|
||||
{
|
||||
var indicator = new VwapsdIndicator { NumDevs = 2.0 };
|
||||
indicator.Initialize();
|
||||
|
||||
// VWAP and Width series names should remain as constructor set them
|
||||
Assert.Equal("VWAP", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Width", indicator.LinesSeries[3].Name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,699 @@
|
||||
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_SessionReset_ResetsIsHotGating()
|
||||
{
|
||||
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 bars until IsHot is true (WarmupPeriod = 2)
|
||||
vwapsd.Update(bars[0]);
|
||||
Assert.False(vwapsd.IsHot);
|
||||
vwapsd.Update(bars[1]);
|
||||
Assert.True(vwapsd.IsHot);
|
||||
|
||||
// Process more bars to ensure we're well past warmup
|
||||
for (int i = 2; i < 10; i++)
|
||||
{
|
||||
vwapsd.Update(bars[i]);
|
||||
}
|
||||
Assert.True(vwapsd.IsHot);
|
||||
|
||||
// Reset session - IsHot should become false
|
||||
var resetBar1 = new TBar(DateTime.UtcNow, 200, 210, 190, 200, 1000);
|
||||
vwapsd.Update(resetBar1, isNew: true, reset: true);
|
||||
Assert.False(vwapsd.IsHot, "IsHot should be false after reset (1 bar accumulated)");
|
||||
|
||||
// Process second bar after reset - IsHot should become true
|
||||
var resetBar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 205, 215, 195, 205, 1100);
|
||||
vwapsd.Update(resetBar2, isNew: true);
|
||||
Assert.True(vwapsd.IsHot, "IsHot should be true after 2 bars accumulated post-reset");
|
||||
}
|
||||
|
||||
[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[] stdDev = new double[5];
|
||||
double[] wrongSize = new double[3];
|
||||
|
||||
// NumDevs must be >= MinNumDevs
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Vwapsd.Batch(price.AsSpan(), volume.AsSpan(),
|
||||
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0));
|
||||
|
||||
// NumDevs must be <= MaxNumDevs
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
Vwapsd.Batch(price.AsSpan(), volume.AsSpan(),
|
||||
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 6.0));
|
||||
|
||||
// All arrays must be same length
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Vwapsd.Batch(price.AsSpan(), volume.AsSpan(),
|
||||
wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.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];
|
||||
double[] stdDev = new double[5];
|
||||
|
||||
Vwapsd.Batch(price.AsSpan(), volume.AsSpan(),
|
||||
upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.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,614 @@
|
||||
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 bar.HLC3 for price to match streaming mode
|
||||
double[] price = new double[bars.Count];
|
||||
double[] volume = new double[bars.Count];
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
price[i] = bars[i].HLC3;
|
||||
volume[i] = bars[i].Volume;
|
||||
}
|
||||
|
||||
double[] spanVwap = new double[bars.Count];
|
||||
double[] spanUpper = new double[bars.Count];
|
||||
double[] spanLower = new double[bars.Count];
|
||||
double[] spanStdDev = new double[bars.Count];
|
||||
|
||||
Vwapsd.Batch(price.AsSpan(), volume.AsSpan(),
|
||||
spanUpper.AsSpan(), spanLower.AsSpan(),
|
||||
spanVwap.AsSpan(), spanStdDev.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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user