docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files

- 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
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,215 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class BbwIndicatorTests
{
[Fact]
public void BbwIndicator_Constructor_SetsDefaults()
{
var indicator = new BbwIndicator();
Assert.Equal(20, indicator.Period);
Assert.Equal(2.0, indicator.Multiplier);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("BBW - Bollinger Band Width", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void BbwIndicator_ShortName_IncludesParameters()
{
var indicator = new BbwIndicator { Period = 14, Multiplier = 2.5 };
Assert.Contains("BBW", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void BbwIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new BbwIndicator();
Assert.Equal(0, BbwIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void BbwIndicator_Initialize_CreatesInternalBbw()
{
var indicator = new BbwIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void BbwIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new BbwIndicator { Period = 5 };
indicator.Initialize();
// Add historical data with volatility
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
double basePrice = 100 + i * 2 + (i % 2 == 0 ? 5 : -5); // Add some volatility
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
// 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 val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
Assert.True(val >= 0); // BBW should be non-negative
}
[Fact]
public void BbwIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new BbwIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
double basePrice = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 128, 115, 125, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void BbwIndicator_DifferentPeriods_Work()
{
int[] periods = { 5, 10, 20, 50 };
foreach (var period in periods)
{
var indicator = new BbwIndicator { Period = period };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 60; i++)
{
double basePrice = 100 + i + (i % 3 == 0 ? 10 : -5); // Add volatility
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Period {period} should produce finite value");
Assert.True(val >= 0, $"Period {period} should produce non-negative BBW");
}
}
[Fact]
public void BbwIndicator_DifferentMultipliers_Work()
{
double[] multipliers = { 1.0, 1.5, 2.0, 2.5, 3.0 };
foreach (var multiplier in multipliers)
{
var indicator = new BbwIndicator { Multiplier = multiplier };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double basePrice = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Multiplier {multiplier} should produce finite value");
Assert.True(val >= 0, $"Multiplier {multiplier} should produce non-negative BBW");
}
}
[Fact]
public void BbwIndicator_DifferentSourceTypes_Work()
{
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new BbwIndicator { Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 30; i++)
{
double basePrice = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 2, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val), $"Source {source} should produce finite value");
}
}
[Fact]
public void BbwIndicator_Period_CanBeChanged()
{
var indicator = new BbwIndicator();
Assert.Equal(20, indicator.Period);
indicator.Period = 14;
Assert.Equal(14, indicator.Period);
indicator.Period = 50;
Assert.Equal(50, indicator.Period);
}
[Fact]
public void BbwIndicator_Multiplier_CanBeChanged()
{
var indicator = new BbwIndicator();
Assert.Equal(2.0, indicator.Multiplier);
indicator.Multiplier = 1.5;
Assert.Equal(1.5, indicator.Multiplier);
indicator.Multiplier = 3.0;
Assert.Equal(3.0, indicator.Multiplier);
}
[Fact]
public void BbwIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new BbwIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void BbwIndicator_SourceCodeLink_IsValid()
{
var indicator = new BbwIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Bbw.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}
+429
View File
@@ -0,0 +1,429 @@
namespace QuanTAlib.Tests;
using Xunit;
public class BbwTests
{
private const double Tolerance = 1e-10;
private static TBarSeries GenerateTestData(int count = 100)
{
var gbm = new GBM(seed: 42);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
[Fact]
public void Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Bbw(0));
Assert.Throws<ArgumentException>(() => new Bbw(-1));
Assert.Throws<ArgumentException>(() => new Bbw(20, 0));
Assert.Throws<ArgumentException>(() => new Bbw(20, -1));
var valid = new Bbw(10, 1.5);
Assert.Equal(10, valid.Period);
Assert.Equal(1.5, valid.Multiplier);
}
[Fact]
public void WarmupPeriod_IsPositive()
{
var bbw = new Bbw(20, 2.0);
Assert.Equal(20, bbw.WarmupPeriod);
Assert.True(bbw.WarmupPeriod > 0);
}
[Fact]
public void Properties_Accessible()
{
var bbw = new Bbw(20, 2.5);
Assert.Equal(20, bbw.Period);
Assert.Equal(2.5, bbw.Multiplier);
Assert.Equal("Bbw(20,2.5)", bbw.Name);
}
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var bbw = new Bbw(5);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
var result = bbw.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void Calc_ReturnsValue()
{
var bbw = new Bbw(10);
for (int i = 0; i < 15; i++)
{
var result = bbw.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.True(double.IsFinite(result.Value) || i < 1);
}
Assert.True(bbw.IsHot);
}
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var bbw = new Bbw(10);
var result1 = bbw.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
var result2 = bbw.Update(new TValue(DateTime.UtcNow, 101), isNew: true);
var result3 = bbw.Update(new TValue(DateTime.UtcNow, 102), isNew: false);
Assert.True(double.IsFinite(result1.Value));
Assert.True(double.IsFinite(result2.Value));
Assert.True(double.IsFinite(result3.Value));
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var bbw = new Bbw(5);
for (int i = 0; i < 5; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
}
var baseline = bbw.Update(new TValue(DateTime.UtcNow, 105), isNew: true);
var updated = bbw.Update(new TValue(DateTime.UtcNow, 150), isNew: false);
Assert.NotEqual(baseline.Value, updated.Value);
}
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
int period = 10;
var bbw = new Bbw(period);
for (int i = 0; i < period - 1; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i));
Assert.False(bbw.IsHot);
}
bbw.Update(new TValue(DateTime.UtcNow, 110));
Assert.True(bbw.IsHot);
}
[Fact]
public void Reset_Works()
{
var bbw = new Bbw(10);
for (int i = 0; i < 15; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i));
}
Assert.True(bbw.IsHot);
bbw.Reset();
Assert.False(bbw.IsHot);
}
[Fact]
public void SingleValue_ReturnsZero()
{
var bbw = new Bbw(5);
var result = bbw.Update(new TValue(DateTime.UtcNow, 100));
Assert.Equal(0.0, result.Value);
}
[Fact]
public void Period1_Works()
{
var bbw = new Bbw(1, 2.0);
var result = bbw.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(bbw.IsHot);
Assert.Equal(0.0, result.Value);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var bbw = new Bbw(20);
var bars = GenerateTestData(50);
var times = bars.Times;
var close = bars.CloseValues;
TValue lastValue = default;
for (int i = 0; i < bars.Count; i++)
{
lastValue = bbw.Update(new TValue(times[i], close[i]), isNew: true);
}
double originalValue = lastValue.Value;
var correctedValue = bbw.Update(new TValue(DateTime.UtcNow, 999.99), isNew: false);
Assert.NotEqual(originalValue, correctedValue.Value);
var restoredValue = bbw.Update(new TValue(lastValue.Time, close[bars.Count - 1]), isNew: false);
Assert.Equal(originalValue, restoredValue.Value, 1e-9);
}
[Fact]
public void IsNew_Consistency()
{
var bbw = new Bbw(10);
for (int i = 0; i < 10; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i), isNew: true);
}
var result1 = bbw.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
_ = bbw.Update(new TValue(DateTime.UtcNow, 115), isNew: false);
var result3 = bbw.Update(new TValue(DateTime.UtcNow, 110), isNew: false);
Assert.Equal(result1.Value, result3.Value, Tolerance);
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var bbw = new Bbw(5);
for (int i = 0; i < 5; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var resultNan = bbw.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(resultNan.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var bbw = new Bbw(5);
for (int i = 0; i < 5; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i));
}
var resultInf = bbw.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
Assert.True(double.IsFinite(resultInf.Value));
}
[Fact]
public void LargeDataset_Performance()
{
var bbw = new Bbw(50);
var bars = GenerateTestData(5000);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
var result = bbw.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value));
}
}
[Fact]
public void TSeries_Update_MatchesStreaming()
{
int period = 20;
var bbwStream = new Bbw(period);
var bbwBatch = new Bbw(period);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
bbwStream.Update(new TValue(times[i], close[i]));
}
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var result = bbwBatch.Update(ts);
Assert.Equal(bbwStream.Last.Value, result[result.Count - 1].Value, 1e-9);
}
[Fact]
public void BatchCalc_MatchesIterativeCalc()
{
var bbw = new Bbw(20);
var bars = GenerateTestData(200);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
bbw.Update(new TValue(times[i], close[i]));
}
var iterativeResult = bbw.Last.Value;
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var batchResult = Bbw.Batch(ts, 20);
Assert.Equal(iterativeResult, batchResult[batchResult.Count - 1].Value, 1e-8);
}
[Fact]
public void Chainability_Works()
{
var bbw = new Bbw(20);
var sma = new Sma(5);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
var bbwResult = bbw.Update(new TValue(times[i], close[i]));
sma.Update(bbwResult);
}
var smaBatch = new Sma(5);
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var bbwBatch = Bbw.Batch(ts, 20);
var smaResult = smaBatch.Update(bbwBatch);
Assert.Equal(sma.Last.Value, smaResult[smaResult.Count - 1].Value, 1e-8);
}
[Fact]
public void StaticBatch_Works()
{
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var result = Bbw.Batch(ts, 20, 2.0);
Assert.Equal(100, result.Count);
Assert.True(double.IsFinite(result[result.Count - 1].Value));
}
[Fact]
public void StaticBatch_ValidatesInput()
{
var ts = new TSeries();
for (int i = 0; i < 10; i++)
{
ts.Add(new TValue(DateTime.UtcNow.AddMinutes(i), 100 + i));
}
Assert.Throws<ArgumentException>(() => Bbw.Batch(ts, 0));
Assert.Throws<ArgumentException>(() => Bbw.Batch(ts, -1));
Assert.Throws<ArgumentException>(() => Bbw.Batch(ts, 5, 0));
Assert.Throws<ArgumentException>(() => Bbw.Batch(ts, 5, -1));
}
[Fact]
public void Batch_NaN_Safe()
{
var values = new double[] { 100, 101, 102, double.NaN, 104, 105 };
var output = new double[values.Length];
Bbw.Batch(values, output, 3);
Assert.True(output.Length == 6);
}
[Fact]
public void BBW_Formula_Verified()
{
var bbw = new Bbw(5, 2.0);
double[] values = { 100, 102, 98, 101, 99 };
foreach (var v in values)
{
bbw.Update(new TValue(DateTime.UtcNow, v));
}
double mean = values.Average();
double variance = values.Select(v => (v - mean) * (v - mean)).Average();
double stddev = Math.Sqrt(variance);
double expectedBbw = (2.0 * 2.0 * stddev) / mean;
Assert.Equal(expectedBbw, bbw.Last.Value, 1e-10);
}
[Fact]
public void BBW_IncreasingVolatility_IncreasesWidth()
{
var bbw = new Bbw(10);
for (int i = 0; i < 10; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i * 0.1));
}
double lowVolatilityBbw = bbw.Last.Value;
bbw.Reset();
for (int i = 0; i < 10; i++)
{
bbw.Update(new TValue(DateTime.UtcNow, 100 + i * 10));
}
double highVolatilityBbw = bbw.Last.Value;
Assert.True(highVolatilityBbw > lowVolatilityBbw);
}
[Fact]
public void BBW_MultiplierEffect_Verified()
{
var bbw1 = new Bbw(10, 1.0);
var bbw2 = new Bbw(10, 2.0);
var bbw3 = new Bbw(10, 3.0);
var bars = GenerateTestData(20);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
bbw1.Update(new TValue(times[i], close[i]));
bbw2.Update(new TValue(times[i], close[i]));
bbw3.Update(new TValue(times[i], close[i]));
}
Assert.Equal(bbw1.Last.Value * 2.0, bbw2.Last.Value, 1e-10);
Assert.Equal(bbw1.Last.Value * 3.0, bbw3.Last.Value, 1e-10);
}
[Fact]
public void AlternatingValues_ProducesExpectedWidth()
{
var bbw = new Bbw(2, 2.0);
bbw.Update(new TValue(DateTime.UtcNow, 100));
bbw.Update(new TValue(DateTime.UtcNow, 110));
double expectedBbw = (2.0 * 2.0 * 5.0) / 105.0;
Assert.Equal(expectedBbw, bbw.Last.Value, 1e-10);
}
}
@@ -0,0 +1,270 @@
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
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");
}
// ── Cross-library: OoplesFinance ──────────────────────────────────────────
[Fact]
public void Bbw_MatchesOoples_Structural()
{
const int period = 20;
const double multiplier = 2.0;
var ooplesData = _testData.SkenderQuotes.Select(static 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 stockData = new StockData(ooplesData);
var oResult = stockData.CalculateBollingerBandsWidth(length: period);
var oValues = oResult.OutputValues.Values.First();
var bbw = new global::QuanTAlib.Bbw(period, multiplier);
var qValues = new List<double>();
foreach (var item in _testData.Data)
{
qValues.Add(bbw.Update(item).Value);
}
Assert.True(oValues.Count > 0, "Ooples BBW must produce output");
int finiteCount = 0;
for (int i = period; i < Math.Min(oValues.Count, qValues.Count); i++)
{
if (double.IsFinite(oValues[i]) && double.IsFinite(qValues[i]))
{
finiteCount++;
}
}
Assert.True(finiteCount > 100, $"Expected >100 finite BBW pairs, got {finiteCount}");
_output.WriteLine($"BBW Ooples structural: {finiteCount} finite pairs verified.");
}
}