namespace QuanTAlib.Tests; /// /// Validation tests for YZVAMA (Yang-Zhang Volatility Adjusted Moving Average). /// Validates mathematical properties rather than comparing against an external library. /// public class YzvamaValidationTests { private const double Tolerance = 1e-10; [Fact] public void Yzvama_ZeroVolatility_EqualsMaxLength_SMA() { const int maxLength = 30; var yzvama = new Yzvama(yzvShortPeriod: 3, yzvLongPeriod: 50, percentileLookback: 50, minLength: 5, maxLength: maxLength); var sma = new Sma(maxLength); // Using close-only input creates synthetic bars with O=H=L=C which yields yzv_short=0, // thus percentile ~ 0 and adjusted length ~= maxLength. var values = Enumerable.Range(1, 300).Select(i => (double)i).ToArray(); foreach (var val in values) { var tv = new TValue(DateTime.UtcNow, val); yzvama.Update(tv, isNew: true); sma.Update(tv, isNew: true); } Assert.Equal(sma.Last.Value, yzvama.Last.Value, 1.0); } [Fact] public void Yzvama_ConstantInput_OutputEqualsInput() { var yzvama = new Yzvama(); const double constantValue = 42.5; for (int i = 0; i < 300; i++) { yzvama.Update(new TValue(DateTime.UtcNow, constantValue), isNew: true); } Assert.Equal(constantValue, yzvama.Last.Value, Tolerance); } [Fact] public void Yzvama_OutputWithinInputRange() { var yzvama = new Yzvama(); var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.2, seed: 123); var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); double minInput = double.MaxValue; double maxInput = double.MinValue; var outputs = new List(); foreach (var bar in bars) { minInput = Math.Min(minInput, bar.Close); maxInput = Math.Max(maxInput, bar.Close); outputs.Add(yzvama.Update(bar, isNew: true).Value); } var hotOutputs = outputs.Skip(150).ToList(); foreach (var output in hotOutputs) { Assert.True(output >= minInput - 1 && output <= maxInput + 1, $"Output {output} should be within input range [{minInput}, {maxInput}]"); } } [Fact] public void Yzvama_VolatilitySpike_DrivesTowardMinLength() { // Prime with a stable low-volatility regime (yzv_short ~ 0 => percentile low => maxLength) const int percentileLookback = 20; const int minLength = 5; const int maxLength = 50; var yzvama = new Yzvama(yzvShortPeriod: 3, yzvLongPeriod: 50, percentileLookback: percentileLookback, minLength: minLength, maxLength: maxLength); long t = DateTime.UtcNow.Ticks; for (int i = 0; i < percentileLookback; i++) { var bar = new TBar(t + i, 100, 100, 100, 100, 0); yzvama.Update(bar, isNew: true); } // One large-range bar should rank at the top of the volatility buffer (percentile ~= 100), // which maps adjusted length to minLength. The SMA then uses the last minLength closes. var spike = new TBar(t + percentileLookback, 100, 200, 50, 200, 0); var result = yzvama.Update(spike, isNew: true); // Last 5 closes: 100,100,100,100,200 => average = 120 Assert.Equal(120.0, result.Value, 1e-6); } }