// CKSTOP Tests - Chande Kroll Stop namespace QuanTAlib.Tests; // ── A) Constructor Validation ──────────────────────────────────────────── public sealed class CkstopConstructorTests { [Fact] public void Constructor_ZeroAtrPeriod_Throws() { var ex = Assert.Throws(() => new Ckstop(atrPeriod: 0)); Assert.Equal("atrPeriod", ex.ParamName); } [Fact] public void Constructor_NegativeAtrPeriod_Throws() { var ex = Assert.Throws(() => new Ckstop(atrPeriod: -1)); Assert.Equal("atrPeriod", ex.ParamName); } [Fact] public void Constructor_ZeroMultiplier_Throws() { var ex = Assert.Throws(() => new Ckstop(multiplier: 0)); Assert.Equal("multiplier", ex.ParamName); } [Fact] public void Constructor_NegativeMultiplier_Throws() { var ex = Assert.Throws(() => new Ckstop(multiplier: -1.0)); Assert.Equal("multiplier", ex.ParamName); } [Fact] public void Constructor_ZeroStopPeriod_Throws() { var ex = Assert.Throws(() => new Ckstop(stopPeriod: 0)); Assert.Equal("stopPeriod", ex.ParamName); } [Fact] public void Constructor_NegativeStopPeriod_Throws() { var ex = Assert.Throws(() => new Ckstop(stopPeriod: -1)); Assert.Equal("stopPeriod", ex.ParamName); } [Fact] public void Constructor_ValidDefaults_SetsProperties() { var ck = new Ckstop(); Assert.Equal(10, ck.AtrPeriod); Assert.Equal(1.0, ck.Multiplier); Assert.Equal(9, ck.StopPeriod); Assert.Equal(19, ck.WarmupPeriod); Assert.Contains("Ckstop", ck.Name, StringComparison.Ordinal); } [Fact] public void Constructor_CustomParams_SetsProperties() { var ck = new Ckstop(atrPeriod: 14, multiplier: 2.0, stopPeriod: 7); Assert.Equal(14, ck.AtrPeriod); Assert.Equal(2.0, ck.Multiplier); Assert.Equal(7, ck.StopPeriod); Assert.Equal(21, ck.WarmupPeriod); } } // ── B) Basic Calculation ───────────────────────────────────────────────── public sealed class CkstopBasicTests { [Fact] public void Update_ReturnsTValue() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); var bar = new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000); TValue result = ck.Update(bar); Assert.IsType(result); } [Fact] public void Update_Last_IsAccessible() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); var bar = new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000); _ = ck.Update(bar); Assert.True(double.IsFinite(ck.Last.Value) || double.IsNaN(ck.Last.Value)); } [Fact] public void Update_StopLong_StopShort_Accessible() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); // Feed enough bars to warm up for (int i = 0; i < 10; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000)); } Assert.True(double.IsFinite(ck.StopLong)); Assert.True(double.IsFinite(ck.StopShort)); } [Fact] public void Name_ContainsParameters() { var ck = new Ckstop(atrPeriod: 14, multiplier: 2.5, stopPeriod: 7); Assert.Contains("14", ck.Name, StringComparison.Ordinal); Assert.Contains("2.5", ck.Name, StringComparison.Ordinal); Assert.Contains("7", ck.Name, StringComparison.Ordinal); } [Fact] public void StopLong_BelowPrice_InUptrend() { var ck = new Ckstop(atrPeriod: 5, multiplier: 1.0, stopPeriod: 3); double basePrice = 100.0; // Steady uptrend for (int i = 0; i < 20; i++) { double price = basePrice + i * 2; _ = ck.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 1, price - 1, price + 0.5, price, 1000)); } Assert.True(ck.StopLong < 100.0 + 19 * 2, "StopLong should be below the current price in an uptrend"); } } // ── C) State + Bar Correction ──────────────────────────────────────────── public sealed class CkstopStateCorrectionTests { [Fact] public void IsNew_True_AdvancesState() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); _ = ck.Update(new TBar(DateTime.UtcNow, 105, 95, 100, 100, 1000), isNew: true); var first = ck.Last; _ = ck.Update(new TBar(DateTime.UtcNow.AddMinutes(1), 110, 100, 105, 105, 1000), isNew: true); var second = ck.Last; // Second update should change (new bar) Assert.NotEqual(first.Time, second.Time); } [Fact] public void IsNew_False_CorrectionRestoresState() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); var dt = DateTime.UtcNow; // Feed some bars to warm up for (int i = 0; i < 5; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000), isNew: true); } // New bar _ = ck.Update(new TBar(dt.AddMinutes(5), 110, 105, 108, 107, 1000), isNew: true); // Correct the bar (isNew=false with different values) _ = ck.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false); // Another correction should produce same result _ = ck.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false); var corrected1 = ck.StopLong; _ = ck.Update(new TBar(dt.AddMinutes(5), 111, 104, 109, 108, 1000), isNew: false); var corrected2 = ck.StopLong; Assert.Equal(corrected1, corrected2); } [Fact] public void IterativeCorrections_ProduceSameResult() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); var dt = DateTime.UtcNow; for (int i = 0; i < 5; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000), isNew: true); } // Add new bar then correct 3 times _ = ck.Update(new TBar(dt.AddMinutes(5), 110, 100, 108, 105, 1000), isNew: true); double[] results = new double[3]; for (int i = 0; i < 3; i++) { _ = ck.Update(new TBar(dt.AddMinutes(5), 112, 101, 110, 107, 1000), isNew: false); results[i] = ck.StopLong; } Assert.Equal(results[0], results[1]); Assert.Equal(results[1], results[2]); } [Fact] public void Reset_ClearsAllState() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); for (int i = 0; i < 10; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000)); } Assert.True(ck.IsHot); ck.Reset(); Assert.False(ck.IsHot); Assert.True(double.IsNaN(ck.StopLong)); Assert.True(double.IsNaN(ck.StopShort)); } } // ── D) Warmup / Convergence ────────────────────────────────────────────── public sealed class CkstopWarmupTests { [Fact] public void IsHot_FlipsAfterWarmup() { int atrPeriod = 5; int stopPeriod = 3; var ck = new Ckstop(atrPeriod: atrPeriod, multiplier: 1.0, stopPeriod: stopPeriod); int warmup = atrPeriod + stopPeriod; for (int i = 0; i < warmup; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000)); if (i < warmup - 1) { Assert.False(ck.IsHot, $"Should not be hot at bar {i}"); } } Assert.True(ck.IsHot, $"Should be hot after {warmup} bars"); } [Fact] public void WarmupPeriod_EqualsAtrPlusStoP() { var ck = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9); Assert.Equal(19, ck.WarmupPeriod); } } // ── E) Robustness ──────────────────────────────────────────────────────── public sealed class CkstopRobustnessTests { [Fact] public void NaN_Input_UsesLastValidValue() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); var dt = DateTime.UtcNow; // Feed valid bars for (int i = 0; i < 5; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000)); } // Feed NaN bar _ = ck.Update(new TBar(dt.AddMinutes(5), double.NaN, double.NaN, double.NaN, double.NaN, 0)); // Should still produce finite output (using last-valid substitution) Assert.True(double.IsFinite(ck.StopLong)); } [Fact] public void Infinity_Input_UsesLastValidValue() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); var dt = DateTime.UtcNow; for (int i = 0; i < 5; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(dt.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000)); } _ = ck.Update(new TBar(dt.AddMinutes(5), double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, double.PositiveInfinity, 0)); Assert.True(double.IsFinite(ck.StopLong)); } [Fact] public void FirstBar_NaN_ReturnsNaN() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); _ = ck.Update(new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, 0)); Assert.True(double.IsNaN(ck.Last.Value)); } } // ── F) Consistency ─────────────────────────────────────────────────────── public sealed class CkstopConsistencyTests { private static TBarSeries CreateGbmBars(int count = 500) { var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.20, seed: 42); return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); } [Fact] public void Streaming_MatchesBatch() { var bars = CreateGbmBars(); int atrPeriod = 10; double multiplier = 1.0; int stopPeriod = 9; // Streaming var streaming = new Ckstop(atrPeriod, multiplier, stopPeriod); var streamResults = new double[bars.Count]; for (int i = 0; i < bars.Count; i++) { _ = streaming.Update(bars[i], isNew: true); streamResults[i] = streaming.StopLong; } // Batch var batchResults = Ckstop.Batch(bars, atrPeriod, multiplier, stopPeriod); int warmup = atrPeriod + stopPeriod; for (int i = warmup; i < bars.Count; i++) { Assert.Equal(streamResults[i], batchResults[i].Value, precision: 10); } } [Fact] public void StopShort_GreaterOrEqual_StopLong_InTrend() { var bars = CreateGbmBars(); var ck = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9); int aboveCount = 0; int belowCount = 0; for (int i = 0; i < bars.Count; i++) { _ = ck.Update(bars[i], isNew: true); if (ck.IsHot) { if (ck.StopShort >= ck.StopLong) { aboveCount++; } else { belowCount++; } } } // In general, StopShort (highest of initial stops) should often be >= StopLong (lowest of initial stops) // but crossovers do happen — just verify both counts are non-zero showing the indicator works Assert.True(aboveCount + belowCount > 0, "Should have some hot bars"); } [Fact] public void TValue_Update_MatchesTBar_Update() { var ck1 = new Ckstop(atrPeriod: 5, multiplier: 1.0, stopPeriod: 3); var ck2 = new Ckstop(atrPeriod: 5, multiplier: 1.0, stopPeriod: 3); double[] prices = [100, 102, 98, 105, 99, 103, 107, 95, 110, 108]; for (int i = 0; i < prices.Length; i++) { double p = prices[i]; // TBar with equal OHLC _ = ck1.Update(new TBar(DateTime.UtcNow.AddMinutes(i), p, p, p, p, 0), isNew: true); // TValue _ = ck2.Update(new TValue(DateTime.UtcNow.AddMinutes(i), p), isNew: true); } Assert.Equal(ck1.StopLong, ck2.StopLong); Assert.Equal(ck1.StopShort, ck2.StopShort); } } // ── G) Span API Tests ──────────────────────────────────────────────────── public sealed class CkstopSpanTests { [Fact] public void Batch_Span_InvalidAtrPeriod_Throws() { var ex = Assert.Throws(() => Ckstop.Batch(new double[10], new double[10], new double[10], new double[10], new double[10], atrPeriod: 0)); Assert.Equal("atrPeriod", ex.ParamName); } [Fact] public void Batch_Span_MismatchedLengths_Throws() { var ex = Assert.Throws(() => Ckstop.Batch(new double[10], new double[10], new double[5], new double[10], new double[10], atrPeriod: 5)); Assert.Equal("high", ex.ParamName); } [Fact] public void Batch_Span_OutputTooShort_Throws() { var ex = Assert.Throws(() => Ckstop.Batch(new double[10], new double[10], new double[10], new double[10], new double[5], atrPeriod: 5)); Assert.Equal("output", ex.ParamName); } [Fact] public void Batch_Span_Empty_NoException() { var output = Array.Empty(); var ex = Record.Exception(() => Ckstop.Batch(ReadOnlySpan.Empty, ReadOnlySpan.Empty, ReadOnlySpan.Empty, ReadOnlySpan.Empty, output.AsSpan(), atrPeriod: 5)); Assert.Null(ex); } } // ── H) Event / Chainability ────────────────────────────────────────────── public sealed class CkstopEventTests { [Fact] public void Pub_FiresOnUpdate() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); int fireCount = 0; ck.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; }; _ = ck.Update(new TBar(DateTime.UtcNow, 100, 95, 98, 97, 1000)); Assert.Equal(1, fireCount); } [Fact] public void Pub_FiresOnEachUpdate() { var ck = new Ckstop(atrPeriod: 3, multiplier: 1.0, stopPeriod: 2); int fireCount = 0; ck.Pub += (object? _, in TValueEventArgs _e) => { fireCount++; }; for (int i = 0; i < 5; i++) { double price = 100.0 + i; _ = ck.Update(new TBar(DateTime.UtcNow.AddMinutes(i), price + 2, price - 2, price + 1, price, 1000)); } Assert.Equal(5, fireCount); } } // ── I) Prime Tests ─────────────────────────────────────────────────────── public sealed class CkstopPrimeTests { [Fact] public void Prime_TBarSeries_SetsState() { var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.20, seed: 42); var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var ck = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9); ck.Prime(bars); Assert.True(ck.IsHot); Assert.True(double.IsFinite(ck.StopLong)); Assert.True(double.IsFinite(ck.StopShort)); } [Fact] public void Prime_EmptySource_NoException() { var ck = new Ckstop(); var bars = new TBarSeries(); var ex = Record.Exception(() => ck.Prime(bars)); Assert.Null(ex); Assert.False(ck.IsHot); } }