mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-02 03:37:42 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
214 lines
7.2 KiB
C#
214 lines
7.2 KiB
C#
using OoplesFinance.StockIndicators;
|
|
using OoplesFinance.StockIndicators.Models;
|
|
using Skender.Stock.Indicators;
|
|
using Xunit.Abstractions;
|
|
|
|
namespace QuanTAlib.Tests;
|
|
|
|
public sealed class BbsValidationTests : IDisposable
|
|
{
|
|
private readonly ValidationTestData _testData;
|
|
private readonly ITestOutputHelper _output;
|
|
private bool _disposed;
|
|
|
|
public BbsValidationTests(ITestOutputHelper output)
|
|
{
|
|
_output = output;
|
|
_testData = new ValidationTestData();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
}
|
|
|
|
private void Dispose(bool disposing)
|
|
{
|
|
if (_disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disposed = true;
|
|
|
|
if (disposing)
|
|
{
|
|
_testData?.Dispose();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Streaming_Batch_Span_Agree()
|
|
{
|
|
int bbPeriod = 20;
|
|
double bbMult = 2.0;
|
|
int kcPeriod = 20;
|
|
double kcMult = 1.5;
|
|
|
|
// Streaming
|
|
var streaming = new Bbs(bbPeriod, bbMult, kcPeriod, kcMult);
|
|
var streamValues = new List<double>(_testData.Bars.Count);
|
|
for (int i = 0; i < _testData.Bars.Count; i++)
|
|
{
|
|
streamValues.Add(streaming.Update(_testData.Bars[i]).Value);
|
|
}
|
|
|
|
// Batch (TBarSeries)
|
|
TSeries batchSeries = Bbs.Batch(_testData.Bars, bbPeriod, bbMult, kcPeriod, kcMult);
|
|
|
|
// Span
|
|
double[] spanOutput = new double[_testData.Bars.Count];
|
|
Bbs.Batch(_testData.Bars.HighValues, _testData.Bars.LowValues, _testData.Bars.CloseValues,
|
|
spanOutput.AsSpan(), bbPeriod, bbMult);
|
|
|
|
// Compare last 200 samples for stability
|
|
int start = Math.Max(0, spanOutput.Length - 200);
|
|
for (int i = start; i < spanOutput.Length; i++)
|
|
{
|
|
Assert.Equal(batchSeries[i].Value, streamValues[i], 7);
|
|
Assert.Equal(batchSeries[i].Value, spanOutput[i], 7);
|
|
}
|
|
|
|
_output.WriteLine("BBS validation: streaming, batch, and span outputs agree.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_SpanWithSqueeze_MatchesStreaming()
|
|
{
|
|
int bbPeriod = 20;
|
|
double bbMult = 2.0;
|
|
int kcPeriod = 20;
|
|
double kcMult = 1.5;
|
|
|
|
// Streaming - collect squeeze states
|
|
var streaming = new Bbs(bbPeriod, bbMult, kcPeriod, kcMult);
|
|
var streamBandwidths = new List<double>(_testData.Bars.Count);
|
|
var streamSqueezes = new List<bool>(_testData.Bars.Count);
|
|
for (int i = 0; i < _testData.Bars.Count; i++)
|
|
{
|
|
streaming.Update(_testData.Bars[i]);
|
|
streamBandwidths.Add(streaming.Last.Value);
|
|
streamSqueezes.Add(streaming.SqueezeOn);
|
|
}
|
|
|
|
// Span with squeeze
|
|
int len = _testData.Bars.Count;
|
|
double[] spanBw = new double[len];
|
|
bool[] spanSq = new bool[len];
|
|
Bbs.Batch(_testData.Bars.HighValues, _testData.Bars.LowValues, _testData.Bars.CloseValues,
|
|
spanBw.AsSpan(), spanSq.AsSpan(), bbPeriod, bbMult, kcPeriod, kcMult);
|
|
|
|
// Compare last 200 samples
|
|
int start = Math.Max(0, len - 200);
|
|
for (int i = start; i < len; i++)
|
|
{
|
|
Assert.Equal(streamBandwidths[i], spanBw[i], 7);
|
|
Assert.Equal(streamSqueezes[i], spanSq[i]);
|
|
}
|
|
|
|
_output.WriteLine("BBS validation: squeeze span matches streaming.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Bandwidth_MatchesBbw()
|
|
{
|
|
// BBS bandwidth should match BBW (Bollinger Band Width) when using same BB parameters.
|
|
// BBS bandwidth = ((upper - lower) / middle) * 100
|
|
// BBW = ((upper - lower) / middle) * 100 (same formula)
|
|
int[] periods = { 5, 10, 20, 50 };
|
|
double multiplier = 2.0;
|
|
|
|
foreach (var period in periods)
|
|
{
|
|
// BBS (uses close for BB, needs OHLC for KC)
|
|
var bbs = new Bbs(bbPeriod: period, bbMult: multiplier, kcPeriod: period, kcMult: 1.5);
|
|
var bbsValues = new List<double>(_testData.Bars.Count);
|
|
for (int i = 0; i < _testData.Bars.Count; i++)
|
|
{
|
|
bbs.Update(_testData.Bars[i]);
|
|
bbsValues.Add(bbs.Last.Value);
|
|
}
|
|
|
|
// Skender Bollinger Bands Width
|
|
var skenderBb = _testData.SkenderQuotes.GetBollingerBands(period, multiplier).ToList();
|
|
|
|
// Compare bandwidth values where both are valid
|
|
int start = period + 10; // skip warmup
|
|
int compared = 0;
|
|
for (int i = start; i < Math.Min(bbsValues.Count, skenderBb.Count); i++)
|
|
{
|
|
var sk = skenderBb[i];
|
|
if (sk.Width is not null and not double.NaN)
|
|
{
|
|
// BBS bandwidth = width * 100 (as percentage)
|
|
// Skender Width = (Upper - Lower) / Middle
|
|
double expected = sk.Width.Value * 100.0;
|
|
Assert.Equal(expected, bbsValues[i], 4);
|
|
compared++;
|
|
}
|
|
}
|
|
|
|
Assert.True(compared > 0, $"No valid comparisons for period {period}");
|
|
}
|
|
|
|
_output.WriteLine("BBS bandwidth validated against Skender BB Width.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_AllOutputsFinite()
|
|
{
|
|
var bbs = new Bbs(bbPeriod: 20, bbMult: 2.0, kcPeriod: 20, kcMult: 1.5);
|
|
|
|
for (int i = 0; i < _testData.Bars.Count; i++)
|
|
{
|
|
var result = bbs.Update(_testData.Bars[i]);
|
|
Assert.True(double.IsFinite(result.Value), $"Non-finite output at bar {i}: {result.Value}");
|
|
}
|
|
|
|
_output.WriteLine("BBS validation: all outputs are finite.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Calculate_ReturnsHotIndicator()
|
|
{
|
|
var (results, indicator) = Bbs.Calculate(_testData.Bars);
|
|
|
|
Assert.Equal(_testData.Bars.Count, results.Count);
|
|
Assert.True(indicator.IsHot);
|
|
Assert.True(double.IsFinite(indicator.Last.Value));
|
|
|
|
_output.WriteLine("BBS validation: Calculate returns hot indicator.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_LargeDataset_Stability()
|
|
{
|
|
var (results, _) = Bbs.Calculate(_testData.Bars, bbPeriod: 50, bbMult: 2.0, kcPeriod: 50, kcMult: 1.5);
|
|
|
|
// Check last 100 values are finite and non-negative
|
|
int start = Math.Max(0, results.Count - 100);
|
|
for (int i = start; i < results.Count; i++)
|
|
{
|
|
Assert.True(double.IsFinite(results[i].Value));
|
|
Assert.True(results[i].Value >= 0, $"Bandwidth should be non-negative at {i}: {results[i].Value}");
|
|
}
|
|
|
|
_output.WriteLine("BBS validation: large dataset stability verified.");
|
|
}
|
|
|
|
[Fact]
|
|
public void Bbs_MatchesOoples_Structural()
|
|
{
|
|
// CalculateSqueezeMomentumIndicator — structural test (BBands width / KC width)
|
|
var ooplesData = _testData.SkenderQuotes
|
|
.Select(q => new TickerData { Date = q.Date, Open = (double)q.Open, High = (double)q.High, Low = (double)q.Low, Close = (double)q.Close, Volume = (double)q.Volume })
|
|
.ToList();
|
|
|
|
var result = new StockData(ooplesData).CalculateSqueezeMomentumIndicator();
|
|
var values = result.CustomValuesList;
|
|
|
|
int finiteCount = values.Count(v => double.IsFinite(v));
|
|
Assert.True(finiteCount > 100, $"Expected >100 finite Ooples BBS/Squeeze values, got {finiteCount}");
|
|
}
|
|
}
|