Files
QuanTAlib/lib/reversals/fractals/tests/Fractals.Validation.Tests.cs
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

269 lines
8.1 KiB
C#

// FRACTALS Validation Tests - Williams Fractals
// Cross-validated against Skender.Stock.Indicators GetFractal()
//
// Important alignment notes:
// - Skender reports fractal at the bar where the fractal occurs (bar[2] in our terms)
// - Our streaming indicator reports at the current bar (bar[0]) when detection happens
// - Therefore: our value at index i corresponds to Skender's value at index (i-2)
// - Skender naming: FractalBear = high point (resistance) = our UpFractal
// FractalBull = low point (support) = our DownFractal
using Skender.Stock.Indicators;
namespace QuanTAlib.Tests;
public sealed class FractalsValidationTests
{
private static TBarSeries CreateGbmBars(int count = 500, int seed = 42)
{
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.20, seed: seed);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
// -- Cross-library: Skender UpFractal (= Skender FractalBear) -----------------
[Fact]
public void StreamingMatchesSkender_UpFractal()
{
var _data = new ValidationTestData();
// Skender: FractalBear = high point = our UpFractal
var skenderResults = _data.SkenderQuotes
.GetFractal()
.ToList();
// QuanTAlib streaming
var f = new Fractals();
var ourUpValues = new double[_data.Bars.Count];
for (int i = 0; i < _data.Bars.Count; i++)
{
_ = f.Update(_data.Bars[i], isNew: true);
ourUpValues[i] = f.UpFractal;
}
// Compare with 2-bar offset: our value at i matches Skender at i-2
int matched = 0;
for (int i = 4; i < _data.Bars.Count; i++)
{
int skenderIdx = i - 2;
if (skenderIdx < 0 || skenderIdx >= skenderResults.Count)
{
continue;
}
decimal? skenderBear = skenderResults[skenderIdx].FractalBear;
bool skenderIsNull = !skenderBear.HasValue;
bool ourIsNaN = double.IsNaN(ourUpValues[i]);
if (skenderIsNull && ourIsNaN)
{
matched++;
continue;
}
if (!skenderIsNull && !ourIsNaN)
{
Assert.Equal((double)skenderBear!.Value, ourUpValues[i], precision: 6);
matched++;
}
}
Assert.True(matched > 0, "Should have matched at least one warm value");
_data.Dispose();
}
// -- Cross-library: Skender DownFractal (= Skender FractalBull) ---------------
[Fact]
public void StreamingMatchesSkender_DownFractal()
{
var _data = new ValidationTestData();
// Skender: FractalBull = low point = our DownFractal
var skenderResults = _data.SkenderQuotes
.GetFractal()
.ToList();
// QuanTAlib streaming
var f = new Fractals();
var ourDownValues = new double[_data.Bars.Count];
for (int i = 0; i < _data.Bars.Count; i++)
{
_ = f.Update(_data.Bars[i], isNew: true);
ourDownValues[i] = f.DownFractal;
}
// Compare with 2-bar offset: our value at i matches Skender at i-2
int matched = 0;
for (int i = 4; i < _data.Bars.Count; i++)
{
int skenderIdx = i - 2;
if (skenderIdx < 0 || skenderIdx >= skenderResults.Count)
{
continue;
}
decimal? skenderBull = skenderResults[skenderIdx].FractalBull;
bool skenderIsNull = !skenderBull.HasValue;
bool ourIsNaN = double.IsNaN(ourDownValues[i]);
if (skenderIsNull && ourIsNaN)
{
matched++;
continue;
}
if (!skenderIsNull && !ourIsNaN)
{
Assert.Equal((double)skenderBull!.Value, ourDownValues[i], precision: 6);
matched++;
}
}
Assert.True(matched > 0, "Should have matched at least one warm value");
_data.Dispose();
}
// -- Self-Consistency: Streaming == Batch --------------------------------------
[Fact]
public void StreamingMatchesBatch_UpFractal()
{
var bars = CreateGbmBars();
// Streaming
var streaming = new Fractals();
var streamUp = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamUp[i] = streaming.UpFractal;
}
// Batch
var batchResults = Fractals.Batch(bars);
for (int i = 4; i < bars.Count; i++)
{
if (double.IsNaN(streamUp[i]))
{
Assert.True(double.IsNaN(batchResults[i].Value), $"Mismatch at {i}: streaming=NaN, batch={batchResults[i].Value}");
}
else
{
Assert.Equal(streamUp[i], batchResults[i].Value, precision: 10);
}
}
}
// -- Self-Consistency: Streaming == Span ---------------------------------------
[Fact]
public void StreamingMatchesSpan_BothFractals()
{
var bars = CreateGbmBars();
// Streaming
var streaming = new Fractals();
var streamUp = new double[bars.Count];
var streamDown = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamUp[i] = streaming.UpFractal;
streamDown[i] = streaming.DownFractal;
}
// Span
var spanUp = new double[bars.Count];
var spanDown = new double[bars.Count];
Fractals.Batch(bars.HighValues, bars.LowValues, spanUp, spanDown);
for (int i = 4; i < bars.Count; i++)
{
if (double.IsNaN(streamUp[i]))
{
Assert.True(double.IsNaN(spanUp[i]));
}
else
{
Assert.Equal(streamUp[i], spanUp[i], precision: 10);
}
if (double.IsNaN(streamDown[i]))
{
Assert.True(double.IsNaN(spanDown[i]));
}
else
{
Assert.Equal(streamDown[i], spanDown[i], precision: 10);
}
}
}
// -- Determinism ---------------------------------------------------------------
[Fact]
public void SameInput_ProducesSameOutput()
{
var bars = CreateGbmBars(count: 200, seed: 123);
var f1 = new Fractals();
var f2 = new Fractals();
for (int i = 0; i < bars.Count; i++)
{
_ = f1.Update(bars[i], isNew: true);
_ = f2.Update(bars[i], isNew: true);
}
Assert.Equal(f1.UpFractal, f2.UpFractal);
Assert.Equal(f1.DownFractal, f2.DownFractal);
}
// -- Calculate Returns Valid Indicator -----------------------------------------
[Fact]
public void Calculate_ReturnsValidIndicatorAndResults()
{
var bars = CreateGbmBars(count: 100);
var (results, indicator) = Fractals.Calculate(bars);
Assert.NotNull(results);
Assert.Equal(bars.Count, results.Count);
Assert.True(indicator.IsHot);
}
// -- BatchDual Returns Both Fractals ------------------------------------------
[Fact]
public void BatchDual_ReturnsBothSeries()
{
var bars = CreateGbmBars(count: 100);
var (upSeries, downSeries) = Fractals.BatchDual(bars);
Assert.Equal(bars.Count, upSeries.Count);
Assert.Equal(bars.Count, downSeries.Count);
// At least some fractals should be detected in 100 bars
bool hasUp = false;
bool hasDown = false;
for (int i = 0; i < upSeries.Count; i++)
{
if (double.IsFinite(upSeries[i].Value))
{
hasUp = true;
}
if (double.IsFinite(downSeries[i].Value))
{
hasDown = true;
}
}
Assert.True(hasUp, "Should detect at least one up fractal in 100 bars");
Assert.True(hasDown, "Should detect at least one down fractal in 100 bars");
}
}