Files
QuanTAlib/lib/reversals/ckstop/Ckstop.Validation.Tests.cs
T
Miha Kralj dfeb23bf3d Add Savitzky-Golay Moving Average (SGMA) Indicator Implementation
- Implemented SgmaIndicator class in C# with properties for Period, Degree, and Source.
- Added unit tests for SgmaIndicator covering constructor defaults, initialization, and various update scenarios.
- Created a new Quantower adapter for the SGMA indicator, including input parameters and line series setup.
- Removed legacy SGMA implementation and tests to streamline the codebase.
- Updated project files to include new indicator and tests in the build process.
- Generated a missing indicators report and outlined a plan for oscillator documentation rewrite.
2026-02-13 21:44:45 -08:00

176 lines
6.2 KiB
C#

// CKSTOP Validation Tests - Chande Kroll Stop
// No external library implements CKSTOP, so validation uses self-consistency checks.
namespace QuanTAlib.Tests;
public sealed class CkstopValidationTests
{
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));
}
// ── Self-Consistency: Streaming == Batch ──────────────────────────────
[Fact]
public void StreamingMatchesBatch_StopLong()
{
var bars = CreateGbmBars();
int atrPeriod = 10;
double multiplier = 1.0;
int stopPeriod = 9;
// Streaming
var streaming = new Ckstop(atrPeriod, multiplier, stopPeriod);
var streamStopLong = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamStopLong[i] = streaming.StopLong;
}
// Batch
var batchResults = Ckstop.Batch(bars, atrPeriod, multiplier, stopPeriod);
int warmup = atrPeriod + stopPeriod;
for (int i = warmup; i < bars.Count; i++)
{
Assert.Equal(streamStopLong[i], batchResults[i].Value, precision: 10);
}
}
// ── Self-Consistency: Streaming == Span ───────────────────────────────
[Fact]
public void StreamingMatchesSpan_StopLong()
{
var bars = CreateGbmBars();
int atrPeriod = 10;
double multiplier = 1.0;
int stopPeriod = 9;
// Streaming
var streaming = new Ckstop(atrPeriod, multiplier, stopPeriod);
var streamStopLong = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
_ = streaming.Update(bars[i], isNew: true);
streamStopLong[i] = streaming.StopLong;
}
// Span
var spanOutput = new double[bars.Count];
Ckstop.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, bars.CloseValues,
spanOutput, atrPeriod, multiplier, stopPeriod);
int warmup = atrPeriod + stopPeriod;
for (int i = warmup; i < bars.Count; i++)
{
Assert.Equal(streamStopLong[i], spanOutput[i], precision: 10);
}
}
// ── Directional Correctness ──────────────────────────────────────────
[Fact]
public void HigherMultiplier_NarrowsStopGap()
{
var bars = CreateGbmBars(count: 100);
var narrow = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9);
var wide = new Ckstop(atrPeriod: 10, multiplier: 3.0, stopPeriod: 9);
for (int i = 0; i < bars.Count; i++)
{
_ = narrow.Update(bars[i], isNew: true);
_ = wide.Update(bars[i], isNew: true);
}
// Higher multiplier: StopLong = LowestLow + q*ATR → goes UP
// StopShort = HighestHigh - q*ATR → goes DOWN
// The gap (StopShort - StopLong) narrows with higher multiplier.
double narrowGap = narrow.StopShort - narrow.StopLong;
double wideGap = wide.StopShort - wide.StopLong;
Assert.True(wideGap < narrowGap,
$"Wide gap ({wideGap}) should be < narrow gap ({narrowGap})");
}
// ── Determinism ──────────────────────────────────────────────────────
[Fact]
public void SameInput_ProducesSameOutput()
{
var bars = CreateGbmBars(count: 200, seed: 123);
var ck1 = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9);
var ck2 = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9);
for (int i = 0; i < bars.Count; i++)
{
_ = ck1.Update(bars[i], isNew: true);
_ = ck2.Update(bars[i], isNew: true);
}
Assert.Equal(ck1.StopLong, ck2.StopLong);
Assert.Equal(ck1.StopShort, ck2.StopShort);
}
// ── StopLong and StopShort Finite After Warmup ───────────────────────
[Fact]
public void AfterWarmup_BothStopsAreFinite()
{
var bars = CreateGbmBars(count: 100);
var ck = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9);
for (int i = 0; i < bars.Count; i++)
{
_ = ck.Update(bars[i], isNew: true);
if (ck.IsHot)
{
Assert.True(double.IsFinite(ck.StopLong), $"StopLong should be finite at bar {i}");
Assert.True(double.IsFinite(ck.StopShort), $"StopShort should be finite at bar {i}");
}
}
}
// ── Different Seeds Produce Different Results ─────────────────────────
[Fact]
public void DifferentSeeds_ProduceDifferentStops()
{
var bars1 = CreateGbmBars(count: 100, seed: 42);
var bars2 = CreateGbmBars(count: 100, seed: 99);
var ck1 = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9);
var ck2 = new Ckstop(atrPeriod: 10, multiplier: 1.0, stopPeriod: 9);
for (int i = 0; i < 100; i++)
{
_ = ck1.Update(bars1[i], isNew: true);
_ = ck2.Update(bars2[i], isNew: true);
}
// Very unlikely to be equal with different random data
Assert.NotEqual(ck1.StopLong, ck2.StopLong);
}
// ── Calculate Returns Valid Indicator ─────────────────────────────────
[Fact]
public void Calculate_ReturnsValidIndicatorAndResults()
{
var bars = CreateGbmBars(count: 100);
var (results, indicator) = Ckstop.Calculate(bars);
Assert.NotNull(results);
Assert.Equal(bars.Count, results.Count);
Assert.True(indicator.IsHot);
Assert.True(double.IsFinite(indicator.StopLong));
Assert.True(double.IsFinite(indicator.StopShort));
}
}