mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08: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,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user