Files
QuanTAlib/lib/statistics/zscore/Zscore.Validation.Tests.cs
T
Miha Kralj b3a64f18fa Implement ZTEST: One-Sample t-Test Statistic with validation tests
- Added Ztest class to compute the one-sample t-statistic using sample standard deviation with Bessel correction.
- Implemented validation tests for Ztest to ensure accuracy against manual calculations and PineScript.
- Updated documentation for Ztest, detailing its mathematical foundation, performance profile, and common pitfalls.
- Adjusted NDepend badges to reflect changes in code metrics after implementation.
- Updated missing indicators report to reflect the completion of statistical indicators, including ZTEST.
2026-02-16 16:54:36 -08:00

123 lines
4.2 KiB
C#

namespace QuanTAlib.Validation;
/// <summary>
/// Validation tests for ZSCORE indicator.
/// No direct TA-Lib/Tulip/Skender/Ooples equivalent exists for population z-score.
/// Validates against manual computation and mathematical properties.
/// </summary>
public sealed class ZscoreValidationTests
{
[Fact]
public void Zscore_ManualComputation_MatchesPineScript()
{
// PineScript formula: z = (x - mean) / sqrt(popVariance)
// Data: {10, 20, 30, 40, 50}, period=5
// mean = 30, popVar = ((10-30)²+(20-30)²+(30-30)²+(40-30)²+(50-30)²)/5 = 1000/5 = 200
// sigma = sqrt(200) ≈ 14.1421
// z(50) = (50-30)/sqrt(200) = 20/14.1421 ≈ 1.4142
var z = new Zscore(5);
double[] data = [10, 20, 30, 40, 50];
foreach (double d in data)
{
z.Update(new TValue(DateTime.UtcNow, d));
}
double expected = 20.0 / Math.Sqrt(200.0);
Assert.Equal(expected, z.Last.Value, 1e-9);
}
[Fact]
public void Zscore_GBMData_BoundedRange()
{
// For GBM-generated data, z-scores should typically be within [-4, 4]
int period = 20;
var z = new Zscore(period);
var rng = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < 200; i++)
{
TBar bar = rng.Next();
z.Update(new TValue(bar.Time, bar.Close));
if (z.IsHot)
{
Assert.True(z.Last.Value > -10.0 && z.Last.Value < 10.0,
$"Z-score {z.Last.Value} outside expected range at i={i}");
}
}
}
[Fact]
public void Zscore_ScalingInvariance_HoldsForLinearTransform()
{
// z(a*x + b) should equal z(x) for constant a > 0, any b
int period = 10;
var z1 = new Zscore(period);
var z2 = new Zscore(period);
var rng = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 88);
for (int i = 0; i < 30; i++)
{
double val = rng.Next().Close;
z1.Update(new TValue(DateTime.UtcNow, val));
z2.Update(new TValue(DateTime.UtcNow, val * 3.0 + 100.0)); // linear transform
if (z1.IsHot && z2.IsHot)
{
Assert.Equal(z1.Last.Value, z2.Last.Value, 1e-8); // FP accumulation drift with scaled values
}
}
}
[Fact]
public void Zscore_MeanIsZero_ForWindowMeanValue()
{
// If the current value equals the window mean, z-score = 0
var z = new Zscore(5);
double[] data = [10, 20, 30, 40, 50];
foreach (double d in data)
{
z.Update(new TValue(DateTime.UtcNow, d));
}
// Now add 30 (== current mean)
_ = z.Update(new TValue(DateTime.UtcNow, 30.0)); // window: {20,30,40,50,30}, mean=34
// Not exactly 0 since window shifts, but demonstrates the property
// Instead test with window where current val == mean
var z2 = new Zscore(3);
z2.Update(new TValue(DateTime.UtcNow, 10.0));
z2.Update(new TValue(DateTime.UtcNow, 20.0));
var r = z2.Update(new TValue(DateTime.UtcNow, 15.0)); // mean = 15, z(15) = 0
Assert.Equal(0.0, r.Value, 1e-9);
}
[Fact]
public void Zscore_MatchesStandardize_WithPopulationCorrection()
{
// ZSCORE uses population stddev, Standardize uses sample stddev
// zscore = value_offset / pop_sigma
// standardize = value_offset / sample_sigma
// sample_sigma = pop_sigma * sqrt(n/(n-1))
// So: zscore = standardize * sqrt(n/(n-1))
int period = 10;
var zs = new Zscore(period);
var st = new Standardize(period);
var rng = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 99);
for (int i = 0; i < 20; i++)
{
double val = rng.Next().Close;
var tv = new TValue(DateTime.UtcNow, val);
zs.Update(tv);
st.Update(tv);
if (zs.IsHot && st.IsHot)
{
// zscore = standardize * sqrt(n / (n-1))
double correction = Math.Sqrt((double)period / (period - 1));
Assert.Equal(st.Last.Value * correction, zs.Last.Value, 1e-6);
}
}
}
}