Files
QuanTAlib/lib/reversals/pivot/Pivot.Validation.Tests.cs
T
2026-02-26 22:02:52 -08:00

283 lines
9.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
// PIVOT Validation Tests - Classic Pivot Points (Floor Trader Pivots)
// Self-consistency validation across all API modes.
//
// Note: Skender.Stock.Indicators ToPivotPoints() uses calendar-window periods
// (Day/Week/Month) which is conceptually different from our bar-to-bar implementation.
// Direct cross-validation is not applicable. TA-Lib, Tulip, and Ooples do not
// implement floor trader pivot points either.
// Validation focuses on mathematical correctness and mode consistency.
namespace QuanTAlib.Tests;
public sealed class PivotValidationTests
{
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));
}
// -- Mathematical Correctness -------------------------------------------------
[Fact]
public void MathCorrectness_PP_EqualsHLC_Over3()
{
var bars = CreateGbmBars(count: 100);
var p = new Pivot();
for (int i = 0; i < bars.Count; i++)
{
_ = p.Update(bars[i], isNew: true);
if (i >= 1) // Need previous bar
{
double prevH = bars[i - 1].High;
double prevL = bars[i - 1].Low;
double prevC = bars[i - 1].Close;
double expectedPP = (prevH + prevL + prevC) / 3.0;
Assert.Equal(expectedPP, p.PP, precision: 10);
}
}
}
[Fact]
public void MathCorrectness_AllLevels_MatchFormula()
{
var bars = CreateGbmBars(count: 100);
var p = new Pivot();
for (int i = 0; i < bars.Count; i++)
{
_ = p.Update(bars[i], isNew: true);
if (i >= 1)
{
double pH = bars[i - 1].High;
double pL = bars[i - 1].Low;
double pC = bars[i - 1].Close;
double pp = (pH + pL + pC) / 3.0;
double range = pH - pL;
Assert.Equal(pp, p.PP, precision: 10);
Assert.Equal(2.0 * pp - pL, p.R1, precision: 10);
Assert.Equal(2.0 * pp - pH, p.S1, precision: 10);
Assert.Equal(pp + range, p.R2, precision: 10);
Assert.Equal(pp - range, p.S2, precision: 10);
Assert.Equal(pH + 2.0 * (pp - pL), p.R3, precision: 10);
Assert.Equal(pL - 2.0 * (pH - pp), p.S3, precision: 10);
}
}
}
// -- Self-Consistency: Streaming == Batch --------------------------------------
[Fact]
public void StreamingMatchesBatch_PP()
{
var bars = CreateGbmBars();
// Streaming
var streaming = new Pivot();
var streamPP = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamPP[i] = streaming.PP;
}
// Batch
var batchResults = Pivot.Batch(bars);
for (int i = 1; i < bars.Count; i++)
{
if (double.IsNaN(streamPP[i]))
{
Assert.True(double.IsNaN(batchResults[i].Value),
$"Mismatch at {i}: streaming=NaN, batch={batchResults[i].Value}");
}
else
{
Assert.Equal(streamPP[i], batchResults[i].Value, precision: 10);
}
}
}
// -- Self-Consistency: Streaming == Span ---------------------------------------
[Fact]
public void StreamingMatchesSpan_PP()
{
var bars = CreateGbmBars();
// Streaming
var streaming = new Pivot();
var streamPP = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamPP[i] = streaming.PP;
}
// Span
var spanPP = new double[bars.Count];
Pivot.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, spanPP);
for (int i = 1; i < bars.Count; i++)
{
if (double.IsNaN(streamPP[i]))
{
Assert.True(double.IsNaN(spanPP[i]));
}
else
{
Assert.Equal(streamPP[i], spanPP[i], precision: 10);
}
}
}
// -- Self-Consistency: Streaming == BatchAll (all 7 levels) --------------------
[Fact]
public void StreamingMatchesBatchAll_AllLevels()
{
var bars = CreateGbmBars(count: 300);
// Streaming
var streaming = new Pivot();
var sPP = new double[bars.Count];
var sR1 = new double[bars.Count];
var sS1 = new double[bars.Count];
var sR2 = new double[bars.Count];
var sS2 = new double[bars.Count];
var sR3 = new double[bars.Count];
var sS3 = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
sPP[i] = streaming.PP;
sR1[i] = streaming.R1;
sS1[i] = streaming.S1;
sR2[i] = streaming.R2;
sS2[i] = streaming.S2;
sR3[i] = streaming.R3;
sS3[i] = streaming.S3;
}
// BatchAll
var bPP = new double[bars.Count];
var bR1 = new double[bars.Count];
var bS1 = new double[bars.Count];
var bR2 = new double[bars.Count];
var bS2 = new double[bars.Count];
var bR3 = new double[bars.Count];
var bS3 = new double[bars.Count];
Pivot.BatchAll(bars.HighValues, bars.LowValues, bars.CloseValues,
bPP, bR1, bS1, bR2, bS2, bR3, bS3);
for (int i = 1; i < bars.Count; i++)
{
if (double.IsNaN(sPP[i]))
{
Assert.True(double.IsNaN(bPP[i]));
continue;
}
Assert.Equal(sPP[i], bPP[i], precision: 10);
Assert.Equal(sR1[i], bR1[i], precision: 10);
Assert.Equal(sS1[i], bS1[i], precision: 10);
Assert.Equal(sR2[i], bR2[i], precision: 10);
Assert.Equal(sS2[i], bS2[i], precision: 10);
Assert.Equal(sR3[i], bR3[i], precision: 10);
Assert.Equal(sS3[i], bS3[i], precision: 10);
}
}
// -- Determinism ---------------------------------------------------------------
[Fact]
public void SameInput_ProducesSameOutput()
{
var bars = CreateGbmBars(count: 200, seed: 123);
var p1 = new Pivot();
var p2 = new Pivot();
for (int i = 0; i < bars.Count; i++)
{
_ = p1.Update(bars[i], isNew: true);
_ = p2.Update(bars[i], isNew: true);
}
Assert.Equal(p1.PP, p2.PP);
Assert.Equal(p1.R1, p2.R1);
Assert.Equal(p1.S1, p2.S1);
Assert.Equal(p1.R2, p2.R2);
Assert.Equal(p1.S2, p2.S2);
Assert.Equal(p1.R3, p2.R3);
Assert.Equal(p1.S3, p2.S3);
}
// -- Calculate Returns Valid Indicator -----------------------------------------
[Fact]
public void Calculate_ReturnsValidIndicatorAndResults()
{
var bars = CreateGbmBars(count: 100);
var (results, indicator) = Pivot.Calculate(bars);
Assert.NotNull(results);
Assert.Equal(bars.Count, results.Count);
Assert.True(indicator.IsHot);
}
// -- Level Ordering Invariant --------------------------------------------------
[Fact]
public void AllBars_LevelsOrdered_S3_S2_S1_PP_R1_R2_R3()
{
var bars = CreateGbmBars(count: 200);
var p = new Pivot();
for (int i = 0; i < bars.Count; i++)
{
_ = p.Update(bars[i], isNew: true);
if (p.IsHot)
{
Assert.True(p.S3 <= p.S2, $"S3 > S2 at bar {i}");
Assert.True(p.S2 <= p.S1, $"S2 > S1 at bar {i}");
Assert.True(p.S1 <= p.PP, $"S1 > PP at bar {i}");
Assert.True(p.PP <= p.R1, $"PP > R1 at bar {i}");
Assert.True(p.R1 <= p.R2, $"R1 > R2 at bar {i}");
Assert.True(p.R2 <= p.R3, $"R2 > R3 at bar {i}");
}
}
}
[Fact(Skip = "Ooples pivot indicators group by calendar day — 500×1-min bars yields ~3 daily pivots. Requires daily OHLCV input; not comparable with intraday GBM data.")]
public void Pivot_MatchesOoples_Structural()
{
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.15, seed: 42);
var bars = gbm.Fetch(500, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ooplesData = bars.Select(b => new TickerData
{
Date = new DateTime(b.Time, DateTimeKind.Utc),
Open = b.Open,
High = b.High,
Low = b.Low,
Close = b.Close,
Volume = b.Volume
}).ToList();
var result = new StockData(ooplesData).CalculateStandardPivotPoints();
var values = result.OutputValues.Values.First();
int finiteCount = values.Count(v => double.IsFinite(v));
Assert.True(finiteCount > 100, $"Expected >100 finite values, got {finiteCount}");
}
}