mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
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:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -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,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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user