mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 21:48:03 +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,329 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VovIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void VovIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.VolatilityPeriod);
|
||||
Assert.Equal(10, indicator.VovPeriod);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("VOV - Volatility of Volatility", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 30, VovPeriod = 15 };
|
||||
Assert.Contains("VOV", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("30", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
|
||||
Assert.Equal(0, VovIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_Initialize_CreatesInternalVov()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 10, VovPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with varying volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
// Create price movement that generates volatility
|
||||
double basePrice = 100 + Math.Sin(i * 0.3) * (5 + i * 0.1);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice, 1000);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "VOV should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 10, VovPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double basePrice = 100 + i;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice + 1, 1000);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 135, 125, 132, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
var periodCombos = new[] { (5, 3), (10, 5), (20, 10), (30, 15) };
|
||||
|
||||
foreach (var (volPeriod, vovPeriod) in periodCombos)
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = volPeriod, VovPeriod = vovPeriod };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
// Create price movement with varying amplitude
|
||||
double basePrice = 100 + Math.Sin(i * 0.2) * 5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 2, basePrice - 2, basePrice, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"Periods ({volPeriod},{vovPeriod}) should produce finite value");
|
||||
Assert.True(val >= 0, $"Periods ({volPeriod},{vovPeriod}) should produce non-negative value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_VolatilityPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
Assert.Equal(20, indicator.VolatilityPeriod);
|
||||
|
||||
indicator.VolatilityPeriod = 30;
|
||||
Assert.Equal(30, indicator.VolatilityPeriod);
|
||||
|
||||
indicator.VolatilityPeriod = 10;
|
||||
Assert.Equal(10, indicator.VolatilityPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_VovPeriod_CanBeChanged()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
Assert.Equal(10, indicator.VovPeriod);
|
||||
|
||||
indicator.VovPeriod = 15;
|
||||
Assert.Equal(15, indicator.VovPeriod);
|
||||
|
||||
indicator.VovPeriod = 5;
|
||||
Assert.Equal(5, indicator.VovPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new VovIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Vov.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_ConstantPrice_ProducesZero()
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 10, VovPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Constant price - no volatility
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100.01, 99.99, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val < 0.1, "Constant price should produce near-zero VOV");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_ChangingVolatility_ProducesPositiveValue()
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 5, VovPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Low volatility period
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
double price = 100 + (i % 2) * 0.5; // Small oscillations
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 0.5, price - 0.5, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
// High volatility period
|
||||
for (int i = 15; i < 30; i++)
|
||||
{
|
||||
double price = 100 + (i % 2) * 10; // Large oscillations
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 5, price - 5, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val > 0, "Changing volatility should produce positive VOV value");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_UsesClosePrice_ForCalculation()
|
||||
{
|
||||
// VOV uses close price for volatility calculation
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 5, VovPeriod = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Price with varying close but constant OHLC range
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double close = 100 + Math.Sin(i * 0.5) * 5; // Varying close
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 110, 90, close, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(val));
|
||||
Assert.True(val >= 0, "VOV should be non-negative");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_LargerVolatilityPeriod_SmootherOutput()
|
||||
{
|
||||
var indicator1 = new VovIndicator { VolatilityPeriod = 5, VovPeriod = 5 };
|
||||
var indicator2 = new VovIndicator { VolatilityPeriod = 20, VovPeriod = 5 };
|
||||
indicator1.Initialize();
|
||||
indicator2.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var results1 = new List<double>();
|
||||
var results2 = new List<double>();
|
||||
|
||||
for (int i = 0; i < 60; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.3) * 5;
|
||||
indicator1.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicator2.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price, 1000);
|
||||
indicator1.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator2.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
if (i >= 25) // After both are fully warmed up
|
||||
{
|
||||
results1.Add(indicator1.LinesSeries[0].GetValue(0));
|
||||
results2.Add(indicator2.LinesSeries[0].GetValue(0));
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate variance of changes
|
||||
double variance1 = CalculateChangeVariance(results1);
|
||||
double variance2 = CalculateChangeVariance(results2);
|
||||
|
||||
// Longer volatility period should be smoother
|
||||
Assert.True(variance2 <= variance1 * 1.5, // Allow some tolerance
|
||||
$"Longer period should be smoother: short variance={variance1:F6}, long variance={variance2:F6}");
|
||||
}
|
||||
|
||||
private static double CalculateChangeVariance(List<double> values)
|
||||
{
|
||||
if (values.Count < 2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var changes = new List<double>();
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
changes.Add(values[i] - values[i - 1]);
|
||||
}
|
||||
|
||||
double mean = changes.Average();
|
||||
double variance = changes.Select(c => (c - mean) * (c - mean)).Average();
|
||||
return variance;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VovIndicator_VolatilityRegimeChange_RespondsCorrectly()
|
||||
{
|
||||
var indicator = new VovIndicator { VolatilityPeriod = 5, VovPeriod = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Stable volatility regime
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.5) * 2;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 1, price - 1, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double stableVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Transition to variable volatility
|
||||
for (int i = 20; i < 40; i++)
|
||||
{
|
||||
double amplitude = 2 + (i - 20) * 0.5; // Increasing amplitude
|
||||
double price = 100 + Math.Sin(i * 0.5) * amplitude;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + amplitude, price - amplitude, price, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double transitionVal = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(stableVal));
|
||||
Assert.True(double.IsFinite(transitionVal));
|
||||
// During volatility regime change, VOV should typically increase
|
||||
Assert.True(transitionVal > 0, "Changing volatility regime should produce positive VOV");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
// Volatility of Volatility (VOV) Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class VovTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
private const int DefaultVolatilityPeriod = 20;
|
||||
private const int DefaultVovPeriod = 10;
|
||||
|
||||
public VovTests()
|
||||
{
|
||||
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
}
|
||||
|
||||
private TSeries GenerateData(int count)
|
||||
{
|
||||
_gbm.Reset(DateTime.UtcNow.Ticks);
|
||||
var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultParameters_SetsCorrectValues()
|
||||
{
|
||||
var vov = new Vov();
|
||||
Assert.Equal(DefaultVolatilityPeriod, vov.VolatilityPeriod);
|
||||
Assert.Equal(DefaultVovPeriod, vov.VovPeriod);
|
||||
Assert.Equal($"Vov({DefaultVolatilityPeriod},{DefaultVovPeriod})", vov.Name);
|
||||
Assert.Equal(DefaultVolatilityPeriod + DefaultVovPeriod - 1, vov.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomParameters_SetsCorrectValues()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 30, vovPeriod: 15);
|
||||
Assert.Equal(30, vov.VolatilityPeriod);
|
||||
Assert.Equal(15, vov.VovPeriod);
|
||||
Assert.Equal("Vov(30,15)", vov.Name);
|
||||
Assert.Equal(44, vov.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroVolatilityPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vov(volatilityPeriod: 0));
|
||||
Assert.Equal("volatilityPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeVolatilityPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vov(volatilityPeriod: -5));
|
||||
Assert.Equal("volatilityPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroVovPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vov(volatilityPeriod: 20, vovPeriod: 0));
|
||||
Assert.Equal("vovPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativeVovPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Vov(volatilityPeriod: 20, vovPeriod: -5));
|
||||
Assert.Equal("vovPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var vov = new Vov(source, volatilityPeriod: 10, vovPeriod: 5);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, vov.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_SingleValue_ReturnsZero()
|
||||
{
|
||||
var vov = new Vov();
|
||||
var result = vov.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ConstantValues_ConvergesToZero()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
// Constant price = zero volatility = zero VOV
|
||||
Assert.True(vov.Last.Value < 0.001, $"Expected near zero, got {vov.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsNonNegativeValue()
|
||||
{
|
||||
var vov = new Vov();
|
||||
var data = GenerateData(100);
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var result = vov.Update(data[i]);
|
||||
Assert.True(result.Value >= 0, $"VOV should be non-negative, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_HighVolatilityVariation_ProducesHigherVov()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
|
||||
// First phase: low volatility
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + (i % 2) * 0.1));
|
||||
}
|
||||
double lowVolVov = vov.Last.Value;
|
||||
|
||||
// Reset and test high volatility variation
|
||||
vov.Reset();
|
||||
|
||||
// Second phase: alternating high/low volatility
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
// High volatility period
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + (j % 2) * 10.0));
|
||||
}
|
||||
// Low volatility period
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + (j % 2) * 0.1));
|
||||
}
|
||||
}
|
||||
double highVolVov = vov.Last.Value;
|
||||
|
||||
Assert.True(highVolVov > lowVolVov, $"High vol variation VOV ({highVolVov}) should exceed low vol VOV ({lowVolVov})");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot and Warmup Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 10, vovPeriod: 5);
|
||||
// WarmupPeriod = 10 + 5 - 1 = 14. IsHot when PriceCount >= 10 AND VolCount >= 5.
|
||||
// After 5 bars: PriceCount=5, VolCount=4 (vol counting starts at bar 2)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
Assert.False(vov.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 10, vovPeriod: 5);
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
Assert.True(vov.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsCorrectlyCombined()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 15, vovPeriod: 8);
|
||||
Assert.Equal(22, vov.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction (isNew) Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewTrue_AdvancesState()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
double valueAfterUpdates = vov.Last.Value;
|
||||
|
||||
// Additional update should change value
|
||||
vov.Update(new TValue(time.AddSeconds(10), 150.0), isNew: true);
|
||||
double valueAfterNew = vov.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueAfterUpdates, valueAfterNew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
double valueBeforeCorrection = vov.Last.Value;
|
||||
|
||||
// First correction
|
||||
vov.Update(new TValue(time.AddSeconds(15), 200.0), isNew: false);
|
||||
double valueAfterCorrection1 = vov.Last.Value;
|
||||
|
||||
// Second correction to different value
|
||||
vov.Update(new TValue(time.AddSeconds(15), 50.0), isNew: false);
|
||||
double valueAfterCorrection2 = vov.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBeforeCorrection, valueAfterCorrection1);
|
||||
Assert.NotEqual(valueAfterCorrection1, valueAfterCorrection2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_RestoresPreviousState()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100.0 + i), isNew: true);
|
||||
}
|
||||
|
||||
// Add a new bar
|
||||
vov.Update(new TValue(time.AddSeconds(15), 110.0), isNew: true);
|
||||
double baseValue = vov.Last.Value;
|
||||
|
||||
// Multiple corrections should all be based on the same previous state
|
||||
vov.Update(new TValue(time.AddSeconds(15), 200.0), isNew: false);
|
||||
vov.Update(new TValue(time.AddSeconds(15), 110.0), isNew: false);
|
||||
double restoredValue = vov.Last.Value;
|
||||
|
||||
Assert.Equal(baseValue, restoredValue, 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsAllState()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(vov.IsHot);
|
||||
|
||||
vov.Reset();
|
||||
|
||||
Assert.False(vov.IsHot);
|
||||
Assert.Equal(default, vov.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsReuse()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
double firstRunValue = vov.Last.Value;
|
||||
|
||||
vov.Reset();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100.0 + i));
|
||||
}
|
||||
double secondRunValue = vov.Last.Value;
|
||||
|
||||
Assert.Equal(firstRunValue, secondRunValue, 10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN and Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNInput_UsesLastValidValue()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
// Update with NaN
|
||||
vov.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
double valueAfterNaN = vov.Last.Value;
|
||||
|
||||
Assert.True(double.IsFinite(valueAfterNaN));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityInput_UsesLastValidValue()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
vov.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(vov.Last.Value));
|
||||
|
||||
vov.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(vov.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleNaNs_StaysFinite()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vov.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(vov.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_WithNaN_ProducesSafeOutput()
|
||||
{
|
||||
double[] source = [100, 102, double.NaN, 98, 101, 103, 99, 100, 101, 102];
|
||||
double[] output = new double[10];
|
||||
|
||||
Vov.Batch(source, output, volatilityPeriod: 5, vovPeriod: 3);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TSeries and Batch Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_ReturnsCorrectLength()
|
||||
{
|
||||
var vov = new Vov();
|
||||
var data = GenerateData(100);
|
||||
|
||||
var result = vov.Update(data);
|
||||
Assert.Equal(data.Count, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Static_ProducesValidResults()
|
||||
{
|
||||
var data = GenerateData(100);
|
||||
|
||||
var result = Vov.Batch(data, volatilityPeriod: 10, vovPeriod: 5);
|
||||
|
||||
Assert.Equal(data.Count, result.Count);
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Values[i]));
|
||||
Assert.True(result.Values[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ProducesConsistentResults()
|
||||
{
|
||||
var data = GenerateData(100);
|
||||
|
||||
double[] output = new double[100];
|
||||
Vov.Batch(data.Values, output, volatilityPeriod: 10, vovPeriod: 5);
|
||||
|
||||
// Verify all outputs are valid
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroVolatilityPeriod_ThrowsArgumentException()
|
||||
{
|
||||
double[] source = [1, 2, 3];
|
||||
double[] output = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vov.Batch(source, output, volatilityPeriod: 0));
|
||||
Assert.Equal("volatilityPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ZeroVovPeriod_ThrowsArgumentException()
|
||||
{
|
||||
double[] source = [1, 2, 3];
|
||||
double[] output = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vov.Batch(source, output, volatilityPeriod: 10, vovPeriod: 0));
|
||||
Assert.Equal("vovPeriod", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooSmall_ThrowsArgumentException()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[3];
|
||||
var ex = Assert.Throws<ArgumentException>(() => Vov.Batch(source, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptySource_DoesNotThrow()
|
||||
{
|
||||
double[] source = [];
|
||||
double[] output = [];
|
||||
Vov.Batch(source, output);
|
||||
// Should complete without exception
|
||||
Assert.Empty(output);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Mode Consistency Tests
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResults()
|
||||
{
|
||||
const int dataLen = 100;
|
||||
var data = GenerateData(dataLen);
|
||||
int volPeriod = 10;
|
||||
int vovPeriod = 5;
|
||||
|
||||
// Mode 1: Streaming
|
||||
var streamingVov = new Vov(volPeriod, vovPeriod);
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
streamingVov.Update(data[i], isNew: true);
|
||||
}
|
||||
|
||||
// Mode 2: TSeries batch
|
||||
var batchResult = Vov.Batch(data, volPeriod, vovPeriod);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[dataLen];
|
||||
Vov.Batch(data.Values, spanOutput, volPeriod, vovPeriod);
|
||||
|
||||
// Compare last 50 values (after warmup)
|
||||
int compareStart = dataLen - 50;
|
||||
for (int i = compareStart; i < dataLen; i++)
|
||||
{
|
||||
double batch = batchResult[i].Value;
|
||||
double span = spanOutput[i];
|
||||
|
||||
// Batch and Span should match exactly
|
||||
Assert.Equal(batch, span, Tolerance);
|
||||
}
|
||||
|
||||
// Final values should match
|
||||
Assert.Equal(streamingVov.Last.Value, batchResult[dataLen - 1].Value, 1e-8);
|
||||
Assert.Equal(streamingVov.Last.Value, spanOutput[dataLen - 1], 1e-8);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_FiresOnUpdate()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
int eventCount = 0;
|
||||
|
||||
vov.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
Assert.Equal(5, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Event_ChainedIndicator_ReceivesUpdates()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var vov = new Vov(source, volatilityPeriod: 10, vovPeriod: 5);
|
||||
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(vov.IsHot);
|
||||
Assert.True(double.IsFinite(vov.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region TBar Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_TBar_UsesClosePrice()
|
||||
{
|
||||
var vov1 = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var vov2 = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
var bar = new TBar(time.AddSeconds(i), 100.0, 105.0, 95.0, 102.0 + i, 1000);
|
||||
vov1.Update(bar);
|
||||
vov2.Update(new TValue(time.AddSeconds(i), bar.Close));
|
||||
}
|
||||
|
||||
// Both should produce same result (using close price)
|
||||
Assert.Equal(vov1.Last.Value, vov2.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Large Period Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeVolatilityPeriod_UsesArrayPool()
|
||||
{
|
||||
const int dataLen = 1000;
|
||||
double[] source = new double[dataLen];
|
||||
double[] output = new double[dataLen];
|
||||
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
source[i] = 100.0 + (i % 50);
|
||||
}
|
||||
|
||||
// Period > 256 should use ArrayPool
|
||||
Vov.Batch(source, output, volatilityPeriod: 300, vovPeriod: 10);
|
||||
|
||||
// Verify outputs are valid
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeVovPeriod_UsesArrayPool()
|
||||
{
|
||||
const int dataLen = 1000;
|
||||
double[] source = new double[dataLen];
|
||||
double[] output = new double[dataLen];
|
||||
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
source[i] = 100.0 + (i % 50);
|
||||
}
|
||||
|
||||
// Period > 256 should use ArrayPool
|
||||
Vov.Batch(source, output, volatilityPeriod: 20, vovPeriod: 300);
|
||||
|
||||
// Verify outputs are valid
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
const int dataLen = 10000;
|
||||
var bars = new GBM(seed: 42).Fetch(dataLen, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] source = bars.CloseValues.ToArray();
|
||||
double[] output = new double[dataLen];
|
||||
|
||||
Vov.Batch(source, output, DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
// Verify all outputs are valid
|
||||
for (int i = 0; i < dataLen; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]));
|
||||
Assert.True(output[i] >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_SetsInitialState()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
double[] warmupData = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114];
|
||||
|
||||
vov.Prime(warmupData);
|
||||
|
||||
Assert.True(vov.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
namespace QuanTAlib.Test;
|
||||
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for VOV (Volatility of Volatility).
|
||||
/// VOV = StdDev(StdDev(price, volatilityPeriod), vovPeriod)
|
||||
/// Uses population standard deviation: sqrt(mean(x²) - mean(x)²)
|
||||
/// </summary>
|
||||
public class VovValidationTests
|
||||
{
|
||||
private const int DefaultVolatilityPeriod = 20;
|
||||
private const int DefaultVovPeriod = 10;
|
||||
|
||||
private static TSeries GenerateTestData(int count = 100)
|
||||
{
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(bars[i].Time, bars[i].Close));
|
||||
}
|
||||
return ts;
|
||||
}
|
||||
|
||||
// === Mathematical Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates the VOV formula: StdDev(StdDev(price, volPeriod), vovPeriod)
|
||||
/// using population standard deviation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_Formula_IsCorrect()
|
||||
{
|
||||
// Test with small periods for manual verification
|
||||
int volPeriod = 3;
|
||||
int vovPeriod = 2;
|
||||
double[] prices = [100, 102, 98, 105, 100, 103];
|
||||
|
||||
var vov = new Vov(volPeriod, vovPeriod);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Manual calculation of inner stddevs using population formula
|
||||
var innerStdDevs = new List<double>();
|
||||
|
||||
for (int i = 0; i < prices.Length; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), prices[i]));
|
||||
|
||||
if (i >= volPeriod - 1)
|
||||
{
|
||||
// Calculate inner stddev manually
|
||||
var window = prices.Skip(i - volPeriod + 1).Take(volPeriod).ToArray();
|
||||
double mean = window.Average();
|
||||
double variance = window.Select(x => (x - mean) * (x - mean)).Average();
|
||||
double stddev = Math.Sqrt(variance);
|
||||
innerStdDevs.Add(stddev);
|
||||
}
|
||||
}
|
||||
|
||||
// Now calculate outer stddev of the last vovPeriod inner stddevs
|
||||
if (innerStdDevs.Count >= vovPeriod)
|
||||
{
|
||||
var recentInnerStdDevs = innerStdDevs.TakeLast(vovPeriod).ToArray();
|
||||
double meanInner = recentInnerStdDevs.Average();
|
||||
double varianceOuter = recentInnerStdDevs.Select(x => (x - meanInner) * (x - meanInner)).Average();
|
||||
double expectedVov = Math.Sqrt(varianceOuter);
|
||||
|
||||
Assert.Equal(expectedVov, vov.Last.Value, 8);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV is zero when price is constant (no volatility).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_ConstantPrice_ReturnsZero()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Constant prices = zero volatility = zero VOV
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var result = vov.Update(new TValue(time.AddSeconds(i), 100.0));
|
||||
|
||||
if (vov.IsHot)
|
||||
{
|
||||
Assert.Equal(0.0, result.Value, 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV is zero when volatility is constant.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_ConstantVolatility_ReturnsZero()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 3, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Repeating pattern with constant volatility
|
||||
// Pattern: 100, 102, 100, 102, 100, 102... has constant stddev
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double price = i % 2 == 0 ? 100.0 : 102.0;
|
||||
vov.Update(new TValue(time.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// After many bars with identical pattern, VOV should stabilize near zero
|
||||
// (constant inner volatility means outer VOV approaches zero)
|
||||
Assert.True(vov.Last.Value < 0.5,
|
||||
$"Constant volatility pattern should produce near-zero VOV, got {vov.Last.Value}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV increases when volatility changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_ChangingVolatility_ProducesPositiveValue()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Low volatility period
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.3) * 0.5; // Small oscillations
|
||||
vov.Update(new TValue(time.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// High volatility period
|
||||
for (int i = 10; i < 20; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.3) * 10; // Large oscillations
|
||||
vov.Update(new TValue(time.AddSeconds(i), price));
|
||||
}
|
||||
|
||||
// VOV should be positive (volatility changed)
|
||||
Assert.True(vov.Last.Value > 0, $"VOV should be positive when volatility changes, got {vov.Last.Value}");
|
||||
}
|
||||
|
||||
// === Streaming vs Batch Consistency ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates streaming calculation matches batch calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_StreamingMatchesBatch()
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingVov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
var streamingResults = new double[data.Count];
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
streamingResults[i] = streamingVov.Update(data[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchOutput = new double[data.Count];
|
||||
Vov.Batch(data.Values, batchOutput, DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchOutput[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates TSeries batch matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_TSeriesBatchMatchesStreaming()
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingVov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
streamingVov.Update(data[i]);
|
||||
}
|
||||
|
||||
// Batch via TSeries
|
||||
var batchResult = Vov.Batch(data, DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
Assert.Equal(streamingVov.Last.Value, batchResult.Last.Value, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates span-based calculation matches streaming.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_SpanMatchesStreaming()
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
|
||||
// Streaming
|
||||
var streamingVov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
streamingVov.Update(data[i]);
|
||||
}
|
||||
|
||||
// Span
|
||||
var spanOutput = new double[data.Count];
|
||||
Vov.Batch(data.Values, spanOutput, DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
Assert.Equal(streamingVov.Last.Value, spanOutput[^1], 10);
|
||||
}
|
||||
|
||||
// === Property Validation ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV is always non-negative.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_Output_IsNonNegative()
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
var vov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var result = vov.Update(data[i]);
|
||||
Assert.True(result.Value >= 0, $"VOV should be non-negative at index {i}, got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV output is always finite.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_Output_IsFinite()
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
var vov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var result = vov.Update(data[i]);
|
||||
Assert.True(double.IsFinite(result.Value), $"VOV should be finite at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
// === Bar Correction Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates bar correction works correctly.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_BarCorrection_WorksCorrectly()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed initial data
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
// Add new bar
|
||||
vov.Update(new TValue(time.AddSeconds(10), 110), isNew: true);
|
||||
double afterNew = vov.Last.Value;
|
||||
|
||||
// Correct with different value
|
||||
vov.Update(new TValue(time.AddSeconds(10), 90), isNew: false);
|
||||
double afterCorrection = vov.Last.Value;
|
||||
|
||||
// Restore original
|
||||
vov.Update(new TValue(time.AddSeconds(10), 110), isNew: false);
|
||||
double afterRestore = vov.Last.Value;
|
||||
|
||||
Assert.NotEqual(afterNew, afterCorrection);
|
||||
Assert.Equal(afterNew, afterRestore, 10);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates iterative corrections converge to fresh calculation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_IterativeCorrections_Converge()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed data
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
// Multiple corrections on same bar
|
||||
for (int j = 0; j < 5; j++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(9), 100 + j * 5), isNew: false);
|
||||
}
|
||||
|
||||
// Final correction back to original
|
||||
vov.Update(new TValue(time.AddSeconds(9), 109), isNew: false);
|
||||
double afterCorrections = vov.Last.Value;
|
||||
|
||||
// Fresh calculation
|
||||
var vovFresh = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vovFresh.Update(new TValue(time.AddSeconds(i), 100 + i), isNew: true);
|
||||
}
|
||||
double freshValue = vovFresh.Last.Value;
|
||||
|
||||
Assert.Equal(freshValue, afterCorrections, 10);
|
||||
}
|
||||
|
||||
// === Reset Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates Reset clears state completely.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_Reset_ClearsState()
|
||||
{
|
||||
var vov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
var data = GenerateTestData(50);
|
||||
|
||||
// Feed data
|
||||
for (int i = 0; i < 40; i++)
|
||||
{
|
||||
vov.Update(data[i]);
|
||||
}
|
||||
|
||||
// Reset
|
||||
vov.Reset();
|
||||
|
||||
// State should be cleared
|
||||
Assert.False(vov.IsHot);
|
||||
Assert.Equal(default, vov.Last);
|
||||
|
||||
// Feed data again
|
||||
for (int i = 0; i < 35; i++)
|
||||
{
|
||||
vov.Update(data[i]);
|
||||
}
|
||||
|
||||
// Fresh indicator
|
||||
var vovFresh = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
for (int i = 0; i < 35; i++)
|
||||
{
|
||||
vovFresh.Update(data[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(vovFresh.Last.Value, vov.Last.Value, 10);
|
||||
}
|
||||
|
||||
// === Warmup Period Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates WarmupPeriod equals volatilityPeriod + vovPeriod - 1.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_WarmupPeriod_EqualsSum()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 20, vovPeriod: 10);
|
||||
Assert.Equal(29, vov.WarmupPeriod); // 20 + 10 - 1
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates IsHot is true after warmup period bars.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_IsHot_AfterWarmupPeriod()
|
||||
{
|
||||
int volPeriod = 5;
|
||||
int vovPeriod = 3;
|
||||
int warmup = volPeriod + vovPeriod - 1; // 7
|
||||
|
||||
var vov = new Vov(volPeriod, vovPeriod);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < warmup - 1; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100 + i));
|
||||
Assert.False(vov.IsHot, $"Should not be hot at bar {i}");
|
||||
}
|
||||
|
||||
vov.Update(new TValue(time.AddSeconds(warmup - 1), 100 + warmup - 1));
|
||||
Assert.True(vov.IsHot, "Should be hot after warmup period");
|
||||
}
|
||||
|
||||
// === NaN/Infinity Handling ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates NaN input uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_NaNInput_UsesLastValid()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
var result = vov.Update(new TValue(time.AddSeconds(10), double.NaN));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates Infinity input uses last valid value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_InfinityInput_UsesLastValid()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
vov.Update(new TValue(time.AddSeconds(i), 100 + i));
|
||||
}
|
||||
|
||||
var result = vov.Update(new TValue(time.AddSeconds(10), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates batch handles NaN values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_BatchNaN_HandledCorrectly()
|
||||
{
|
||||
var source = new double[] { 100, 102, double.NaN, 98, 101, 103, 99, 104, 100, 102 };
|
||||
var output = new double[10];
|
||||
|
||||
Vov.Batch(source, output, volatilityPeriod: 3, vovPeriod: 3);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output at index {i} should be finite");
|
||||
Assert.True(output[i] >= 0, $"Output at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
// === Period Sensitivity ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates longer volatility period produces smoother inner volatility.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_LongerVolatilityPeriod_SmootherResults()
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
|
||||
var vovShort = new Vov(volatilityPeriod: 5, vovPeriod: 5);
|
||||
var vovLong = new Vov(volatilityPeriod: 20, vovPeriod: 5);
|
||||
|
||||
var shortResults = new List<double>();
|
||||
var longResults = new List<double>();
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
shortResults.Add(vovShort.Update(data[i]).Value);
|
||||
longResults.Add(vovLong.Update(data[i]).Value);
|
||||
}
|
||||
|
||||
// Calculate variance of changes (smoothness measure) after warmup
|
||||
double shortVariance = CalculateChangeVariance(shortResults.Skip(25).ToList());
|
||||
double longVariance = CalculateChangeVariance(longResults.Skip(25).ToList());
|
||||
|
||||
// Longer volatility period should produce more stable VOV
|
||||
Assert.True(longVariance < shortVariance,
|
||||
$"Longer period should be smoother: short variance={shortVariance:F6}, long variance={longVariance:F6}");
|
||||
}
|
||||
|
||||
private static double CalculateChangeVariance(List<double> values)
|
||||
{
|
||||
if (values.Count < 2)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var changes = new List<double>();
|
||||
for (int i = 1; i < values.Count; i++)
|
||||
{
|
||||
changes.Add(values[i] - values[i - 1]);
|
||||
}
|
||||
|
||||
double mean = changes.Average();
|
||||
double variance = changes.Select(c => (c - mean) * (c - mean)).Average();
|
||||
return variance;
|
||||
}
|
||||
|
||||
// === Stability Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates stability over repeated runs with same seed.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_Stability_ConsistentOverRepeatedRuns()
|
||||
{
|
||||
var results = new List<double>();
|
||||
|
||||
for (int run = 0; run < 3; run++)
|
||||
{
|
||||
var data = GenerateTestData(100);
|
||||
var vov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
vov.Update(data[i]);
|
||||
}
|
||||
results.Add(vov.Last.Value);
|
||||
}
|
||||
|
||||
Assert.Equal(results[0], results[1], 15);
|
||||
Assert.Equal(results[1], results[2], 15);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV responds to volatility regime changes.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_RespondsToVolatilityRegimeChange()
|
||||
{
|
||||
var vov = new Vov(volatilityPeriod: 5, vovPeriod: 5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Stable volatility regime
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.5) * 2; // Consistent amplitude
|
||||
vov.Update(new TValue(time.AddSeconds(i), price));
|
||||
}
|
||||
double stableVov = vov.Last.Value;
|
||||
|
||||
// Transition to higher volatility
|
||||
for (int i = 20; i < 35; i++)
|
||||
{
|
||||
double price = 100 + Math.Sin(i * 0.5) * (2 + (i - 20) * 0.5); // Increasing amplitude
|
||||
vov.Update(new TValue(time.AddSeconds(i), price));
|
||||
}
|
||||
double transitionVov = vov.Last.Value;
|
||||
|
||||
// During transition, VOV should increase (volatility is changing)
|
||||
Assert.True(transitionVov > stableVov * 0.5,
|
||||
$"VOV should respond to volatility regime change: stable={stableVov:F4}, transition={transitionVov:F4}");
|
||||
}
|
||||
|
||||
// === Large Data Tests ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates handling of large datasets.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_LargeDataset_HandledCorrectly()
|
||||
{
|
||||
var data = GenerateTestData(1000);
|
||||
var vov = new Vov(DefaultVolatilityPeriod, DefaultVovPeriod);
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
var result = vov.Update(data[i]);
|
||||
Assert.True(double.IsFinite(result.Value), $"Value at index {i} should be finite");
|
||||
Assert.True(result.Value >= 0, $"Value at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates batch handles large periods.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_LargePeriods_BatchHandled()
|
||||
{
|
||||
var data = GenerateTestData(500);
|
||||
var output = new double[500];
|
||||
|
||||
// Large periods that exceed stackalloc threshold
|
||||
Vov.Batch(data.Values, output, volatilityPeriod: 100, vovPeriod: 50);
|
||||
|
||||
// Last values should be finite and non-negative
|
||||
for (int i = 150; i < output.Length; i++) // After full warmup
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"Output at index {i} should be finite");
|
||||
Assert.True(output[i] >= 0, $"Output at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
// === Known Value Test ===
|
||||
|
||||
/// <summary>
|
||||
/// Validates VOV against manually calculated known values.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Vov_KnownValues_MatchExpected()
|
||||
{
|
||||
// Simple case: period 2 for both, prices: 100, 102, 98, 104
|
||||
var vov = new Vov(volatilityPeriod: 2, vovPeriod: 2);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Inner stddev calculations:
|
||||
// Bar 0-1: stddev([100,102]) = sqrt(mean([10000,10404]) - mean([100,102])^2)
|
||||
// = sqrt(10202 - 10201) = sqrt(1) = 1
|
||||
// Bar 1-2: stddev([102,98]) = sqrt(mean([10404,9604]) - mean([102,98])^2)
|
||||
// = sqrt(10004 - 10000) = sqrt(4) = 2
|
||||
// Bar 2-3: stddev([98,104]) = sqrt(mean([9604,10816]) - mean([98,104])^2)
|
||||
// = sqrt(10210 - 10201) = sqrt(9) = 3
|
||||
|
||||
// Outer VOV (last 2 inner stddevs):
|
||||
// At bar 2: stddev([1,2]) = sqrt(mean([1,4]) - mean([1,2])^2) = sqrt(2.5 - 2.25) = sqrt(0.25) = 0.5
|
||||
// At bar 3: stddev([2,3]) = sqrt(mean([4,9]) - mean([2,3])^2) = sqrt(6.5 - 6.25) = sqrt(0.25) = 0.5
|
||||
|
||||
vov.Update(new TValue(time.AddSeconds(0), 100));
|
||||
vov.Update(new TValue(time.AddSeconds(1), 102));
|
||||
vov.Update(new TValue(time.AddSeconds(2), 98));
|
||||
var result = vov.Update(new TValue(time.AddSeconds(3), 104));
|
||||
|
||||
Assert.Equal(0.5, result.Value, 8);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user