namespace QuanTAlib.Test; using Xunit; /// /// Validation tests for CCV (Close-to-Close Volatility). /// CCV is a standard volatility measure but with specific smoothing options. /// These tests validate the mathematical correctness of the implementation. /// public class CcvValidationTests { private static TBarSeries GenerateTestData(int count = 100) { var gbm = new GBM(seed: 42); return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); } // === Mathematical Validation === /// /// Validates that CCV calculates annualized log return volatility correctly. /// Formula: σ_annual = StdDev(ln(C_t/C_{t-1})) × √252 /// [Fact] public void Ccv_MatchesManualLogReturnCalculation() { int period = 10; var ccv = new Ccv(period, 1); // SMA method // Use fixed prices for deterministic testing double[] prices = { 100, 102, 101, 103, 105, 104, 106, 108, 107, 109, 110 }; // Feed all prices to the indicator (first price initializes, rest produce returns) for (int i = 0; i < prices.Length; i++) { ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i])); } // Calculate log returns (prices[1]/prices[0], prices[2]/prices[1], etc.) double[] logReturns = new double[prices.Length - 1]; for (int i = 1; i < prices.Length; i++) { logReturns[i - 1] = Math.Log(prices[i] / prices[i - 1]); } // Calculate expected stddev manually for last 'period' returns int startIdx = Math.Max(0, logReturns.Length - period); double sum = 0; int count = 0; for (int i = startIdx; i < logReturns.Length; i++) { sum += logReturns[i]; count++; } double mean = sum / count; double squaredSum = 0; for (int i = startIdx; i < logReturns.Length; i++) { squaredSum += Math.Pow(logReturns[i] - mean, 2); } double stdDev = Math.Sqrt(squaredSum / count); double expectedAnnualized = stdDev * Math.Sqrt(252); // Compare (allow for floating-point tolerance - small differences expected due to // the indicator using a rolling window vs manual batch calculation) Assert.Equal(expectedAnnualized, ccv.Last.Value, 2); } /// /// Validates the annualization factor √252 is correctly applied. /// [Fact] public void Ccv_AnnualizationFactor_IsCorrect() { // √252 ≈ 15.8745 double expectedFactor = Math.Sqrt(252); Assert.Equal(15.874507866387544, expectedFactor, 10); } /// /// Validates that constant prices produce zero volatility. /// [Fact] public void Ccv_ConstantPrices_ProducesZeroVolatility() { var ccv = new Ccv(10, 1); for (int i = 0; i < 20; i++) { ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100.0)); } // Constant prices = zero log returns = zero stddev = zero volatility Assert.Equal(0.0, ccv.Last.Value, 10); } /// /// Validates that the EMA method (2) applies warmup compensation correctly. /// [Fact] public void Ccv_EmaMethod_WarmsUpCorrectly() { var ccv = new Ccv(20, 2); // EMA method var bars = GenerateTestData(50); var times = bars.Times; var close = bars.CloseValues; var results = new List(); for (int i = 0; i < bars.Count; i++) { var result = ccv.Update(new TValue(times[i], close[i])); results.Add(result.Value); } // Early values should exist and be finite Assert.All(results, r => Assert.True(double.IsFinite(r))); // Values should generally stabilize after warmup Assert.True(results[^1] >= 0); } /// /// Validates known volatility scenario with specific returns. /// [Fact] public void Ccv_KnownReturns_ProducesExpectedVolatility() { var ccv = new Ccv(5, 1); // SMA method, 5 periods // Create prices that produce known log returns // If we have returns of: 1%, 1%, 1%, 1%, 1% (all same) // Then stddev = 0, volatility = 0 double price = 100.0; double returnRate = 0.01; // 1% daily return ccv.Update(new TValue(DateTime.UtcNow, price)); // First price for (int i = 0; i < 5; i++) { price *= (1 + returnRate); ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i + 1), price)); } // Constant returns should produce near-zero volatility // (log(1.01) is constant, so stddev ≈ 0) Assert.True(ccv.Last.Value < 0.01, "Constant returns should have near-zero volatility"); } /// /// Validates that CCV responds to varying volatility correctly. /// [Fact] public void Ccv_VaryingVolatility_RespondsCorrectly() { var ccvLow = new Ccv(10, 1); var ccvHigh = new Ccv(10, 1); // Low volatility: small price changes double priceLow = 100.0; for (int i = 0; i < 20; i++) { priceLow *= (1 + 0.001 * (i % 2 == 0 ? 1 : -1)); // ±0.1% ccvLow.Update(new TValue(DateTime.UtcNow.AddMinutes(i), priceLow)); } // High volatility: large price changes double priceHigh = 100.0; for (int i = 0; i < 20; i++) { priceHigh *= (1 + 0.05 * (i % 2 == 0 ? 1 : -1)); // ±5% ccvHigh.Update(new TValue(DateTime.UtcNow.AddMinutes(i), priceHigh)); } Assert.True(ccvHigh.Last.Value > ccvLow.Last.Value, "Higher price volatility should produce higher CCV"); } // === Consistency Tests === /// /// Validates streaming and batch produce identical results. /// [Fact] public void Ccv_StreamingMatchesBatch() { var bars = GenerateTestData(100); var times = bars.Times; var close = bars.CloseValues; // Streaming calculation var streamingCcv = new Ccv(20, 1); for (int i = 0; i < bars.Count; i++) { streamingCcv.Update(new TValue(times[i], close[i])); } // Batch calculation var source = new double[bars.Count]; var output = new double[bars.Count]; for (int i = 0; i < bars.Count; i++) { source[i] = close[i]; } Ccv.Batch(source, output, 20, 1); // Compare last values Assert.Equal(output[^1], streamingCcv.Last.Value, 8); } /// /// Validates all three smoothing methods produce valid results. /// [Theory] [InlineData(1)] // SMA [InlineData(2)] // EMA [InlineData(3)] // WMA public void Ccv_AllMethods_ProduceConsistentResults(int method) { var bars = GenerateTestData(100); var times = bars.Times; var close = bars.CloseValues; var ccv = new Ccv(20, method); for (int i = 0; i < bars.Count; i++) { var result = ccv.Update(new TValue(times[i], close[i])); Assert.True(double.IsFinite(result.Value), $"Method {method} should produce finite values"); Assert.True(result.Value >= 0, $"Method {method} should produce non-negative values"); } } // === Edge Cases === /// /// Validates handling of very small price changes. /// [Fact] public void Ccv_SmallPriceChanges_HandledCorrectly() { var ccv = new Ccv(10, 1); double price = 100.0; for (int i = 0; i < 20; i++) { price += 0.0001; // Very small changes ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price)); } Assert.True(double.IsFinite(ccv.Last.Value)); Assert.True(ccv.Last.Value >= 0); } /// /// Validates handling of large price swings. /// [Fact] public void Ccv_LargePriceSwings_HandledCorrectly() { var ccv = new Ccv(10, 1); for (int i = 0; i < 20; i++) { double price = 100.0 * (i % 2 == 0 ? 2.0 : 0.5); // 100% swings ccv.Update(new TValue(DateTime.UtcNow.AddMinutes(i), price)); } Assert.True(double.IsFinite(ccv.Last.Value)); Assert.True(ccv.Last.Value > 0, "Large swings should produce positive volatility"); } /// /// Validates that different periods produce different sensitivities. /// [Fact] public void Ccv_DifferentPeriods_ProduceDifferentValues() { var bars = GenerateTestData(100); var times = bars.Times; var close = bars.CloseValues; var ccv5 = new Ccv(5, 1); var ccv20 = new Ccv(20, 1); var ccv50 = new Ccv(50, 1); for (int i = 0; i < bars.Count; i++) { ccv5.Update(new TValue(times[i], close[i])); ccv20.Update(new TValue(times[i], close[i])); ccv50.Update(new TValue(times[i], close[i])); } // All should be valid Assert.True(double.IsFinite(ccv5.Last.Value)); Assert.True(double.IsFinite(ccv20.Last.Value)); Assert.True(double.IsFinite(ccv50.Last.Value)); // Shorter periods typically react more to recent volatility // (but this depends on market data, so just check they're different or similar) Assert.True(ccv5.Last.Value >= 0); Assert.True(ccv20.Last.Value >= 0); Assert.True(ccv50.Last.Value >= 0); } }