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,217 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class BbwpIndicatorTests
{
[Fact]
public void BbwpIndicator_Constructor_SetsDefaults()
{
var indicator = new BbwpIndicator();
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("BBWP - Bollinger Band Width Percentile", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void BbwpIndicator_ShortName_IncludesParameters()
{
var indicator = new BbwpIndicator { Period = 14, Multiplier = 2.5, Lookback = 100 };
Assert.Contains("BBWP", 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 BbwpIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new BbwpIndicator();
Assert.Equal(0, BbwpIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void BbwpIndicator_Initialize_CreatesInternalBbwp()
{
var indicator = new BbwpIndicator();
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void BbwpIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new BbwpIndicator { 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); // BBWP should be in [0,1] range
}
[Fact]
public void BbwpIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new BbwpIndicator { 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 BbwpIndicator_DifferentPeriods_Work()
{
int[] periods = { 5, 10, 20, 50 };
foreach (var period in periods)
{
var indicator = new BbwpIndicator { 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 BBWP percentile in range");
}
}
[Fact]
public void BbwpIndicator_DifferentLookbacks_Work()
{
int[] lookbacks = { 10, 20, 50, 100 };
foreach (var lookback in lookbacks)
{
var indicator = new BbwpIndicator { 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 BBWP percentile in range");
}
}
[Fact]
public void BbwpIndicator_DifferentSourceTypes_Work()
{
SourceType[] sources = { SourceType.Close, SourceType.High, SourceType.Low, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new BbwpIndicator { 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 BbwpIndicator_Period_CanBeChanged()
{
var indicator = new BbwpIndicator();
Assert.Equal(20, indicator.Period);
indicator.Period = 14;
Assert.Equal(14, indicator.Period);
indicator.Period = 50;
Assert.Equal(50, indicator.Period);
}
[Fact]
public void BbwpIndicator_Lookback_CanBeChanged()
{
var indicator = new BbwpIndicator();
Assert.Equal(252, indicator.Lookback);
indicator.Lookback = 100;
Assert.Equal(100, indicator.Lookback);
indicator.Lookback = 50;
Assert.Equal(50, indicator.Lookback);
}
[Fact]
public void BbwpIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new BbwpIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void BbwpIndicator_SourceCodeLink_IsValid()
{
var indicator = new BbwpIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Bbwp.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
}
+486
View File
@@ -0,0 +1,486 @@
namespace QuanTAlib.Tests;
using Xunit;
public class BbwpTests
{
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 Bbwp(0));
Assert.Throws<ArgumentException>(() => new Bbwp(-1));
Assert.Throws<ArgumentException>(() => new Bbwp(20, 0));
Assert.Throws<ArgumentException>(() => new Bbwp(20, -1));
Assert.Throws<ArgumentException>(() => new Bbwp(20, 2.0, 0));
Assert.Throws<ArgumentException>(() => new Bbwp(20, 2.0, -1));
var valid = new Bbwp(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 bbwp = new Bbwp(20, 2.0, 252);
Assert.Equal(272, bbwp.WarmupPeriod); // period + lookback
Assert.True(bbwp.WarmupPeriod > 0);
}
[Fact]
public void Properties_Accessible()
{
var bbwp = new Bbwp(20, 2.5, 100);
Assert.Equal(20, bbwp.Period);
Assert.Equal(2.5, bbwp.Multiplier);
Assert.Equal(100, bbwp.Lookback);
Assert.Equal("Bbwp(20,2.5,100)", bbwp.Name);
}
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var bbwp = new Bbwp(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 = bbwp.Update(new TValue(times[i], close[i]));
Assert.True(double.IsFinite(result.Value), $"Invalid value at index {i}: {result.Value}");
}
Assert.True(bbwp.Last.Value >= 0.0, "BBWP should be >= 0");
Assert.True(bbwp.Last.Value <= 1.0, "BBWP should be <= 1");
}
[Fact]
public void IsHot_BehavesCorrectly()
{
var bbwp = new Bbwp(5, 2.0, 10);
var bars = GenerateTestData(20);
var close = bars.CloseValues;
// Should not be hot initially
Assert.False(bbwp.IsHot);
// Feed data until warm
for (int i = 0; i < 15; i++)
{
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + i, close[i]));
}
// Should be hot after sufficient data
Assert.True(bbwp.IsHot);
}
[Fact]
public void OutputRange_IsPercentile()
{
var bbwp = new Bbwp(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 = bbwp.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 percentile values
Assert.True(max - min > 0.1, "Should have meaningful variation in percentile values");
}
}
[Fact]
public void Update_IsNew_BehavesCorrectly()
{
var bbwp = new Bbwp(5, 2.0, 20);
var bars = GenerateTestData(30);
// First load up enough data to create variation
for (int i = 0; i < 25; i++)
{
bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]), isNew: true);
}
// First update (new)
var result1 = bbwp.Update(new TValue(bars.Times[25], bars.CloseValues[25]), isNew: true);
// Second update (revision) - with very different value to potentially change percentile
var revisedValue = new TValue(bars.Times[25], bars.CloseValues[25] * 1.5);
var result2 = bbwp.Update(revisedValue, isNew: false);
// After revision, the result might differ
Assert.True(double.IsFinite(result1.Value) && double.IsFinite(result2.Value));
}
[Fact]
public void Reset_ClearsState()
{
var bbwp = new Bbwp(5, 2.0, 20);
var bars = GenerateTestData(20);
var close = bars.CloseValues;
// Feed some data
for (int i = 0; i < 10; i++)
{
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + i, close[i]));
}
Assert.True(bbwp.Last.Value != 0.0);
// Reset and check
bbwp.Reset();
Assert.Equal(0.0, bbwp.Last.Value);
Assert.False(bbwp.IsHot);
}
[Fact]
public void Prime_LoadsDataCorrectly()
{
var bbwp = new Bbwp(5, 2.0, 20);
var bars = GenerateTestData(30);
var close = bars.CloseValues.ToArray();
bbwp.Prime(close);
Assert.True(bbwp.IsHot);
Assert.True(double.IsFinite(bbwp.Last.Value));
Assert.True(bbwp.Last.Value >= 0.0 && bbwp.Last.Value <= 1.0);
}
[Fact]
public void Batch_ProducesConsistentResults()
{
var bbwp = new Bbwp(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 = bbwp.Update(new TValue(times[i], close[i]));
updateResults.Add(result.Value);
}
// Calculate using Batch method
var batchResults = new double[bars.Count];
Bbwp.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 = Bbwp.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 bbwp = new Bbwp(5, 2.0, 20);
// Test with NaN
var result1 = bbwp.Update(new TValue(DateTime.UtcNow.Ticks, double.NaN));
Assert.True(double.IsFinite(result1.Value));
// Test with infinity
var result2 = bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 1, double.PositiveInfinity));
Assert.True(double.IsFinite(result2.Value));
// Test with negative infinity
var result3 = bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 2, double.NegativeInfinity));
Assert.True(double.IsFinite(result3.Value));
}
[Fact]
public void ZeroVarianceData_HandledCorrectly()
{
var bbwp = new Bbwp(5, 2.0, 20);
// Feed constant values (zero variance)
for (int i = 0; i < 30; i++)
{
var result = bbwp.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 bbwp = new Bbwp(3, 2.0, 5);
// Test with minimal data
for (int i = 0; i < 3; i++)
{
var result = bbwp.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 bbwp = new Bbwp(5, 2.0, 20);
// Test with large values
var largeValues = new[] { 1e6, 1e7, 1e8, 1e6, 1e7 };
foreach (var value in largeValues)
{
var result = bbwp.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 bbwpStream = new Bbwp(period, 2.0, lookback);
var bbwpBatch = new Bbwp(period, 2.0, lookback);
var bars = GenerateTestData(50);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
bbwpStream.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 = bbwpBatch.Update(ts);
Assert.Equal(bbwpStream.Last.Value, result[result.Count - 1].Value, 1e-9);
}
[Fact]
public void BatchCalc_MatchesIterativeCalc()
{
var bbwp = new Bbwp(10, 2.0, 30);
var bars = GenerateTestData(100);
var times = bars.Times;
var close = bars.CloseValues;
for (int i = 0; i < bars.Count; i++)
{
bbwp.Update(new TValue(times[i], close[i]));
}
var iterativeResult = bbwp.Last.Value;
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var batchResult = Bbwp.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 = Bbwp.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>(() => Bbwp.Batch(ts, 0));
Assert.Throws<ArgumentException>(() => Bbwp.Batch(ts, -1));
Assert.Throws<ArgumentException>(() => Bbwp.Batch(ts, 5, 0));
Assert.Throws<ArgumentException>(() => Bbwp.Batch(ts, 5, -1));
Assert.Throws<ArgumentException>(() => Bbwp.Batch(ts, 5, 2.0, 0));
Assert.Throws<ArgumentException>(() => Bbwp.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];
Bbwp.Batch(values, output, 3, 2.0, 3);
Assert.True(output.Length == 6);
}
[Fact]
public void BBWP_Percentile_Verified()
{
var bbwp = new Bbwp(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++)
{
bbwp.Update(new TValue(times[i], close[i]));
}
// Result should be between 0 and 1
Assert.True(bbwp.Last.Value >= 0.0);
Assert.True(bbwp.Last.Value <= 1.0);
}
[Fact]
public void BBWP_HighVolatility_HigherPercentile()
{
var bbwp = new Bbwp(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 = bbwp.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(bbwp.IsHot);
}
[Fact]
public void BBWP_LookbackEffect_Verified()
{
var bars = GenerateTestData(100);
// Short lookback
var bbwp1 = new Bbwp(10, 2.0, 20);
// Long lookback
var bbwp2 = new Bbwp(10, 2.0, 50);
for (int i = 0; i < bars.Count; i++)
{
bbwp1.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
bbwp2.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
}
// Both should be in valid range
Assert.True(bbwp1.Last.Value >= 0.0 && bbwp1.Last.Value <= 1.0);
Assert.True(bbwp2.Last.Value >= 0.0 && bbwp2.Last.Value <= 1.0);
// They may differ due to different historical context
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var bbwp = new Bbwp(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 = bbwp.Update(new TValue(times[i], close[i]), isNew: true);
}
double originalValue = lastValue.Value;
// Test with a much more extreme correction value
_ = bbwp.Update(new TValue(DateTime.UtcNow.Ticks, close[bars.Count - 1] * 100), isNew: false);
// Restore to original and verify exact match
var restoredValue = bbwp.Update(new TValue(lastValue.Time, close[bars.Count - 1]), isNew: false);
Assert.Equal(originalValue, restoredValue.Value, 1e-9);
}
[Fact]
public void IsNew_Consistency()
{
var bbwp = new Bbwp(5, 2.0, 10);
for (int i = 0; i < 20; i++)
{
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + i, 100 + i), isNew: true);
}
var result1 = bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 100, 120), isNew: true);
_ = bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 100, 150), isNew: false);
var result3 = bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 100, 120), isNew: false);
Assert.Equal(result1.Value, result3.Value, Tolerance);
}
}
@@ -0,0 +1,225 @@
namespace QuanTAlib.Tests;
using Xunit;
/// <summary>
/// Validation tests for BBWP (Bollinger Band Width Percentile).
/// BBWP is a proprietary indicator, so we validate against internal consistency
/// and mathematical properties rather than external libraries.
/// </summary>
public class BbwpValidationTests
{
private static TBarSeries GenerateTestData(int count = 500)
{
var gbm = new GBM(seed: 42);
return gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
[Fact]
public void BBWP_OutputRange_AlwaysValid()
{
var bars = GenerateTestData(500);
var bbwp = new Bbwp(20, 2.0, 100);
for (int i = 0; i < bars.Count; i++)
{
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
Assert.True(result.Value >= 0.0, $"BBWP at {i} should be >= 0, got {result.Value}");
Assert.True(result.Value <= 1.0, $"BBWP at {i} should be <= 1, got {result.Value}");
}
}
[Fact]
public void BBWP_StreamingVsBatch_Match()
{
var bars = GenerateTestData(200);
var times = bars.Times;
var close = bars.CloseValues;
// Streaming calculation
var bbwpStream = new Bbwp(10, 2.0, 50);
var streamResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
var result = bbwpStream.Update(new TValue(times[i], close[i]));
streamResults.Add(result.Value);
}
// Batch calculation
var ts = new TSeries();
for (int i = 0; i < bars.Count; i++)
{
ts.Add(new TValue(times[i], close[i]));
}
var batchResults = Bbwp.Batch(ts, 10, 2.0, 50);
// Compare results (should be identical)
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamResults[i], batchResults.Values[i], 1e-10);
}
}
[Fact]
public void BBWP_DifferentPeriods_ProduceValidResults()
{
var bars = GenerateTestData(300);
int[] periods = { 5, 10, 20, 50 };
foreach (int period in periods)
{
var bbwp = new Bbwp(period, 2.0, 100);
for (int i = 0; i < bars.Count; i++)
{
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
Assert.True(double.IsFinite(result.Value), $"Period {period} at {i} should be finite");
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Period {period} at {i} should be in [0,1]");
}
}
}
[Fact]
public void BBWP_DifferentLookbacks_ProduceValidResults()
{
var bars = GenerateTestData(300);
int[] lookbacks = { 20, 50, 100, 200 };
foreach (int lookback in lookbacks)
{
var bbwp = new Bbwp(20, 2.0, lookback);
for (int i = 0; i < bars.Count; i++)
{
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
Assert.True(double.IsFinite(result.Value), $"Lookback {lookback} at {i} should be finite");
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Lookback {lookback} at {i} should be in [0,1]");
}
}
}
[Fact]
public void BBWP_DifferentMultipliers_ProduceValidResults()
{
var bars = GenerateTestData(200);
double[] multipliers = { 1.0, 1.5, 2.0, 2.5, 3.0 };
foreach (double mult in multipliers)
{
var bbwp = new Bbwp(20, mult, 100);
for (int i = 0; i < bars.Count; i++)
{
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
Assert.True(double.IsFinite(result.Value), $"Multiplier {mult} at {i} should be finite");
Assert.True(result.Value >= 0.0 && result.Value <= 1.0, $"Multiplier {mult} at {i} should be in [0,1]");
}
}
}
[Fact]
public void BBWP_ConstantInput_ProducesZeroPercentile()
{
var bbwp = new Bbwp(10, 2.0, 50);
// Feed constant values - BBW will be 0, and percentile of 0 among 0s is 0
for (int i = 0; i < 100; i++)
{
var result = bbwp.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);
}
// With constant input, BBW=0 always, so percentile should be 0 (nothing below 0)
Assert.Equal(0.0, bbwp.Last.Value, 1e-10);
}
[Fact]
public void BBWP_HighVolatilitySpike_ProducesHighPercentile()
{
var bbwp = new Bbwp(5, 2.0, 20);
// Feed low volatility data first
for (int i = 0; i < 25; i++)
{
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + i, 100.0 + (i % 2) * 0.1));
}
// Then introduce a high volatility spike
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 25, 100.0));
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 26, 110.0)); // Big move
bbwp.Update(new TValue(DateTime.UtcNow.Ticks + 27, 105.0));
// After high volatility, percentile should be elevated
Assert.True(bbwp.Last.Value > 0.3, $"High volatility should produce elevated percentile, got {bbwp.Last.Value}");
}
[Fact]
public void BBWP_PercentileDistribution_Reasonable()
{
var bars = GenerateTestData(500);
var bbwp = new Bbwp(20, 2.0, 100);
var results = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
var result = bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]));
if (i >= 120) // After warmup
{
results.Add(result.Value);
}
}
// Percentile values should be distributed - check quartiles
results.Sort();
int q1Idx = results.Count / 4;
int q3Idx = 3 * results.Count / 4;
double q1 = results[q1Idx];
double q3 = results[q3Idx];
// Should have meaningful spread
Assert.True(q3 - q1 > 0.1, $"Percentile spread should be meaningful, Q1={q1:F3}, Q3={q3:F3}");
}
[Fact]
public void BBWP_BarCorrection_Works()
{
var bbwp = new Bbwp(10, 2.0, 30);
var bars = GenerateTestData(50);
// Process all bars
for (int i = 0; i < bars.Count; i++)
{
bbwp.Update(new TValue(bars.Times[i], bars.CloseValues[i]), isNew: true);
}
double originalValue = bbwp.Last.Value;
// Correct the last bar with different value
bbwp.Update(new TValue(bars.Times[bars.Count - 1], bars.CloseValues[bars.Count - 1] * 2), isNew: false);
// Restore original value
var restored = bbwp.Update(new TValue(bars.Times[bars.Count - 1], bars.CloseValues[bars.Count - 1]), isNew: false);
Assert.Equal(originalValue, restored.Value, 1e-10);
}
[Fact]
public void BBWP_SpanBatch_MatchesStreaming()
{
var bars = GenerateTestData(100);
var close = bars.CloseValues.ToArray();
// Streaming
var bbwpStream = new Bbwp(10, 2.0, 30);
for (int i = 0; i < close.Length; i++)
{
bbwpStream.Update(new TValue(DateTime.UtcNow.Ticks + i, close[i]));
}
// Batch via span
var output = new double[close.Length];
Bbwp.Batch(close, output, 10, 2.0, 30);
Assert.Equal(bbwpStream.Last.Value, output[output.Length - 1], 1e-10);
}
}