// 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; /// /// 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. /// /// /// Calculation steps: /// /// Calculate initial volatility: StdDev(price, volatilityPeriod) /// Calculate VOV: StdDev(volatility, vovPeriod) /// /// /// Key characteristics: /// /// High VOV indicates unstable/changing volatility regime /// Low VOV indicates stable/consistent volatility /// Useful for volatility regime detection and risk management /// Can signal transitions between calm and turbulent markets /// /// /// Interpretation: /// /// Rising VOV may precede major market moves /// Falling VOV suggests volatility is stabilizing /// Extreme VOV values can indicate regime changes /// /// [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; /// /// Initializes a new instance of the Vov class. /// /// The lookback period for initial volatility calculation (default 20). /// The lookback period for VOV calculation (default 10). /// Thrown when any period is less than 1. 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; } /// /// Initializes a new instance of the Vov class with a source. /// /// The data source for chaining. /// The volatility period (default 20). /// The VOV period (default 10). 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); /// /// True if the indicator has enough data for valid results. /// public override bool IsHot => _s.PriceCount >= _volatilityPeriod && _s.VolCount >= _vovPeriod; /// /// The volatility lookback period. /// public int VolatilityPeriod => _volatilityPeriod; /// /// The VOV lookback period. /// public int VovPeriod => _vovPeriod; /// /// Updates the indicator with a TValue input. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override TValue Update(TValue input, bool isNew = true) { return UpdateCore(input.Time, input.Value, isNew); } /// /// Updates the indicator with a new bar (uses close price). /// /// The input bar. /// Whether this is a new bar or an update. /// The calculated VOV value. [MethodImpl(MethodImplOptions.AggressiveInlining)] public TValue Update(TBar bar, bool isNew = true) { return UpdateCore(bar.Time, bar.Close, isNew); } /// public override TSeries Update(TSeries source) { int len = source.Count; var t = new List(len); var v = new List(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; } /// public override void Prime(ReadOnlySpan source, TimeSpan? step = null) { for (int i = 0; i < source.Length; i++) { Update(new TValue(DateTime.UtcNow, source[i]), isNew: true); } } /// 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; } /// /// Calculates VOV for a series (static). /// /// The source series. /// The volatility period. /// The VOV period. /// A TSeries containing the VOV values. public static TSeries Batch(TSeries source, int volatilityPeriod = 20, int vovPeriod = 10) { var vov = new Vov(volatilityPeriod, vovPeriod); return vov.Update(source); } /// /// Batch calculation using spans. /// /// Price values. /// Output VOV values. /// The volatility period. /// The VOV period. public static void Batch( ReadOnlySpan source, Span 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.Shared.Rent(volatilityPeriod); } if (vovPeriod > StackallocThreshold) { volRented = ArrayPool.Shared.Rent(vovPeriod); } try { scoped Span priceBuffer = volatilityPeriod <= StackallocThreshold ? stackalloc double[volatilityPeriod] : priceRented.AsSpan(0, volatilityPeriod); scoped Span 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.Shared.Return(priceRented); } if (volRented != null) { ArrayPool.Shared.Return(volRented); } } } public static (TSeries Results, Vov Indicator) Calculate(TSeries source, int volatilityPeriod = 20, int vovPeriod = 10) { var indicator = new Vov(volatilityPeriod, vovPeriod); TSeries results = indicator.Update(source); return (results, indicator); } }