Files
QuanTAlib/lib/numerics/tdist/tests/Tdist.Validation.Tests.cs
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

277 lines
10 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// 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.
/// </summary>
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)");
}
}