mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-05 20:47: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
269 lines
8.1 KiB
C#
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");
|
|
}
|
|
}
|