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;
namespace QuanTAlib.Tests;
public class BwmaIndicatorTests
{
[Fact]
public void BwmaIndicator_Constructor_SetsDefaults()
{
var indicator = new BwmaIndicator();
Assert.Equal(10, indicator.Period);
Assert.Equal(0, indicator.Order);
Assert.Equal(SourceType.Close, indicator.Source);
Assert.True(indicator.ShowColdValues);
Assert.Equal("BWMA - Bessel-Weighted Moving Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void BwmaIndicator_MinHistoryDepths_IsZero()
{
var indicator = new BwmaIndicator { Period = 20 };
Assert.Equal(0, BwmaIndicator.MinHistoryDepths);
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void BwmaIndicator_ShortName_IncludesPeriodOrderAndSource()
{
var indicator = new BwmaIndicator { Period = 15, Order = 2 };
Assert.Contains("BWMA", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("2", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void BwmaIndicator_SourceCodeLink_IsValid()
{
var indicator = new BwmaIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Bwma.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void BwmaIndicator_Initialize_CreatesInternalBwma()
{
var indicator = new BwmaIndicator { Period = 10 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void BwmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new BwmaIndicator { Period = 3 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
// Process update
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
Assert.True(indicator.LinesSeries[0].Count > 0);
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
}
[Fact]
public void BwmaIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new BwmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void BwmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new BwmaIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 50; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 102);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double firstValue = indicator.LinesSeries[0].GetValue(0);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
double secondValue = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(firstValue));
Assert.True(double.IsFinite(secondValue));
}
[Fact]
public void BwmaIndicator_MultipleUpdates_ProducesCorrectSequence()
{
var indicator = new BwmaIndicator { Period = 3, Order = 0 };
indicator.Initialize();
var now = DateTime.UtcNow;
double[] closes = { 100, 102, 104, 103, 105 };
foreach (var close in closes)
{
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
now = now.AddMinutes(1);
}
// All values should be finite
for (int i = 0; i < closes.Length; i++)
{
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
}
// BWMA result should be in reasonable range
double lastBwma = indicator.LinesSeries[0].GetValue(0);
Assert.True(lastBwma >= 100 && lastBwma <= 110);
}
[Fact]
public void BwmaIndicator_DifferentSourceTypes_Work()
{
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
foreach (var source in sources)
{
var indicator = new BwmaIndicator { Period = 3, Source = source };
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Source {source} should produce finite value");
}
}
[Fact]
public void BwmaIndicator_DifferentOrders_Work()
{
int[] orders = { 0, 1, 2, 3 };
foreach (var order in orders)
{
var indicator = new BwmaIndicator { Period = 5, Order = order };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
$"Order {order} should produce finite value");
}
}
[Fact]
public void BwmaIndicator_Period_CanBeChanged()
{
var indicator = new BwmaIndicator { Period = 5 };
Assert.Equal(5, indicator.Period);
indicator.Period = 20;
Assert.Equal(20, indicator.Period);
Assert.Equal(0, BwmaIndicator.MinHistoryDepths);
}
[Fact]
public void BwmaIndicator_Order_CanBeChanged()
{
var indicator = new BwmaIndicator { Order = 0 };
Assert.Equal(0, indicator.Order);
indicator.Order = 3;
Assert.Equal(3, indicator.Order);
}
[Fact]
public void BwmaIndicator_DescriptionIsSet()
{
var indicator = new BwmaIndicator();
Assert.Contains("Bessel", indicator.Description, StringComparison.Ordinal);
}
}
+352
View File
@@ -0,0 +1,352 @@
namespace QuanTAlib;
public class BwmaTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var bwma = new Bwma(10);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
bwma.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(bwma.Last.Value));
}
[Fact]
public void DifferentOrders_ProduceDifferentResults()
{
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var bwma0 = new Bwma(10, 0);
var bwma1 = new Bwma(10, 1);
var bwma3 = new Bwma(10, 3);
for (int i = 0; i < series.Count; i++)
{
bwma0.Update(series[i]);
bwma1.Update(series[i]);
bwma3.Update(series[i]);
}
// Different orders should produce different results
// Note: order 1 and 2 both use power=1.5 (PineScript special cases)
Assert.NotEqual(bwma0.Last.Value, bwma1.Last.Value, 1e-9);
Assert.NotEqual(bwma1.Last.Value, bwma3.Last.Value, 1e-9);
}
[Fact]
public void IsNew_Consistency()
{
var bwma = new Bwma(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < 99; i++)
{
bwma.Update(new TValue(bars[i].Time, bars[i].Close));
}
bwma.Update(new TValue(bars[99].Time, bars[99].Close), true);
var val2 = bwma.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), false);
var bwma2 = new Bwma(10);
for (int i = 0; i < 99; i++)
{
bwma2.Update(new TValue(bars[i].Time, bars[i].Close));
}
var val3 = bwma2.Update(new TValue(bars[99].Time, bars[99].Close + 1.0), true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var bwma = new Bwma(10);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
bwma.Update(new TValue(bars[i].Time, bars[i].Close));
}
bwma.Reset();
Assert.Equal(0, bwma.Last.Value);
Assert.False(bwma.IsHot);
for (int i = 0; i < bars.Count; i++)
{
bwma.Update(new TValue(bars[i].Time, bars[i].Close));
}
Assert.True(double.IsFinite(bwma.Last.Value));
}
[Fact]
public void TSeries_Update_Matches_Streaming()
{
var bwma = new Bwma(10);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(bwma.Update(series[i]).Value);
}
var bwma2 = new Bwma(10);
var seriesResults = bwma2.Update(series);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticBatch_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var bwma = new Bwma(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(bwma.Update(series[i]).Value);
}
var staticResults = Bwma.Batch(series, 10);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticBatchSpan_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var bwma = new Bwma(10);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(bwma.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Bwma.Batch(series.Values, spanResults, 10);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void StaticBatchSpan_WithOrder_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var bwma = new Bwma(10, 2);
var streamingResults = new List<double>();
for (int i = 0; i < series.Count; i++)
{
streamingResults.Add(bwma.Update(series[i]).Value);
}
var spanResults = new double[series.Count];
Bwma.Batch(series.Values, spanResults, 10, 2);
for (int i = 0; i < spanResults.Length; i++)
{
Assert.Equal(streamingResults[i], spanResults[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var bwma = new Bwma(10);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var series = bars.Close;
var result = bwma.Update(series);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
var result2 = bwma.Update(series[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Bwma(0));
Assert.Throws<ArgumentException>(() => new Bwma(-1));
Assert.Throws<ArgumentOutOfRangeException>(() => new Bwma(10, -1));
}
[Fact]
public void Dispose_UnsubscribesFromSource()
{
// Create a custom publisher that fires events
var publisher = new TestPublisher();
var bwma = new Bwma(publisher, 3); // Use small period
// Feed enough values to get a stable result
publisher.Publish(new TValue(DateTime.UtcNow, 100));
publisher.Publish(new TValue(DateTime.UtcNow, 100));
publisher.Publish(new TValue(DateTime.UtcNow, 100));
var lastBeforeDispose = bwma.Last.Value;
Assert.True(double.IsFinite(lastBeforeDispose));
bwma.Dispose();
publisher.Publish(new TValue(DateTime.UtcNow, 200));
// After dispose, indicator should not update
Assert.Equal(lastBeforeDispose, bwma.Last.Value);
}
[Fact]
public void NaN_Handling_Works()
{
var bwma = new Bwma(3);
// First valid values
bwma.Update(new TValue(DateTime.UtcNow, 1.0));
bwma.Update(new TValue(DateTime.UtcNow, 2.0));
// Then NaN - should use last valid value
bwma.Update(new TValue(DateTime.UtcNow, double.NaN));
Assert.True(double.IsFinite(bwma.Last.Value));
// Continue with valid values
bwma.Update(new TValue(DateTime.UtcNow, 3.0));
Assert.True(double.IsFinite(bwma.Last.Value));
}
[Fact]
public void InitialNaN_HandledGracefully()
{
var bwma = new Bwma(3);
// When first value is NaN and no valid value exists, the result depends on weights
// Edge weights may be 0, causing NaN*0 to produce 0 rather than NaN
var result = bwma.Update(new TValue(DateTime.UtcNow, double.NaN));
// Just verify it doesn't crash and produces a finite value or NaN
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
// After valid values, indicator should work normally
bwma.Update(new TValue(DateTime.UtcNow, 100.0));
bwma.Update(new TValue(DateTime.UtcNow, 100.0));
var finalResult = bwma.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.True(double.IsFinite(finalResult.Value));
}
// Helper class for testing event-based subscription
private sealed class TestPublisher : ITValuePublisher
{
public event TValuePublishedHandler? Pub;
public void Publish(TValue value)
{
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = true });
}
}
[Fact]
public void Order0_IsParabolic()
{
// For order 0, weights are (1 - x²) which forms a parabola
var bwma = new Bwma(5, 0);
// Feed simple values
for (int i = 1; i <= 5; i++)
{
bwma.Update(new TValue(DateTime.UtcNow, i));
}
Assert.True(double.IsFinite(bwma.Last.Value));
Assert.True(bwma.IsHot);
}
[Fact]
public void Period1_ReturnsInput()
{
var bwma = new Bwma(1);
var val = bwma.Update(new TValue(DateTime.UtcNow, 42.0));
Assert.Equal(42.0, val.Value, 1e-9);
}
[Fact]
public void Warmup_Period3_Order0_MatchesReference()
{
var bwma = new Bwma(3, 0);
var t = DateTime.UtcNow;
Assert.Equal(1.0, bwma.Update(new TValue(t, 1.0)).Value, 1e-9);
Assert.Equal(2.0, bwma.Update(new TValue(t, 2.0)).Value, 1e-9);
Assert.Equal(2.0, bwma.Update(new TValue(t, 3.0)).Value, 1e-9);
}
[Fact]
public void Period2_Order0_FallsBackToCurrentValue()
{
var bwma = new Bwma(2, 0);
var t = DateTime.UtcNow;
Assert.Equal(10.0, bwma.Update(new TValue(t, 10.0)).Value, 1e-9);
Assert.Equal(20.0, bwma.Update(new TValue(t, 20.0)).Value, 1e-9);
}
[Fact]
public void TSeries_Update_Matches_Streaming_WithNaNAtReplayStart()
{
const int period = 5;
var series = new TSeries();
var start = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
double v = i == 5 ? double.NaN : 100.0 + i;
series.Add(new TValue(start.AddMinutes(i), v));
}
var bwmaStreaming = new Bwma(period);
var streaming = new List<double>(series.Count);
foreach (var item in series)
{
streaming.Add(bwmaStreaming.Update(item).Value);
}
var bwmaBatch = new Bwma(period);
var batch = bwmaBatch.Update(series);
Assert.Equal(streaming.Count, batch.Count);
for (int i = 0; i < batch.Count; i++)
{
Assert.Equal(streaming[i], batch.Values[i], 1e-9);
}
}
}
@@ -0,0 +1,352 @@
namespace QuanTAlib.Tests;
/// <summary>
/// BWMA Validation Tests
/// Note: BWMA (Bessel-Weighted Moving Average) is not available in TA-Lib, Skender,
/// Tulip, or OoplesFinance. Validation is limited to self-consistency tests
/// verifying that streaming, batch, and span APIs produce identical results.
/// </summary>
public sealed class BwmaValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private bool _disposed;
public BwmaValidationTests()
{
_testData = new ValidationTestData(count: 10000, seed: 42);
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Streaming_Batch_Span_Consistency()
{
int[] periods = { 5, 10, 20, 50 };
int[] orders = { 0, 1, 2, 3 };
foreach (var period in periods)
{
foreach (var order in orders)
{
// 1. Streaming API
var bwmaStreaming = new Bwma(period, order);
var streamingResults = new List<double>();
foreach (var item in _testData.Data)
{
streamingResults.Add(bwmaStreaming.Update(item).Value);
}
// 2. Batch API (TSeries)
var bwmaBatch = new Bwma(period, order);
var batchResults = bwmaBatch.Update(_testData.Data);
// 3. Span API
ReadOnlySpan<double> sourceData = _testData.RawData.Span;
double[] spanOutput = new double[sourceData.Length];
Bwma.Batch(sourceData, spanOutput.AsSpan(), period, order);
// Verify streaming vs batch
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < batchResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9);
}
// Verify streaming vs span
for (int i = 0; i < spanOutput.Length; i++)
{
Assert.Equal(streamingResults[i], spanOutput[i], 1e-9);
}
}
}
}
[Fact]
public void Validate_StaticBatch_Matches_Instance()
{
int[] periods = { 5, 10, 20, 50 };
int[] orders = { 0, 1, 2 };
foreach (var period in periods)
{
foreach (var order in orders)
{
// Instance batch
var bwma = new Bwma(period, order);
var instanceResult = bwma.Update(_testData.Data);
// Static batch
var staticResult = Bwma.Batch(_testData.Data, period, order);
Assert.Equal(instanceResult.Count, staticResult.Count);
for (int i = 0; i < staticResult.Count; i++)
{
Assert.Equal(instanceResult.Values[i], staticResult.Values[i], 1e-9);
}
}
}
}
[Fact]
public void Validate_BarCorrection_Consistency()
{
int[] periods = { 5, 10, 20 };
foreach (var period in periods)
{
var bwma1 = new Bwma(period);
var bwma2 = new Bwma(period);
// Process most of the data
for (int i = 0; i < _testData.Data.Count - 1; i++)
{
bwma1.Update(_testData.Data[i]);
bwma2.Update(_testData.Data[i]);
}
// bwma1: update with original value, then correct with modified value
var lastItem = _testData.Data[^1];
bwma1.Update(lastItem, isNew: true);
var correctedResult = bwma1.Update(new TValue(lastItem.Time, lastItem.Value + 10.0), isNew: false);
// bwma2: directly update with modified value
var directResult = bwma2.Update(new TValue(lastItem.Time, lastItem.Value + 10.0), isNew: true);
Assert.Equal(directResult.Value, correctedResult.Value, 1e-9);
}
}
[Fact]
public void Validate_Reset_ProducesSameResults()
{
int period = 14;
int order = 1;
var bwma = new Bwma(period, order);
// First pass
var firstPassResults = new List<double>();
foreach (var item in _testData.Data)
{
firstPassResults.Add(bwma.Update(item).Value);
}
// Reset
bwma.Reset();
// Second pass
var secondPassResults = new List<double>();
foreach (var item in _testData.Data)
{
secondPassResults.Add(bwma.Update(item).Value);
}
Assert.Equal(firstPassResults.Count, secondPassResults.Count);
for (int i = 0; i < firstPassResults.Count; i++)
{
Assert.Equal(firstPassResults[i], secondPassResults[i], 1e-9);
}
}
[Fact]
public void Validate_DifferentOrders_ProduceDifferentWeights()
{
int period = 20;
// Calculate with different orders
var results = new Dictionary<int, double[]>();
foreach (var order in new[] { 0, 1, 3 }) // Skip order 2 as it uses same power as order 1 (1.5)
{
var bwma = new Bwma(period, order);
var orderResults = new List<double>();
foreach (var item in _testData.Data)
{
orderResults.Add(bwma.Update(item).Value);
}
results[order] = orderResults.ToArray();
}
// Verify that order 0 vs 1 produce different results
bool order0vs1AllEqual = true;
for (int j = period; j < results[0].Length; j++)
{
if (Math.Abs(results[0][j] - results[1][j]) > 1e-9)
{
order0vs1AllEqual = false;
break;
}
}
Assert.False(order0vs1AllEqual, "Order 0 and 1 produced identical results");
// Verify that order 1 vs 3 produce different results
bool order1vs3AllEqual = true;
for (int j = period; j < results[1].Length; j++)
{
if (Math.Abs(results[1][j] - results[3][j]) > 1e-9)
{
order1vs3AllEqual = false;
break;
}
}
Assert.False(order1vs3AllEqual, "Order 1 and 3 produced identical results");
}
[Fact]
public void Validate_WarmupPeriod_IsCorrect()
{
int[] periods = { 5, 10, 20, 50 };
foreach (var period in periods)
{
var bwma = new Bwma(period);
Assert.Equal(period, bwma.WarmupPeriod);
// Verify IsHot transitions correctly
for (int i = 0; i < period - 1; i++)
{
bwma.Update(new TValue(DateTime.UtcNow, i + 1.0));
Assert.False(bwma.IsHot);
}
bwma.Update(new TValue(DateTime.UtcNow, period));
Assert.True(bwma.IsHot);
}
}
[Fact]
public void Validate_NaN_Handling_Consistency()
{
int period = 10;
// Create data with NaN values
var dataWithNaN = new TSeries();
for (int i = 0; i < 100; i++)
{
double value = (i == 25 || i == 50 || i == 75) ? double.NaN : _testData.Data[i].Value;
dataWithNaN.Add(new TValue(_testData.Data[i].Time, value));
}
// Streaming
var bwmaStreaming = new Bwma(period);
var streamingResults = new List<double>();
foreach (var item in dataWithNaN)
{
streamingResults.Add(bwmaStreaming.Update(item).Value);
}
// Batch
var bwmaBatch = new Bwma(period);
var batchResults = bwmaBatch.Update(dataWithNaN);
// Span
double[] spanOutput = new double[dataWithNaN.Count];
Bwma.Batch(dataWithNaN.Values, spanOutput.AsSpan(), period);
// Verify all produce same results
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9);
Assert.Equal(streamingResults[i], spanOutput[i], 1e-9);
}
}
[Fact]
public void Validate_LargeDataset_NoOverflow()
{
int period = 50;
int order = 2;
int dataSize = 10000;
var largeData = new TSeries();
var gbm = new GBM();
var bars = gbm.Fetch(dataSize, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
largeData.Add(new TValue(bar.Time, bar.Close));
}
var bwma = new Bwma(period, order);
var results = bwma.Update(largeData);
Assert.Equal(dataSize, results.Count);
Assert.True(bwma.IsHot);
// Verify no overflow or NaN in results after warmup
for (int i = period; i < results.Count; i++)
{
Assert.True(double.IsFinite(results.Values[i]), $"Value at index {i} is not finite");
}
}
[Fact]
public void Validate_EdgeCase_Period1()
{
// Period 1 should return input values directly
var bwma = new Bwma(1);
foreach (var item in _testData.Data)
{
var result = bwma.Update(item);
Assert.Equal(item.Value, result.Value, 1e-9);
}
}
[Fact]
public void Validate_EdgeCase_Period2()
{
// Period 2 with order 0: weights are [0, 1] (x = -1, 0 -> w = 0, 1)
// Actually for period 2: x = [0*2/1 - 1, 1*2/1 - 1] = [-1, 1]
// w = 1 - x² = [0, 0] which is degenerate
// Let's verify it handles this gracefully
var bwma = new Bwma(2, 0);
var item = new TValue(DateTime.UtcNow, 100.0);
var result = bwma.Update(item);
Assert.True(double.IsFinite(result.Value) || double.IsNaN(result.Value));
bwma.Update(new TValue(DateTime.UtcNow, 200.0));
// Should handle degenerate case without crashing
Assert.True(bwma.IsHot);
}
[Fact]
public void Validate_Symmetry_Order0()
{
// For order 0, the Bessel window is symmetric (parabolic)
// Verify that symmetric input produces expected center-weighted result
int period = 5;
var bwma = new Bwma(period, 0);
// Feed symmetric values: 1, 2, 3, 2, 1
var values = new double[] { 1, 2, 3, 2, 1 };
TValue result = default;
foreach (var v in values)
{
result = bwma.Update(new TValue(DateTime.UtcNow, v));
}
// With symmetric weights and symmetric data, result should be close to center value (3)
// but weighted more toward center
Assert.True(double.IsFinite(result.Value));
// The parabolic window emphasizes the center, so result should be > mean (1.8)
Assert.True(result.Value > 1.8);
}
}