Files
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

184 lines
5.4 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for IQR — self-consistency and mathematical properties.
/// No external library implements rolling IQR with linear interpolation,
/// so validation is based on known mathematical properties.
/// </summary>
public class IqrValidationTests
{
[Fact]
public void ConstantSeries_IqrIsZero()
{
var iqr = new Iqr(20);
for (int i = 0; i < 50; i++)
{
iqr.Update(new TValue(DateTime.UtcNow, 100.0));
}
Assert.Equal(0.0, iqr.Last.Value, 10);
}
[Fact]
public void LinearSequence_KnownIqr()
{
// Window of {1,2,3,...,20} → sorted [1..20]
// Q1: rank = 0.25*19 = 4.75 → 5 + 0.75*(6-5) = 5.75
// Q3: rank = 0.75*19 = 14.25 → 15 + 0.25*(16-15) = 15.25
// IQR = 15.25 - 5.75 = 9.5
var iqr = new Iqr(20);
for (int i = 1; i <= 20; i++)
{
iqr.Update(new TValue(DateTime.UtcNow, i));
}
Assert.Equal(9.5, iqr.Last.Value, 10);
}
[Fact]
public void SymmetricDistribution_IqrSymmetric()
{
// Values: {-5,-4,-3,-2,-1,0,1,2,3,4,5} → sorted [-5..5], n=11
// Q1: rank = 0.25*10 = 2.5 → -3 + 0.5*(-2-(-3)) = -2.5
// Q3: rank = 0.75*10 = 7.5 → 3 + 0.5*(4-3) = 2.5 (wait, index 7=2, index 8=3)
// Actually: sorted = [-5,-4,-3,-2,-1,0,1,2,3,4,5]
// index: 0 1 2 3 4 5 6 7 8 9 10
// Q1: rank=2.5 → sorted[2] + 0.5*(sorted[3]-sorted[2]) = -3 + 0.5*1 = -2.5
// Q3: rank=7.5 → sorted[7] + 0.5*(sorted[8]-sorted[7]) = 2 + 0.5*1 = 2.5
// IQR = 2.5 - (-2.5) = 5.0
var iqr = new Iqr(11);
for (int i = -5; i <= 5; i++)
{
iqr.Update(new TValue(DateTime.UtcNow, i));
}
Assert.Equal(5.0, iqr.Last.Value, 10);
}
[Fact]
public void Deterministic_SameInputSameOutput()
{
int period = 10;
var iqr1 = new Iqr(period);
var iqr2 = new Iqr(period);
var rng1 = new GBM(seed: 42);
var rng2 = new GBM(seed: 42);
for (int i = 0; i < 50; i++)
{
var bar1 = rng1.Next();
var bar2 = rng2.Next();
iqr1.Update(new TValue(bar1.Time, bar1.Close));
iqr2.Update(new TValue(bar2.Time, bar2.Close));
}
Assert.Equal(iqr1.Last.Value, iqr2.Last.Value, 1e-10);
}
[Fact]
public void BatchVsStreaming_Match()
{
int period = 10;
int bars = 100;
var rng = new GBM();
var source = new TSeries();
for (int i = 0; i < bars; i++)
{
var bar = rng.Next();
source.Add(new TValue(bar.Time, bar.Close));
}
// Streaming
var streaming = new Iqr(period);
double lastStreaming = 0;
for (int i = 0; i < bars; i++)
{
streaming.Update(source[i]);
lastStreaming = streaming.Last.Value;
}
// Batch
var batchSeries = Iqr.Batch(source, period);
Assert.Equal(lastStreaming, batchSeries[bars - 1].Value, 1e-10);
}
[Fact]
public void SpanVsStreaming_Match()
{
int period = 10;
int bars = 100;
var rng = new GBM();
var source = new TSeries();
for (int i = 0; i < bars; i++)
{
var bar = rng.Next();
source.Add(new TValue(bar.Time, bar.Close));
}
// Streaming
var streaming = new Iqr(period);
var streamResults = new double[bars];
for (int i = 0; i < bars; i++)
{
streaming.Update(source[i]);
streamResults[i] = streaming.Last.Value;
}
// Span
var spanOutput = new double[bars];
Iqr.Batch(source.Values, spanOutput.AsSpan(), period);
for (int i = period - 1; i < bars; i++)
{
Assert.Equal(streamResults[i], spanOutput[i], 1e-10);
}
}
[Fact]
public void CalculateBridge_ReturnsIndicatorAndResults()
{
int period = 10;
var rng = new GBM();
var source = new TSeries();
for (int i = 0; i < 50; i++)
{
var bar = rng.Next();
source.Add(new TValue(bar.Time, bar.Close));
}
var (results, indicator) = Iqr.Calculate(source, period);
Assert.Equal(50, results.Count);
Assert.True(indicator.IsHot);
}
[Fact]
public void IqrNonNegative_ForAllInputs()
{
var iqr = new Iqr(20);
var rng = new GBM();
for (int i = 0; i < 200; i++)
{
var bar = rng.Next();
iqr.Update(new TValue(bar.Time, bar.Close));
Assert.True(iqr.Last.Value >= 0.0, $"IQR negative at bar {i}");
}
}
[Fact]
public void OutlierResistance_IqrLessThanRange()
{
// IQR should always be <= full range for any window
var iqr = new Iqr(10);
var values = new double[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 100 };
double min = double.MaxValue, max = double.MinValue;
for (int i = 0; i < values.Length; i++)
{
iqr.Update(new TValue(DateTime.UtcNow, values[i]));
if (values[i] < min) { min = values[i]; }
if (values[i] > max) { max = values[i]; }
}
double range = max - min;
Assert.True(iqr.Last.Value <= range, $"IQR ({iqr.Last.Value}) > range ({range})");
}
}