mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-30 02:27:43 +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
131 lines
3.9 KiB
C#
131 lines
3.9 KiB
C#
namespace QuanTAlib.Validation;
|
|
|
|
/// <summary>
|
|
/// Quantile validation tests — cross-indicator validation against Percentile and Median.
|
|
/// Quantile(q) must equal Percentile(q*100) for all q ∈ [0, 1].
|
|
/// </summary>
|
|
public sealed class QuantileValidationTests
|
|
{
|
|
[Fact]
|
|
public void Quantile50_Matches_MedianIndicator()
|
|
{
|
|
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
|
var source = new TSeries();
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
source.Add(gbm.Next());
|
|
}
|
|
int period = 14;
|
|
|
|
// Quantile at q=0.5
|
|
var quantile = new Quantile(period, 0.5);
|
|
var qResults = new double[source.Count];
|
|
|
|
// Median
|
|
var median = new Median(period);
|
|
var mResults = new double[source.Count];
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
var tv = new TValue(source.Times[i], source.Values[i]);
|
|
qResults[i] = quantile.Update(tv).Value;
|
|
mResults[i] = median.Update(tv).Value;
|
|
}
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Assert.Equal(mResults[i], qResults[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Quantile_Matches_Percentile()
|
|
{
|
|
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 456);
|
|
var source = new TSeries();
|
|
for (int i = 0; i < 100; i++)
|
|
{
|
|
source.Add(gbm.Next());
|
|
}
|
|
int period = 14;
|
|
|
|
// Quantile at q=0.25
|
|
var quantile = new Quantile(period, 0.25);
|
|
var qResults = new double[source.Count];
|
|
|
|
// Percentile at p=25
|
|
var percentile = new Percentile(period, 25.0);
|
|
var pResults = new double[source.Count];
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
var tv = new TValue(source.Times[i], source.Values[i]);
|
|
qResults[i] = quantile.Update(tv).Value;
|
|
pResults[i] = percentile.Update(tv).Value;
|
|
}
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Assert.Equal(pResults[i], qResults[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Quantile_BatchAndStreaming_Match()
|
|
{
|
|
double[] data = [10, 20, 15, 30, 25, 40, 35, 50, 45, 60, 55, 70, 65, 80, 75];
|
|
int period = 5;
|
|
double quantileLevel = 0.25;
|
|
|
|
// Streaming
|
|
var q = new Quantile(period, quantileLevel);
|
|
var streamingResults = new double[data.Length];
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
streamingResults[i] = q.Update(new TValue(DateTime.UtcNow, data[i])).Value;
|
|
}
|
|
|
|
// Batch via spans
|
|
var spanOutput = new double[data.Length];
|
|
Quantile.Batch(data.AsSpan(), spanOutput.AsSpan(), period, quantileLevel);
|
|
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
Assert.Equal(streamingResults[i], spanOutput[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Quantile_KnownValues()
|
|
{
|
|
// {10, 20, 30, 40, 50} sorted, q=0.25 → rank = 0.25*4 = 1.0 → sorted[1] = 20
|
|
var q = new Quantile(5, 0.25);
|
|
q.Update(new TValue(DateTime.UtcNow, 10));
|
|
q.Update(new TValue(DateTime.UtcNow, 20));
|
|
q.Update(new TValue(DateTime.UtcNow, 30));
|
|
q.Update(new TValue(DateTime.UtcNow, 40));
|
|
var result = q.Update(new TValue(DateTime.UtcNow, 50));
|
|
|
|
Assert.Equal(20.0, result.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Quantile_BoundaryValues()
|
|
{
|
|
// q=0 → minimum, q=1 → maximum
|
|
var q0 = new Quantile(5, 0.0);
|
|
var q1 = new Quantile(5, 1.0);
|
|
|
|
double[] data = { 30, 10, 50, 20, 40 };
|
|
for (int i = 0; i < data.Length; i++)
|
|
{
|
|
var tv = new TValue(DateTime.UtcNow, data[i]);
|
|
q0.Update(tv);
|
|
q1.Update(tv);
|
|
}
|
|
|
|
Assert.Equal(10.0, q0.Last.Value);
|
|
Assert.Equal(50.0, q1.Last.Value);
|
|
}
|
|
}
|