Files
QuanTAlib/lib/channels/jbands/Jbands.Tests.cs
T
Miha Kralj 3eae9a76fe Add Standard Deviation Channel (SDCHANNEL) implementation and documentation
- Implemented Sdchannel class for calculating standard deviation channels based on linear regression.
- Added detailed documentation for SDCHANNEL, including overview, calculation methods, and interpretation.
- Updated project files to include new numerics library components in Channels and Volatility projects.
2026-01-21 14:41:31 -05:00

320 lines
9.7 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
public class JbandsTests
{
[Fact]
public void Jbands_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Jbands(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Jbands(-5));
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NaN));
var j = new Jbands(14);
Assert.Contains("Jbands", j.Name, StringComparison.OrdinalIgnoreCase);
Assert.True(j.WarmupPeriod > 0);
}
[Fact]
public void Jbands_InitialState_Defaults()
{
var j = new Jbands(14);
Assert.Equal(0, j.Last.Value);
Assert.Equal(0, j.Upper.Value);
Assert.Equal(0, j.Lower.Value);
Assert.False(j.IsHot);
}
[Fact]
public void Jbands_FirstBar_AllBandsEqual()
{
var j = new Jbands(14);
j.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, j.Last.Value, 1e-10);
Assert.Equal(100.0, j.Upper.Value, 1e-10);
Assert.Equal(100.0, j.Lower.Value, 1e-10);
}
[Fact]
public void Jbands_UpperBand_SnapToNewHigh()
{
var j = new Jbands(14);
j.Update(new TValue(DateTime.UtcNow, 100.0));
j.Update(new TValue(DateTime.UtcNow, 105.0));
// Upper should snap to new high
Assert.Equal(105.0, j.Upper.Value, 1e-10);
}
[Fact]
public void Jbands_LowerBand_SnapToNewLow()
{
var j = new Jbands(14);
j.Update(new TValue(DateTime.UtcNow, 100.0));
j.Update(new TValue(DateTime.UtcNow, 95.0));
// Lower should snap to new low
Assert.Equal(95.0, j.Lower.Value, 1e-10);
}
[Fact]
public void Jbands_BandsDecay_TowardPrice()
{
var j = new Jbands(14);
// Create a spike then return to baseline
j.Update(new TValue(DateTime.UtcNow, 100.0));
j.Update(new TValue(DateTime.UtcNow, 120.0)); // Upper snaps to 120
double upperAfterSpike = j.Upper.Value;
// Feed lower prices - upper band should decay
for (int i = 0; i < 20; i++)
{
j.Update(new TValue(DateTime.UtcNow, 100.0));
}
// Upper band should have decayed toward price
Assert.True(j.Upper.Value < upperAfterSpike);
Assert.True(j.Upper.Value > 100.0); // But not below price yet
}
[Fact]
public void Jbands_IsHot_TurnsTrueAfterWarmup()
{
var j = new Jbands(7);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.05, seed: 42);
int warmup = j.WarmupPeriod;
for (int i = 0; i < warmup - 1; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close));
Assert.False(j.IsHot);
}
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close));
Assert.True(j.IsHot);
}
[Fact]
public void Jbands_IsNewFalse_RestoresState()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 7);
TValue remembered = default;
for (int i = 0; i < 50; i++)
{
remembered = new TValue(DateTime.UtcNow, gbm.Next().Close);
j.Update(remembered, isNew: true);
}
double mid = j.Last.Value;
double up = j.Upper.Value;
double lo = j.Lower.Value;
// Multiple corrections
for (int i = 0; i < 5; i++)
{
var corrected = new TValue(DateTime.UtcNow, gbm.Next().Close);
j.Update(corrected, isNew: false);
}
// Restore with original value
j.Update(remembered, isNew: false);
Assert.Equal(mid, j.Last.Value, 1e-10);
Assert.Equal(up, j.Upper.Value, 1e-10);
Assert.Equal(lo, j.Lower.Value, 1e-10);
}
[Fact]
public void Jbands_NaN_UsesLastValid()
{
var j = new Jbands(14);
j.Update(new TValue(DateTime.UtcNow, 100.0));
j.Update(new TValue(DateTime.UtcNow, 105.0));
var result = j.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(result.Value));
Assert.True(double.IsFinite(j.Upper.Value));
Assert.True(double.IsFinite(j.Lower.Value));
var result2 = j.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(result2.Value));
}
[Fact]
public void Jbands_Reset_Clears()
{
var j = new Jbands(14);
for (int i = 0; i < 50; i++)
{
j.Update(new TValue(DateTime.UtcNow, 100 + i));
}
j.Reset();
Assert.Equal(0, j.Last.Value);
Assert.Equal(0, j.Upper.Value);
Assert.Equal(0, j.Lower.Value);
Assert.False(j.IsHot);
j.Update(new TValue(DateTime.UtcNow, 50.0));
Assert.Equal(50.0, j.Last.Value);
}
[Fact]
public void Jbands_BatchVsStreaming_Match()
{
var jStream = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
double expectedMid = jStream.Last.Value;
double expectedUp = jStream.Upper.Value;
double expectedLo = jStream.Lower.Value;
var (midBatch, upBatch, loBatch) = Jbands.Batch(series, 14, 0, 0.45);
Assert.Equal(expectedMid, midBatch.Last.Value, 1e-10);
Assert.Equal(expectedUp, upBatch.Last.Value, 1e-10);
Assert.Equal(expectedLo, loBatch.Last.Value, 1e-10);
}
[Fact]
public void Jbands_SpanCalculate_ValidatesArgs()
{
double[] source = [100, 105, 110];
double[] middle = new double[3];
double[] upper = new double[3];
double[] lower = new double[3];
double[] shortOut = new double[2];
Assert.Throws<ArgumentException>(() =>
Jbands.Calculate(source.AsSpan(), shortOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14));
Assert.Throws<ArgumentException>(() =>
Jbands.Calculate(source.AsSpan(), middle.AsSpan(), shortOut.AsSpan(), lower.AsSpan(), 14));
Assert.Throws<ArgumentException>(() =>
Jbands.Calculate(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), shortOut.AsSpan(), 14));
}
[Fact]
public void Jbands_SpanCalculate_MatchesStreaming()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 123);
double[] source = new double[100];
for (int i = 0; i < source.Length; i++)
source[i] = gbm.Next().Close;
double[] middle = new double[100];
double[] upper = new double[100];
double[] lower = new double[100];
Jbands.Calculate(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[^1], 1e-10);
Assert.Equal(jStream.Upper.Value, upper[^1], 1e-10);
Assert.Equal(jStream.Lower.Value, lower[^1], 1e-10);
}
[Fact]
public void Jbands_Event_Publishes()
{
var j = new Jbands(14);
bool fired = false;
j.Pub += (object? sender, in TValueEventArgs args) => fired = true;
j.Update(new TValue(DateTime.UtcNow, 100.0), isNew: true);
Assert.True(fired);
}
[Fact]
public void Jbands_Chaining_Works()
{
var src = new TSeries();
var j = new Jbands(14);
var downstream = new Sma(j, 5);
for (int i = 0; i < 100; i++)
{
var val = new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i * 0.5);
src.Add(val.Time, val.Value);
j.Update(val, isNew: true);
}
Assert.True(downstream.IsHot);
Assert.True(double.IsFinite(downstream.Last.Value));
}
[Fact]
public void Jbands_MiddleBand_MatchesJma()
{
// Verify that middle band matches standalone JMA
var jbands = new Jbands(14, 0, 0.45);
var jma = new Jma(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 999);
for (int i = 0; i < 200; 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, 1e-10);
}
[Fact]
public void Jbands_Phase_AffectsBehavior()
{
var jNeutral = new Jbands(14, 0);
var jPositive = new Jbands(14, 50);
var jNegative = new Jbands(14, -50);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.15, seed: 42);
for (int i = 0; i < 100; i++)
{
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jNeutral.Update(tv, isNew: true);
jPositive.Update(tv, isNew: true);
jNegative.Update(tv, isNew: true);
}
// Different phase settings should produce different JMA values
Assert.NotEqual(jNeutral.Last.Value, jPositive.Last.Value, 1e-6);
Assert.NotEqual(jNeutral.Last.Value, jNegative.Last.Value, 1e-6);
}
[Fact]
public void Jbands_UpperAlwaysAboveLower()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
for (int i = 0; i < 500; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
Assert.True(j.Upper.Value >= j.Lower.Value);
}
}
}