mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 21:48:03 +00:00
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:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,48 @@ namespace QuanTAlib.Tests;
|
||||
|
||||
public class CovarianceTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidatesPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Covariance(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Covariance(-1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Covariance(1)); // Period must be >= 2
|
||||
|
||||
// Valid period should not throw
|
||||
var cov = new Covariance(2);
|
||||
Assert.NotNull(cov);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var cov = new Covariance(10);
|
||||
|
||||
Assert.Equal(0, cov.Last.Value);
|
||||
Assert.False(cov.IsHot);
|
||||
Assert.Contains("Cov", cov.Name, StringComparison.Ordinal);
|
||||
|
||||
cov.Update(100, 100);
|
||||
cov.Update(101, 101);
|
||||
Assert.NotEqual(0, cov.Last.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
int period = 5;
|
||||
var cov = new Covariance(period);
|
||||
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
Assert.False(cov.IsHot, $"IsHot should be false at index {i}");
|
||||
cov.Update(i, i * 2);
|
||||
}
|
||||
|
||||
cov.Update(period - 1, (period - 1) * 2);
|
||||
Assert.True(cov.IsHot, "IsHot should be true after period updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Covariance_CalculatesCorrectly()
|
||||
{
|
||||
@@ -164,4 +206,115 @@ public class CovarianceTests
|
||||
Assert.Throws<NotSupportedException>(() => cov.Update(new TSeries()));
|
||||
Assert.Throws<NotSupportedException>(() => cov.Prime(new double[] { 1, 2, 3 }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var cov = new Covariance(5);
|
||||
|
||||
// Feed 10 updates
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
cov.Update(i, i * 2);
|
||||
}
|
||||
|
||||
double stateAfterTen = cov.Last.Value;
|
||||
|
||||
// Apply 5 corrections with isNew=false
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
cov.Update(100 + i, 200 + i, isNew: false);
|
||||
}
|
||||
|
||||
// Restore to original values
|
||||
cov.Update(9, 18, isNew: false);
|
||||
|
||||
Assert.Equal(stateAfterTen, cov.Last.Value, precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var cov = new Covariance(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
cov.Update(i, i * 2);
|
||||
}
|
||||
Assert.True(cov.IsHot);
|
||||
|
||||
cov.Reset();
|
||||
Assert.False(cov.IsHot);
|
||||
Assert.Equal(0, cov.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_ProducesNaN()
|
||||
{
|
||||
var cov = new Covariance(5);
|
||||
|
||||
// Add some valid values
|
||||
cov.Update(1, 2);
|
||||
cov.Update(2, 4);
|
||||
cov.Update(3, 6);
|
||||
|
||||
// Add NaN - Covariance propagates NaN (two-input indicators don't have last valid value substitution)
|
||||
var result = cov.Update(double.NaN, double.NaN);
|
||||
|
||||
// For two-input indicators, NaN may propagate or produce 0
|
||||
// The behavior depends on implementation - just verify no exception
|
||||
Assert.True(double.IsNaN(result.Value) || double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_ProducesInfinity()
|
||||
{
|
||||
var cov = new Covariance(5);
|
||||
|
||||
// Add some valid values
|
||||
cov.Update(1, 2);
|
||||
cov.Update(2, 4);
|
||||
cov.Update(3, 6);
|
||||
|
||||
// Add Infinity - Covariance propagates infinity (two-input indicators don't have last valid value substitution)
|
||||
var result = cov.Update(double.PositiveInfinity, double.PositiveInfinity);
|
||||
|
||||
// For two-input indicators, infinity may propagate
|
||||
// The behavior depends on implementation - just verify no exception
|
||||
Assert.True(double.IsInfinity(result.Value) || double.IsNaN(result.Value) || double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchSpan_MatchesStreaming()
|
||||
{
|
||||
int period = 5;
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
|
||||
double[] x = new double[count];
|
||||
double[] y = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
x[i] = bar.Close;
|
||||
y[i] = bar.Close * 1.5 + 10; // Correlated series
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var cov = new Covariance(period);
|
||||
var streamingResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamingResults[i] = cov.Update(x[i], y[i]).Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
double[] batchResults = new double[count];
|
||||
Covariance.Batch(x, y, batchResults, period);
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batchResults[i], precision: 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,149 @@ public class LinRegTests
|
||||
Assert.Throws<ArgumentException>(() => new LinReg(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var linreg = new LinReg(10);
|
||||
Assert.Equal(0, linreg.Last.Value);
|
||||
Assert.False(linreg.IsHot);
|
||||
Assert.Contains("LinReg", linreg.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(0, linreg.Slope);
|
||||
Assert.Equal(0, linreg.Intercept);
|
||||
Assert.Equal(0, linreg.RSquared);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var linreg = new LinReg(5);
|
||||
Assert.False(linreg.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
linreg.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(linreg.IsHot);
|
||||
}
|
||||
|
||||
linreg.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.True(linreg.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var linreg = new LinReg(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
linreg.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
}
|
||||
Assert.True(linreg.IsHot);
|
||||
|
||||
linreg.Reset();
|
||||
Assert.False(linreg.IsHot);
|
||||
Assert.Equal(0, linreg.Last.Value);
|
||||
Assert.Equal(0, linreg.Slope);
|
||||
Assert.Equal(0, linreg.Intercept);
|
||||
Assert.Equal(0, linreg.RSquared);
|
||||
|
||||
// After reset, should accept new values
|
||||
var result = linreg.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(50, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var linreg = new LinReg(5);
|
||||
linreg.Update(new TValue(DateTime.UtcNow, 10));
|
||||
linreg.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var resultPosInf = linreg.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPosInf.Value));
|
||||
|
||||
var resultNegInf = linreg.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var linreg = new LinReg(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
linreg.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = linreg.Last.Value;
|
||||
double slopeAfterTen = linreg.Slope;
|
||||
double interceptAfterTen = linreg.Intercept;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
linreg.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = linreg.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
// Use relaxed tolerance due to floating point accumulation in complex calculations
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-2);
|
||||
Assert.Equal(slopeAfterTen, linreg.Slope, 1e-2);
|
||||
Assert.Equal(interceptAfterTen, linreg.Intercept, 1e-2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Period must be > 0
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
LinReg.Calculate(source.AsSpan(), output.AsSpan(), 0));
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
LinReg.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source[i] = bar.Close;
|
||||
series.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
var tseriesResult = LinReg.Batch(series, period);
|
||||
|
||||
double[] output = new double[100];
|
||||
LinReg.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,115 @@ namespace QuanTAlib;
|
||||
|
||||
public class MedianTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Median(0));
|
||||
Assert.Throws<ArgumentException>(() => new Median(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var median = new Median(5);
|
||||
Assert.Equal(0, median.Last.Value);
|
||||
Assert.False(median.IsHot);
|
||||
Assert.Contains("Median", median.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var median = new Median(5);
|
||||
Assert.False(median.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
median.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(median.IsHot);
|
||||
}
|
||||
|
||||
median.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.True(median.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var median = new Median(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
median.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
}
|
||||
Assert.True(median.IsHot);
|
||||
|
||||
median.Reset();
|
||||
Assert.False(median.IsHot);
|
||||
Assert.Equal(0, median.Last.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
var result = median.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(50, result.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var median = new Median(3);
|
||||
median.Update(new TValue(DateTime.UtcNow, 10));
|
||||
median.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var result = median.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var median = new Median(3);
|
||||
median.Update(new TValue(DateTime.UtcNow, 10));
|
||||
median.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var resultPosInf = median.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPosInf.Value));
|
||||
|
||||
var resultNegInf = median.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var median = new Median(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
median.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = median.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
median.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = median.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Median_OddPeriod_ReturnsMiddleValue()
|
||||
{
|
||||
@@ -69,10 +178,11 @@ public class MedianTests
|
||||
// Arrange
|
||||
int period = 5;
|
||||
var source = new TSeries();
|
||||
var r = new Random(123);
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
source.Add(new TValue(DateTime.MinValue.AddSeconds(i), r.NextDouble() * 100));
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Act
|
||||
@@ -92,6 +202,63 @@ public class MedianTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
int period = 5;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = Median.Batch(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Median.Batch(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Median(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Median(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, eventingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Period must be > 0
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Median.Batch(source.AsSpan(), output.AsSpan(), 0));
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Median.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Median_StaticBatch_Matches_ClassBatch()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -9,10 +10,200 @@ public class SkewTests
|
||||
public void Constructor_ValidatesPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Skew(2));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Skew(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Skew(-1));
|
||||
var skew = new Skew(3);
|
||||
Assert.NotNull(skew);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var skew = new Skew(5);
|
||||
|
||||
Assert.Equal(0, skew.Last.Value);
|
||||
|
||||
TValue result = skew.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(result.Value, skew.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var skew = new Skew(5);
|
||||
|
||||
skew.Update(new TValue(DateTime.UtcNow, 1), isNew: true);
|
||||
skew.Update(new TValue(DateTime.UtcNow, 2), isNew: true);
|
||||
skew.Update(new TValue(DateTime.UtcNow, 3), isNew: true);
|
||||
skew.Update(new TValue(DateTime.UtcNow, 4), isNew: true);
|
||||
double value1 = skew.Update(new TValue(DateTime.UtcNow, 5), isNew: true).Value;
|
||||
|
||||
skew.Update(new TValue(DateTime.UtcNow, 10), isNew: true);
|
||||
double value2 = skew.Last.Value;
|
||||
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var skew = new Skew(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
skew.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = skew.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
skew.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = skew.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
// Use looser tolerance due to floating-point accumulation in Skew's 3rd moment calculation
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var skew = new Skew(5);
|
||||
|
||||
Assert.False(skew.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
skew.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(skew.IsHot);
|
||||
}
|
||||
|
||||
skew.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.True(skew.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var skew = new Skew(5);
|
||||
|
||||
skew.Update(new TValue(DateTime.UtcNow, 1));
|
||||
skew.Update(new TValue(DateTime.UtcNow, 2));
|
||||
skew.Update(new TValue(DateTime.UtcNow, 3));
|
||||
|
||||
// Skew doesn't do last-valid-value substitution - it treats non-finite as 0
|
||||
// Just verify it doesn't crash and returns a finite value
|
||||
var resultAfterPosInf = skew.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value) || double.IsNaN(resultAfterPosInf.Value));
|
||||
|
||||
var resultAfterNegInf = skew.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value) || double.IsNaN(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
int count = 200;
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
times.Add(bar.Time);
|
||||
values.Add(bar.Close);
|
||||
}
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
// 1. Batch Mode (static method)
|
||||
var batchSeries = Skew.Calculate(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode (static method with spans)
|
||||
var spanInput = values.ToArray();
|
||||
var spanOutput = new double[count];
|
||||
Skew.Batch(spanInput.AsSpan(), spanOutput.AsSpan(), period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode (instance, one value at a time)
|
||||
var streamingInd = new Skew(period);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// Assert all modes produce identical results
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Period must be >= 3
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Skew.Batch(source.AsSpan(), output.AsSpan(), 2));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Skew.Batch(source.AsSpan(), output.AsSpan(), 0));
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Skew.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
int count = 100;
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
times.Add(bar.Time);
|
||||
values.Add(bar.Close);
|
||||
source[i] = bar.Close;
|
||||
}
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
var tseriesResult = Skew.Calculate(series, 10);
|
||||
Skew.Batch(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_CalculatesCorrectly_Sample()
|
||||
{
|
||||
|
||||
@@ -9,6 +9,138 @@ public class StdDevTests
|
||||
public void Constructor_ValidatesPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StdDev(1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StdDev(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new StdDev(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var stddev = new StdDev(5);
|
||||
Assert.Equal(0, stddev.Last.Value);
|
||||
Assert.False(stddev.IsHot);
|
||||
Assert.Contains("StdDev", stddev.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var stddev = new StdDev(3);
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 10));
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 20));
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
double valueBefore = stddev.Last.Value;
|
||||
|
||||
// Update with isNew=false should change the result
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 100), isNew: false);
|
||||
double valueAfter = stddev.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBefore, valueAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var stddev = new StdDev(5);
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 10));
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var result = stddev.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var stddev = new StdDev(5);
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 10));
|
||||
stddev.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var resultPosInf = stddev.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPosInf.Value));
|
||||
|
||||
var resultNegInf = stddev.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var stddev = new StdDev(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
stddev.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = stddev.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
stddev.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = stddev.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Period must be > 1
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
StdDev.Batch(source.AsSpan(), output.AsSpan(), 1));
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
StdDev.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode (static span)
|
||||
var tValues = series.Values.ToArray();
|
||||
var batchOutput = new double[tValues.Length];
|
||||
StdDev.Batch(tValues, batchOutput, period);
|
||||
double expected = batchOutput[^1];
|
||||
|
||||
// 2. Streaming Mode
|
||||
var streamingInd = new StdDev(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 3. TSeries Batch Mode
|
||||
var batchSeriesResult = StdDev.Calculate(series, period);
|
||||
double tseriesResult = batchSeriesResult.Last.Value;
|
||||
|
||||
Assert.Equal(expected, streamingResult, precision: 6);
|
||||
Assert.Equal(expected, tseriesResult, precision: 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
@@ -9,6 +10,181 @@ public class VarianceTests
|
||||
public void Constructor_ValidatesPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Variance(1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Variance(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Variance(-1));
|
||||
var variance = new Variance(2);
|
||||
Assert.NotNull(variance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_ReturnsValue()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
|
||||
Assert.Equal(0, variance.Last.Value);
|
||||
|
||||
TValue result = variance.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(result.Value, variance.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
|
||||
variance.Update(new TValue(DateTime.UtcNow, 1), isNew: true);
|
||||
variance.Update(new TValue(DateTime.UtcNow, 2), isNew: true);
|
||||
variance.Update(new TValue(DateTime.UtcNow, 3), isNew: true);
|
||||
variance.Update(new TValue(DateTime.UtcNow, 4), isNew: true);
|
||||
double value1 = variance.Update(new TValue(DateTime.UtcNow, 5), isNew: true).Value;
|
||||
|
||||
variance.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value2 = variance.Last.Value;
|
||||
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
variance.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = variance.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
variance.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = variance.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var variance = new Variance(5);
|
||||
|
||||
variance.Update(new TValue(DateTime.UtcNow, 1));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 2));
|
||||
variance.Update(new TValue(DateTime.UtcNow, 3));
|
||||
|
||||
// Variance doesn't do last-valid-value substitution
|
||||
// Just verify it doesn't crash
|
||||
var resultAfterPosInf = variance.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
// May be NaN or finite depending on implementation
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value) || double.IsNaN(resultAfterPosInf.Value) || double.IsInfinity(resultAfterPosInf.Value));
|
||||
|
||||
var resultAfterNegInf = variance.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value) || double.IsNaN(resultAfterNegInf.Value) || double.IsInfinity(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
int count = 200;
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
times.Add(bar.Time);
|
||||
values.Add(bar.Close);
|
||||
}
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
// 1. Batch Mode (static method)
|
||||
var batchSeries = Variance.Calculate(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode (static method with spans)
|
||||
var spanInput = values.ToArray();
|
||||
var spanOutput = new double[count];
|
||||
Variance.Batch(spanInput.AsSpan(), spanOutput.AsSpan(), period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode (instance, one value at a time)
|
||||
var streamingInd = new Variance(period);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// Assert all modes produce identical results
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Period must be >= 2
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Variance.Batch(source.AsSpan(), output.AsSpan(), 1));
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Variance.Batch(source.AsSpan(), output.AsSpan(), 0));
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Variance.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
int count = 100;
|
||||
|
||||
var times = new List<long>(count);
|
||||
var values = new List<double>(count);
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
times.Add(bar.Time);
|
||||
values.Add(bar.Close);
|
||||
source[i] = bar.Close;
|
||||
}
|
||||
|
||||
var series = new TSeries(times, values);
|
||||
|
||||
var tseriesResult = Variance.Calculate(series, 10);
|
||||
Variance.Batch(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user