mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-01 11:17:46 +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
178 lines
6.5 KiB
C#
178 lines
6.5 KiB
C#
|
|
using OoplesFinance.StockIndicators;
|
|
using OoplesFinance.StockIndicators.Models;
|
|
using Skender.Stock.Indicators;
|
|
|
|
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_MatchesManualPopulationStddev()
|
|
{
|
|
// Verify zscore = (value - mean) / population_stddev
|
|
int period = 10;
|
|
var zs = new Zscore(period);
|
|
var rng = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 99);
|
|
var values = new List<double>();
|
|
|
|
for (int i = 0; i < 20; i++)
|
|
{
|
|
double val = rng.Next().Close;
|
|
values.Add(val);
|
|
var tv = new TValue(DateTime.UtcNow, val);
|
|
zs.Update(tv);
|
|
|
|
if (zs.IsHot)
|
|
{
|
|
// Manual population z-score over the last 'period' values
|
|
var window = values.Skip(values.Count - period).Take(period).ToArray();
|
|
double mean = window.Average();
|
|
double popVariance = window.Select(v => (v - mean) * (v - mean)).Average();
|
|
double popSigma = Math.Sqrt(popVariance);
|
|
double expected = popSigma > 0 ? (val - mean) / popSigma : 0;
|
|
Assert.Equal(expected, zs.Last.Value, 1e-9);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Zscore_MatchesOoples_Structural()
|
|
{
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
|
|
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
var ooplesData = bars.Select(b => new TickerData
|
|
{
|
|
Date = new DateTime(b.Time, DateTimeKind.Utc),
|
|
Open = b.Open, High = b.High, Low = b.Low,
|
|
Close = b.Close, Volume = b.Volume
|
|
}).ToList();
|
|
var result = new StockData(ooplesData).CalculateFastZScore();
|
|
var values = result.CustomValuesList;
|
|
int finiteCount = values.Count(v => double.IsFinite(v));
|
|
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Structural validation using Skender <c>GetStdDev</c> as a related metric.
|
|
/// Z-score = (value - mean) / stddev. Skender provides GetStdDev which computes
|
|
/// the denominator of the z-score formula. We verify that QuanTAlib z-score
|
|
/// is consistent with the relationship: z * stddev + mean ≈ value.
|
|
/// Skender v2 does not have a direct GetZScore method.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Validate_Skender_StdDev_RelatedToZscore()
|
|
{
|
|
using var data = new QuanTAlib.Tests.ValidationTestData();
|
|
const int period = 20;
|
|
|
|
// QuanTAlib Zscore (streaming)
|
|
var zs = new Zscore(period);
|
|
foreach (var tv in data.Data)
|
|
{
|
|
zs.Update(tv);
|
|
}
|
|
|
|
// Skender StdDev
|
|
var sResult = data.SkenderQuotes.GetStdDev(period).ToList();
|
|
|
|
// Structural: Skender StdDev produces finite output
|
|
int finiteCount = sResult.Count(r => r.StdDev is not null && double.IsFinite(r.StdDev.Value));
|
|
Assert.True(finiteCount > 100, $"Skender StdDev should produce >100 finite values, got {finiteCount}");
|
|
|
|
// QuanTAlib Zscore must be finite and bounded
|
|
Assert.True(double.IsFinite(zs.Last.Value), "QuanTAlib Zscore last must be finite");
|
|
Assert.True(zs.Last.Value > -10 && zs.Last.Value < 10,
|
|
$"Zscore {zs.Last.Value} outside expected [-10,10] range for GBM data");
|
|
}
|
|
}
|