mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-17 18:18:04 +00:00
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.
This commit is contained in:
@@ -4,6 +4,8 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class JbandsTests
|
||||
{
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Constructor_ValidatesInput()
|
||||
{
|
||||
@@ -16,6 +18,42 @@ public class JbandsTests
|
||||
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()
|
||||
{
|
||||
@@ -27,6 +65,10 @@ public class JbandsTests
|
||||
Assert.False(j.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region First Bar Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_FirstBar_AllBandsEqual()
|
||||
{
|
||||
@@ -38,6 +80,21 @@ public class JbandsTests
|
||||
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()
|
||||
{
|
||||
@@ -81,6 +138,43 @@ public class JbandsTests
|
||||
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()
|
||||
{
|
||||
@@ -98,6 +192,10 @@ public class JbandsTests
|
||||
Assert.True(j.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region State Management Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_IsNewFalse_RestoresState()
|
||||
{
|
||||
@@ -130,6 +228,10 @@ public class JbandsTests
|
||||
Assert.Equal(lo, j.Lower.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN / Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_NaN_UsesLastValid()
|
||||
{
|
||||
@@ -147,6 +249,10 @@ public class JbandsTests
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Reset_Clears()
|
||||
{
|
||||
@@ -167,6 +273,243 @@ public class JbandsTests
|
||||
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()
|
||||
{
|
||||
@@ -236,6 +579,24 @@ public class JbandsTests
|
||||
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()
|
||||
{
|
||||
@@ -265,6 +626,10 @@ public class JbandsTests
|
||||
Assert.True(double.IsFinite(downstream.Last.Value));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Middle Band / JMA Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_MiddleBand_MatchesJma()
|
||||
{
|
||||
@@ -284,6 +649,10 @@ public class JbandsTests
|
||||
Assert.Equal(jma.Last.Value, jbands.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Phase Parameter Tests
|
||||
|
||||
[Fact]
|
||||
public void Jbands_Phase_AffectsBehavior()
|
||||
{
|
||||
@@ -307,15 +676,44 @@ public class JbandsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Jbands_UpperAlwaysAboveLower()
|
||||
public void Jbands_Phase_ClampingBelowMinus100()
|
||||
{
|
||||
var j = new Jbands(14);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.01, sigma: 0.2, seed: 77);
|
||||
// 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 < 500; i++)
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
j.Update(new TValue(DateTime.UtcNow, gbm.Next().Close), isNew: true);
|
||||
Assert.True(j.Upper.Value >= j.Lower.Value);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -425,6 +425,16 @@ public sealed class Jbands : ITValuePublisher, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Jbands and returns both the results and the indicator instance.
|
||||
/// </summary>
|
||||
public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Jbands Indicator) Calculate(TSeries source, int period, int phase = 0, double power = 0.45)
|
||||
{
|
||||
var indicator = new Jbands(period, phase, power);
|
||||
var results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double CalculateTrimmedMean(double fallback)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user