using QuanTAlib.Tests; using OoplesFinance.StockIndicators; using OoplesFinance.StockIndicators.Models; namespace QuanTAlib; public class RsxValidationTests { private readonly GBM _gbm; public RsxValidationTests() { _gbm = new GBM(); } [Fact] public void Validate_Against_Reference_Implementation() { // Generate data const int count = 1000; int period = 14; var bars = _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var prices = bars.Close.Values; // QuanTAlib implementation var rsx = new Rsx(period); var quantalibResults = new double[count]; for (int i = 0; i < count; i++) { quantalibResults[i] = rsx.Update(new TValue(DateTime.UtcNow, prices[i])).Value; } // Reference implementation (from user prompt) var refRsx = new ReferenceRsx(period); var refResults = new double[count]; for (int i = 0; i < count; i++) { refResults[i] = refRsx.Add(prices[i]); } // Compare for (int i = 0; i < count; i++) { // Allow small difference due to floating point arithmetic order Assert.Equal(refResults[i], quantalibResults[i], ValidationHelper.DefaultTolerance); } } // Reference implementation provided in the task description private class ReferenceRsx { private readonly double alpha, ialpha; // Internal state variables for filter registers: private double f28, f30, f38, f40, f48, f50; private double f58, f60, f68, f70, f78, f80; // Added state for f10 logic private double lastF8; private bool initialized; public double Current { get; private set; } public ReferenceRsx(int length) { // Initialize constants: this.alpha = 3.0 / (length + 2.0); this.ialpha = 1.0 - this.alpha; // Initialize filters to 0: f28 = f30 = f38 = f40 = f48 = f50 = 0.0; f58 = f60 = f68 = f70 = f78 = f80 = 0.0; this.Current = 50.0; // neutral start this.initialized = false; } public double Add(double price) { // Core RSX calculations (assuming price input as closing price): double f8 = 100 * price; if (!initialized) { lastF8 = f8; initialized = true; } double v8 = f8 - lastF8; lastF8 = f8; // First smoothing stage: f28 = (ialpha * f28) + (alpha * v8); f30 = (alpha * f28) + (ialpha * f30); double vC = (1.5 * f28) - (0.5 * f30); // Second smoothing stage: f38 = (ialpha * f38) + (alpha * vC); f40 = (alpha * f38) + (ialpha * f40); double v10 = (1.5 * f38) - (0.5 * f40); // Third smoothing stage: f48 = (ialpha * f48) + (alpha * v10); f50 = (alpha * f48) + (ialpha * f50); double v14 = (1.5 * f48) - (0.5 * f50); // Repeat stages for absolute value (momentum magnitude): f58 = (ialpha * f58) + (alpha * Math.Abs(v8)); f60 = (alpha * f58) + (ialpha * f60); double v18 = (1.5 * f58) - (0.5 * f60); f68 = (ialpha * f68) + (alpha * v18); f70 = (alpha * f68) + (ialpha * f70); double v1C = (1.5 * f68) - (0.5 * f70); f78 = (ialpha * f78) + (alpha * v1C); f80 = (alpha * f78) + (ialpha * f80); double v20 = (1.5 * f78) - (0.5 * f80); // Final RSX value: double rsx; if (v20 > 1e-10) // Avoid division by zero { double v4 = ((v14 / v20) + 1.0) * 50.0; rsx = Math.Clamp(v4, 0.0, 100.0); } else { rsx = 50.0; } this.Current = rsx; return rsx; } } [Fact] public void Rsx_MatchesOoples_Structural() { var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42); var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var ooplesData = bars.Select(b => new TickerData { Date = new DateTime(b.Time, DateTimeKind.Utc), Open = b.Open, High = b.High, Low = b.Low, Close = b.Close, Volume = b.Volume }).ToList(); var result = new StockData(ooplesData).CalculateApirineSlowRelativeStrengthIndex(); var values = result.CustomValuesList; int finiteCount = values.Count(v => double.IsFinite(v)); Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}"); } [Fact] public void Rsx_Correction_Recomputes() { var ind = new Rsx(14); var t0 = DateTime.MinValue; // Build state well past warmup for (int i = 0; i < 50; i++) { ind.Update(new TValue(t0.AddSeconds(i), 100.0 + (i * 0.5))); } // Anchor bar var anchorTime = t0.AddSeconds(50); const double anchorPrice = 125.0; ind.Update(new TValue(anchorTime, anchorPrice), isNew: true); double anchorResult = ind.Last.Value; // Correction with dramatically different value — use a large drop to move RSX away from ceiling ind.Update(new TValue(anchorTime, anchorPrice / 10), isNew: false); Assert.NotEqual(anchorResult, ind.Last.Value); // Correction back to original — must exactly restore ind.Update(new TValue(anchorTime, anchorPrice), isNew: false); Assert.Equal(anchorResult, ind.Last.Value, 1e-9); } }