mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-09 06:27:45 +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
469 lines
13 KiB
C#
469 lines
13 KiB
C#
namespace QuanTAlib.Tests;
|
||
|
||
public class SpearmanTests
|
||
{
|
||
[Fact]
|
||
public void Constructor_PeriodOne_Throws()
|
||
{
|
||
Assert.Throws<ArgumentException>(() => new Spearman(1));
|
||
}
|
||
|
||
[Fact]
|
||
public void Constructor_PeriodZero_Throws()
|
||
{
|
||
Assert.Throws<ArgumentException>(() => new Spearman(0));
|
||
}
|
||
|
||
[Fact]
|
||
public void Constructor_ValidPeriod_SetsName()
|
||
{
|
||
var s = new Spearman(10);
|
||
Assert.Equal("Spearman(10)", s.Name);
|
||
}
|
||
|
||
[Fact]
|
||
public void SingleInput_Update_ThrowsNotSupported()
|
||
{
|
||
var s = new Spearman(5);
|
||
Assert.Throws<NotSupportedException>(() => s.Update(new TValue(DateTime.UtcNow, 1.0)));
|
||
}
|
||
|
||
[Fact]
|
||
public void SingleInput_UpdateTSeries_ThrowsNotSupported()
|
||
{
|
||
var s = new Spearman(5);
|
||
var ts = new TSeries();
|
||
Assert.Throws<NotSupportedException>(() => s.Update(ts));
|
||
}
|
||
|
||
[Fact]
|
||
public void Prime_ThrowsNotSupported()
|
||
{
|
||
var s = new Spearman(5);
|
||
Assert.Throws<NotSupportedException>(() => s.Prime(stackalloc double[] { 1, 2, 3 }));
|
||
}
|
||
|
||
[Fact]
|
||
public void PerfectConcordance_ReturnsOne()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
s.Update((double)i, (double)i, isNew: true);
|
||
}
|
||
Assert.Equal(1.0, s.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void PerfectDiscordance_ReturnsMinusOne()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
s.Update((double)i, 6.0 - i, isNew: true);
|
||
}
|
||
Assert.Equal(-1.0, s.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void KnownSequence_MatchesExpected()
|
||
{
|
||
// X = [1,2,3,4,5], Y = [1,3,2,5,4]
|
||
// Ranks X = [1,2,3,4,5], Ranks Y = [1,3,2,5,4]
|
||
// d = [0,-1,1,-1,1], d² = [0,1,1,1,1], Σd² = 4
|
||
// ρ = 1 - 6*4/(5*24) = 1 - 24/120 = 1 - 0.2 = 0.8
|
||
var s = new Spearman(5);
|
||
double[] x = [1, 2, 3, 4, 5];
|
||
double[] y = [1, 3, 2, 5, 4];
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s.Update(x[i], y[i], isNew: true);
|
||
}
|
||
|
||
Assert.Equal(0.8, s.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void Symmetry_RhoXY_EqualsRhoYX()
|
||
{
|
||
var s1 = new Spearman(5);
|
||
var s2 = new Spearman(5);
|
||
double[] x = [10, 20, 15, 30, 25];
|
||
double[] y = [5, 15, 10, 25, 20];
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s1.Update(x[i], y[i], isNew: true);
|
||
s2.Update(y[i], x[i], isNew: true);
|
||
}
|
||
|
||
Assert.Equal(s1.Last.Value, s2.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void Antisymmetry_RhoXNegY_EqualsNegRhoXY()
|
||
{
|
||
var s1 = new Spearman(5);
|
||
var s2 = new Spearman(5);
|
||
double[] x = [10, 20, 15, 30, 25];
|
||
double[] y = [5, 15, 10, 25, 20];
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s1.Update(x[i], y[i], isNew: true);
|
||
s2.Update(x[i], -y[i], isNew: true);
|
||
}
|
||
|
||
Assert.Equal(-s1.Last.Value, s2.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void ConstantSeries_ReturnsZero()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s.Update(42.0, (double)(i + 1), isNew: true);
|
||
}
|
||
Assert.Equal(0.0, s.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void BothConstant_ReturnsZero()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s.Update(42.0, 42.0, isNew: true);
|
||
}
|
||
Assert.Equal(0.0, s.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void TiedValues_HandledCorrectly()
|
||
{
|
||
// X = [1, 2, 2, 4, 5], Y = [5, 4, 3, 2, 1]
|
||
// Ranks X = [1, 2.5, 2.5, 4, 5] (ties → average rank)
|
||
// Ranks Y = [5, 4, 3, 2, 1]
|
||
// Pearson on these ranks → negative correlation
|
||
var s = new Spearman(5);
|
||
double[] x = [1, 2, 2, 4, 5];
|
||
double[] y = [5, 4, 3, 2, 1];
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s.Update(x[i], y[i], isNew: true);
|
||
}
|
||
|
||
// Should be close to -1 (strong negative monotonic relationship)
|
||
Assert.True(s.Last.Value < -0.9);
|
||
}
|
||
|
||
[Fact]
|
||
public void IsHot_RequiresAtLeastTwo()
|
||
{
|
||
var s = new Spearman(5);
|
||
Assert.False(s.IsHot);
|
||
|
||
s.Update(1.0, 2.0, isNew: true);
|
||
Assert.False(s.IsHot);
|
||
|
||
s.Update(2.0, 3.0, isNew: true);
|
||
Assert.True(s.IsHot);
|
||
}
|
||
|
||
[Fact]
|
||
public void SingleValue_ReturnsNaN()
|
||
{
|
||
var s = new Spearman(5);
|
||
s.Update(1.0, 2.0, isNew: true);
|
||
Assert.True(double.IsNaN(s.Last.Value));
|
||
}
|
||
|
||
[Fact]
|
||
public void IsNewFalse_CorrectsBars()
|
||
{
|
||
var s = new Spearman(5);
|
||
double[] x = [1, 2, 3, 4, 5];
|
||
double[] y = [2, 4, 6, 8, 10];
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s.Update(x[i], y[i], isNew: true);
|
||
}
|
||
|
||
double original = s.Last.Value;
|
||
|
||
// Correct with different value — break rank correlation
|
||
s.Update(100.0, 1.0, isNew: false);
|
||
double corrected = s.Last.Value;
|
||
Assert.NotEqual(original, corrected);
|
||
|
||
// Correct back to original
|
||
s.Update(x[4], y[4], isNew: false);
|
||
double restored = s.Last.Value;
|
||
Assert.Equal(original, restored, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void NaN_SubstitutesLastValid()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 1; i <= 4; i++)
|
||
{
|
||
s.Update((double)i, (double)i, isNew: true);
|
||
}
|
||
|
||
// Feed NaN — should use last valid value
|
||
s.Update(double.NaN, double.NaN, isNew: true);
|
||
Assert.True(double.IsFinite(s.Last.Value));
|
||
}
|
||
|
||
[Fact]
|
||
public void Infinity_SubstitutesLastValid()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 1; i <= 4; i++)
|
||
{
|
||
s.Update((double)i, (double)i, isNew: true);
|
||
}
|
||
|
||
s.Update(double.PositiveInfinity, double.NegativeInfinity, isNew: true);
|
||
Assert.True(double.IsFinite(s.Last.Value));
|
||
}
|
||
|
||
[Fact]
|
||
public void Reset_ClearsState()
|
||
{
|
||
var s = new Spearman(5);
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
s.Update((double)i, (double)i, isNew: true);
|
||
}
|
||
|
||
Assert.True(s.IsHot);
|
||
|
||
s.Reset();
|
||
Assert.False(s.IsHot);
|
||
Assert.Equal(default, s.Last);
|
||
}
|
||
|
||
[Fact]
|
||
public void SlidingWindow_DropOldValues()
|
||
{
|
||
var s = new Spearman(3);
|
||
|
||
// Fill window: X=[1,2,3], Y=[1,2,3] → ρ = 1.0
|
||
s.Update(1.0, 1.0, isNew: true);
|
||
s.Update(2.0, 2.0, isNew: true);
|
||
s.Update(3.0, 3.0, isNew: true);
|
||
Assert.Equal(1.0, s.Last.Value, 1e-10);
|
||
|
||
// Push to window: X=[2,3,100], Y=[2,3,-100] → mixed correlation
|
||
s.Update(100.0, -100.0, isNew: true);
|
||
// Window now [2,3,100] vs [2,3,-100]: ranks X=[1,2,3], Y=[2,3,1] → not perfect
|
||
Assert.True(s.Last.Value < 1.0);
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchTSeries_MatchesStreaming()
|
||
{
|
||
var gbmX = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||
var gbmY = new GBM(startPrice: 100, mu: 0.03, sigma: 0.15, seed: 99);
|
||
|
||
var seriesX = new TSeries();
|
||
var seriesY = new TSeries();
|
||
|
||
for (int i = 0; i < 50; i++)
|
||
{
|
||
var barX = gbmX.Next();
|
||
var barY = gbmY.Next();
|
||
seriesX.Add(new TValue(barX.Time, barX.Close));
|
||
seriesY.Add(new TValue(barY.Time, barY.Close));
|
||
}
|
||
|
||
TSeries batch = Spearman.Batch(seriesX, seriesY, 10);
|
||
|
||
var streaming = new Spearman(10);
|
||
for (int i = 0; i < 50; i++)
|
||
{
|
||
streaming.Update(seriesX[i], seriesY[i], isNew: true);
|
||
Assert.Equal(streaming.Last.Value, batch[i].Value, 1e-10);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchSpan_MatchesStreaming()
|
||
{
|
||
var gbmX = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||
var gbmY = new GBM(startPrice: 100, mu: 0.03, sigma: 0.15, seed: 99);
|
||
|
||
double[] xValues = new double[50];
|
||
double[] yValues = new double[50];
|
||
|
||
for (int i = 0; i < 50; i++)
|
||
{
|
||
xValues[i] = gbmX.Next().Close;
|
||
yValues[i] = gbmY.Next().Close;
|
||
}
|
||
|
||
double[] output = new double[50];
|
||
Spearman.Batch(xValues.AsSpan(), yValues.AsSpan(), output.AsSpan(), 10);
|
||
|
||
var streaming = new Spearman(10);
|
||
for (int i = 0; i < 50; i++)
|
||
{
|
||
streaming.Update(xValues[i], yValues[i], isNew: true);
|
||
Assert.Equal(streaming.Last.Value, output[i], 1e-10);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchSpan_MismatchedLengths_Throws()
|
||
{
|
||
double[] x = new double[10];
|
||
double[] y = new double[5];
|
||
double[] output = new double[10];
|
||
|
||
Assert.Throws<ArgumentException>(() =>
|
||
Spearman.Batch(x.AsSpan(), y.AsSpan(), output.AsSpan(), 3));
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchSpan_MismatchedOutput_Throws()
|
||
{
|
||
double[] x = new double[10];
|
||
double[] y = new double[10];
|
||
double[] output = new double[5];
|
||
|
||
Assert.Throws<ArgumentException>(() =>
|
||
Spearman.Batch(x.AsSpan(), y.AsSpan(), output.AsSpan(), 3));
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchSpan_InvalidPeriod_Throws()
|
||
{
|
||
double[] x = new double[10];
|
||
double[] y = new double[10];
|
||
double[] output = new double[10];
|
||
|
||
Assert.Throws<ArgumentException>(() =>
|
||
Spearman.Batch(x.AsSpan(), y.AsSpan(), output.AsSpan(), 1));
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchTSeries_MismatchedLengths_Throws()
|
||
{
|
||
var sx = new TSeries();
|
||
var sy = new TSeries();
|
||
sx.Add(new TValue(DateTime.UtcNow, 1.0));
|
||
|
||
Assert.Throws<ArgumentException>(() => Spearman.Batch(sx, sy, 3));
|
||
}
|
||
|
||
[Fact]
|
||
public void Calculate_ReturnsTupleWithResults()
|
||
{
|
||
var gbmX = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||
var gbmY = new GBM(startPrice: 100, mu: 0.03, sigma: 0.15, seed: 99);
|
||
|
||
var seriesX = new TSeries();
|
||
var seriesY = new TSeries();
|
||
|
||
for (int i = 0; i < 30; i++)
|
||
{
|
||
var barX = gbmX.Next();
|
||
var barY = gbmY.Next();
|
||
seriesX.Add(new TValue(barX.Time, barX.Close));
|
||
seriesY.Add(new TValue(barY.Time, barY.Close));
|
||
}
|
||
|
||
var (results, indicator) = Spearman.Calculate(seriesX, seriesY, 10);
|
||
Assert.Equal(30, results.Count);
|
||
Assert.NotNull(indicator);
|
||
}
|
||
|
||
[Fact]
|
||
public void OutputBounded_BetweenMinusOneAndPlusOne()
|
||
{
|
||
var gbmX = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||
var gbmY = new GBM(startPrice: 100, mu: 0.03, sigma: 0.15, seed: 99);
|
||
|
||
var s = new Spearman(10);
|
||
for (int i = 0; i < 100; i++)
|
||
{
|
||
var barX = gbmX.Next();
|
||
var barY = gbmY.Next();
|
||
s.Update(barX.Close, barY.Close, isNew: true);
|
||
|
||
if (double.IsFinite(s.Last.Value))
|
||
{
|
||
Assert.InRange(s.Last.Value, -1.0, 1.0);
|
||
}
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void MonotonicTransform_PreservesCorrelation()
|
||
{
|
||
// Spearman measures monotonic association — applying a strictly increasing
|
||
// transform to either series should not change ρ
|
||
var s1 = new Spearman(5);
|
||
var s2 = new Spearman(5);
|
||
double[] x = [1, 2, 3, 4, 5];
|
||
double[] y = [10, 20, 15, 30, 25];
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
s1.Update(x[i], y[i], isNew: true);
|
||
// Apply f(x) = x³ (strictly increasing)
|
||
s2.Update(x[i] * x[i] * x[i], y[i], isNew: true);
|
||
}
|
||
|
||
Assert.Equal(s1.Last.Value, s2.Last.Value, 1e-10);
|
||
}
|
||
|
||
[Fact]
|
||
public void EventChaining_Fires()
|
||
{
|
||
var s = new Spearman(3);
|
||
int eventCount = 0;
|
||
s.Pub += (object? _, in TValueEventArgs _) => eventCount++;
|
||
|
||
for (int i = 1; i <= 5; i++)
|
||
{
|
||
s.Update((double)i, (double)i, isNew: true);
|
||
}
|
||
|
||
Assert.Equal(5, eventCount);
|
||
}
|
||
|
||
[Fact]
|
||
public void BatchSpan_NaN_HandledSafely()
|
||
{
|
||
double[] x = [1, 2, double.NaN, 4, 5];
|
||
double[] y = [5, 4, 3, 2, 1];
|
||
double[] output = new double[5];
|
||
|
||
Spearman.Batch(x.AsSpan(), y.AsSpan(), output.AsSpan(), 3);
|
||
|
||
for (int i = 0; i < 5; i++)
|
||
{
|
||
Assert.True(double.IsFinite(output[i]) || double.IsNaN(output[i]));
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void LargePeriod_NoStackOverflow()
|
||
{
|
||
// Test with period > StackallocThreshold (256)
|
||
var s = new Spearman(300);
|
||
for (int i = 1; i <= 300; i++)
|
||
{
|
||
s.Update((double)i, (double)i, isNew: true);
|
||
}
|
||
Assert.Equal(1.0, s.Last.Value, 1e-10);
|
||
}
|
||
}
|