mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user