more volatilty

This commit is contained in:
Miha Kralj
2026-02-02 13:42:47 -08:00
parent dde19f2226
commit a03d7aa0ce
89 changed files with 21551 additions and 438 deletions
+329
View File
@@ -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");
}
}
+52
View File
@@ -0,0 +1,52 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class VovIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Volatility Period", sortIndex: 1, 1, 200, 1, 0)]
public int VolatilityPeriod { get; set; } = 20;
[InputParameter("VOV Period", sortIndex: 2, 1, 200, 1, 0)]
public int VovPeriod { get; set; } = 10;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Vov _vov = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"VOV({VolatilityPeriod},{VovPeriod})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volatility/vov/Vov.Quantower.cs";
public VovIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "VOV - Volatility of Volatility";
Description = "Volatility of Volatility measures the standard deviation of volatility itself, quantifying how much volatility fluctuates over time";
_series = new LineSeries(name: "VOV", color: IndicatorExtensions.Volatility, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
protected override void OnInit()
{
_vov = new Vov(VolatilityPeriod, VovPeriod);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TBar bar = this.GetInputBar(args);
TValue result = _vov.Update(bar, isNew: args.IsNewBar());
_series.SetValue(result.Value, _vov.IsHot, ShowColdValues);
}
}
+656
View File
@@ -0,0 +1,656 @@
// 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.Calculate(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.Calculate(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;
double[] source = new double[dataLen];
double[] output = new double[dataLen];
// Fill with realistic data
double price = 100.0;
var rng = new Random(42);
for (int i = 0; i < dataLen; i++)
{
double change = (rng.NextDouble() - 0.5) * 2; // -1% to +1%
price *= (1 + change / 100);
source[i] = price;
}
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
}
+626
View File
@@ -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.Calculate(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);
}
}
+465
View File
@@ -0,0 +1,465 @@
// Volatility of Volatility (VOV) Indicator
// Measures the stability of volatility by calculating the standard deviation of volatility
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// VOV: Volatility of Volatility
/// A second-order volatility indicator that measures how stable or unstable
/// the volatility itself is, by calculating the standard deviation of a volatility series.
/// </summary>
/// <remarks>
/// <b>Calculation steps:</b>
/// <list type="number">
/// <item>Calculate initial volatility: StdDev(price, volatilityPeriod)</item>
/// <item>Calculate VOV: StdDev(volatility, vovPeriod)</item>
/// </list>
///
/// <b>Key characteristics:</b>
/// <list type="bullet">
/// <item>High VOV indicates unstable/changing volatility regime</item>
/// <item>Low VOV indicates stable/consistent volatility</item>
/// <item>Useful for volatility regime detection and risk management</item>
/// <item>Can signal transitions between calm and turbulent markets</item>
/// </list>
///
/// <b>Interpretation:</b>
/// <list type="bullet">
/// <item>Rising VOV may precede major market moves</item>
/// <item>Falling VOV suggests volatility is stabilizing</item>
/// <item>Extreme VOV values can indicate regime changes</item>
/// </list>
/// </remarks>
[SkipLocalsInit]
public sealed class Vov : AbstractBase
{
private readonly int _volatilityPeriod;
private readonly int _vovPeriod;
private readonly RingBuffer _priceBuffer;
private readonly RingBuffer _volatilityBuffer;
[StructLayout(LayoutKind.Auto)]
private record struct State(
double PriceSum,
double PriceSumSq,
double VolSum,
double VolSumSq,
double LastValidPrice,
double LastVov,
int PriceCount,
int VolCount
);
private State _s;
private State _ps;
// Backup buffers for state rollback
private readonly double[] _priceBackup;
private readonly double[] _volatilityBackup;
/// <summary>
/// Initializes a new instance of the Vov class.
/// </summary>
/// <param name="volatilityPeriod">The lookback period for initial volatility calculation (default 20).</param>
/// <param name="vovPeriod">The lookback period for VOV calculation (default 10).</param>
/// <exception cref="ArgumentException">Thrown when any period is less than 1.</exception>
public Vov(int volatilityPeriod = 20, int vovPeriod = 10)
{
if (volatilityPeriod <= 0)
{
throw new ArgumentException("Volatility period must be greater than 0", nameof(volatilityPeriod));
}
if (vovPeriod <= 0)
{
throw new ArgumentException("VOV period must be greater than 0", nameof(vovPeriod));
}
_volatilityPeriod = volatilityPeriod;
_vovPeriod = vovPeriod;
WarmupPeriod = volatilityPeriod + vovPeriod - 1;
Name = $"Vov({volatilityPeriod},{vovPeriod})";
_priceBuffer = new RingBuffer(volatilityPeriod);
_volatilityBuffer = new RingBuffer(vovPeriod);
_priceBackup = new double[volatilityPeriod];
_volatilityBackup = new double[vovPeriod];
_s = new State(0, 0, 0, 0, 0, 0, 0, 0);
_ps = _s;
}
/// <summary>
/// Initializes a new instance of the Vov class with a source.
/// </summary>
/// <param name="source">The data source for chaining.</param>
/// <param name="volatilityPeriod">The volatility period (default 20).</param>
/// <param name="vovPeriod">The VOV period (default 10).</param>
public Vov(ITValuePublisher source, int volatilityPeriod = 20, int vovPeriod = 10) : this(volatilityPeriod, vovPeriod)
{
source.Pub += Handle;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the indicator has enough data for valid results.
/// </summary>
public override bool IsHot => _s.PriceCount >= _volatilityPeriod && _s.VolCount >= _vovPeriod;
/// <summary>
/// The volatility lookback period.
/// </summary>
public int VolatilityPeriod => _volatilityPeriod;
/// <summary>
/// The VOV lookback period.
/// </summary>
public int VovPeriod => _vovPeriod;
/// <summary>
/// Updates the indicator with a TValue input.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
return UpdateCore(input.Time, input.Value, isNew);
}
/// <summary>
/// Updates the indicator with a new bar (uses close price).
/// </summary>
/// <param name="bar">The input bar.</param>
/// <param name="isNew">Whether this is a new bar or an update.</param>
/// <returns>The calculated VOV value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
return UpdateCore(bar.Time, bar.Close, isNew);
}
/// <inheritdoc/>
public override TSeries Update(TSeries source)
{
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.Values, vSpan, _volatilityPeriod, _vovPeriod);
source.Times.CopyTo(tSpan);
// Update internal state
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]), isNew: true);
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(long timeTicks, double price, bool isNew)
{
if (isNew)
{
_ps = _s;
// Backup buffers
_priceBuffer.CopyTo(_priceBackup);
_volatilityBuffer.CopyTo(_volatilityBackup);
}
else
{
_s = _ps;
// Restore buffers
_priceBuffer.Clear();
for (int i = 0; i < _priceBackup.Length && i < _ps.PriceCount; i++)
{
_priceBuffer.Add(_priceBackup[i]);
}
_volatilityBuffer.Clear();
for (int i = 0; i < _volatilityBackup.Length && i < _ps.VolCount; i++)
{
_volatilityBuffer.Add(_volatilityBackup[i]);
}
}
var s = _s;
// Handle non-finite values
if (!double.IsFinite(price))
{
price = s.LastValidPrice;
}
else
{
s.LastValidPrice = price;
}
// Update price running sums (remove oldest if buffer is full)
double priceSum = s.PriceSum;
double priceSumSq = s.PriceSumSq;
if (_priceBuffer.Count >= _volatilityPeriod)
{
double oldest = _priceBuffer[0];
priceSum -= oldest;
priceSumSq -= oldest * oldest;
}
priceSum += price;
priceSumSq += price * price;
_priceBuffer.Add(price);
int priceCount = Math.Min(_priceBuffer.Count, _volatilityPeriod);
// Calculate initial volatility (population standard deviation)
double volatility = 0;
if (priceCount > 1)
{
double mean = priceSum / priceCount;
double variance = (priceSumSq / priceCount) - (mean * mean);
volatility = Math.Sqrt(Math.Max(0.0, variance));
}
// Update volatility running sums (remove oldest if buffer is full)
double volSum = s.VolSum;
double volSumSq = s.VolSumSq;
if (_volatilityBuffer.Count >= _vovPeriod)
{
double oldestVol = _volatilityBuffer[0];
volSum -= oldestVol;
volSumSq -= oldestVol * oldestVol;
}
volSum += volatility;
volSumSq += volatility * volatility;
_volatilityBuffer.Add(volatility);
int volCount = Math.Min(_volatilityBuffer.Count, _vovPeriod);
// Calculate VOV (population standard deviation of volatility)
double vov = 0;
if (volCount > 1)
{
double volMean = volSum / volCount;
double volVariance = (volSumSq / volCount) - (volMean * volMean);
vov = Math.Sqrt(Math.Max(0.0, volVariance));
}
if (!double.IsFinite(vov) || vov < 0)
{
vov = s.LastVov;
}
else
{
s.LastVov = vov;
}
// Update state
s.PriceSum = priceSum;
s.PriceSumSq = priceSumSq;
s.VolSum = volSum;
s.VolSumSq = volSumSq;
if (isNew)
{
s.PriceCount = Math.Min(s.PriceCount + 1, _volatilityPeriod);
// Only start counting vol after we have enough prices for valid volatility
if (s.PriceCount >= _volatilityPeriod)
{
s.VolCount = Math.Min(s.VolCount + 1, _vovPeriod);
}
}
_s = s;
Last = new TValue(timeTicks, vov);
PubEvent(Last, isNew);
return Last;
}
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
}
}
/// <inheritdoc/>
public override void Reset()
{
_priceBuffer.Clear();
_volatilityBuffer.Clear();
Array.Clear(_priceBackup);
Array.Clear(_volatilityBackup);
_s = new State(0, 0, 0, 0, 0, 0, 0, 0);
_ps = _s;
Last = default;
}
/// <summary>
/// Calculates VOV for a series (static).
/// </summary>
/// <param name="source">The source series.</param>
/// <param name="volatilityPeriod">The volatility period.</param>
/// <param name="vovPeriod">The VOV period.</param>
/// <returns>A TSeries containing the VOV values.</returns>
public static TSeries Calculate(TSeries source, int volatilityPeriod = 20, int vovPeriod = 10)
{
var vov = new Vov(volatilityPeriod, vovPeriod);
return vov.Update(source);
}
/// <summary>
/// Batch calculation using spans.
/// </summary>
/// <param name="source">Price values.</param>
/// <param name="output">Output VOV values.</param>
/// <param name="volatilityPeriod">The volatility period.</param>
/// <param name="vovPeriod">The VOV period.</param>
public static void Batch(
ReadOnlySpan<double> source,
Span<double> output,
int volatilityPeriod = 20,
int vovPeriod = 10)
{
if (volatilityPeriod <= 0)
{
throw new ArgumentException("Volatility period must be greater than 0", nameof(volatilityPeriod));
}
if (vovPeriod <= 0)
{
throw new ArgumentException("VOV period must be greater than 0", nameof(vovPeriod));
}
if (output.Length < source.Length)
{
throw new ArgumentException("Output span must be at least as long as source span", nameof(output));
}
int len = source.Length;
if (len == 0)
{
return;
}
const int StackallocThreshold = 256;
// Use ArrayPool for larger allocations
double[]? priceRented = null;
double[]? volRented = null;
if (volatilityPeriod > StackallocThreshold)
{
priceRented = ArrayPool<double>.Shared.Rent(volatilityPeriod);
}
if (vovPeriod > StackallocThreshold)
{
volRented = ArrayPool<double>.Shared.Rent(vovPeriod);
}
try
{
scoped Span<double> priceBuffer = volatilityPeriod <= StackallocThreshold
? stackalloc double[volatilityPeriod]
: priceRented.AsSpan(0, volatilityPeriod);
scoped Span<double> volBuffer = vovPeriod <= StackallocThreshold
? stackalloc double[vovPeriod]
: volRented.AsSpan(0, vovPeriod);
priceBuffer.Clear();
volBuffer.Clear();
double lastValidPrice = 0;
double priceSum = 0, priceSumSq = 0;
double volSum = 0, volSumSq = 0;
int priceIdx = 0, volIdx = 0;
int priceCount = 0, volCount = 0;
for (int i = 0; i < len; i++)
{
double price = source[i];
// Handle non-finite values
if (!double.IsFinite(price))
{
price = lastValidPrice;
}
else
{
lastValidPrice = price;
}
// Update price running sums
if (priceCount >= volatilityPeriod)
{
priceSum -= priceBuffer[priceIdx];
priceSumSq -= priceBuffer[priceIdx] * priceBuffer[priceIdx];
}
priceSum += price;
priceSumSq += price * price;
priceBuffer[priceIdx] = price;
priceIdx = (priceIdx + 1) % volatilityPeriod;
if (priceCount < volatilityPeriod)
{
priceCount++;
}
// Calculate volatility
double volatility = 0;
if (priceCount > 1)
{
double mean = priceSum / priceCount;
double variance = (priceSumSq / priceCount) - (mean * mean);
volatility = Math.Sqrt(Math.Max(0.0, variance));
}
// Update volatility running sums
if (volCount >= vovPeriod)
{
volSum -= volBuffer[volIdx];
volSumSq -= volBuffer[volIdx] * volBuffer[volIdx];
}
volSum += volatility;
volSumSq += volatility * volatility;
volBuffer[volIdx] = volatility;
volIdx = (volIdx + 1) % vovPeriod;
if (volCount < vovPeriod)
{
volCount++;
}
// Calculate VOV
double vov = 0;
if (volCount > 1)
{
double volMean = volSum / volCount;
double volVariance = (volSumSq / volCount) - (volMean * volMean);
vov = Math.Sqrt(Math.Max(0.0, volVariance));
}
if (!double.IsFinite(vov) || vov < 0)
{
vov = i > 0 ? output[i - 1] : 0;
}
output[i] = vov;
}
}
finally
{
if (priceRented != null)
{
ArrayPool<double>.Shared.Return(priceRented);
}
if (volRented != null)
{
ArrayPool<double>.Shared.Return(volRented);
}
}
}
}
+257
View File
@@ -0,0 +1,257 @@
# VOV: Volatility of Volatility
> "When markets become uncertain about their own uncertainty, that's when things get interesting."
Volatility of Volatility (VOV) measures the standard deviation of volatility itself, quantifying how much volatility fluctuates over time. While standard volatility tells you how much prices move, VOV tells you how stable or unstable that movement pattern is. High VOV indicates volatility is erratic and unpredictable; low VOV suggests volatility is relatively stable and consistent.
## Historical Context
The concept of "vol of vol" emerged from options pricing and derivatives trading, where understanding the stability of volatility became crucial for pricing exotic options and managing portfolio risk. The Heston stochastic volatility model (1993) introduced a dedicated parameter (σ, often called "vol of vol") to capture this phenomenon, recognizing that volatility itself follows a random process.
In practice, traders noticed that implied volatility surfaces exhibit their own dynamics—sometimes stable, sometimes wildly fluctuating. The 2008 financial crisis and subsequent "flash crashes" demonstrated that periods of extreme VOV correlate with market stress and liquidity crises. When volatility becomes volatile, hedging becomes difficult and option pricing models break down.
This implementation uses a straightforward approach: compute rolling standard deviation (inner volatility), then compute the standard deviation of those values (outer VOV). Simple, interpretable, and effective for detecting volatility regime changes.
## Architecture & Physics
### 1. Inner Volatility Calculation
For each bar, compute the population standard deviation of prices over the volatility period:
$$
\sigma_{t}^{inner} = \sqrt{\frac{1}{n}\sum_{i=0}^{n-1}(P_{t-i} - \bar{P})^2}
$$
where:
- $P_t$ = Price (close) at time $t$
- $n$ = Volatility period (default 20)
- $\bar{P}$ = Mean price over the window
Using the computationally efficient form:
$$
\sigma = \sqrt{E[X^2] - E[X]^2} = \sqrt{\frac{\sum x^2}{n} - \left(\frac{\sum x}{n}\right)^2}
$$
### 2. Outer VOV Calculation
Apply the same standard deviation formula to the series of inner volatilities:
$$
VOV_t = \sqrt{\frac{1}{m}\sum_{j=0}^{m-1}(\sigma_{t-j}^{inner} - \bar{\sigma})^2}
$$
where:
- $\sigma^{inner}$ = Inner volatility values
- $m$ = VOV period (default 10)
- $\bar{\sigma}$ = Mean of inner volatilities over the window
### 3. Population vs Sample Standard Deviation
This implementation uses **population** standard deviation (dividing by $n$, not $n-1$). For rolling window calculations with consistent period sizes, population stddev is appropriate and avoids the Bessel's correction bias that's designed for estimating population parameters from small samples.
## Mathematical Foundation
### Nested Standard Deviation
The core formula is simply:
$$
VOV = StdDev(StdDev(Price, volatilityPeriod), vovPeriod)
$$
### Efficient Streaming Computation
For O(1) updates, maintain running sums:
**Inner volatility buffer:**
- `priceSum` = $\sum P_i$
- `priceSumSq` = $\sum P_i^2$
**Outer VOV buffer:**
- `volSum` = $\sum \sigma_i$
- `volSumSq` = $\sum \sigma_i^2$
Update formulas when adding new value $x$ and removing old value $x_{old}$:
$$
sum_{new} = sum_{old} + x - x_{old}
$$
$$
sumSq_{new} = sumSq_{old} + x^2 - x_{old}^2
$$
### Example Calculation
Consider volatilityPeriod=2, vovPeriod=2, prices: [100, 102, 98, 104]
**Inner stddevs:**
- Bar 1: StdDev([100, 102]) = $\sqrt{(10202) - (101)^2}$ = $\sqrt{1}$ = 1
- Bar 2: StdDev([102, 98]) = $\sqrt{(10004) - (100)^2}$ = $\sqrt{4}$ = 2
- Bar 3: StdDev([98, 104]) = $\sqrt{(10210) - (101)^2}$ = $\sqrt{9}$ = 3
**Outer VOV:**
- Bar 2: StdDev([1, 2]) = $\sqrt{(2.5) - (1.5)^2}$ = $\sqrt{0.25}$ = 0.5
- Bar 3: StdDev([2, 3]) = $\sqrt{(6.5) - (2.5)^2}$ = $\sqrt{0.25}$ = 0.5
### Properties
1. **Non-negativity**: VOV ≥ 0 always (standard deviation is non-negative)
2. **Zero when constant**: If volatility doesn't change, VOV = 0
3. **Scale independence**: VOV is in the same units as volatility (price units)
4. **Warmup requirement**: Needs (volatilityPeriod + vovPeriod - 1) bars before valid output
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
Per-bar operations:
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| ADD/SUB | 8 | 1 | 8 |
| MUL | 6 | 3 | 18 |
| DIV | 4 | 15 | 60 |
| SQRT | 2 | 15 | 30 |
| Ring buffer ops | 4 | 2 | 8 |
| **Total** | — | — | **~124 cycles** |
O(1) complexity per bar—no iteration over window required.
### Batch Mode (512 values, SIMD/FMA)
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
| :--- | :---: | :---: | :---: |
| Arithmetic | 4096 | 512 | 8× |
| SQRT | 1024 | 128 | 8× |
The nested nature limits SIMD benefit for streaming, but batch mode can vectorize the inner stddev calculation significantly.
### Memory Profile
- **Per instance:** ~200 bytes base + ring buffers
- **Price buffer:** volatilityPeriod × 8 bytes
- **Volatility buffer:** vovPeriod × 8 bytes
- **Backup arrays:** 2 × max(volatilityPeriod, vovPeriod) × 8 bytes for bar correction
- **Default (20, 10):** ~480 bytes per instance
- **100 instances:** ~47 KB
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Exact calculation, no approximation |
| **Timeliness** | 6/10 | Dual-period lag inherent |
| **Smoothness** | 7/10 | Outer stddev provides smoothing |
| **Interpretability** | 8/10 | Clear meaning: volatility instability |
| **Regime Detection** | 9/10 | Excellent at detecting volatility transitions |
## Validation
| Library | Status | Notes |
| :--- | :---: | :--- |
| **TA-Lib** | N/A | Not implemented |
| **Skender** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented |
| **OoplesFinance** | N/A | Not implemented |
| **PineScript** | ✅ | Matches vov.pine reference |
| **Self-consistency** | ✅ | Streaming = Batch = Span modes match |
## Common Pitfalls
1. **Warmup period confusion**: VOV requires (volatilityPeriod + vovPeriod - 1) bars before producing valid output. Default (20, 10) needs 29 bars. Early values during warmup may be misleading.
2. **Interpretation**: Low VOV doesn't mean low volatility—it means volatility is *stable* (could be stably high). High VOV means volatility is unpredictable, regardless of its absolute level.
3. **Parameter selection**:
- Shorter volatilityPeriod captures faster price dynamics but noisier inner vol
- Shorter vovPeriod reacts faster to vol changes but noisier VOV
- Common defaults: (20, 10) for daily data, (60, 20) for intraday
4. **Scale awareness**: VOV is in price units (like standard deviation). A VOV of 0.5 on a $100 stock is very different from VOV of 0.5 on a $10 stock. Consider normalizing by price or using percentage returns.
5. **Regime lag**: Due to the nested calculation, VOV inherently lags regime changes. By the time VOV spikes, the volatility shift has already begun.
6. **Memory footprint**: With two ring buffers plus backup arrays, VOV uses more memory than simpler indicators. For many simultaneous instances, consider the cumulative impact.
## Trading Applications
### Volatility Regime Detection
Track VOV to identify when volatility is transitioning:
```
If VOV rising from low base: Volatility regime change underway
If VOV falling toward zero: Volatility stabilizing
If VOV persistently high: Unstable market conditions
```
### Options Trading
VOV correlates with the value of volatility derivatives:
```
High VOV: Straddles/strangles more valuable (vol could move either way)
Low VOV: Stable vol environment, directional bets may be safer
```
### Position Sizing
Adjust exposure based on volatility predictability:
```
Position size = Base size × (Target VOV / Actual VOV)
Higher VOV → smaller positions (unpredictable conditions)
```
### Risk Management
Use VOV as an early warning signal:
```
If VOV > 2 × average: Consider hedging
If VOV crosses threshold: Reduce leverage
```
### Mean Reversion Strategies
Volatility tends to mean-revert; VOV helps time entries:
```
High VOV + High Vol: Wait for VOV to decline before selling vol
Low VOV + Low Vol: Vol likely to rise; prepare for expansion
```
## Relationship to Other Indicators
| Indicator | Relationship to VOV |
| :--- | :--- |
| **ATR** | ATR is level; VOV measures ATR stability |
| **Bollinger Bandwidth** | Bandwidth measures vol level; VOV measures bandwidth stability |
| **VIX/VVIX** | VVIX is the market's implied VOV; this is realized VOV |
| **Heston σ** | Heston vol-of-vol parameter; VOV is the realized equivalent |
| **RVI** | RVI measures vol direction; VOV measures vol instability |
| **Standard Deviation** | VOV is StdDev of StdDev |
## Implementation Notes
### State Management
The indicator maintains four running sums (price sum, price sum-squared, vol sum, vol sum-squared) plus two ring buffers. For bar correction (isNew=false), backup arrays store previous buffer states.
### NaN/Infinity Handling
Invalid inputs are replaced with the last valid price to prevent corruption of running sums. This ensures continuous operation even with data gaps.
### Numerical Stability
The formula $\sqrt{E[X^2] - E[X]^2}$ can produce small negative values due to floating-point errors when variance is near zero. The implementation guards against this by returning 0 when the computed variance is negative.
## References
- Heston, S. L. (1993). "A Closed-Form Solution for Options with Stochastic Volatility with Applications to Bond and Currency Options." *Review of Financial Studies*, 6(2), 327-343.
- Gatheral, J. (2006). *The Volatility Surface: A Practitioner's Guide*. Wiley Finance.
- CBOE. "VVIX Index." Chicago Board Options Exchange white paper on volatility-of-volatility indices.