Files
QuanTAlib/lib/volatility/bbw/Bbw.Validation.Tests.cs
T
Miha Kralj bcb52ef5ec Add Close-to-Close Volatility (CCV) implementation and validation tests
- Implemented CCV class for calculating annualized log return volatility using SMA, EMA, and WMA smoothing methods.
- Added comprehensive unit tests for CCV to validate mathematical correctness, consistency across methods, and edge cases.
- Created documentation for CCV detailing its mathematical foundation, smoothing methods, and performance metrics.
2026-01-31 17:25:39 -08:00

228 lines
7.6 KiB
C#

using Skender.Stock.Indicators;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for BBW (Bollinger Band Width).
/// Compares against Skender's BollingerBands implementation.
/// </summary>
public sealed class BbwValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public BbwValidationTests(ITestOutputHelper output)
{
_output = output;
_testData = new ValidationTestData();
}
void IDisposable.Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 20 };
double[] multipliers = { 2.0 };
foreach (var period in periods)
{
foreach (var multiplier in multipliers)
{
// Calculate QuanTAlib BBW (batch TSeries) using Close prices
var bbw = new global::QuanTAlib.Bbw(period, multiplier);
var qResult = bbw.Update(_testData.Bars.Close);
// Calculate Skender Bollinger Bands (width = upper - lower)
var sResult = _testData.SkenderQuotes.GetBollingerBands(period, multiplier).ToList();
// Compare last 100 records (using Width property from Skender)
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Width, tolerance: ValidationHelper.SkenderTolerance);
}
}
_output.WriteLine("BBW Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 20 };
double[] multipliers = { 2.0 };
foreach (var period in periods)
{
foreach (var multiplier in multipliers)
{
// Calculate QuanTAlib BBW (streaming) using Close prices
var bbw = new global::QuanTAlib.Bbw(period, multiplier);
var qResults = new List<double>();
foreach (var item in _testData.Bars.Close)
{
qResults.Add(bbw.Update(item).Value);
}
// Calculate Skender Bollinger Bands (width = upper - lower)
var sResult = _testData.SkenderQuotes.GetBollingerBands(period, multiplier).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Width, tolerance: ValidationHelper.SkenderTolerance);
}
}
_output.WriteLine("BBW Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Span()
{
int[] periods = { 20 };
double[] multipliers = { 2.0 };
// Prepare Close price data
var closeData = _testData.Bars.Close.Select(x => x.Value).ToArray();
var output = new double[closeData.Length];
foreach (var period in periods)
{
foreach (var multiplier in multipliers)
{
// Calculate QuanTAlib BBW (Span API)
global::QuanTAlib.Bbw.Batch(closeData, output, period, multiplier);
// Calculate Skender Bollinger Bands (width = upper - lower)
var sResult = _testData.SkenderQuotes.GetBollingerBands(period, multiplier).ToList();
// Compare last 100 records
int lookback = period - 1;
int startIndex = Math.Max(0, closeData.Length - 100);
int skenderStartIndex = Math.Max(0, sResult.Count - 100);
for (int i = 0; i < Math.Min(100, closeData.Length - lookback); i++)
{
int qIdx = startIndex + i;
int sIdx = skenderStartIndex + i;
if (qIdx >= lookback && sIdx < sResult.Count && sResult[sIdx].Width.HasValue)
{
Assert.Equal(sResult[sIdx].Width!.Value, output[qIdx], ValidationHelper.SkenderTolerance);
}
}
}
}
_output.WriteLine("BBW Span validated successfully against Skender");
}
[Fact]
public void Validate_DifferentPeriods()
{
int[] periods = { 10, 14, 20, 50 };
foreach (var period in periods)
{
// Calculate QuanTAlib BBW
var bbw = new global::QuanTAlib.Bbw(period);
var qResult = bbw.Update(_testData.Bars.Close);
// Calculate Skender Bollinger Bands
var sResult = _testData.SkenderQuotes.GetBollingerBands(period).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Width, tolerance: ValidationHelper.SkenderTolerance);
}
_output.WriteLine("BBW validated successfully for different periods against Skender");
}
[Fact]
public void Validate_DifferentMultipliers()
{
double[] multipliers = { 1.0, 1.5, 2.0, 2.5, 3.0 };
int period = 20;
foreach (var multiplier in multipliers)
{
// Calculate QuanTAlib BBW
var bbw = new global::QuanTAlib.Bbw(period, multiplier);
var qResult = bbw.Update(_testData.Bars.Close);
// Calculate Skender Bollinger Bands
var sResult = _testData.SkenderQuotes.GetBollingerBands(period, multiplier).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Width, tolerance: ValidationHelper.SkenderTolerance);
}
_output.WriteLine("BBW validated successfully for different multipliers against Skender");
}
[Fact]
public void Validate_StreamingBatchParity()
{
int period = 20;
double multiplier = 2.0;
// Streaming calculation
var bbwStreaming = new global::QuanTAlib.Bbw(period, multiplier);
var streamingResults = new List<double>();
foreach (var item in _testData.Bars.Close)
{
streamingResults.Add(bbwStreaming.Update(item).Value);
}
// Batch calculation
var bbwBatch = new global::QuanTAlib.Bbw(period, multiplier);
var batchResult = bbwBatch.Update(_testData.Bars.Close);
// Compare all records
Assert.Equal(streamingResults.Count, batchResult.Count);
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResult[i].Value, 1e-10);
}
_output.WriteLine("BBW streaming/batch parity validated successfully");
}
[Fact]
public void Validate_SpanBatchParity()
{
int period = 20;
double multiplier = 2.0;
// Prepare Close price data
var closeData = _testData.Bars.Close.Select(x => x.Value).ToArray();
// Span calculation
var spanOutput = new double[closeData.Length];
global::QuanTAlib.Bbw.Batch(closeData, spanOutput, period, multiplier);
// Instance batch calculation
var bbw = new global::QuanTAlib.Bbw(period, multiplier);
var batchResult = bbw.Update(_testData.Bars.Close);
// Compare all records
Assert.Equal(spanOutput.Length, batchResult.Count);
for (int i = 0; i < spanOutput.Length; i++)
{
Assert.Equal(spanOutput[i], batchResult[i].Value, 1e-10);
}
_output.WriteLine("BBW span/batch parity validated successfully");
}
}