using Xunit; namespace QuanTAlib.Tests; /// /// Mathematical validation of the Student's t-Distribution CDF implementation. /// Validates known values, symmetry properties, and convergence to the normal distribution. /// No external library required — all validations use mathematical identities. /// public class TdistValidationTests { private const double Tolerance = 1e-9; private const double LooseTolerance = 1e-4; // ─── CDF boundary properties ────────────────────────────────────────────── [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] [InlineData(30)] [InlineData(100)] public void StaticCdf_AlwaysInUnitInterval(int nu) { double[] tValues = { -10.0, -3.0, -1.96, -1.0, -0.5, 0.0, 0.5, 1.0, 1.96, 3.0, 10.0 }; foreach (double t in tValues) { double cdf = Tdist.StaticCdf(t, nu); Assert.True(cdf >= 0.0 && cdf <= 1.0, $"CDF({t}, ν={nu}) = {cdf} is outside [0,1]"); } } // ─── Symmetry and anti-symmetry ────────────────────────────────────────── [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] [InlineData(30)] public void StaticCdf_AtZero_IsHalf(int nu) { double cdf = Tdist.StaticCdf(0.0, nu); Assert.Equal(0.5, cdf, Tolerance); } [Theory] [InlineData(1, 1.0)] [InlineData(5, 1.5)] [InlineData(10, 2.0)] [InlineData(30, 1.96)] [InlineData(100, 2.5)] public void StaticCdf_Antisymmetry(int nu, double t) { double cdfPos = Tdist.StaticCdf(t, nu); double cdfNeg = Tdist.StaticCdf(-t, nu); Assert.Equal(1.0, cdfPos + cdfNeg, Tolerance); } // ─── Monotonicity ───────────────────────────────────────────────────────── [Theory] [InlineData(1)] [InlineData(5)] [InlineData(10)] [InlineData(100)] public void StaticCdf_IsMonotonicallyIncreasing(int nu) { double[] tValues = { -10.0, -5.0, -3.0, -2.0, -1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 3.0, 5.0, 10.0 }; for (int i = 1; i < tValues.Length; i++) { double prev = Tdist.StaticCdf(tValues[i - 1], nu); double curr = Tdist.StaticCdf(tValues[i], nu); Assert.True(curr >= prev, $"CDF not monotone at t={tValues[i]}, ν={nu}: prev={prev}, curr={curr}"); } } // ─── Known values: Cauchy (ν=1) ────────────────────────────────────────── [Fact] public void StaticCdf_Nu1_AtT1_IsThreeQuarters() { // t(ν=1) is Cauchy. CDF(1; 1) = 0.5 + (1/π)·arctan(1) = 0.5 + 1/4 = 0.75 double cdf = Tdist.StaticCdf(1.0, 1); Assert.Equal(0.75, cdf, 1e-9); } [Fact] public void StaticCdf_Nu1_AtTNeg1_IsOneQuarter() { double cdf = Tdist.StaticCdf(-1.0, 1); Assert.Equal(0.25, cdf, 1e-9); } [Fact] public void StaticCdf_Nu1_AtT0_IsHalf() { double cdf = Tdist.StaticCdf(0.0, 1); Assert.Equal(0.5, cdf, Tolerance); } // ─── Convergence to Normal as ν → ∞ ───────────────────────────────────── [Fact] public void StaticCdf_LargeNu_ApproximatesNormal_1_96() { // Normal CDF(1.96) ≈ 0.97500210931... // t(ν=1000) should be very close double cdf = Tdist.StaticCdf(1.96, 1000); Assert.Equal(0.975, cdf, 1e-3); } [Fact] public void StaticCdf_LargeNu_ApproximatesNormal_1_645() { // Normal CDF(1.645) ≈ 0.95002... double cdf = Tdist.StaticCdf(1.645, 1000); Assert.Equal(0.95, cdf, 2e-3); } [Fact] public void StaticCdf_LargeNu_ApproximatesNormal_Neg1_96() { // Normal CDF(-1.96) ≈ 0.025 double cdf = Tdist.StaticCdf(-1.96, 1000); Assert.Equal(0.025, cdf, 1e-3); } // ─── Known values across different ν ───────────────────────────────────── [Fact] public void StaticCdf_Nu2_AtT1_KnownValue() { // t(ν=2): CDF(1; 2) = 0.5 + t/(2√(ν+t²)) = 0.5 + 1/(2√3) ≈ 0.78868... // Verify it's between ν=1 (0.75) and ν→∞ (0.8413) double cdf = Tdist.StaticCdf(1.0, 2); Assert.True(cdf > 0.75 && cdf < 0.85, $"CDF(1.0; ν=2) = {cdf}, expected between 0.75 and 0.85"); } [Fact] public void StaticCdf_HeavierTails_LowerCdfForPositiveT() { // Lower ν → heavier tails → lower CDF for positive t (mass in tails) double cdf1 = Tdist.StaticCdf(2.0, 1); // Cauchy double cdf5 = Tdist.StaticCdf(2.0, 5); double cdf30 = Tdist.StaticCdf(2.0, 30); double cdf1000 = Tdist.StaticCdf(2.0, 1000); Assert.True(cdf1 < cdf5, $"ν=1 CDF should be < ν=5 CDF at t=2"); Assert.True(cdf5 < cdf30, $"ν=5 CDF should be < ν=30 CDF at t=2"); Assert.True(cdf30 < cdf1000, $"ν=30 CDF should be < ν=1000 CDF at t=2"); } // ─── Known-value verification (values from this implementation, verified against // Cauchy/t-distribution formula and cross-checked for mathematical consistency) ───── [Theory] // ν=1 (Cauchy): CDF(t;1) = 0.5 + (1/π)·arctan(t) — exact formula [InlineData(1, -3.0, 0.10241638234956672)] // 0.5 + arctan(-3)/π [InlineData(1, 0.0, 0.5)] [InlineData(1, 1.0, 0.75)] // 0.5 + arctan(1)/π = 0.5 + 0.25 [InlineData(1, 3.0, 0.89758361765043328)] // 0.5 + arctan(3)/π // ν=5: values verified self-consistently [InlineData(5, 0.0, 0.5)] // ν=10: values verified self-consistently [InlineData(10, 0.0, 0.5)] // ν=30: values verified self-consistently [InlineData(30, 0.0, 0.5)] public void StaticCdf_KnownValues_MatchExpected(int nu, double t, double expected) { double actual = Tdist.StaticCdf(t, nu); Assert.Equal(expected, actual, 1e-9); } [Theory] // Self-consistency: verify our implementation gives stable, bounded values // at non-trivial t. Tolerance 1e-5 because these are reference vs computed. [InlineData(5, -2.0, 0.0510)] // t(5): CDF(-2) ≈ 0.051 [InlineData(5, 2.0, 0.9490)] // t(5): CDF(+2) ≈ 0.949 [InlineData(10, -1.96, 0.0392)] // t(10): CDF(-1.96) ≈ 0.0392 [InlineData(10, 1.96, 0.9608)] // t(10): CDF(+1.96) ≈ 0.9608 [InlineData(30, -1.96, 0.0297)] // t(30): CDF(-1.96) ≈ 0.0297 [InlineData(30, 1.96, 0.9703)] // t(30): CDF(+1.96) ≈ 0.9703 public void StaticCdf_ApproximateValues_InExpectedRange(int nu, double t, double expected) { double actual = Tdist.StaticCdf(t, nu); Assert.Equal(expected, actual, 1e-3); } // ─── Streaming output always in [0,1] ───────────────────────────────────── [Fact] public void Streaming_OutputAlwaysInUnitInterval() { int count = 200; var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 71001); var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); int[] nuValues = { 1, 5, 10, 30, 100 }; foreach (int nu in nuValues) { var indicator = new Tdist(nu: nu, period: 20); for (int i = 0; i < count; i++) { var result = indicator.Update(bars.Close[i]); Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"ν={nu}, bar={i}: output {result.Value} outside [0,1]"); } } } // ─── Flat range → 0.5 ───────────────────────────────────────────────────── [Theory] [InlineData(1)] [InlineData(10)] [InlineData(100)] public void Streaming_FlatRange_ReturnsMidpoint(int nu) { var indicator = new Tdist(nu: nu, period: 5); var time = DateTime.UtcNow; for (int i = 0; i < 10; i++) { indicator.Update(new TValue(time.AddMinutes(i), 100.0)); } Assert.Equal(0.5, indicator.Last.Value, 1e-6); } // ─── Extreme t-values ───────────────────────────────────────────────────── [Theory] [InlineData(5)] [InlineData(30)] public void StaticCdf_LargePositiveT_NearOne_HighNu(int nu) { // For ν ≥ 5, t=100 → CDF ≈ 1.0 (within 1e-6) double cdf = Tdist.StaticCdf(100.0, nu); Assert.Equal(1.0, cdf, 1e-6); } [Fact] public void StaticCdf_LargePositiveT_Nu1_Cauchy() { // Cauchy (ν=1): CDF(100; 1) = 0.5 + arctan(100)/π ≈ 0.99681... // Heavy tails — does NOT approach 1 quickly double cdf = Tdist.StaticCdf(100.0, 1); double expected = 0.5 + Math.Atan(100.0) / Math.PI; Assert.Equal(expected, cdf, 1e-9); Assert.True(cdf > 0.99 && cdf < 1.0, $"Cauchy CDF(100) = {cdf} should be in (0.99, 1.0)"); } [Theory] [InlineData(5)] [InlineData(30)] public void StaticCdf_LargeNegativeT_NearZero_HighNu(int nu) { // For ν ≥ 5, t=-100 → CDF ≈ 0.0 (within 1e-6) double cdf = Tdist.StaticCdf(-100.0, nu); Assert.Equal(0.0, cdf, 1e-6); } [Fact] public void StaticCdf_LargeNegativeT_Nu1_Cauchy() { // Cauchy (ν=1): CDF(-100; 1) = 0.5 - arctan(100)/π ≈ 0.00319... double cdf = Tdist.StaticCdf(-100.0, 1); double expected = 0.5 - Math.Atan(100.0) / Math.PI; Assert.Equal(expected, cdf, 1e-9); Assert.True(cdf > 0.0 && cdf < 0.01, $"Cauchy CDF(-100) = {cdf} should be in (0, 0.01)"); } }