SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
+217
View File
@@ -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);
}
}
+65
View File
@@ -0,0 +1,65 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class BwmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
public int Period { get; set; } = 10;
[InputParameter("Order", sortIndex: 2, 0, 10, 1, 0)]
public int Order { get; set; } = 0;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Bwma _ma = null!;
private readonly LineSeries _series;
private string _sourceName = null!;
private Func<IHistoryItem, double> _priceSelector = null!;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"BWMA {Period},{Order}:{_sourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_FIR/bwma/Bwma.Quantower.cs";
public BwmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
_sourceName = Source.ToString();
Name = "BWMA - Bessel-Weighted Moving Average";
Description = "Moving average using Bessel window function for weighting";
_series = new LineSeries(name: $"BWMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_ma = new Bwma(Period, Order);
_sourceName = Source.ToString();
_priceSelector = Source.GetPriceSelector();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
if (args.Reason != UpdateReason.NewBar && args.Reason != UpdateReason.HistoricalBar && args.Reason != UpdateReason.NewTick)
return;
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
TValue result = _ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), args.IsNewBar());
_series.SetValue(result.Value, _ma.IsHot, ShowColdValues);
_series.SetMarker(0, Color.Transparent);
}
}
+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.Calculate(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.Calculate(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,348 @@
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: 1000, 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.Calculate(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.Calculate(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);
}
}
+430
View File
@@ -0,0 +1,430 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// BWMA: Bessel-Weighted Moving Average
/// </summary>
/// <remarks>
/// BWMA applies a Bessel window over the last N samples (FIR).
/// <para>Window coefficient definition:</para>
/// <para>x(i) = 2*i/(p-1) - 1 (maps i to [-1, 1]), arg = 1 - x(i)^2</para>
/// <para>w(i) = arg^(order/2 + 0.5) (with PineScript special-cases for order 0 and 1)</para>
/// <para>Output = sum(window[i] * w(i)) / sum(w(i))</para>
/// </remarks>
[SkipLocalsInit]
public sealed class Bwma : AbstractBase
{
private readonly int _period;
private readonly int _order;
private readonly double _power;
private readonly double[] _weights;
private readonly double _invWeightSum;
private readonly RingBuffer _buffer;
private readonly ITValuePublisher? _source;
private readonly TValuePublishedHandler? _pubHandler;
private bool _isNew = true;
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double LastValidValue;
public bool IsInitialized;
}
private State _state;
private State _p_state;
public bool IsNew => _isNew;
public override bool IsHot => _buffer.IsFull;
/// <summary>
/// Creates BWMA with specified parameters.
/// </summary>
/// <param name="period">Window size (must be > 0)</param>
/// <param name="order">Bessel function order (0-3, default 0). Higher orders produce sharper windows.</param>
public Bwma(int period, int order = 0)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (order < 0)
throw new ArgumentOutOfRangeException(nameof(order), "Order must be non-negative");
_period = period;
_order = order;
_power = order * 0.5 + 0.5;
_buffer = new RingBuffer(period);
_weights = new double[period];
Name = $"Bwma({period}, {order})";
WarmupPeriod = period;
ComputeWeights(_weights, period, order, out _invWeightSum);
_state = new State { LastValidValue = double.NaN, IsInitialized = false };
}
public Bwma(ITValuePublisher source, int period, int order = 0)
: this(period, order)
{
_source = source;
_pubHandler = Handle;
_source.Pub += _pubHandler;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
protected override void Dispose(bool disposing)
{
if (disposing && _source != null && _pubHandler != null)
{
_source.Pub -= _pubHandler;
}
base.Dispose(disposing);
}
/// <summary>
/// Computes Bessel window weights.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeWeights(Span<double> weights, int period, int order, out double invWeightSum)
{
double sum = 0;
double scale = period > 1 ? 2.0 / (period - 1) : 0.0;
double power = order * 0.5 + 0.5;
for (int i = 0; i < period; i++)
{
double x = period > 1 ? i * scale - 1.0 : 0.0;
double arg = 1.0 - x * x;
double w;
if (arg > 0.0)
{
// Match PineScript behavior exactly:
// order=0: w = arg (parabolic, power=1)
// order=1: w = arg * sqrt(arg) (power=1.5)
// order>=2: w = pow(arg, order/2 + 0.5)
if (order == 0)
{
w = arg; // (1 - x²)^1.0 - parabolic window
}
else if (order == 1)
{
w = arg * Math.Sqrt(arg); // (1 - x²)^1.5
}
else
{
w = Math.Pow(arg, power); // (1 - x²)^power
}
}
else
{
w = 0.0;
}
weights[i] = w;
sum += w;
}
invWeightSum = sum > 0 ? 1.0 / sum : 0.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
return input;
}
return _state.IsInitialized ? _state.LastValidValue : double.NaN;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
_isNew = isNew;
return Update(input, isNew, publish: true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue Update(TValue input, bool isNew, bool publish)
{
if (isNew)
{
_p_state = _state;
}
else
{
_state = _p_state;
}
if (double.IsFinite(input.Value))
{
_state.LastValidValue = input.Value;
_state.IsInitialized = true;
}
double val = GetValidValue(input.Value);
_buffer.Add(val, isNew);
double result = _buffer.Count > 0 ? CalculateWeightedSum(fallbackValue: val) : 0.0;
Last = new TValue(input.Time, result);
if (publish)
{
PubEvent(Last);
}
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _period, _order);
source.Times.CopyTo(tSpan);
_buffer.Clear();
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
_state = default;
_state.LastValidValue = double.NaN;
_state.IsInitialized = false;
if (startIndex > 0)
{
for (int i = startIndex - 1; i >= 0; i--)
{
double v0 = source.Values[i];
if (double.IsFinite(v0))
{
_state.LastValidValue = v0;
_state.IsInitialized = true;
break;
}
}
}
else
{
_state.LastValidValue = double.NaN;
_state.IsInitialized = false;
}
for (int i = startIndex; i < len; i++)
{
Update(source[i], isNew: true, publish: false);
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
foreach (var value in source)
{
Update(new TValue(DateTime.MinValue, value));
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWeightedSum(double fallbackValue)
{
int count = _buffer.Count;
if (count == 0) return 0;
if (count < _period)
return CalculateWeightedSumWarmup(_buffer.GetSpan(), count, _order, _power, fallbackValue);
if (_invWeightSum == 0.0)
return fallbackValue;
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
int head = _buffer.StartIndex;
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double CalculateWeightedSumWarmup(ReadOnlySpan<double> window, int p, int order, double power, double fallbackValue)
{
if (p <= 0) return 0.0;
if (p == 1) return fallbackValue;
if (p == 2) return fallbackValue;
double scale = 2.0 / (p - 1);
double sum = 0.0;
double wSum = 0.0;
for (int i = 0; i < p; i++)
{
double x = Math.FusedMultiplyAdd(i, scale, -1.0);
double arg = Math.FusedMultiplyAdd(-x, x, 1.0);
if (arg <= 0.0)
continue;
double w;
if (order == 0)
{
w = arg;
}
else if (order == 1)
{
w = arg * Math.Sqrt(arg);
}
else
{
w = Math.Pow(arg, power);
}
if (w == 0.0)
continue;
sum = Math.FusedMultiplyAdd(window[i], w, sum);
wSum += w;
}
return wSum > 0.0 ? sum / wSum : fallbackValue;
}
public static TSeries Batch(TSeries source, int period, int order = 0)
{
var bwma = new Bwma(period, order);
return bwma.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period, int order = 0)
{
if (period <= 0)
throw new ArgumentException("Period must be greater than 0", nameof(period));
if (order < 0)
throw new ArgumentOutOfRangeException(nameof(order), "Order must be non-negative");
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length", nameof(output));
int len = source.Length;
if (len == 0) return;
double power = order * 0.5 + 0.5;
if (period > len)
{
double[]? bufferArray = len > 256 ? ArrayPool<double>.Shared.Rent(len) : null;
Span<double> buffer = len <= 256
? stackalloc double[len]
: bufferArray!.AsSpan(0, len);
double lastValid = double.NaN;
try
{
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid = val;
}
else if (double.IsFinite(lastValid))
{
val = lastValid;
}
buffer[i] = val;
int p = i + 1;
output[i] = CalculateWeightedSumWarmup(buffer, p, order, power, fallbackValue: val);
}
}
finally
{
if (bufferArray != null) ArrayPool<double>.Shared.Return(bufferArray);
}
return;
}
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
double[]? ringArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> ring = period <= 256
? stackalloc double[period]
: ringArray!.AsSpan(0, period);
ComputeWeights(weights, period, order, out double invWeightSum);
int ringIdx = 0;
int count = 0;
double lastValid2 = double.NaN;
try
{
for (int i = 0; i < len; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
lastValid2 = val;
}
else if (double.IsFinite(lastValid2))
{
val = lastValid2;
}
ring[ringIdx] = val;
ringIdx++;
if (ringIdx >= period) ringIdx = 0;
if (count < period) count++;
if (count < period)
{
output[i] = CalculateWeightedSumWarmup(ring, count, order, power, fallbackValue: val);
continue;
}
if (invWeightSum == 0.0)
{
output[i] = val;
continue;
}
int part1Len = period - ringIdx;
double sum = ring.Slice(ringIdx, part1Len).DotProduct(weights.Slice(0, part1Len))
+ ring.Slice(0, ringIdx).DotProduct(weights.Slice(part1Len));
output[i] = sum * invWeightSum;
}
}
finally
{
if (weightsArray != null) ArrayPool<double>.Shared.Return(weightsArray);
if (ringArray != null) ArrayPool<double>.Shared.Return(ringArray);
}
}
public override void Reset()
{
_buffer.Clear();
_state = new State { LastValidValue = double.NaN, IsInitialized = false };
_p_state = _state;
Last = default;
}
}
+347
View File
@@ -0,0 +1,347 @@
# BWMA: Bessel-Weighted Moving Average
> "The Bessel function appears in problems involving cylindrical symmetry—heat flow in pipes, vibration of drumheads, and apparently, the smoothing of financial time series. Mathematics doesn't care about your asset class."
BWMA is a Finite Impulse Response (FIR) filter that applies a Bessel-derived window function to weight price data. The weighting follows a parabolic (or higher-order polynomial) profile that emphasizes the center of the lookback window while smoothly tapering to zero at the edges. Unlike rectangular (SMA) or exponential (EMA) weighting, BWMA provides a mathematically smooth transition that reduces spectral leakage and Gibbs phenomenon artifacts.
## Historical Context
The Bessel window function derives from the modified Bessel function of the first kind, $I_0$, which Friedrich Bessel studied in the early 19th century while analyzing planetary motion perturbations. The simplified polynomial approximation used in BWMA—$(1 - x^2)^{\text{power}}$—captures the essential shape without requiring the full Bessel function computation.
In signal processing, Bessel-derived windows are prized for their smooth rolloff characteristics. The Kaiser window (a close relative) is standard in FIR filter design for its ability to trade off between main lobe width and side lobe attenuation. BWMA brings this engineering discipline to technical analysis.
## Architecture & Physics
BWMA maps each position in the lookback window to a normalized coordinate $x \in [-1, 1]$, then applies the weighting function:
$$w_i = (1 - x_i^2)^{\text{power}}$$
The window is inherently symmetric around the center, creating a bell-shaped weight distribution. Higher order values sharpen the bell, concentrating weight more tightly around the center bar.
### Physical Interpretation
Think of BWMA as a mass-spring system where:
* **Order 0** (parabolic): The weight distribution follows a simple parabola—gentle tapering, broad response
* **Order 1**: The curve steepens, emphasizing center values more strongly
* **Order 2+**: Increasingly focused on the center, approaching a "soft" impulse response
The key advantage over rectangular windows (SMA) is the elimination of the "boxcar" effect—the abrupt inclusion/exclusion of data points that causes artificial oscillations in the frequency response.
### The Compute Challenge
Like other FIR filters, BWMA precomputes weights at initialization. Runtime becomes a weighted dot product:
$$\text{BWMA}_t = \frac{\sum_{i=0}^{L-1} P_{t-i} \cdot w_i}{\sum_{i=0}^{L-1} w_i}$$
QuanTAlib stores both the weight vector and the precomputed inverse of the weight sum, reducing division to multiplication in the hot path.
## Mathematical Foundation
### 1. Coordinate Mapping
For a window of length $L$, each index $i \in [0, L-1]$ maps to:
$$x_i = \frac{2i}{L-1} - 1$$
This places $x_0 = -1$ (oldest), $x_{(L-1)/2} = 0$ (center), and $x_{L-1} = 1$ (newest).
### 2. Power Calculation
The exponent depends on the order parameter:
$$\text{power} = \frac{\text{order}}{2} + 0.5$$
| Order | Power | Window Shape |
| :--- | :--- | :--- |
| 0 | 0.5 | Square root parabola: $(1-x^2)^{0.5}$ |
| 1 | 1.0 | Linear parabola: $(1-x^2)$ |
| 2 | 1.5 | Steeper: $(1-x^2)^{1.5}$ |
| 3 | 2.0 | Even sharper: $(1-x^2)^2$ |
*Note: The reference PineScript uses `order/2 + 0.5` which differs slightly from some textbook definitions.*
### 3. Weight Generation
For each index:
$$w_i = \begin{cases}
(1 - x_i^2)^{\text{power}} & \text{if } |x_i| < 1 \\
0 & \text{otherwise}
\end{cases}$$
The edge case handling ensures weights at exactly $x = \pm 1$ are zero, providing smooth cutoff.
### 4. Normalization
The final BWMA value:
$$\text{BWMA}_t = \frac{\sum_{i=0}^{L-1} P_{t-i} \cdot w_{L-1-i}}{W_{\text{sum}}}$$
Where $W_{\text{sum}} = \sum w_i$.
## Performance Profile
### Operation Count (Streaming Mode, Scalar)
**Constructor (one-time weight precomputation):**
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| MUL | 2L | 3 | 6L |
| ADD/SUB | 2L | 1 | 2L |
| POW | L | 80 | 80L |
| **Total (init)** | — | — | **~88L cycles** |
For period=20: ~1,760 cycles (one-time).
**Hot path (per bar):**
| Operation | Count | Cost (cycles) | Subtotal |
| :--- | :---: | :---: | :---: |
| MUL | L + 1 | 3 | 3L + 3 |
| ADD | L | 1 | L |
| **Total** | **2L + 1** | — | **~4L + 3 cycles** |
For period=20: ~83 cycles per bar.
**Hot path breakdown:**
- Dot product: `buffer.DotProduct(weights)` → L MUL + L ADD
- Normalization: `result × invWeightSum` → 1 MUL (precomputed inverse avoids DIV)
### Batch Mode (SIMD)
The dot product is highly vectorizable:
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
| :--- | :---: | :---: | :---: |
| Weighted products | L | L/8 | 8× |
| Horizontal sum | L | log₂(8) | ~L/3× |
**Batch efficiency (512 bars, period=20):**
| Mode | Cycles/bar | Total | Notes |
| :--- | :---: | :---: | :--- |
| Scalar streaming | ~83 | ~42,496 | O(L) per bar |
| SIMD batch | ~22 | ~11,264 | Vectorized dot product |
| **Improvement** | **~4×** | **~31K saved** | — |
### Quality Metrics
| Metric | Score | Notes |
| :--- | :---: | :--- |
| **Accuracy** | 10/10 | Matches mathematical definition |
| **Timeliness** | 7/10 | Symmetric window introduces inherent lag |
| **Overshoot** | 9/10 | Smooth window prevents ringing |
| **Smoothness** | 9/10 | Excellent noise rejection |
### Implementation Highlights
```csharp
// Weight computation (constructor)
double scale = period > 1 ? 2.0 / (period - 1) : 0.0;
double power = order * 0.5 + 0.5;
for (int i = 0; i < period; i++)
{
double x = period > 1 ? i * scale - 1.0 : 0.0;
double arg = 1.0 - x * x;
weights[i] = arg > 0 ? Math.Pow(arg, power) : 0.0;
sum += weights[i];
}
// Runtime (Update) - SIMD-friendly dot product
double result = buffer.DotProduct(weights) * invWeightSum;
```
## Validation
BWMA is a custom indicator not found in standard technical analysis libraries. Validation relies on self-consistency tests.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **QuanTAlib** | ✅ | Reference implementation |
| **TradingView** | ✅ | Matches PineScript `bwma.pine` |
| **TA-Lib** | ❌ | Not included |
| **Skender** | ❌ | Not included |
| **Tulip** | ❌ | Not included |
| **Ooples** | ❌ | Not included |
Self-consistency validation ensures:
* Streaming, batch, and span APIs produce identical results
* Bar correction (isNew=false) restores previous state correctly
* NaN handling substitutes last valid value
* Reset produces identical results on replay
### C# Implementation Considerations
The QuanTAlib BWMA implementation optimizes for streaming throughput with precomputed weights and zero-allocation hot paths:
#### Precomputed Weights with Inverse Sum
Weights and the inverse of their sum are calculated once in the constructor, replacing division with multiplication:
```csharp
public Bwma(int period, int order = 0)
{
_weights = new double[period];
ComputeWeights(_weights, period, order, out _invWeightSum);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeWeights(Span<double> weights, int period, int order, out double invWeightSum)
{
double sum = 0;
double scale = period > 1 ? 2.0 / (period - 1) : 0.0;
double power = order * 0.5 + 0.5;
for (int i = 0; i < period; i++)
{
double x = period > 1 ? i * scale - 1.0 : 0.0;
double arg = 1.0 - x * x;
double w = arg > 0.0 ? Math.Pow(arg, power) : 0.0;
weights[i] = w;
sum += w;
}
invWeightSum = sum > 0 ? 1.0 / sum : 0.0; // Precompute inverse
}
```
#### State Record Struct with Auto Layout
State uses `LayoutKind.Auto` for compiler-optimized field arrangement:
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double LastValidValue;
public bool IsInitialized;
}
private State _state;
private State _p_state; // Previous state for bar correction
```
#### FusedMultiplyAdd in Warmup Path
The warmup calculation uses FMA for coordinate mapping and argument computation:
```csharp
for (int i = 0; i < p; i++)
{
double x = Math.FusedMultiplyAdd(i, scale, -1.0); // x = i * scale - 1.0
double arg = Math.FusedMultiplyAdd(-x, x, 1.0); // arg = 1.0 - x * x
// ...
sum = Math.FusedMultiplyAdd(window[i], w, sum); // sum += window[i] * w
}
```
#### Optimized Circular Buffer DotProduct
The hot path handles ring buffer wraparound with two slice dot products:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWeightedSum(double fallbackValue)
{
if (_invWeightSum == 0.0) return fallbackValue;
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
int head = _buffer.StartIndex;
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum; // Multiply by precomputed inverse
}
```
#### ArrayPool for Large Periods in Batch Mode
The static `Calculate` method uses ArrayPool for periods >256 to avoid large stack allocations:
```csharp
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
double[]? ringArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> ring = period <= 256
? stackalloc double[period]
: ringArray!.AsSpan(0, period);
try
{
// Processing loop...
}
finally
{
if (weightsArray != null) ArrayPool<double>.Shared.Return(weightsArray);
if (ringArray != null) ArrayPool<double>.Shared.Return(ringArray);
}
```
#### PineScript-Exact Order Handling
The implementation matches PineScript behavior with special cases for orders 0 and 1:
```csharp
if (order == 0)
{
w = arg; // (1 - x²)^1.0 - parabolic
}
else if (order == 1)
{
w = arg * Math.Sqrt(arg); // (1 - x²)^1.5 - avoids Math.Pow overhead
}
else
{
w = Math.Pow(arg, power); // (1 - x²)^power
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Window length |
| `_order` | `int` | 4 | Bessel order parameter |
| `_power` | `double` | 8 | Precomputed exponent |
| `_weights` | `double[]` | 8 (ref) | Precomputed weights |
| `_invWeightSum` | `double` | 8 | Inverse of weight sum |
| `_buffer` | `RingBuffer` | 8 (ref) | Circular price storage |
| `_state` | `State` | 16 | Current state (LastValidValue, IsInitialized) |
| `_p_state` | `State` | 16 | Previous state for rollback |
| **Total** | | **~72 bytes** | Per instance (excluding buffer/array internals) |
**Weight array storage:** `period × 8` bytes (e.g., 160 bytes for period=20)
## Common Pitfalls
1. **Order Selection Paralysis**: Start with order 0 (parabolic). It's the most balanced choice. Higher orders provide sharper filtering but may over-smooth trend transitions.
2. **Period 2 Degeneracy**: At period 2, the window points land exactly at $x = \pm 1$, where weights become zero. QuanTAlib handles this gracefully, but the output is mathematically degenerate. Use period ≥ 3.
3. **Symmetric Lag**: Unlike offset-adjustable windows (ALMA), BWMA's symmetry means the center of gravity is always at the middle of the window. Expect lag of approximately $L/2$ bars.
4. **Confusion with Kaiser-Bessel**: The full Kaiser-Bessel window uses $I_0(\beta \sqrt{1-x^2}) / I_0(\beta)$ with a shape parameter $\beta$. BWMA uses the polynomial approximation $(1-x^2)^p$, which is simpler but different. Don't mix the two in discussions.
5. **Edge Effects During Warmup**: The first $L-1$ values are computed with partial windows. Trust results only after `IsHot` becomes true.
## Parameter Guidelines
| Use Case | Period | Order | Rationale |
| :--- | :--- | :--- | :--- |
| Scalping (1-5 min) | 8-12 | 0 | Quick response, mild smoothing |
| Swing trading | 14-21 | 1 | Balanced filtering |
| Position trading | 50-100 | 2 | Heavy smoothing, trend focus |
| Noise floor analysis | 20-30 | 3 | Maximum smoothing |
## See Also
* [ALMA](../alma/Alma.md) - Gaussian window with adjustable offset
* [WMA](../wma/Wma.md) - Linear weighting (triangular window)
* [SINEMA](../sinema/Sinema.md) - Sine-weighted moving average
+66
View File
@@ -0,0 +1,66 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Bessel-Weighted Moving Average (BWMA)", "BWMA", overlay=true)
//@function Calculates BWMA using Bessel window weighting
//@param source Series to calculate BWMA from
//@param period Lookback period - FIR window size
//@param order Bessel function order (default: 0)
//@returns BWMA value, calculates from first bar using available data
//@optimized Uses Bessel window coefficients with O(n) complexity per bar due to lookback loop
bwma(series float source, simple int period, simple int order=0) =>
if period <= 0
runtime.error("Period must be greater than 0")
if order < 0
runtime.error("Bessel order must be non-negative")
int p = math.min(bar_index + 1, period)
var array<float> weights = array.new_float(1, 1.0)
var int last_p = 1
var int last_order = order
if last_p != p or last_order != order
weights := array.new_float(p, 0.0)
float total_weight = 0.0
float scale = 2.0 / (p - 1)
float power = order / 2.0 + 0.5
for i = 0 to p - 1
float x = i * scale - 1.0
float arg = 1.0 - x * x
float w = 0.0
if arg > 0.0
if order == 0
w := arg
else if order == 1
w := arg * math.sqrt(arg)
else
w := math.pow(arg, power)
array.set(weights, i, w)
total_weight += w
if total_weight > 0.0
float inv_total = 1.0 / total_weight
for i = 0 to p - 1
array.set(weights, i, array.get(weights, i) * inv_total)
last_p := p
last_order := order
float sum = 0.0
float weight_sum = 0.0
for i = 0 to p - 1
float price = source[i]
if not na(price)
float w = array.get(weights, i)
sum += price * w
weight_sum += w
nz(sum / weight_sum, source)
// ---------- Main loop ----------
// Inputs
i_period = input.int(10, "Period", minval=1)
i_order = input.int(0, "Bessel Order", minval=0, maxval=3, tooltip="Order of the Bessel function (0-3)")
i_source = input.source(close, "Source")
// Calculation
bwma_value = bwma(i_source, i_period, i_order)
// Plot
plot(bwma_value, "BWMA", color=color.yellow, linewidth=2)