mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-03 11:47:44 +00:00
15f4bb90f3
New indicators: - HWC (Holt-Winters Channel) — channels, 27 tests - VWMACD (Volume-Weighted MACD) — momentum, 38 tests - Squeeze Pro — oscillators, 69 tests - BW_MFI (Bill Williams MFI) — oscillators - DSTOCH (Double Stochastic) — oscillators - ATRSTOP (ATR Trailing Stop) — reversals - VSTOP (Volatility Stop) — reversals - Convexity (Beta Convexity) — statistics, 23 tests Integration: - Python bridge: Exports.cs, _bridge.py, wrapper modules - Documentation: _sidebar.md, _index.md pages, SPEC.md - All analyzer warnings fixed (MA0074, xUnit2013, S2699) Build: 0 warnings, 0 errors | Tests: 15,933 passed, 0 failed
232 lines
6.5 KiB
C#
232 lines
6.5 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public sealed class DstochValidationTests
|
|
{
|
|
// ── Self-consistency: streaming == batch ──
|
|
|
|
[Fact]
|
|
public void StreamingMatchesBatch()
|
|
{
|
|
const int period = 14;
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.3, seed: 42);
|
|
for (int i = 0; i < 100; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var batch = Dstoch.Batch(source, period);
|
|
|
|
var streaming = new Dstoch(period);
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
streaming.Update(source[i]);
|
|
Assert.Equal(batch[i].Value, streaming.Last.Value, 10);
|
|
}
|
|
}
|
|
|
|
// ── Span matches TBarSeries batch ──
|
|
|
|
[Fact]
|
|
public void SpanMatchesTBarSeries()
|
|
{
|
|
const int period = 10;
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 55);
|
|
for (int i = 0; i < 80; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var tbResult = Dstoch.Batch(source, period);
|
|
|
|
var spanOut = new double[source.Count];
|
|
Dstoch.Batch(source.HighValues, source.LowValues, source.CloseValues,
|
|
spanOut.AsSpan(), period);
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Assert.Equal(tbResult[i].Value, spanOut[i], 10);
|
|
}
|
|
}
|
|
|
|
// ── Determinism ──
|
|
|
|
[Fact]
|
|
public void Deterministic_AcrossRuns()
|
|
{
|
|
const int period = 10;
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 77);
|
|
for (int i = 0; i < 60; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var r1 = Dstoch.Batch(source, period);
|
|
var r2 = Dstoch.Batch(source, period);
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Assert.Equal(r1[i].Value, r2[i].Value, 15);
|
|
}
|
|
}
|
|
|
|
// ── Constant input ──
|
|
|
|
[Fact]
|
|
public void ConstantBars_OutputIsZero()
|
|
{
|
|
const int period = 5;
|
|
var bars = new TBarSeries();
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
bars.Add(new TBar(DateTime.UtcNow.AddDays(i), 50, 50, 50, 50, 100));
|
|
}
|
|
|
|
var result = Dstoch.Batch(bars, period);
|
|
for (int i = period; i < result.Count; i++)
|
|
{
|
|
Assert.Equal(0.0, result[i].Value, 10);
|
|
}
|
|
}
|
|
|
|
// ── Boundedness ──
|
|
|
|
[Fact]
|
|
public void Output_AlwaysBoundedZeroToHundred()
|
|
{
|
|
const int period = 14;
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.3, seed: 88);
|
|
for (int i = 0; i < 200; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var result = Dstoch.Batch(source, period);
|
|
|
|
for (int i = period; i < result.Count; i++)
|
|
{
|
|
Assert.InRange(result[i].Value, -0.01, 100.01);
|
|
}
|
|
}
|
|
|
|
// ── Different periods produce different results ──
|
|
|
|
[Fact]
|
|
public void DifferentPeriods_ProduceDifferentResults()
|
|
{
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.3, seed: 99);
|
|
for (int i = 0; i < 100; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var r5 = Dstoch.Batch(source, 5);
|
|
var r21 = Dstoch.Batch(source, 21);
|
|
|
|
bool anyDifferent = false;
|
|
for (int i = 25; i < source.Count; i++)
|
|
{
|
|
if (Math.Abs(r5[i].Value - r21[i].Value) > 1e-6)
|
|
{
|
|
anyDifferent = true;
|
|
break;
|
|
}
|
|
}
|
|
Assert.True(anyDifferent);
|
|
}
|
|
|
|
// ── Monotonic-up → high DSS ──
|
|
|
|
[Fact]
|
|
public void MonotonicUp_ConvergesHighDSS()
|
|
{
|
|
var d = new Dstoch(5);
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
double price = 100 + i;
|
|
d.Update(new TBar(DateTime.UtcNow.AddDays(i), price, price + 1, price - 1, price, 1000));
|
|
}
|
|
Assert.True(d.Last.Value > 50.0);
|
|
}
|
|
|
|
// ── Monotonic-down → low DSS ──
|
|
|
|
[Fact]
|
|
public void MonotonicDown_ConvergesLowDSS()
|
|
{
|
|
var d = new Dstoch(5);
|
|
for (int i = 0; i < 30; i++)
|
|
{
|
|
double price = 200 - i;
|
|
d.Update(new TBar(DateTime.UtcNow.AddDays(i), price, price + 1, price - 1, price, 1000));
|
|
}
|
|
Assert.True(d.Last.Value < 50.0);
|
|
}
|
|
|
|
// ── Reset+replay matches fresh run ──
|
|
|
|
[Fact]
|
|
public void ResetReplay_MatchesFreshRun()
|
|
{
|
|
const int period = 7;
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 111);
|
|
var bars = new List<TBar>();
|
|
for (int i = 0; i < 50; i++) { bars.Add(gbm.Next(isNew: true)); }
|
|
|
|
var d = new Dstoch(period);
|
|
foreach (var bar in bars) { d.Update(bar); }
|
|
double firstRun = d.Last.Value;
|
|
|
|
d.Reset();
|
|
foreach (var bar in bars) { d.Update(bar); }
|
|
Assert.Equal(firstRun, d.Last.Value, 12);
|
|
}
|
|
|
|
// ── Primed indicator matches manual feed ──
|
|
|
|
[Fact]
|
|
public void PrimedIndicator_MatchesManualFeed()
|
|
{
|
|
const int period = 10;
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 222);
|
|
for (int i = 0; i < 60; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var manual = new Dstoch(period);
|
|
for (int i = 0; i < source.Count; i++) { manual.Update(source[i]); }
|
|
|
|
var primed = new Dstoch(period);
|
|
primed.Prime(source);
|
|
|
|
Assert.Equal(manual.Last.Value, primed.Last.Value, 12);
|
|
}
|
|
|
|
// ── Calculate factory consistency ──
|
|
|
|
[Fact]
|
|
public void Calculate_MatchesBatch()
|
|
{
|
|
const int period = 10;
|
|
var source = new TBarSeries();
|
|
var gbm = new GBM(100.0, 0.05, 0.2, seed: 333);
|
|
for (int i = 0; i < 50; i++) { source.Add(gbm.Next(isNew: true)); }
|
|
|
|
var batch = Dstoch.Batch(source, period);
|
|
var (calcResult, _) = Dstoch.Calculate(source, period);
|
|
|
|
for (int i = 0; i < source.Count; i++)
|
|
{
|
|
Assert.Equal(batch[i].Value, calcResult[i].Value, 12);
|
|
}
|
|
}
|
|
|
|
// ── NaN propagation safety ──
|
|
|
|
[Fact]
|
|
public void BatchNaN_NoPropagation()
|
|
{
|
|
var d = new Dstoch(5);
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
d.Update(new TBar(DateTime.UtcNow.AddDays(i), 100 + i, 105 + i, 95 + i, 102 + i, 500));
|
|
}
|
|
|
|
// Feed a NaN bar
|
|
d.Update(new TBar(DateTime.UtcNow.AddDays(10), double.NaN, double.NaN, double.NaN, double.NaN, 0));
|
|
// Then valid data
|
|
d.Update(new TBar(DateTime.UtcNow.AddDays(11), 112, 117, 107, 114, 500));
|
|
Assert.True(double.IsFinite(d.Last.Value));
|
|
}
|
|
}
|