Files
QuanTAlib/lib/channels/jbands/Jbands.Tests.cs
T
Miha Kralj 653aafacd8 feat: Add Prime method to various indicators for initializing state with historical data
- Implemented Prime method in Vel, Ao, Apo, Frama, Adl, Adosc, Aobv, Cmf, Efi, Eom, Iii, Kvo, Mfi, Nvi, Obv, Pvd, Pvi, Pvo, Pvr, Pvt, Tvi, Twap, Va, Vf, Vo, Vroc, Vwad, Vwap, and Vwma classes.
- The Prime method resets the indicator state and processes the provided historical bar data to initialize the indicator.
- Added warmup period property to Adl and Wad classes to define the minimum number of data points required for validity.
- Updated benchmark tests to use Batch methods for performance evaluation.
2026-02-11 20:38:38 -08:00

720 lines
20 KiB
C#

using Xunit;
namespace QuanTAlib.Tests;
public class JbandsTests
{
#region Constructor Tests
[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_Constructor_InfinityPower_Throws()
{
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.PositiveInfinity));
Assert.Throws<ArgumentException>(() => new Jbands(14, 0, double.NegativeInfinity));
}
[Fact]
public void Jbands_Period1_EdgeCase()
{
var j = new Jbands(1);
Assert.True(j.WarmupPeriod > 0);
j.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(100.0, j.Last.Value, 1e-10);
}
[Fact]
public void Jbands_ConstructorWithSource_ReceivesUpdates()
{
var source = new Sma(3);
using var j = new Jbands(source, 14);
for (int i = 0; i < 50; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
Assert.True(double.IsFinite(j.Last.Value));
Assert.NotEqual(0, j.Last.Value);
}
#endregion
#region Initial State Tests
[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);
}
#endregion
#region First Bar Tests
[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_NaN_FirstBar_ReturnsNaN()
{
var j = new Jbands(14);
var result = j.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsNaN(result.Value));
Assert.True(double.IsNaN(j.Upper.Value));
Assert.True(double.IsNaN(j.Lower.Value));
}
#endregion
#region Band Behavior Tests
[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_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);
}
}
[Fact]
public void Jbands_MiddleBand_IsSmoothed()
{
// JMA middle band is an IIR-smoothed value that can briefly
// exceed the envelope bands during fast transitions. Verify
// it converges close to price over time.
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.01, seed: 55);
// Feed steady data — middle should stay within a reasonable range
for (int i = 0; i < 500; i++)
{
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
}
// After convergence with low-volatility data, middle should be close to price
Assert.True(double.IsFinite(j.Last.Value));
Assert.True(j.Upper.Value >= j.Lower.Value);
}
#endregion
#region IsHot / WarmupPeriod Tests
[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);
}
#endregion
#region State Management Tests
[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);
}
#endregion
#region NaN / Infinity Handling Tests
[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));
}
#endregion
#region Reset Tests
[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_Reset_ThenReuse_ProducesSameResults()
{
var j = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.02, sigma: 0.1, seed: 88);
double[] prices = new double[100];
for (int i = 0; i < prices.Length; i++)
{
prices[i] = gbm.Next().Close;
}
// First pass
for (int i = 0; i < prices.Length; i++)
{
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
}
double midFirst = j.Last.Value;
double upFirst = j.Upper.Value;
double loFirst = j.Lower.Value;
// Reset and second pass
j.Reset();
for (int i = 0; i < prices.Length; i++)
{
j.Update(new TValue(DateTime.UtcNow.AddMinutes(i), prices[i]), isNew: true);
}
Assert.Equal(midFirst, j.Last.Value, 1e-10);
Assert.Equal(upFirst, j.Upper.Value, 1e-10);
Assert.Equal(loFirst, j.Lower.Value, 1e-10);
}
#endregion
#region Dispose Tests
[Fact]
public void Jbands_Dispose_UnsubscribesFromSource()
{
var source = new Sma(3);
var j = new Jbands(source, 14);
// Feed some data through source
for (int i = 0; i < 20; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
double valueBeforeDispose = j.Last.Value;
// Dispose - should unsubscribe
j.Dispose();
// Feed more data - Jbands should NOT update
for (int i = 20; i < 40; i++)
{
source.Update(new TValue(DateTime.UtcNow.AddMinutes(i), 200 + i));
}
Assert.Equal(valueBeforeDispose, j.Last.Value, 1e-10);
}
[Fact]
public void Jbands_Dispose_Idempotent()
{
var source = new Sma(3);
var j = new Jbands(source, 14);
j.Dispose();
j.Dispose(); // Should not throw
// Verify indicator is still in a valid state after double dispose
Assert.True(double.IsFinite(j.Last.Value) || j.Last.Value == 0);
}
[Fact]
public void Jbands_Dispose_WithoutSource_DoesNotThrow()
{
var j = new Jbands(14);
j.Dispose(); // No source subscription, should not throw
Assert.Equal(0, j.Last.Value);
}
#endregion
#region Prime Tests
[Fact]
public void Jbands_Prime_SetsIndicatorToHot()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int count = j.WarmupPeriod + 10;
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
j.Prime(series);
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
Assert.True(double.IsFinite(j.Upper.Value));
Assert.True(double.IsFinite(j.Lower.Value));
}
[Fact]
public void Jbands_Prime_EmptySeries_DoesNotThrow()
{
var j = new Jbands(14);
var empty = new TSeries();
j.Prime(empty); // Should not throw
Assert.False(j.IsHot);
}
[Fact]
public void Jbands_Prime_ThenUpdate_ContinuesCorrectly()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int primeCount = j.WarmupPeriod + 5;
for (int i = 0; i < primeCount; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
j.Prime(series);
Assert.True(j.IsHot);
// Continue streaming
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next();
j.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
}
[Fact]
public void Jbands_Prime_MatchesStreamingResults()
{
var jPrime = new Jbands(14, 0, 0.45);
var jStream = new Jbands(14, 0, 0.45);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
int count = 100;
for (int i = 0; i < count; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
jStream.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
jPrime.Prime(series);
Assert.Equal(jStream.Last.Value, jPrime.Last.Value, 1e-10);
Assert.Equal(jStream.Upper.Value, jPrime.Upper.Value, 1e-10);
Assert.Equal(jStream.Lower.Value, jPrime.Lower.Value, 1e-10);
}
#endregion
#region Calculate Tests
[Fact]
public void Jbands_Calculate_ReturnsResultsAndHotIndicator()
{
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
var (results, indicator) = Jbands.Calculate(series, 14, 0, 0.45);
Assert.True(indicator.IsHot);
Assert.Equal(300, results.Middle.Count);
Assert.Equal(300, results.Upper.Count);
Assert.Equal(300, results.Lower.Count);
Assert.True(double.IsFinite(indicator.Last.Value));
}
#endregion
#region Update(TSeries) Tests
[Fact]
public void Jbands_UpdateTSeries_Direct()
{
var j = new Jbands(14);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
var series = new TSeries();
for (int i = 0; i < 300; i++)
{
var bar = gbm.Next();
series.Add(bar.Time, bar.Close);
}
var (middle, upper, lower) = j.Update(series);
Assert.Equal(300, middle.Count);
Assert.Equal(300, upper.Count);
Assert.Equal(300, lower.Count);
Assert.True(j.IsHot);
Assert.True(double.IsFinite(j.Last.Value));
}
[Fact]
public void Jbands_UpdateTSeries_EmptySeries_ReturnsEmptyTuples()
{
var j = new Jbands(14);
var empty = new TSeries();
var (middle, upper, lower) = j.Update(empty);
Assert.Empty(middle);
Assert.Empty(upper);
Assert.Empty(lower);
}
#endregion
#region Batch Tests
[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.Batch(source.AsSpan(), shortOut.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14));
Assert.Throws<ArgumentException>(() =>
Jbands.Batch(source.AsSpan(), middle.AsSpan(), shortOut.AsSpan(), lower.AsSpan(), 14));
Assert.Throws<ArgumentException>(() =>
Jbands.Batch(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.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[^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_SpanBatch_EmptySource_DoesNotThrow()
{
double[] source = [];
double[] middle = [];
double[] upper = [];
double[] lower = [];
Jbands.Batch(source.AsSpan(), middle.AsSpan(), upper.AsSpan(), lower.AsSpan(), 14);
Assert.Empty(source);
Assert.Empty(middle);
}
#endregion
#region Event / Chaining Tests
[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));
}
#endregion
#region Middle Band / JMA Tests
[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);
}
#endregion
#region Phase Parameter Tests
[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_Phase_ClampingBelowMinus100()
{
// Phase < -100 should clamp phaseParam to 0.5
var jClamped = new Jbands(14, -200);
var jEdge = new Jbands(14, -100);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jClamped.Update(tv, isNew: true);
jEdge.Update(tv, isNew: true);
}
// Phase -200 should clamp to same as -100 (both → 0.5)
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
}
[Fact]
public void Jbands_Phase_ClampingAbove100()
{
// Phase > 100 should clamp phaseParam to 2.5
var jClamped = new Jbands(14, 200);
var jEdge = new Jbands(14, 100);
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.1, seed: 42);
for (int i = 0; i < 100; i++)
{
double price = gbm.Next().Close;
var tv = new TValue(DateTime.UtcNow, price);
jClamped.Update(tv, isNew: true);
jEdge.Update(tv, isNew: true);
}
// Phase 200 should clamp to same as 100 (both → 2.5)
Assert.Equal(jEdge.Last.Value, jClamped.Last.Value, 1e-10);
}
#endregion
}