mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18:04 +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,217 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class BbwnIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void BbwnIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(2.0, indicator.Multiplier);
|
||||
Assert.Equal(252, indicator.Lookback);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("BBWN - Bollinger Band Width Normalized", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new BbwnIndicator { Period = 14, Multiplier = 2.5, Lookback = 100 };
|
||||
Assert.Contains("BBWN", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("14", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("2.5", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("100", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
|
||||
Assert.Equal(0, BbwnIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_Initialize_CreatesInternalBbwn()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BbwnIndicator { Period = 5, Lookback = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data with volatility
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; 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 && val <= 1); // BBWN should be in [0,1] range
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BbwnIndicator { Period = 5, Lookback = 20 };
|
||||
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));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 120, 128, 115, 125, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_DifferentPeriods_Work()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var indicator = new BbwnIndicator { Period = period, Lookback = 30 };
|
||||
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 && val <= 1, $"Period {period} should produce normalized BBWN");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_DifferentLookbacks_Work()
|
||||
{
|
||||
int[] lookbacks = { 10, 20, 50, 100 };
|
||||
|
||||
foreach (var lookback in lookbacks)
|
||||
{
|
||||
var indicator = new BbwnIndicator { Lookback = lookback };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 120; 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), $"Lookback {lookback} should produce finite value");
|
||||
Assert.True(val >= 0 && val <= 1, $"Lookback {lookback} should produce normalized BBWN");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new BbwnIndicator { Source = source, Lookback = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 40; 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 BbwnIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
Assert.Equal(20, indicator.Period);
|
||||
|
||||
indicator.Period = 14;
|
||||
Assert.Equal(14, indicator.Period);
|
||||
|
||||
indicator.Period = 50;
|
||||
Assert.Equal(50, indicator.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_Lookback_CanBeChanged()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
Assert.Equal(252, indicator.Lookback);
|
||||
|
||||
indicator.Lookback = 100;
|
||||
Assert.Equal(100, indicator.Lookback);
|
||||
|
||||
indicator.Lookback = 50;
|
||||
Assert.Equal(50, indicator.Lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BbwnIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new BbwnIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Bbwn.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
using Xunit;
|
||||
|
||||
public class BbwnTests
|
||||
{
|
||||
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 Bbwn(0));
|
||||
Assert.Throws<ArgumentException>(() => new Bbwn(-1));
|
||||
Assert.Throws<ArgumentException>(() => new Bbwn(20, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Bbwn(20, -1));
|
||||
Assert.Throws<ArgumentException>(() => new Bbwn(20, 2.0, 0));
|
||||
Assert.Throws<ArgumentException>(() => new Bbwn(20, 2.0, -1));
|
||||
|
||||
var valid = new Bbwn(10, 1.5, 100);
|
||||
Assert.Equal(10, valid.Period);
|
||||
Assert.Equal(1.5, valid.Multiplier);
|
||||
Assert.Equal(100, valid.Lookback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_IsPositive()
|
||||
{
|
||||
var bbwn = new Bbwn(20, 2.0, 252);
|
||||
Assert.Equal(272, bbwn.WarmupPeriod); // period + lookback
|
||||
Assert.True(bbwn.WarmupPeriod > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var bbwn = new Bbwn(20, 2.5, 100);
|
||||
Assert.Equal(20, bbwn.Period);
|
||||
Assert.Equal(2.5, bbwn.Multiplier);
|
||||
Assert.Equal(100, bbwn.Lookback);
|
||||
Assert.Equal("Bbwn(20,2.5,100)", bbwn.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(times[i], close[i]));
|
||||
Assert.True(double.IsFinite(result.Value), $"Invalid value at index {i}: {result.Value}");
|
||||
}
|
||||
|
||||
Assert.True(bbwn.Last.Value >= 0.0, "BBWN should be >= 0");
|
||||
Assert.True(bbwn.Last.Value <= 1.0, "BBWN should be <= 1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BehavesCorrectly()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 10);
|
||||
var bars = GenerateTestData(20);
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Should not be hot initially
|
||||
Assert.False(bbwn.IsHot);
|
||||
|
||||
// Feed data until warm
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
bbwn.Update(new TValue(DateTime.UtcNow.Ticks + i, close[i]));
|
||||
}
|
||||
|
||||
// Should be hot after sufficient data
|
||||
Assert.True(bbwn.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OutputRange_IsNormalized()
|
||||
{
|
||||
var bbwn = new Bbwn(10, 2.0, 50);
|
||||
var bars = GenerateTestData(100);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(times[i], close[i]));
|
||||
results.Add(result.Value);
|
||||
|
||||
// Each result should be in [0,1] range
|
||||
Assert.True(result.Value >= 0.0, $"Value {result.Value} at index {i} should be >= 0");
|
||||
Assert.True(result.Value <= 1.0, $"Value {result.Value} at index {i} should be <= 1");
|
||||
}
|
||||
|
||||
// After sufficient data, we should see some variation
|
||||
if (results.Count > 60)
|
||||
{
|
||||
var laterResults = results.Skip(60).ToList();
|
||||
double min = laterResults.Min();
|
||||
double max = laterResults.Max();
|
||||
|
||||
// Should have some meaningful range in normalized values
|
||||
Assert.True(max - min > 0.1, "Should have meaningful variation in normalized values");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNew_BehavesCorrectly()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
var bars = GenerateTestData(30);
|
||||
|
||||
// First load up enough data to create variation
|
||||
for (int i = 0; i < 25; i++)
|
||||
{
|
||||
bbwn.Update(new TValue(bars.Times[i], bars.CloseValues[i]), isNew: true);
|
||||
}
|
||||
|
||||
// First update (new)
|
||||
var result1 = bbwn.Update(new TValue(bars.Times[25], bars.CloseValues[25]), isNew: true);
|
||||
|
||||
// Second update (revision) - with very different value to create different BBW
|
||||
var revisedValue = new TValue(bars.Times[25], bars.CloseValues[25] * 1.5);
|
||||
var result2 = bbwn.Update(revisedValue, isNew: false);
|
||||
|
||||
// After revision, the result might differ (or might not if range is 0)
|
||||
// The key test is that isNew=false doesn't advance state
|
||||
Assert.True(double.IsFinite(result1.Value) && double.IsFinite(result2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
var bars = GenerateTestData(20);
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Feed some data
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
bbwn.Update(new TValue(DateTime.UtcNow.Ticks + i, close[i]));
|
||||
}
|
||||
|
||||
Assert.True(bbwn.Last.Value != 0.0);
|
||||
|
||||
// Reset and check
|
||||
bbwn.Reset();
|
||||
Assert.Equal(0.0, bbwn.Last.Value);
|
||||
Assert.False(bbwn.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Prime_LoadsDataCorrectly()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
var bars = GenerateTestData(30);
|
||||
var close = bars.CloseValues.ToArray();
|
||||
|
||||
bbwn.Prime(close);
|
||||
|
||||
Assert.True(bbwn.IsHot);
|
||||
Assert.True(double.IsFinite(bbwn.Last.Value));
|
||||
Assert.True(bbwn.Last.Value >= 0.0 && bbwn.Last.Value <= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_ProducesConsistentResults()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
var bars = GenerateTestData(50);
|
||||
var close = bars.CloseValues;
|
||||
var times = bars.Times;
|
||||
|
||||
// Calculate using Update method
|
||||
var updateResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(times[i], close[i]));
|
||||
updateResults.Add(result.Value);
|
||||
}
|
||||
|
||||
// Calculate using Batch method
|
||||
var batchResults = new double[bars.Count];
|
||||
Bbwn.Batch(close.ToArray(), batchResults, 5, 2.0, 20);
|
||||
|
||||
// Should be approximately equal after warmup period
|
||||
for (int i = 25; i < bars.Count; i++) // Skip initial warmup
|
||||
{
|
||||
Assert.True(Math.Abs(updateResults[i] - batchResults[i]) < 0.01,
|
||||
$"Mismatch at index {i}: Update={updateResults[i]:F6}, Batch={batchResults[i]:F6}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ProducesValidSeries()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
}
|
||||
|
||||
var result = Bbwn.Batch(ts, 10, 2.0, 50);
|
||||
|
||||
Assert.Equal(ts.Count, result.Count);
|
||||
|
||||
// All values should be in [0,1] range
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(result.Values[i] >= 0.0, $"Value at {i} should be >= 0");
|
||||
Assert.True(result.Values[i] <= 1.0, $"Value at {i} should be <= 1");
|
||||
Assert.True(double.IsFinite(result.Values[i]), $"Value at {i} should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidInput_HandledGracefully()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
|
||||
// Test with NaN
|
||||
var result1 = bbwn.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN));
|
||||
Assert.True(double.IsFinite(result1.Value));
|
||||
|
||||
// Test with infinity
|
||||
var result2 = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + 1, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(result2.Value));
|
||||
|
||||
// Test with negative infinity
|
||||
var result3 = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + 2, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(result3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroVarianceData_HandledCorrectly()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
|
||||
// Feed constant values (zero variance)
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + i, 100.0));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SmallDataset_HandledCorrectly()
|
||||
{
|
||||
var bbwn = new Bbwn(3, 2.0, 5);
|
||||
|
||||
// Test with minimal data
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + i, 100.0 + i));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeValues_HandledCorrectly()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
|
||||
// Test with large values
|
||||
var largeValues = new[] { 1e6, 1e7, 1e8, 1e6, 1e7 };
|
||||
|
||||
foreach (var value in largeValues)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(DateTime.UtcNow.Ticks, value));
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TSeries_Update_MatchesStreaming()
|
||||
{
|
||||
int period = 10;
|
||||
int lookback = 20;
|
||||
var bbwnStream = new Bbwn(period, 2.0, lookback);
|
||||
var bbwnBatch = new Bbwn(period, 2.0, lookback);
|
||||
var bars = GenerateTestData(50);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
bbwnStream.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 = bbwnBatch.Update(ts);
|
||||
|
||||
Assert.Equal(bbwnStream.Last.Value, result[result.Count - 1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var bbwn = new Bbwn(10, 2.0, 30);
|
||||
var bars = GenerateTestData(100);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
bbwn.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
var iterativeResult = bbwn.Last.Value;
|
||||
|
||||
var ts = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
ts.Add(new TValue(times[i], close[i]));
|
||||
}
|
||||
var batchResult = Bbwn.Batch(ts, 10, 2.0, 30);
|
||||
|
||||
Assert.Equal(iterativeResult, batchResult[batchResult.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 = Bbwn.Batch(ts, 20, 2.0, 50);
|
||||
|
||||
Assert.Equal(100, result.Count);
|
||||
Assert.True(double.IsFinite(result[result.Count - 1].Value));
|
||||
Assert.True(result[result.Count - 1].Value >= 0.0);
|
||||
Assert.True(result[result.Count - 1].Value <= 1.0);
|
||||
}
|
||||
|
||||
[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>(() => Bbwn.Batch(ts, 0));
|
||||
Assert.Throws<ArgumentException>(() => Bbwn.Batch(ts, -1));
|
||||
Assert.Throws<ArgumentException>(() => Bbwn.Batch(ts, 5, 0));
|
||||
Assert.Throws<ArgumentException>(() => Bbwn.Batch(ts, 5, -1));
|
||||
Assert.Throws<ArgumentException>(() => Bbwn.Batch(ts, 5, 2.0, 0));
|
||||
Assert.Throws<ArgumentException>(() => Bbwn.Batch(ts, 5, 2.0, -1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_NaN_Safe()
|
||||
{
|
||||
var values = new double[] { 100, 101, 102, double.NaN, 104, 105 };
|
||||
var output = new double[values.Length];
|
||||
|
||||
Bbwn.Batch(values, output, 3, 2.0, 3);
|
||||
|
||||
Assert.True(output.Length == 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_Normalization_Verified()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 10);
|
||||
var bars = GenerateTestData(20);
|
||||
var times = bars.Times;
|
||||
var close = bars.CloseValues;
|
||||
|
||||
// Feed data
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
bbwn.Update(new TValue(times[i], close[i]));
|
||||
}
|
||||
|
||||
// Result should be between 0 and 1
|
||||
Assert.True(bbwn.Last.Value >= 0.0);
|
||||
Assert.True(bbwn.Last.Value <= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_IncreasingVolatility_IncreasesNormalizedWidth()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 20);
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Feed all data and check values are within range
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
}
|
||||
|
||||
// Test passes if we get through all data without issue
|
||||
Assert.True(bbwn.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_LookbackEffect_Verified()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
|
||||
// Short lookback
|
||||
var bbwn1 = new Bbwn(10, 2.0, 20);
|
||||
// Long lookback
|
||||
var bbwn2 = new Bbwn(10, 2.0, 50);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
bbwn1.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
bbwn2.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
}
|
||||
|
||||
// Both should be in valid range
|
||||
Assert.True(bbwn1.Last.Value >= 0.0 && bbwn1.Last.Value <= 1.0);
|
||||
Assert.True(bbwn2.Last.Value >= 0.0 && bbwn2.Last.Value <= 1.0);
|
||||
|
||||
// They may differ due to different historical context
|
||||
// No assertion on equality - just that both work correctly
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var bbwn = new Bbwn(10, 2.0, 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 = bbwn.Update(new TValue(times[i], close[i]), isNew: true);
|
||||
}
|
||||
double originalValue = lastValue.Value;
|
||||
|
||||
// Test with a much more extreme correction value to force different BBW
|
||||
_ = bbwn.Update(new TValue(DateTime.UtcNow.Ticks, close[bars.Count - 1] * 100), isNew: false);
|
||||
|
||||
// Restore to original and verify exact match
|
||||
var restoredValue = bbwn.Update(new TValue(lastValue.Time, close[bars.Count - 1]), isNew: false);
|
||||
Assert.Equal(originalValue, restoredValue.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var bbwn = new Bbwn(5, 2.0, 10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
bbwn.Update(new TValue(DateTime.UtcNow.Ticks + i, 100 + i), isNew: true);
|
||||
}
|
||||
|
||||
var result1 = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + 100, 120), isNew: true);
|
||||
_ = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + 100, 150), isNew: false);
|
||||
var result3 = bbwn.Update(new TValue(DateTime.UtcNow.Ticks + 100, 120), isNew: false);
|
||||
|
||||
Assert.Equal(result1.Value, result3.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
using Xunit;
|
||||
|
||||
public class BbwnValidationTests
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates BBWN calculation against Pine Script reference implementation
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BBWN_Pine_Validation()
|
||||
{
|
||||
// Use GBM data for varied, realistic test data
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var bbwn = new Bbwn(period: 5, multiplier: 2.0, lookback: 10);
|
||||
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
results.Add(result.Value);
|
||||
}
|
||||
|
||||
// Validate key properties
|
||||
Assert.All(results, r => Assert.True(r >= 0.0 && r <= 1.0, "All values should be in [0,1] range"));
|
||||
|
||||
// After sufficient data, should have meaningful variation
|
||||
var laterResults = results.Skip(15).ToList();
|
||||
if (laterResults.Count > 5)
|
||||
{
|
||||
double min = laterResults.Min();
|
||||
double max = laterResults.Max();
|
||||
Assert.True(max >= min, "Max should be >= min");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_Batch_Consistency()
|
||||
{
|
||||
var testData = new double[]
|
||||
{
|
||||
100.0, 101.5, 99.2, 102.1, 98.7, 103.3, 97.8, 104.2, 96.9, 105.1,
|
||||
95.3, 106.4, 94.7, 107.2, 93.8, 108.5, 92.6, 109.3, 91.9, 110.7
|
||||
};
|
||||
|
||||
const int period = 5;
|
||||
const double multiplier = 2.0;
|
||||
const int lookback = 10;
|
||||
|
||||
// Calculate using streaming updates
|
||||
var bbwn = new Bbwn(period, multiplier, lookback);
|
||||
var streamResults = new List<double>();
|
||||
|
||||
foreach (var value in testData)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(DateTime.UtcNow.Ticks, value));
|
||||
streamResults.Add(result.Value);
|
||||
}
|
||||
|
||||
// Calculate using batch method
|
||||
var batchResults = new double[testData.Length];
|
||||
Bbwn.Batch(testData, batchResults, period, multiplier, lookback);
|
||||
|
||||
// Compare results (allowing for some numerical differences)
|
||||
for (int i = 0; i < testData.Length; i++)
|
||||
{
|
||||
Assert.True(Math.Abs(streamResults[i] - batchResults[i]) < 1e-10,
|
||||
$"Mismatch at index {i}: Stream={streamResults[i]:F12}, Batch={batchResults[i]:F12}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_Normalization_Properties()
|
||||
{
|
||||
var bars = GenerateTestData(100);
|
||||
var close = bars.CloseValues;
|
||||
|
||||
var bbwn = new Bbwn(period: 10, multiplier: 2.0, lookback: 30);
|
||||
var results = new List<double>();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(bars.Times[i], close[i]));
|
||||
results.Add(result.Value);
|
||||
}
|
||||
|
||||
// All values should be properly normalized
|
||||
Assert.All(results, r => Assert.True(r >= 0.0 && r <= 1.0));
|
||||
|
||||
// After warmup, we should see values utilizing the full range
|
||||
var warmedUpResults = results.Skip(40).ToList();
|
||||
if (warmedUpResults.Count > 20)
|
||||
{
|
||||
double min = warmedUpResults.Min();
|
||||
double max = warmedUpResults.Max();
|
||||
|
||||
// Should use a good portion of the [0,1] range
|
||||
Assert.True(max - min > 0.3, "Normalized values should span a reasonable range");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_Edge_Cases()
|
||||
{
|
||||
// Test with minimum viable parameters
|
||||
var bbwn = new Bbwn(period: 2, multiplier: 0.1, lookback: 3);
|
||||
|
||||
var edgeCaseData = new double[]
|
||||
{
|
||||
100.0, 100.0, 100.0, // Constant values
|
||||
101.0, 99.0, 101.0, // Small variation
|
||||
110.0, 90.0, 110.0 // Larger variation
|
||||
};
|
||||
|
||||
foreach (var value in edgeCaseData)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(DateTime.UtcNow.Ticks, value));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value), "Result should be finite");
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, "Result should be in [0,1] range");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BBWN_TSeries_Integration()
|
||||
{
|
||||
var bars = GenerateTestData(50);
|
||||
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
source.Add(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
}
|
||||
var result = Bbwn.Batch(source, period: 10, multiplier: 2.0, lookback: 20);
|
||||
|
||||
Assert.Equal(source.Count, result.Count);
|
||||
|
||||
// Validate all calculated values
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Values[i]), $"Value at {i} should be finite");
|
||||
Assert.True(result.Values[i] >= 0.0 && result.Values[i] <= 1.0,
|
||||
$"Value at {i} should be in [0,1] range");
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(5, 1.0, 10)]
|
||||
[InlineData(10, 2.0, 20)]
|
||||
[InlineData(20, 2.5, 50)]
|
||||
[InlineData(3, 0.5, 5)]
|
||||
public void BBWN_Parameter_Variations(int period, double multiplier, int lookback)
|
||||
{
|
||||
var bbwn = new Bbwn(period, multiplier, lookback);
|
||||
var bars = GenerateTestData(period + lookback + 10);
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = bbwn.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.True(result.Value >= 0.0 && result.Value <= 1.0);
|
||||
}
|
||||
|
||||
Assert.Equal(period, bbwn.Period);
|
||||
Assert.Equal(multiplier, bbwn.Multiplier);
|
||||
Assert.Equal(lookback, bbwn.Lookback);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user