Files
QuanTAlib/lib/channels/jbands/Jbands.Validation.Tests.cs
T

260 lines
8.7 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Jbands against JMA internal bands.
/// Since Jbands exposes JMA's internal envelope bands, we validate:
/// 1. Middle band matches standalone JMA exactly
/// 2. All four API modes produce consistent results
/// 3. Band behavior matches JMA specification
/// </summary>
public class JbandsValidationTests
{
private const double Tolerance = 1e-10;
[Fact]
public void Jbands_MiddleBand_MatchesJma_Period7()
{
ValidateMiddleBandMatchesJma(7, 0, 42);
}
[Fact]
public void Jbands_MiddleBand_MatchesJma_Period14()
{
ValidateMiddleBandMatchesJma(14, 0, 123);
}
[Fact]
public void Jbands_MiddleBand_MatchesJma_Period20()
{
ValidateMiddleBandMatchesJma(20, 0, 456);
}
[Fact]
public void Jbands_MiddleBand_MatchesJma_WithPhase()
{
ValidateMiddleBandMatchesJma(14, 50, 789);
ValidateMiddleBandMatchesJma(14, -50, 321);
ValidateMiddleBandMatchesJma(14, 100, 654);
ValidateMiddleBandMatchesJma(14, -100, 987);
}
private static void ValidateMiddleBandMatchesJma(int period, int phase, int seed)
{
var jbands = new Jbands(period, phase);
var jma = new Jma(period, phase);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: seed);
for (int i = 0; i < 500; i++)
{
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jbands.Update(tv, isNew: true);
jma.Update(tv, isNew: true);
Assert.Equal(jma.Last.Value, jbands.Last.Value, Tolerance);
}
}
[Fact]
public void Jbands_StreamingVsBatch_Match()
{
var jStream = new Jbands(14, 0);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0);
// Compare last 100 values
for (int i = series.Count - 100; i < series.Count; i++)
{
// Rebuild streaming to get value at index i
var jCheck = new Jbands(14, 0);
for (int j = 0; j <= i; j++)
{
jCheck.Update(new TValue(new DateTime(series.Times[j], DateTimeKind.Utc), series.Values[j]), isNew: true);
}
Assert.Equal(jCheck.Last.Value, midBatch.Values[i], Tolerance);
Assert.Equal(jCheck.Upper.Value, upBatch.Values[i], Tolerance);
Assert.Equal(jCheck.Lower.Value, loBatch.Values[i], Tolerance);
}
}
[Fact]
public void Jbands_StreamingVsSpan_Match()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 777);
double[] source = new double[200];
for (int i = 0; i < source.Length; i++)
{
source[i] = gbm.Next().Close;
}
double[] middle = new double[200];
double[] upper = new double[200];
double[] lower = new double[200];
Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14);
var jStream = new Jbands(14);
for (int i = 0; i < source.Length; i++)
{
jStream.Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
Assert.Equal(jStream.Last.Value, middle[i], Tolerance);
Assert.Equal(jStream.Upper.Value, upper[i], Tolerance);
Assert.Equal(jStream.Lower.Value, lower[i], Tolerance);
}
}
[Fact]
public void Jbands_AllFourModes_Consistent()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.15, seed: 555);
var series = new TSeries();
double[] rawValues = new double[150];
for (int i = 0; i < 150; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
rawValues[i] = bar.Close;
}
// Mode 1: Streaming
var jStream = new Jbands(14, 25);
for (int i = 0; i < rawValues.Length; i++)
{
jStream.Update(new TValue(DateTime.UtcNow, rawValues[i]), isNew: true);
}
// Mode 2: Batch (TSeries)
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 25);
// Mode 3: Span Calculate
double[] middleSpan = new double[150];
double[] upperSpan = new double[150];
double[] lowerSpan = new double[150];
Jbands.Batch(rawValues.AsSpan(), middleSpan.AsSpan(), upperSpan.AsSpan(), lowerSpan.AsSpan(), 14, 25);
// Mode 4: Event-based
var jEvent = new Jbands(14, 25);
double lastEventMid = 0, lastEventUp = 0, lastEventLo = 0;
jEvent.Pub += (object? sender, in TValueEventArgs args) =>
{
lastEventMid = args.Value.Value;
};
for (int i = 0; i < rawValues.Length; i++)
{
jEvent.Update(new TValue(DateTime.UtcNow, rawValues[i]), isNew: true);
}
lastEventUp = jEvent.Upper.Value;
lastEventLo = jEvent.Lower.Value;
// All modes should match
Assert.Equal(jStream.Last.Value, midBatch.Last.Value, Tolerance);
Assert.Equal(jStream.Upper.Value, upBatch.Last.Value, Tolerance);
Assert.Equal(jStream.Lower.Value, loBatch.Last.Value, Tolerance);
Assert.Equal(jStream.Last.Value, middleSpan[^1], Tolerance);
Assert.Equal(jStream.Upper.Value, upperSpan[^1], Tolerance);
Assert.Equal(jStream.Lower.Value, lowerSpan[^1], Tolerance);
Assert.Equal(jStream.Last.Value, lastEventMid, Tolerance);
Assert.Equal(jStream.Upper.Value, lastEventUp, Tolerance);
Assert.Equal(jStream.Lower.Value, lastEventLo, Tolerance);
}
[Fact]
public void Jbands_BandBehavior_SnapAndDecay()
{
var j = new Jbands(14);
// Start at baseline
for (int i = 0; i < 50; i++)
{
j.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
}
// Spike up - upper should snap instantly
j.Update(new TValue(DateTime.UtcNow, 110.0), isNew: true);
Assert.Equal(110.0, j.Upper.Value, Tolerance);
Assert.True(j.Lower.Value < 110.0); // Lower should NOT snap up
// Return to baseline - upper should decay gradually
double prevUpper = j.Upper.Value;
for (int i = 0; i < 30; i++)
{
j.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
Assert.True(j.Upper.Value <= prevUpper); // Monotonically decreasing
prevUpper = j.Upper.Value;
}
// Spike down - lower should snap instantly
j.Update(new TValue(DateTime.UtcNow, 90.0), isNew: true);
Assert.Equal(90.0, j.Lower.Value, Tolerance);
Assert.True(j.Upper.Value > 90.0); // Upper should NOT snap down
// Return to baseline - lower should decay gradually
double prevLower = j.Lower.Value;
for (int i = 0; i < 30; i++)
{
j.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
Assert.True(j.Lower.Value >= prevLower); // Monotonically increasing
prevLower = j.Lower.Value;
}
}
[Fact]
public void Jbands_Warmup_ConsistentAcrossModes()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 333);
var series = new TSeries();
double[] rawValues = new double[50];
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
rawValues[i] = bar.Close;
}
// Streaming warmup
var jStream = new Jbands(14);
for (int i = 0; i < rawValues.Length; i++)
{
jStream.Update(new TValue(DateTime.UtcNow, rawValues[i]), isNew: true);
}
int warmupPeriod = jStream.WarmupPeriod;
// Span mode warmup values
double[] middle = new double[50];
double[] upper = new double[50];
double[] lower = new double[50];
Jbands.Batch(rawValues.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14);
// After warmup, values should be stable and match
for (int i = warmupPeriod; i < rawValues.Length; i++)
{
var jCheck = new Jbands(14);
for (int j = 0; j <= i; j++)
{
jCheck.Update(new TValue(DateTime.UtcNow, rawValues[j]), isNew: true);
}
Assert.Equal(jCheck.Last.Value, middle[i], Tolerance);
Assert.Equal(jCheck.Upper.Value, upper[i], Tolerance);
Assert.Equal(jCheck.Lower.Value, lower[i], Tolerance);
}
}
}