mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-04 12:07:44 +00:00
060649192f
- 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
277 lines
10 KiB
C#
277 lines
10 KiB
C#
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)");
|
||
}
|
||
}
|