Files
QuanTAlib/lib/momentum/rsx/tests/Rsx.Validation.Tests.cs
T
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
2026-03-12 12:34:16 -07:00

172 lines
5.7 KiB
C#

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);
}
}