using OoplesFinance.StockIndicators; using OoplesFinance.StockIndicators.Models; using System; using System.Linq; using Skender.Stock.Indicators; using Xunit; using Xunit.Abstractions; namespace QuanTAlib.Tests; public sealed class StcValidationTests : IDisposable { private readonly ValidationTestData _testData; private readonly ITestOutputHelper _output; public StcValidationTests(ITestOutputHelper output) { _output = output; _testData = new ValidationTestData(); } public void Dispose() { _testData.Dispose(); } [Fact] public void Validate_Skender_Stc_Deviation() { // Skender's STC implementation uses a "Single Smoothed" approach (Stoch of MACD). // QuanTAlib implements the standard "Double Smoothed" approach (Stoch of Stoch of MACD), // as originally defined by Schaff. // // Example mismatch at index 333: // QuanTAlib (Double Smoothed) = 50.0 // Skender (Single Smoothed) = 97.05 // // This test documents this known deviation rather than failing on it. const int cycle = 10; int fast = 23; int slow = 50; var sResult = _testData.SkenderQuotes.GetStc(cycle, fast, slow).ToList(); var qStc = new Stc(kPeriod: cycle, dPeriod: 3, fastLength: fast, slowLength: slow, smoothing: StcSmoothing.Ema); var qResult = qStc.Update(_testData.Data); // Skender recommends S+C+250 warmup. 50+10+250 = 310. int skip = 310; double sumSq = 0; int count = 0; for (int i = skip; i < qResult.Count; i++) { double sVal = sResult[i].Stc ?? double.NaN; double qVal = qResult[i].Value; if (!double.IsNaN(sVal) && !double.IsNaN(qVal)) { sumSq += (sVal - qVal) * (sVal - qVal); count++; } } double rmse = Math.Sqrt(sumSq / count); _output.WriteLine($"Known Methodology Deviation - RMSE: {rmse:F4}"); // Assert that we are essentially different (RMSE > 5.0 implies significant deviation) // If they accidentally matched (e.g. if we broke our logic to match Skender), this should fail. Assert.True(rmse > 5.0, "QuanTAlib STC matches Skender STC, which suggests regression to Single Smoothed logic."); // Assert values are valid for (int i = skip; i < qResult.Count; i++) { Assert.True(double.IsFinite(qResult[i].Value)); Assert.InRange(qResult[i].Value, 0, 100); } } // ── Cross-library: OoplesFinance ────────────────────────────────────────── [Fact] public void Stc_MatchesOoples_Structural() { var ooplesData = _testData.SkenderQuotes.Select(static q => new TickerData { Date = q.Date, Open = (double)q.Open, High = (double)q.High, Low = (double)q.Low, Close = (double)q.Close, Volume = (double)q.Volume }).ToList(); var stockData = new StockData(ooplesData); var oResult = stockData.CalculateSchaffTrendCycle(); var oValues = oResult.OutputValues.Values.First(); var stc = new Stc(kPeriod: 10, dPeriod: 3, fastLength: 23, slowLength: 50, smoothing: StcSmoothing.Ema); var qValues = new List(); foreach (var item in _testData.Data) { qValues.Add(stc.Update(item).Value); } Assert.True(oValues.Count > 0, "Ooples STC must produce output"); int finiteCount = 0; for (int i = 50; i < Math.Min(oValues.Count, qValues.Count); i++) { if (double.IsFinite(oValues[i]) && double.IsFinite(qValues[i])) { finiteCount++; } } Assert.True(finiteCount > 100, $"Expected >100 finite STC pairs, got {finiteCount}"); _output.WriteLine($"STC Ooples structural: {finiteCount} finite pairs verified."); } }