Add validation tests for USF and enhance ATR indicator tests

- Introduced Usf.Validation.Tests.cs to validate the USF (Ehlers Ultimate Smoother Filter) for consistency across batch, streaming, and span modes, as well as mathematical properties and coefficient calculations.
- Added comprehensive tests for the ATR indicator in Atr.Quantower.Tests.cs, including constructor validation, historical data processing, and handling of NaN/Infinity inputs.
- Enhanced Atr.Tests.cs with additional tests for iterative corrections, warmup behavior, and true range calculations.
- Updated Atr.cs to ensure warmup period is derived from RMA.
- Added new tests for Adosc in Adosc.Tests.cs to validate handling of NaN and Infinity inputs, and to ensure batch calculations match iterative results.
- Created a new Volatility.csproj to organize volatility-related implementations.
This commit is contained in:
Miha Kralj
2025-12-28 23:33:46 -08:00
parent 3cc2726654
commit 84ff67fb50
22 changed files with 2813 additions and 1284 deletions
+128
View File
@@ -0,0 +1,128 @@
using Xunit;
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class BetaIndicatorTests
{
[Fact]
public void BetaIndicator_Constructor_SetsDefaults()
{
var indicator = new BetaIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(SourceType.Close, indicator.AssetSource);
Assert.Equal(SourceType.Close, indicator.MarketSource);
Assert.True(indicator.ShowColdValues);
Assert.Equal("Beta Coefficient", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void BetaIndicator_MinHistoryDepths_EqualsTwo()
{
var indicator = new BetaIndicator { Period = 20 };
Assert.Equal(2, BetaIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(2, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void BetaIndicator_ShortName_IncludesParameters()
{
var indicator = new BetaIndicator { Period = 14 };
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("Beta", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void BetaIndicator_Initialize_CreatesInternalBeta()
{
var indicator = new BetaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
Assert.Equal("Beta", indicator.LinesSeries[0].Name);
}
[Fact]
public void BetaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new BetaIndicator { Period = 5 };
indicator.Initialize();
// Add historical data - need enough bars for warmup
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
double beta = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(beta));
}
[Fact]
public void BetaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new BetaIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
// Add initial bars
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
// Add a new bar
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(11, indicator.LinesSeries[0].Count);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void BetaIndicator_DifferentSourceTypes_Work()
{
var assetSources = new[]
{
SourceType.Open,
SourceType.High,
SourceType.Low,
SourceType.Close,
};
foreach (var source in assetSources)
{
var indicator = new BetaIndicator { Period = 5, AssetSource = source, MarketSource = SourceType.Close };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"AssetSource {source} should produce finite value");
}
}
}
+68
View File
@@ -0,0 +1,68 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class BetaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 2000, 1, 0)]
public int Period { get; set; } = 20;
[InputParameter("Asset Source", sortIndex: 2)]
public SourceType AssetSource { get; set; } = SourceType.Close;
[InputParameter("Market Source", sortIndex: 3)]
public SourceType MarketSource { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Beta? _beta;
private readonly LineSeries? _series;
private Func<IHistoryItem, double>? _assetSelector;
private Func<IHistoryItem, double>? _marketSelector;
public static int MinHistoryDepths => 2;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"Beta({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/beta/Beta.Quantower.cs";
public BetaIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "Beta Coefficient";
Description = "Measures the volatility of an asset in relation to the overall market.";
_series = new(name: "Beta", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_beta = new Beta(Period);
_assetSelector = AssetSource.GetPriceSelector();
_marketSelector = MarketSource.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
var item = this.HistoricalData[this.Count - 1, SeekOriginHistory.Begin];
double assetVal = _assetSelector!(item);
double marketVal = _marketSelector!(item);
var time = this.HistoricalData.Time();
var assetInput = new TValue(time, assetVal);
var marketInput = new TValue(time, marketVal);
TValue result = _beta!.Update(assetInput, marketInput, args.IsNewBar());
_series!.SetValue(result.Value, _beta.IsHot, ShowColdValues);
}
}
+163 -3
View File
@@ -9,6 +9,11 @@ public class BetaTests
public void Constructor_ValidatesPeriod()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new Beta(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new Beta(-1));
// Valid period should not throw
var beta = new Beta(1);
Assert.NotNull(beta);
}
[Fact]
@@ -16,6 +21,23 @@ public class BetaTests
{
var beta = new Beta(10);
Assert.Throws<NotSupportedException>(() => beta.Update(new TValue(DateTime.UtcNow, 100)));
Assert.Throws<NotSupportedException>(() => beta.Update(new TSeries()));
Assert.Throws<NotSupportedException>(() => beta.Prime(new double[] { 1, 2, 3 }));
}
[Fact]
public void Properties_Accessible()
{
var beta = new Beta(10);
Assert.Equal(0, beta.Last.Value);
Assert.False(beta.IsHot);
Assert.Contains("Beta", beta.Name, StringComparison.Ordinal);
Assert.Equal(11, beta.WarmupPeriod); // period + 1 for first return
beta.Update(100, 100);
beta.Update(101, 101);
Assert.NotEqual(0, beta.Last.Time);
}
[Fact]
@@ -76,21 +98,159 @@ public class BetaTests
}
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var beta = new Beta(5);
// Initialize
beta.Update(100, 100);
// Add 5 more updates with different ratios to get non-1 beta
beta.Update(102, 101); // Asset up 2%, market up 1%
beta.Update(104, 102); // Asset up ~2%, market up ~1%
beta.Update(108, 103); // Asset up ~4%, market up ~1%
beta.Update(112, 104); // Asset up ~4%, market up ~1%
beta.Update(116, 105); // Asset up ~4%, market up ~1%
double valueBefore = beta.Last.Value;
// Update last value with isNew=false with very different values
beta.Update(90, 110, isNew: false); // Drastically different
double valueAfter = beta.Last.Value;
// Value should change since we're updating the last bar
Assert.NotEqual(valueBefore, valueAfter);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var beta = new Beta(5);
// Initialize with 10 updates
beta.Update(100, 100);
for (int i = 1; i <= 9; i++)
{
beta.Update(100 + i, 100 + i);
}
double stateAfterTen = beta.Last.Value;
// Apply 5 corrections with isNew=false
for (int i = 0; i < 5; i++)
{
beta.Update(200 + i, 200 + i, isNew: false);
}
// Restore to original value
beta.Update(109, 109, isNew: false);
Assert.Equal(stateAfterTen, beta.Last.Value, precision: 10);
}
[Fact]
public void Reset_ClearsState()
{
var beta = new Beta(5);
for (int i = 0; i < 10; i++)
{
beta.Update(100 + i, 100 + i);
beta.Update(100 + i * 2, 100 + i); // Different ratios
}
Assert.True(beta.IsHot);
beta.Reset();
Assert.False(beta.IsHot);
// Re-initialize
// Re-initialize and verify it can accept new values
// After reset, beta should be able to calculate fresh values
beta.Update(100, 100);
Assert.False(beta.IsHot);
Assert.False(beta.IsHot); // Not hot yet, needs period+1 updates
// Feed more updates to reach hot state again
for (int i = 1; i <= 5; i++)
{
beta.Update(100 + i, 100 + i);
}
Assert.True(beta.IsHot);
// With equal proportional changes, beta should be 1
Assert.Equal(1.0, beta.Last.Value, precision: 6);
}
[Fact]
public void NaN_Input_ReturnsFiniteValue()
{
var beta = new Beta(5);
// Initialize
beta.Update(100, 100);
// Add some valid values
beta.Update(101, 101);
beta.Update(102, 102);
// Add NaN - Beta should handle gracefully
var result = beta.Update(double.NaN, double.NaN);
// Result should be finite (may be 0 or previous value)
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_ReturnsFiniteValue()
{
var beta = new Beta(5);
// Initialize
beta.Update(100, 100);
// Add some valid values
beta.Update(101, 101);
beta.Update(102, 102);
// Add Infinity - Beta should handle gracefully
var result = beta.Update(double.PositiveInfinity, double.PositiveInfinity);
// Result should be finite (may be 0 or previous value)
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void ZeroMarketVariance_ReturnsZero()
{
// When market returns are constant (zero variance), beta is undefined
// The implementation should return 0 in this case
var beta = new Beta(5);
// Initialize
beta.Update(100, 100);
// Same market price (zero returns/variance)
for (int i = 0; i < 10; i++)
{
beta.Update(100 + i, 100); // Asset changes, market constant
}
// Beta should be 0 (or undefined) when market variance is 0
Assert.Equal(0, beta.Last.Value);
}
[Fact]
public void Resync_DoesNotDrift()
{
// Run for > 1000 updates to trigger Resync
var beta = new Beta(10);
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
beta.Update(100, 100); // Initialize
for (int i = 0; i < 1100; i++)
{
var bar = gbm.Next();
beta.Update(bar.Close * 1.5, bar.Close); // Asset follows market with beta ~1.5
}
Assert.True(double.IsFinite(beta.Last.Value));
}
}