namespace QuanTAlib; public class RsiTests { [Fact] public void Constructor_InvalidParameters_ThrowsArgumentException() { Assert.Throws(() => new Rsi(0)); Assert.Throws(() => new Rsi(-1)); } [Fact] public void BasicCalculation_DoesNotCrash() { var rsi = new Rsi(14); var gbm = new GBM(); var series = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < series.Count; i++) { rsi.Update(series.Close[i]); } Assert.True(double.IsFinite(rsi.Last.Value)); } [Fact] public void Properties_Accessible() { var rsi = new Rsi(14); Assert.Equal("Rsi(14)", rsi.Name); Assert.False(rsi.IsHot); Assert.Equal(0, rsi.Last.Value); } [Fact] public void IsHot_BecomesTrueAfterWarmup() { var rsi = new Rsi(14); var gbm = new GBM(); var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); Assert.False(rsi.IsHot); // RSI wraps two RMA indicators (gain and loss) // RMA wraps EMA with alpha = 1/period // EMA becomes hot when E <= 0.05, which occurs after ~-ln(0.05)/ln(1-alpha) values // For period=14: alpha=1/14, needs ~40 values to become hot // Both RMAs must be hot for RSI to be hot for (int i = 0; i < 45; i++) { rsi.Update(series.Close[i]); if (i < 40) { Assert.False(rsi.IsHot, $"Should not be hot at index {i}"); } } // After sufficient warmup, IsHot should be true Assert.True(rsi.IsHot); } [Fact] public void IsNew_True_AdvancesState() { var rsi = new Rsi(5); var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.1, seed: 42); var series = gbm.Fetch(15, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Feed enough values to get past warmup for (int i = 0; i < 10; i++) { rsi.Update(series.Close[i], isNew: true); } // Get stable state var val1 = rsi.Update(series.Close[10], isNew: true); // Advance with a significantly different value var nextTime = series.Close[10].Time + TimeSpan.FromMinutes(1).Ticks; var nextValue = series.Close[10].Value * 1.05; // 5% increase var val2 = rsi.Update(new TValue(nextTime, nextValue), isNew: true); // RSI should change when we advance to a new bar with a different value Assert.NotEqual(val1.Value, val2.Value); } [Fact] public void IsNew_False_UpdatesCurrentBar() { var rsi = new Rsi(5); var gbm = new GBM(); var series = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 9; i++) { rsi.Update(series.Close[i]); } var val1 = rsi.Update(series.Close[9], isNew: true); var modifiedValue = new TValue(series.Close[9].Time, series.Close[9].Value + 5); var val2 = rsi.Update(modifiedValue, isNew: false); // Should update the same bar Assert.Equal(val1.Time, val2.Time); Assert.NotEqual(val1.Value, val2.Value); } [Fact] public void IsNew_Consistency() { var rsi = new Rsi(14); var gbm = new GBM(); var series = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Feed first 99 for (int i = 0; i < 99; i++) { rsi.Update(series.Close[i]); } // Update with 100th point (isNew=true) rsi.Update(series.Close[99], true); // Update with modified 100th point (isNew=false) var modifiedValue = new TValue(series.Close[99].Time, series.Close[99].Value + 2.0); var val2 = rsi.Update(modifiedValue, false); // Create new instance and feed up to modified var rsi2 = new Rsi(14); for (int i = 0; i < 99; i++) { rsi2.Update(series.Close[i]); } var val3 = rsi2.Update(modifiedValue, true); Assert.Equal(val3.Value, val2.Value, 1e-9); } [Fact] public void IterativeCorrections_RestoreToOriginalState() { var rsi = new Rsi(10); var gbm = new GBM(); var series = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Feed N values for (int i = 0; i < 30; i++) { rsi.Update(series.Close[i]); } var originalValue = rsi.Last; // Make M updates with isNew=false for (int m = 0; m < 5; m++) { var modifiedValue = new TValue(series.Close[29].Time, series.Close[29].Value + m); rsi.Update(modifiedValue, isNew: false); } // Restore with original 30th value var restoredValue = rsi.Update(series.Close[29], isNew: false); Assert.Equal(originalValue.Value, restoredValue.Value, 1e-9); } [Fact] public void Reset_ClearsState() { var rsi = new Rsi(14); var gbm = new GBM(); var series = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < series.Count; i++) { rsi.Update(series.Close[i]); } Assert.True(rsi.IsHot); var valueBefore = rsi.Last.Value; rsi.Reset(); Assert.False(rsi.IsHot); Assert.Equal(0, rsi.Last.Value); // Feed again for (int i = 0; i < series.Count; i++) { rsi.Update(series.Close[i]); } Assert.True(rsi.IsHot); Assert.Equal(valueBefore, rsi.Last.Value, 1e-9); } [Fact] public void NaN_Input_DoesNotCrash() { var rsi = new Rsi(10); var gbm = new GBM(); var series = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 15; i++) { rsi.Update(series.Close[i]); } // Feed NaN var nanValue = new TValue(DateTime.UtcNow, double.NaN); var result = rsi.Update(nanValue); // Should not crash and should return finite value Assert.True(double.IsFinite(result.Value)); } [Fact] public void Infinity_Input_DoesNotCrash() { var rsi = new Rsi(10); var gbm = new GBM(); var series = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 15; i++) { rsi.Update(series.Close[i]); } var infValue = new TValue(DateTime.UtcNow, double.PositiveInfinity); var result = rsi.Update(infValue); Assert.True(double.IsFinite(result.Value)); } [Fact] public void MultipleNaN_ContinuesCorrectly() { var rsi = new Rsi(10); var gbm = new GBM(); var series = gbm.Fetch(30, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 20; i++) { rsi.Update(series.Close[i]); } // Feed multiple NaN for (int i = 0; i < 5; i++) { var nanValue = new TValue(DateTime.UtcNow.AddMinutes(i), double.NaN); var result = rsi.Update(nanValue); Assert.True(double.IsFinite(result.Value)); } // Continue with valid data for (int i = 20; i < 30; i++) { var result = rsi.Update(series.Close[i]); Assert.True(double.IsFinite(result.Value)); } } [Fact] public void HandlesFlatLine() { var rsi = new Rsi(5); var series = new TSeries(); for (int i = 0; i < 20; i++) { series.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100)); } var result = rsi.Update(series); // Flat line: no gains or losses, RSI = 50 Assert.Equal(50, result.Last.Value); } [Fact] public void TSeries_Update_Matches_Streaming() { var rsi = new Rsi(14); var gbm = new GBM(); var series = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var streamingResults = new List(); for (int i = 0; i < series.Count; i++) { streamingResults.Add(rsi.Update(series.Close[i]).Value); } var rsi2 = new Rsi(14); var seriesResults = rsi2.Update(series.Close); Assert.Equal(streamingResults.Count, seriesResults.Count); for (int i = 0; i < seriesResults.Count; i++) { Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9); } } [Fact] public void StaticBatch_Matches_Streaming() { var gbm = new GBM(); var series = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var rsi = new Rsi(14); var streamingResults = new List(); for (int i = 0; i < series.Count; i++) { streamingResults.Add(rsi.Update(series.Close[i]).Value); } var batchResults = Rsi.Batch(series.Close, 14); Assert.Equal(streamingResults.Count, batchResults.Count); for (int i = 0; i < batchResults.Count; i++) { Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9); } } [Fact] public void SpanCalc_ValidatesInput() { var source = new double[10]; var output = new double[5]; Assert.Throws(() => Rsi.Batch(source, output, 14)); } [Fact] public void SpanCalc_InvalidPeriod_Throws() { var source = new double[10]; var output = new double[10]; Assert.Throws(() => Rsi.Batch(source, output, 0)); Assert.Throws(() => Rsi.Batch(source, output, -1)); } [Fact] public void SpanCalc_MatchesTSeriesCalc() { var gbm = new GBM(); var series = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); var batchResults = Rsi.Batch(series.Close, 14); var output = new double[series.Count]; Rsi.Batch(series.Close.Values, output, 14); for (int i = 0; i < output.Length; i++) { Assert.Equal(batchResults.Values[i], output[i], 1e-9); } } [Fact] public void SpanCalc_HandlesNaN() { var source = new double[20]; var gbm = new GBM(); var series = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); for (int i = 0; i < 20; i++) { source[i] = series.Close[i].Value; } source[10] = double.NaN; source[15] = double.NaN; var output = new double[20]; Rsi.Batch(source, output, 10); // Should not crash and produce finite results for (int i = 0; i < output.Length; i++) { Assert.True(double.IsFinite(output[i]) || Math.Abs(output[i]) < 1e-10); } } [Fact] public void AllModes_ProduceSameResult() { const int period = 14; var gbm = new GBM(); var series = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // 1. Batch Mode var batchSeries = Rsi.Batch(series.Close, period); double expected = batchSeries.Last.Value; // 2. Span Mode var spanOutput = new double[series.Count]; Rsi.Batch(series.Close.Values, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode var streamingRsi = new Rsi(period); for (int i = 0; i < series.Count; i++) { streamingRsi.Update(series.Close[i]); } double streamingResult = streamingRsi.Last.Value; // 4. Eventing Mode var pubSource = new TSeries(); var eventingRsi = new Rsi(pubSource, period); for (int i = 0; i < series.Count; i++) { pubSource.Add(series.Close[i]); } double eventingResult = eventingRsi.Last.Value; // Assert Assert.Equal(expected, spanResult, 9); Assert.Equal(expected, streamingResult, 9); Assert.Equal(expected, eventingResult, 9); } [Fact] public void Chainability_Works() { var rsi = new Rsi(14); var gbm = new GBM(); var series = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); // Test TSeries chain var result = rsi.Update(series.Close); Assert.NotNull(result); Assert.IsType(result); // Test TValue chain (returns TValue) var result2 = rsi.Update(series.Close[0]); Assert.IsType(result2); } }