Files
QuanTAlib/lib/numerics/binomdist/tests/Binomdist.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

288 lines
11 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Binomdist validation tests — validates PMF/CDF against exact combinatorial values.
/// Known-value tests call Binomdist.BinomialCdf directly (bypassing windowing) so
/// results are exact. Streaming/batch tests check invariants that hold regardless
/// of window state.
/// </summary>
public class BinomdistValidationTests
{
private const double Tolerance = 1e-9;
private const double LooseTolerance = 1e-6;
// ─── PMF known values ────────────────────────────────────────────────────
// P(X=k; n, p) = C(n,k) * p^k * (1-p)^(n-k)
// CDF P(X<=k) = sum_{i=0}^{k} P(X=i)
[Theory]
// P(X=3; n=10, p=0.5) = C(10,3) * 0.5^10 = 120/1024 = 0.1171875
// CDF P(X<=3; n=10, p=0.5) = (1+10+45+120)/1024 = 176/1024 = 0.171875
[InlineData(0.5, 10, 3, 0.171875)]
// P(X<=5; n=10, p=0.5) = 638/1024 = 0.623046875 (exact)
[InlineData(0.5, 10, 5, 0.623046875)]
// P(X<=0; n=5, p=0.3) = (0.7)^5 = 0.16807
[InlineData(0.3, 5, 0, 0.16807)]
// P(X<=5; n=5, p=0.3) = 1.0 (k >= n)
[InlineData(0.3, 5, 5, 1.0)]
// P(X<=0; n=10, p=0.5) = 0.5^10 = 1/1024 ≈ 0.0009765625
[InlineData(0.5, 10, 0, 0.0009765625)]
// P(X<=10; n=10, p=0.5) = 1.0
[InlineData(0.5, 10, 10, 1.0)]
// P(X<=0; n=1, p=0.5) = 0.5
[InlineData(0.5, 1, 0, 0.5)]
// P(X<=1; n=1, p=0.5) = 1.0
[InlineData(0.5, 1, 1, 1.0)]
// P(X<=2; n=5, p=0.5) = (1+5+10)/32 = 16/32 = 0.5
[InlineData(0.5, 5, 2, 0.5)]
// P(X<=4; n=5, p=0.3) = 1 - P(X=5) = 1 - 0.3^5 = 1 - 0.00243 = 0.99757
[InlineData(0.3, 5, 4, 0.99757)]
public void BinomCdf_KnownValues(double p, int n, int k, double expected)
{
double actual = Binomdist.BinomialCdf(p, n, k);
Assert.Equal(expected, actual, LooseTolerance);
}
// ─── PMF direct known values ─────────────────────────────────────────────
[Fact]
public void BinomPmf_Exact_n10_p05_k3()
{
// P(X=3; n=10, p=0.5) = C(10,3) / 2^10 = 120/1024 = 0.1171875
// PMF = CDF(k) - CDF(k-1)
double cdfK = Binomdist.BinomialCdf(0.5, 10, 3);
double cdfKm1 = Binomdist.BinomialCdf(0.5, 10, 2);
double pmf = cdfK - cdfKm1;
Assert.Equal(0.1171875, pmf, Tolerance);
}
[Fact]
public void BinomPmf_Exact_n5_p03_k0()
{
// P(X=0; n=5, p=0.3) = (0.7)^5 = 0.16807
// CDF(0) - CDF(-1) = CDF(0) = 0.16807
double cdf = Binomdist.BinomialCdf(0.3, 5, 0);
Assert.Equal(0.16807, cdf, Tolerance);
}
// ─── Monotonicity ─────────────────────────────────────────────────────────
[Theory]
[InlineData(0.3, 10)]
[InlineData(0.5, 10)]
[InlineData(0.7, 20)]
[InlineData(0.1, 5)]
public void BinomCdf_Monotonic_InK(double p, int n)
{
// CDF must be non-decreasing in k
double prev = 0.0;
for (int k = 0; k <= n; k++)
{
double cdf = Binomdist.BinomialCdf(p, n, k);
Assert.True(cdf >= prev - 1e-12,
$"CDF not monotonic at k={k}, p={p}, n={n}: got {cdf}, prev={prev}");
prev = cdf;
}
}
[Fact]
public void BinomCdf_MonotonicInP()
{
// P(X<=5; n=10, p) must be bounded [0,1] for all p
int n = 10, k = 5;
for (int i = 1; i <= 9; i++)
{
double p = i / 10.0;
double cdf = Binomdist.BinomialCdf(p, n, k);
Assert.True(cdf >= 0.0 && cdf <= 1.0,
$"CDF out of bounds: {cdf} at p={p}");
}
}
// ─── Boundary behavior ────────────────────────────────────────────────────
[Fact]
public void BinomCdf_P_Zero_ReturnsOne()
{
Assert.Equal(1.0, Binomdist.BinomialCdf(0.0, 10, 0), Tolerance);
Assert.Equal(1.0, Binomdist.BinomialCdf(0.0, 10, 10), Tolerance);
}
[Fact]
public void BinomCdf_P_One_KLessN_ReturnsZero()
{
Assert.Equal(0.0, Binomdist.BinomialCdf(1.0, 10, 5), Tolerance);
Assert.Equal(0.0, Binomdist.BinomialCdf(1.0, 10, 9), Tolerance);
}
[Fact]
public void BinomCdf_P_One_KEqualN_ReturnsOne()
{
Assert.Equal(1.0, Binomdist.BinomialCdf(1.0, 10, 10), Tolerance);
}
[Fact]
public void BinomCdf_KN_ReturnsOne()
{
// P(X<=n; n, p) = 1 for all p in (0,1)
Assert.Equal(1.0, Binomdist.BinomialCdf(0.3, 5, 5), Tolerance);
Assert.Equal(1.0, Binomdist.BinomialCdf(0.5, 10, 10), Tolerance);
Assert.Equal(1.0, Binomdist.BinomialCdf(0.9, 20, 20), Tolerance);
}
// ─── Output bounds ─────────────────────────────────────────────────────────
[Fact]
public void BinomCdf_OutputBounded_Zero_To_One()
{
int count = 200;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 52001);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var indicator = new Binomdist(period: 20, trials: 10, threshold: 5);
for (int i = 0; i < count; i++)
{
indicator.Update(bars.Close[i]);
double v = indicator.Last.Value;
Assert.True(v >= 0.0 && v <= 1.0, $"Output {v} at bar {i} out of [0,1]");
}
}
// ─── Flat range → neutral CDF ─────────────────────────────────────────────
[Fact]
public void BinomCdf_FlatRange_ReturnsSymmetricCdf()
{
// Flat range → p=0.5; for symmetric n=10, k=5: CDF = 0.623046875
var ind = new Binomdist(20, trials: 10, threshold: 5);
var time = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
ind.Update(new TValue(time.AddSeconds(i), 100.0));
}
Assert.Equal(0.623046875, ind.Last.Value, LooseTolerance);
}
// ─── Period=1 trivial case ────────────────────────────────────────────────
[Fact]
public void BinomCdf_Period1_AlwaysReturnsCdfAtHalf()
{
// period=1: single-element window → range=0 → p=0.5 always
var ind = new Binomdist(1, trials: 10, threshold: 5);
var time = DateTime.UtcNow;
double expected = Binomdist.BinomialCdf(0.5, 10, 5);
double[] prices = { 100.0, 50.0, 200.0, 1.0, 1000.0 };
foreach (double p in prices)
{
ind.Update(new TValue(time, p));
time = time.AddMinutes(1);
Assert.Equal(expected, ind.Last.Value, LooseTolerance);
}
}
// ─── Span batch consistency ───────────────────────────────────────────────
[Fact]
public void Batch_Span_MatchesTSeries()
{
int count = 150;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.25, seed: 52002);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
double[] rawValues = new double[count];
for (int i = 0; i < count; i++)
{
rawValues[i] = bars.Close[i].Value;
}
var tseriesResult = Binomdist.Batch(bars.Close, period: 30, trials: 15, threshold: 7);
double[] spanResult = new double[count];
Binomdist.Batch(rawValues, spanResult, period: 30, trials: 15, threshold: 7);
for (int i = 0; i < count; i++)
{
Assert.Equal(tseriesResult[i].Value, spanResult[i], Tolerance);
}
}
// ─── Large n stability ────────────────────────────────────────────────────
[Fact]
public void BinomCdf_LargeN_Stable()
{
// Large n tests log-space summation's overflow avoidance
double cdf = Binomdist.BinomialCdf(0.5, 100, 50);
Assert.True(double.IsFinite(cdf) && cdf >= 0.0 && cdf <= 1.0,
$"Large n CDF invalid: {cdf}");
// n=100, k=50, p=0.5 should be near 0.54 (slightly above 0.5)
Assert.True(cdf > 0.5 && cdf < 0.7, $"CDF={cdf} expected near 0.54");
}
[Fact]
public void BinomCdf_LargeDataset_Stable()
{
int count = 2000;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 52003);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var indicator = new Binomdist(period: 50, trials: 20, threshold: 10);
for (int i = 0; i < count; i++)
{
indicator.Update(bars.Close[i]);
double v = indicator.Last.Value;
Assert.True(double.IsFinite(v) && v >= 0.0 && v <= 1.0,
$"Invalid output {v} at bar {i}");
}
}
// ─── Different parameter combos all produce output in range ──────────────
[Theory]
[InlineData(5, 5, 2)]
[InlineData(14, 10, 5)]
[InlineData(50, 20, 10)]
[InlineData(100, 50, 25)]
[InlineData(30, 1, 0)]
public void BinomCdf_ParameterCombos_OutputBounded(int period, int trials, int threshold)
{
int count = period + 50;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 52004 + period);
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var indicator = new Binomdist(period, trials, threshold);
for (int i = 0; i < count; i++)
{
indicator.Update(bars.Close[i]);
double v = indicator.Last.Value;
Assert.True(v >= 0.0 && v <= 1.0);
}
}
// ─── Streaming convergence ────────────────────────────────────────────────
[Fact]
public void BinomCdf_HighPeriod_StillConverges()
{
int period = 200;
var indicator = new Binomdist(period, trials: 20, threshold: 10);
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 52005);
var bars = gbm.Fetch(period + 50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Close.Count; i++)
{
indicator.Update(bars.Close[i]);
Assert.True(double.IsFinite(indicator.Last.Value),
$"Non-finite output at bar {i}");
}
}
}