mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-07-28 01:37:43 +00:00
bcb52ef5ec
- 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.
228 lines
7.6 KiB
C#
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");
|
|
}
|
|
} |