mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-13 08:08:05 +00:00
322 lines
11 KiB
C#
322 lines
11 KiB
C#
using Xunit;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public class BbandsTests
|
|
{
|
|
[Fact]
|
|
public void Bbands_Constructor_ValidParameters()
|
|
{
|
|
// Arrange & Act
|
|
Bbands bbands = new(period: 20, multiplier: 2.0);
|
|
|
|
// Assert
|
|
Assert.NotNull(bbands);
|
|
Assert.Equal("Bbands(20,2.0)", bbands.Name);
|
|
Assert.Equal(20, bbands.WarmupPeriod);
|
|
Assert.False(bbands.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_Constructor_InvalidPeriod_ThrowsArgumentOutOfRangeException()
|
|
{
|
|
// Arrange, Act & Assert
|
|
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
|
|
() => new Bbands(period: 1));
|
|
Assert.Equal("period", exception.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_Constructor_InvalidMultiplier_ThrowsArgumentOutOfRangeException()
|
|
{
|
|
// Arrange, Act & Assert
|
|
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
|
|
() => new Bbands(period: 20, multiplier: 0.05));
|
|
Assert.Equal("multiplier", exception.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_Update_ReturnsCorrectMiddleBand()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 5, multiplier: 2.0);
|
|
double[] prices = [10.0, 11.0, 12.0, 13.0, 14.0, 15.0];
|
|
DateTime time = DateTime.UtcNow;
|
|
|
|
// Act
|
|
TValue result = default;
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
result = bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
|
}
|
|
|
|
// Assert - Middle should be SMA(5) = (11+12+13+14+15)/5 = 13.0
|
|
Assert.Equal(13.0, result.Value, precision: 10);
|
|
Assert.True(bbands.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_BandCalculations_CorrectValues()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 3, multiplier: 2.0);
|
|
DateTime time = DateTime.UtcNow;
|
|
double[] prices = [10.0, 12.0, 14.0];
|
|
|
|
// Act
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
|
}
|
|
|
|
// Assert
|
|
// SMA = (10+12+14)/3 = 12.0
|
|
Assert.Equal(12.0, bbands.Middle.Value, precision: 10);
|
|
|
|
// StdDev = sqrt(((10-12)^2 + (12-12)^2 + (14-12)^2) / 3) = sqrt(8/3) ≈ 1.6329931618554521
|
|
double expectedStdDev = Math.Sqrt(8.0 / 3.0);
|
|
double expectedUpper = 12.0 + (2.0 * expectedStdDev);
|
|
double expectedLower = 12.0 - (2.0 * expectedStdDev);
|
|
|
|
Assert.Equal(expectedUpper, bbands.Upper.Value, precision: 10);
|
|
Assert.Equal(expectedLower, bbands.Lower.Value, precision: 10);
|
|
Assert.Equal(expectedUpper - expectedLower, bbands.Width.Value, precision: 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_PercentB_CorrectCalculation()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 3, multiplier: 2.0);
|
|
DateTime time = DateTime.UtcNow;
|
|
double[] prices = [10.0, 12.0, 14.0, 13.0];
|
|
|
|
// Act
|
|
for (int i = 0; i < prices.Length; i++)
|
|
{
|
|
bbands.Update(new TValue(time.AddSeconds(i), prices[i]));
|
|
}
|
|
|
|
// Assert
|
|
// For the last value (13.0) with window [12.0, 14.0, 13.0]
|
|
// SMA = 13.0, StdDev = sqrt(2/3), Lower ≈ 11.367, Upper ≈ 14.633
|
|
// %B = (13.0 - Lower) / (Upper - Lower)
|
|
double percentB = bbands.PercentB.Value;
|
|
Assert.True(percentB >= 0.0 && percentB <= 1.0);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_IsNew_False_RollsBackCorrectly()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 3, multiplier: 2.0);
|
|
DateTime time = DateTime.UtcNow;
|
|
|
|
// Act
|
|
bbands.Update(new TValue(time, 10.0), isNew: true);
|
|
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
|
|
bbands.Update(new TValue(time.AddSeconds(2), 14.0), isNew: true);
|
|
double middleBefore = bbands.Middle.Value;
|
|
|
|
bbands.Update(new TValue(time.AddSeconds(2), 15.0), isNew: false);
|
|
double middleAfter = bbands.Middle.Value;
|
|
|
|
// Assert - Value should change due to replacement
|
|
Assert.NotEqual(middleBefore, middleAfter);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_NaN_HandledGracefully()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 3, multiplier: 2.0);
|
|
DateTime time = DateTime.UtcNow;
|
|
|
|
// Act
|
|
bbands.Update(new TValue(time, 10.0), isNew: true);
|
|
bbands.Update(new TValue(time.AddSeconds(1), 12.0), isNew: true);
|
|
|
|
bbands.Update(new TValue(time.AddSeconds(2), double.NaN), isNew: true);
|
|
double afterNaN = bbands.Middle.Value;
|
|
|
|
// Assert - Should substitute last valid value
|
|
Assert.True(double.IsFinite(afterNaN));
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_Reset_ClearsState()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 3, multiplier: 2.0);
|
|
DateTime time = DateTime.UtcNow;
|
|
|
|
bbands.Update(new TValue(time, 10.0));
|
|
bbands.Update(new TValue(time.AddSeconds(1), 12.0));
|
|
bbands.Update(new TValue(time.AddSeconds(2), 14.0));
|
|
|
|
// Act
|
|
bbands.Reset();
|
|
|
|
// Assert
|
|
Assert.False(bbands.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_WarmupPeriod_IsHotTransition()
|
|
{
|
|
// Arrange
|
|
Bbands bbands = new(period: 5, multiplier: 2.0);
|
|
DateTime time = DateTime.UtcNow;
|
|
|
|
// Act & Assert
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
bbands.Update(new TValue(time.AddSeconds(i), 10.0 + i));
|
|
Assert.False(bbands.IsHot);
|
|
}
|
|
|
|
bbands.Update(new TValue(time.AddSeconds(4), 14.0));
|
|
Assert.True(bbands.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_UpdateTSeries_ReturnsValidSeries()
|
|
{
|
|
// Arrange
|
|
int period = 5;
|
|
Bbands bbands = new(period, multiplier: 2.0);
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
TSeries source = bars.Close;
|
|
|
|
// Act
|
|
TSeries result = bbands.Update(source);
|
|
|
|
// Assert
|
|
Assert.Equal(source.Count, result.Count);
|
|
Assert.True(bbands.IsHot);
|
|
|
|
// Verify last values match streaming
|
|
Bbands streamingBbands = new(period, multiplier: 2.0);
|
|
for (int i = Math.Max(0, source.Count - period); i < source.Count; i++)
|
|
{
|
|
streamingBbands.Update(source[i], isNew: true);
|
|
}
|
|
Assert.Equal(streamingBbands.Middle.Value, result[^1].Value, precision: 10);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_StaticCalculate_ReturnsValidSeries()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
TSeries source = bars.Close;
|
|
|
|
// Act
|
|
TSeries result = Bbands.Calculate(source, period: 5, multiplier: 2.0);
|
|
|
|
// Assert
|
|
Assert.Equal(source.Count, result.Count);
|
|
// Note: Static Calculate doesn't set IsHot on any instance, just returns the series
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_SpanCalculate_MatchesTSeries()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
TSeries source = bars.Close;
|
|
int period = 20;
|
|
double multiplier = 2.0;
|
|
|
|
// Act - Copy to arrays before Span usage to avoid ref local issues
|
|
double[] sourceArray = source.Values.ToArray();
|
|
double[] middleArray = new double[source.Count];
|
|
double[] upperArray = new double[source.Count];
|
|
double[] lowerArray = new double[source.Count];
|
|
Bbands.Calculate(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier);
|
|
|
|
TSeries seriesResult = Bbands.Calculate(source, period, multiplier);
|
|
|
|
// Assert - Compare last 10 values
|
|
for (int i = source.Count - 10; i < source.Count; i++)
|
|
{
|
|
Assert.Equal(seriesResult[i].Value, middleArray[i], precision: 10);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_SpanCalculate_InvalidLength_ThrowsArgumentException()
|
|
{
|
|
// Arrange
|
|
double[] sourceArr = new double[10];
|
|
double[] middleArr = new double[10];
|
|
double[] upperArr = new double[10];
|
|
double[] lowerArr = new double[9]; // Wrong length
|
|
|
|
// Act & Assert
|
|
ArgumentException exception = Assert.Throws<ArgumentException>(
|
|
() => Bbands.Calculate(sourceArr.AsSpan(), middleArr.AsSpan(), upperArr.AsSpan(), lowerArr.AsSpan()));
|
|
Assert.Equal("source", exception.ParamName);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_Chainability_WorksCorrectly()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
TSeries source = bars.Close;
|
|
var pubSource = new TSeries();
|
|
Bbands bbands1 = new(pubSource, period: 5, multiplier: 2.0);
|
|
Bbands bbands2 = new(bbands1, period: 3, multiplier: 1.5);
|
|
|
|
// Act
|
|
bool eventFired = false;
|
|
bbands2.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
|
|
|
foreach (var item in source)
|
|
{
|
|
pubSource.Add(item);
|
|
}
|
|
|
|
// Assert
|
|
Assert.True(eventFired);
|
|
Assert.True(bbands2.IsHot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbands_Consistency_StreamingVsBatchVsSpan()
|
|
{
|
|
// Arrange
|
|
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
|
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
|
TSeries source = bars.Close;
|
|
int period = 20;
|
|
double multiplier = 2.0;
|
|
|
|
// Streaming
|
|
Bbands streamingBbands = new(period, multiplier);
|
|
foreach (var item in source)
|
|
{
|
|
streamingBbands.Update(item);
|
|
}
|
|
|
|
// Batch
|
|
TSeries batchResult = Bbands.Calculate(source, period, multiplier);
|
|
|
|
// Span - Copy arrays before using to avoid ref local lambda issue
|
|
double[] sourceArray = source.Values.ToArray();
|
|
double[] middleArray = new double[source.Count];
|
|
double[] upperArray = new double[source.Count];
|
|
double[] lowerArray = new double[source.Count];
|
|
Bbands.Calculate(sourceArray.AsSpan(), middleArray, upperArray, lowerArray, period, multiplier);
|
|
|
|
// Assert - Compare last 50 values (streaming only has last value)
|
|
Assert.Equal(batchResult[^1].Value, streamingBbands.Middle.Value, precision: 8);
|
|
Assert.Equal(middleArray[^1], streamingBbands.Middle.Value, precision: 8);
|
|
}
|
|
}
|