Files
QuanTAlib/lib/channels/jbands/tests/Jbands.Tests.cs
T
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
2026-03-12 12:34:16 -07: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));
// power parameter removed - no longer applicable
var j = new Jbands(14);
Assert.Contains("Jbands", j.Name, StringComparison.OrdinalIgnoreCase);
Assert.True(j.WarmupPeriod > 0);
}
[Fact]
public void Jbands_Constructor_Period1_IsValid()
{
var j = new Jbands(1);
Assert.True(j.WarmupPeriod > 0);
}
[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);
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);
var jStream = new Jbands(14, 0);
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);
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);
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);
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);
var jma = new Jma(14, 0);
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
}