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,167 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HammaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void HammaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new HammaIndicator();
|
||||
|
||||
Assert.Equal(10, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("HAMMA - Hamming-Weighted Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_MinHistoryDepths_ReturnsZero()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, HammaIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("HAMMA", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("15", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_Initialize_CreatesInternalHamma()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
// Process first update
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
// Line series should have values
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process historical bar first
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Update with new tick (same bar data - simulates intrabar update)
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
// Both values should be finite
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_MultipleUpdates_ProducesCorrectHammaSequence()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105, 107, 106 };
|
||||
|
||||
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)));
|
||||
}
|
||||
|
||||
// HAMMA should be smoothing the values
|
||||
double lastHamma = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastHamma >= 100 && lastHamma <= 110);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HammaIndicator_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 HammaIndicator { 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 HammaIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new HammaIndicator { Period = 5 };
|
||||
Assert.Equal(5, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(0, HammaIndicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public class HammaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 10;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Hamma ma = null!;
|
||||
protected LineSeries Series;
|
||||
protected string SourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"HAMMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends_FIR/hamma/Hamma.Quantower.cs";
|
||||
|
||||
public HammaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "HAMMA - Hamming-Weighted Moving Average";
|
||||
Description = "Hamming-Weighted Moving Average with optimized side lobe suppression";
|
||||
Series = new LineSeries(name: $"HAMMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
ma = new Hamma(Period);
|
||||
SourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
|
||||
TValue result = ma.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew: args.IsNewBar());
|
||||
|
||||
Series.SetValue(result.Value, ma.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class HammaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Hamma_Constructor_ValidatesInput()
|
||||
{
|
||||
var ex1 = Assert.Throws<ArgumentException>(() => new Hamma(0));
|
||||
Assert.Equal("period", ex1.ParamName);
|
||||
|
||||
var ex2 = Assert.Throws<ArgumentException>(() => new Hamma(-1));
|
||||
Assert.Equal("period", ex2.ParamName);
|
||||
|
||||
var hamma = new Hamma(10);
|
||||
Assert.NotNull(hamma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_Calc_ReturnsValue()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
TValue result = hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var hamma = new Hamma(5);
|
||||
|
||||
Assert.False(hamma.IsHot);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.False(hamma.IsHot);
|
||||
}
|
||||
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.True(hamma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_StreamingMatchesBatch()
|
||||
{
|
||||
var hammaStreaming = new Hamma(10);
|
||||
var hammaBatch = new Hamma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
var series = new TSeries();
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var streamingResults = new TSeries();
|
||||
Assert.True(series.Count > 0);
|
||||
foreach (var item in series)
|
||||
{
|
||||
streamingResults.Add(hammaStreaming.Update(item));
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = hammaBatch.Update(series);
|
||||
|
||||
Assert.Equal(streamingResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < batchResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_StaticCalculate_MatchesInstance()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var instanceResults = new Hamma(10).Update(series);
|
||||
var staticResults = Hamma.Batch(series, 10);
|
||||
|
||||
for (int i = 0; i < instanceResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(instanceResults[i].Value, staticResults[i].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_SpanCalculate_MatchesSeries()
|
||||
{
|
||||
var series = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var seriesResults = Hamma.Batch(series, 10);
|
||||
|
||||
double[] input = series.Values.ToArray();
|
||||
double[] output = new double[input.Length];
|
||||
|
||||
Hamma.Calculate(input.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
{
|
||||
Assert.Equal(seriesResults[i].Value, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_Update_IsNewFalse_CorrectsValue()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
|
||||
// Feed initial data
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
hamma.Update(new TValue(bar.Time, bar.Close), isNew: true);
|
||||
}
|
||||
|
||||
// Update with isNew=false (correction)
|
||||
var newBar = gbm.Next(isNew: true);
|
||||
hamma.Update(new TValue(newBar.Time, newBar.Close), isNew: true);
|
||||
|
||||
double valueAfterCommit = hamma.Last.Value;
|
||||
|
||||
// Now update the SAME bar with a different value
|
||||
hamma.Update(new TValue(newBar.Time, newBar.Close + 10.0), isNew: false);
|
||||
|
||||
double valueAfterCorrection = hamma.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueAfterCommit, valueAfterCorrection);
|
||||
|
||||
// Now restore original value
|
||||
hamma.Update(new TValue(newBar.Time, newBar.Close), isNew: false);
|
||||
|
||||
Assert.Equal(valueAfterCommit, hamma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var hamma = new Hamma(5);
|
||||
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterNaN = hamma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_Reset_ClearsState()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
Assert.True(hamma.Last.Value > 0);
|
||||
|
||||
hamma.Reset();
|
||||
|
||||
Assert.Equal(0, hamma.Last.Value);
|
||||
Assert.False(hamma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_FirstValue_ReturnsExpected()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
TValue result = hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(100.0, result.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_Properties_Accessible()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
Assert.False(hamma.IsHot);
|
||||
Assert.Equal(0, hamma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
Assert.Equal(100, hamma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
hamma.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double valueAfterTen = hamma.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
hamma.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalValue = hamma.Update(tenthInput, isNew: false);
|
||||
|
||||
// Should match the original state after 10 values
|
||||
Assert.Equal(valueAfterTen, finalValue.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultPosInf = hamma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPosInf.Value));
|
||||
|
||||
var resultNegInf = hamma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var hamma = new Hamma(10);
|
||||
hamma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
var r1 = hamma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = hamma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(r1.Value));
|
||||
Assert.True(double.IsFinite(r2.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
const int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = Hamma.Batch(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Hamma.Calculate(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Hamma(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Hamma(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, 1e-9);
|
||||
Assert.Equal(expected, streamingResult, 1e-9);
|
||||
Assert.Equal(expected, eventingResult, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_SpanCalc_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Hamma.Calculate(source.AsSpan(), output.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Hamma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_SpanCalc_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 120, 130];
|
||||
double[] output = new double[5];
|
||||
|
||||
Hamma.Calculate(source.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_HammingWindow_WeightSymmetry()
|
||||
{
|
||||
// Hamming window should be symmetric around center
|
||||
// w[i] = w[period-1-i] for all i
|
||||
int period = 11; // Odd for exact center
|
||||
|
||||
// Verify weight symmetry by checking equal outputs for symmetric inputs
|
||||
var hamma1 = new Hamma(period);
|
||||
var hamma2 = new Hamma(period);
|
||||
|
||||
// Feed ascending values to hamma1
|
||||
double[] ascending = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||
foreach (var v in ascending)
|
||||
{
|
||||
hamma1.Update(new TValue(DateTime.UtcNow, v));
|
||||
}
|
||||
|
||||
// Feed descending values to hamma2
|
||||
double[] descending = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
|
||||
foreach (var v in descending)
|
||||
{
|
||||
hamma2.Update(new TValue(DateTime.UtcNow, v));
|
||||
}
|
||||
|
||||
// Results should be the same (symmetric weights applied to symmetric data)
|
||||
Assert.Equal(hamma1.Last.Value, hamma2.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_KnownValues_ManualCalculation()
|
||||
{
|
||||
// Manual verification with known Hamming weights
|
||||
// period=5: w[i] = 0.54 - 0.46 * cos(2π*i/4)
|
||||
// w[0] = 0.54 - 0.46 * cos(0) = 0.54 - 0.46 = 0.08
|
||||
// w[1] = 0.54 - 0.46 * cos(π/2) = 0.54 - 0 = 0.54
|
||||
// w[2] = 0.54 - 0.46 * cos(π) = 0.54 + 0.46 = 1.0
|
||||
// w[3] = 0.54 - 0.46 * cos(3π/2) = 0.54 - 0 = 0.54
|
||||
// w[4] = 0.54 - 0.46 * cos(2π) = 0.54 - 0.46 = 0.08
|
||||
|
||||
int period = 5;
|
||||
var hamma = new Hamma(period);
|
||||
|
||||
double[] prices = [100, 102, 104, 103, 101];
|
||||
foreach (var price in prices)
|
||||
{
|
||||
hamma.Update(new TValue(DateTime.UtcNow, price));
|
||||
}
|
||||
|
||||
// Calculate expected manually
|
||||
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
|
||||
double[] weights = new double[period];
|
||||
double weightSum = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
weights[i] = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * i);
|
||||
weightSum += weights[i];
|
||||
}
|
||||
|
||||
double expected = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
expected += prices[i] * weights[i];
|
||||
}
|
||||
expected /= weightSum;
|
||||
|
||||
Assert.Equal(expected, hamma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_PeriodOne_ReturnsInputValue()
|
||||
{
|
||||
var hamma = new Hamma(1);
|
||||
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
var input = new TValue(DateTime.UtcNow, i * 10.0);
|
||||
var result = hamma.Update(input);
|
||||
Assert.Equal(i * 10.0, result.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// HAMMA validation tests.
|
||||
/// Note: HAMMA is not available in TA-Lib, Tulip, Skender, or OoplesFinance.
|
||||
/// Validation is performed against internal consistency checks and mathematical verification.
|
||||
/// </summary>
|
||||
public sealed class HammaValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private bool _disposed;
|
||||
|
||||
public HammaValidationTests()
|
||||
{
|
||||
_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 Hamma_BatchMatchesStreaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib HAMMA (batch TSeries)
|
||||
var hammaBatch = new Hamma(period);
|
||||
var batchResult = hammaBatch.Update(_testData.Data);
|
||||
|
||||
// Calculate QuanTAlib HAMMA (streaming)
|
||||
var hammaStreaming = new Hamma(period);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingResults.Add(hammaStreaming.Update(item).Value);
|
||||
}
|
||||
|
||||
// Compare all records
|
||||
Assert.Equal(batchResult.Count, streamingResults.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, streamingResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_SpanMatchesBatch()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
// Prepare data for Span API
|
||||
ReadOnlySpan<double> sourceData = _testData.RawData.Span;
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib HAMMA (Span API)
|
||||
double[] spanOutput = new double[sourceData.Length];
|
||||
Hamma.Calculate(sourceData, spanOutput.AsSpan(), period);
|
||||
|
||||
// Calculate QuanTAlib HAMMA (batch TSeries)
|
||||
var hammaBatch = new Hamma(period);
|
||||
var batchResult = hammaBatch.Update(_testData.Data);
|
||||
|
||||
// Compare all records
|
||||
Assert.Equal(batchResult.Count, spanOutput.Length);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, spanOutput[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_EventingMatchesBatch()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib HAMMA (batch TSeries)
|
||||
var hammaBatch = new Hamma(period);
|
||||
var batchResult = hammaBatch.Update(_testData.Data);
|
||||
|
||||
// Calculate QuanTAlib HAMMA (eventing)
|
||||
var pubSource = new TSeries();
|
||||
var hammaEventing = new Hamma(pubSource, period);
|
||||
var eventingResults = new List<double>();
|
||||
hammaEventing.Pub += (object? sender, in TValueEventArgs e) => eventingResults.Add(e.Value.Value);
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
pubSource.Add(item);
|
||||
}
|
||||
|
||||
// Compare all records
|
||||
Assert.Equal(batchResult.Count, eventingResults.Count);
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, eventingResults[i], 1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_HammingWindow_WeightsAreSymmetric()
|
||||
{
|
||||
// Hamming window is symmetric: w[i] = w[period-1-i]
|
||||
int period = 11; // Odd period for exact center
|
||||
|
||||
var hamma = new Hamma(period);
|
||||
|
||||
// Feed symmetric data: [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
|
||||
double[] symmetricData = [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1];
|
||||
|
||||
foreach (var val in symmetricData)
|
||||
{
|
||||
hamma.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
// The HAMMA result should be reasonable (between min and max of data)
|
||||
Assert.True(hamma.Last.Value >= 1 && hamma.Last.Value <= 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_KnownValues_ManualCalculation()
|
||||
{
|
||||
// Manual verification of HAMMA calculation with known values
|
||||
// period=5: w[i] = 0.54 - 0.46 * cos(2πi/4)
|
||||
|
||||
int period = 5;
|
||||
var hamma = new Hamma(period);
|
||||
|
||||
// Feed 5 values: [100, 102, 104, 103, 101]
|
||||
double[] prices = [100, 102, 104, 103, 101];
|
||||
foreach (var price in prices)
|
||||
{
|
||||
hamma.Update(new TValue(DateTime.UtcNow, price));
|
||||
}
|
||||
|
||||
// Calculate expected manually using Hamming window formula
|
||||
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
|
||||
double[] weights = new double[period];
|
||||
double weightSum = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
weights[i] = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * i);
|
||||
weightSum += weights[i];
|
||||
}
|
||||
|
||||
double expected = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
expected += prices[i] * weights[i];
|
||||
}
|
||||
expected /= weightSum;
|
||||
|
||||
Assert.Equal(expected, hamma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Hamma_HammingCoefficients_Verify()
|
||||
{
|
||||
// Verify Hamming window coefficients match the standard formula
|
||||
// w[i] = 0.54 - 0.46 * cos(2πi/(N-1))
|
||||
// For period=5: w[0]=0.08, w[1]≈0.54, w[2]=1.0, w[3]≈0.54, w[4]=0.08
|
||||
|
||||
int period = 5;
|
||||
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
|
||||
|
||||
double w0 = 0.54 - 0.46 * Math.Cos(0); // 0.08
|
||||
double w1 = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * 1); // ≈0.54
|
||||
double w2 = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * 2); // 1.0
|
||||
double w3 = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * 3); // ≈0.54
|
||||
double w4 = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * 4); // 0.08
|
||||
|
||||
Assert.Equal(0.08, w0, 1e-10);
|
||||
Assert.Equal(0.08, w4, 1e-10);
|
||||
Assert.Equal(1.0, w2, 1e-10);
|
||||
|
||||
// w1 and w3 should be equal (symmetric)
|
||||
Assert.Equal(w1, w3, 1e-10);
|
||||
|
||||
// All edge weights should be equal
|
||||
Assert.Equal(w0, w4, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
// Hamma.cs - Hamming Moving Average
|
||||
// Finite Impulse Response (FIR) filter using Hamming window weighting.
|
||||
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// HAMMA: Hamming Moving Average
|
||||
/// A weighted moving average using Hamming window coefficients, providing good
|
||||
/// spectral characteristics with reduced side lobes compared to simple windowing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Key characteristics</b>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Hamming window: w[i] = 0.54 - 0.46 × cos(2πi/(period-1))</description></item>
|
||||
/// <item><description>Raised-cosine window with specific coefficients for optimal side-lobe suppression</description></item>
|
||||
/// <item><description>First side lobe is approximately -43 dB down from main lobe</description></item>
|
||||
/// <item><description>Widely used in digital signal processing and spectral analysis</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <b>Calculation</b>
|
||||
/// <code>
|
||||
/// w[i] = 0.54 - 0.46 × cos(2π × i / (period - 1))
|
||||
/// HAMMA = Σ(price[i] × w[i]) / Σ(w[i])
|
||||
/// </code>
|
||||
///
|
||||
/// <b>Sources</b>
|
||||
/// Richard W. Hamming - "Digital Filters" (1977)
|
||||
/// Oppenheim, Schafer - "Discrete-Time Signal Processing"
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Hamma : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
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 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 HAMMA with specified parameters.
|
||||
/// </summary>
|
||||
/// <param name="period">Window size (must be > 0)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Hamma(int period = 10)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
_weights = new double[period];
|
||||
Name = $"Hamma({period})";
|
||||
WarmupPeriod = period;
|
||||
|
||||
ComputeWeights(_weights, period, out _invWeightSum);
|
||||
_state = default;
|
||||
_state.LastValidValue = double.NaN;
|
||||
}
|
||||
|
||||
/// <param name="source">Data source for event-based updates</param>
|
||||
/// <param name="period">Lookback period for the Hamming window (default: 10)</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Hamma(ITValuePublisher source, int period = 10) : this(period)
|
||||
{
|
||||
_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 Hamming window weights.
|
||||
/// w[i] = 0.54 - 0.46 * cos(2πi/(period-1))
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ComputeWeights(Span<double> weights, int period, out double invWeightSum)
|
||||
{
|
||||
double sum = 0;
|
||||
|
||||
if (period == 1)
|
||||
{
|
||||
weights[0] = 1.0;
|
||||
sum = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
double w = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * i);
|
||||
weights[i] = w;
|
||||
sum += w;
|
||||
}
|
||||
}
|
||||
|
||||
invWeightSum = 1.0 / sum;
|
||||
}
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
// Retrieve valid value (handles NaN propagation prevention)
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
_buffer.Add(val, isNew);
|
||||
|
||||
double result = 0;
|
||||
if (_buffer.Count > 0)
|
||||
{
|
||||
result = CalculateWeightedSum();
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
if (publish)
|
||||
{
|
||||
PubEvent(Last, isNew);
|
||||
}
|
||||
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);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Restore state
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
|
||||
// Replay last part to restore buffer state
|
||||
int startIndex = Math.Max(0, len - _period);
|
||||
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()
|
||||
{
|
||||
int count = _buffer.Count;
|
||||
if (count == 0) return 0;
|
||||
|
||||
if (count < _period)
|
||||
{
|
||||
// Partial buffer: align newest with newest
|
||||
// Buffer[0] (oldest) -> Weights[period - count]
|
||||
ReadOnlySpan<double> bufferSpan = _buffer.GetSpan();
|
||||
int weightOffset = _period - count;
|
||||
|
||||
// Use DotProduct for partial sum
|
||||
double sum = bufferSpan.DotProduct(_weights.AsSpan(weightOffset, count));
|
||||
|
||||
// Calculate weightSum for this subset
|
||||
double wSum = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
wSum += _weights[weightOffset + i];
|
||||
}
|
||||
|
||||
return wSum > 0 ? sum / wSum : 0;
|
||||
}
|
||||
|
||||
// Full buffer: use precomputed _weightSum and SIMD DotProduct
|
||||
// We use InternalBuffer and StartIndex to avoid allocation and handle wrapping
|
||||
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
|
||||
int head = _buffer.StartIndex;
|
||||
|
||||
// Part 1: Oldest to End of Buffer -> InternalBuffer[Head ... Cap-1]
|
||||
// Matches Weights[0 ... Cap-Head-1]
|
||||
int part1Len = _period - head;
|
||||
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
|
||||
|
||||
// Part 2: Start of Buffer to Newest -> InternalBuffer[0 ... Head-1]
|
||||
// Matches Weights[Cap-Head ... Cap-1]
|
||||
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
|
||||
|
||||
return (sum1 + sum2) * _invWeightSum;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates HAMMA from a TSeries using streaming updates.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period = 10)
|
||||
{
|
||||
var hamma = new Hamma(period);
|
||||
return hamma.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates HAMMA over a span of values (SIMD-optimized for batch processing).
|
||||
/// </summary>
|
||||
/// <param name="source">Input values</param>
|
||||
/// <param name="output">Output buffer (must be same length as source)</param>
|
||||
/// <param name="period">Lookback period for the Hamming window (default: 10)</param>
|
||||
/// <exception cref="ArgumentException">Thrown when output length doesn't match source length.</exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 10)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
|
||||
// Allocation Strategy: Stack for small periods, Pool for large
|
||||
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
|
||||
Span<double> weights = period <= 256
|
||||
? stackalloc double[period]
|
||||
: weightsArray!.AsSpan(0, period);
|
||||
|
||||
double[]? bufferArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
|
||||
Span<double> buffer = period <= 256
|
||||
? stackalloc double[period]
|
||||
: bufferArray!.AsSpan(0, period);
|
||||
|
||||
// Precompute weights using shared helper
|
||||
ComputeWeights(weights, period, out double invWeightSum);
|
||||
|
||||
int bufferIdx = 0;
|
||||
int count = 0;
|
||||
double lastValid = double.NaN; // Start with NaN to detect first valid value
|
||||
double currentWeightSum = 0;
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
|
||||
// Strict NaN handling: maintain NaN until first valid value
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else if (double.IsFinite(lastValid))
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = double.NaN; // Preserve NaN until first valid value seen
|
||||
}
|
||||
|
||||
// Add to circular buffer
|
||||
buffer[bufferIdx] = val;
|
||||
bufferIdx = (bufferIdx + 1) % period;
|
||||
|
||||
if (count < period)
|
||||
{
|
||||
count++;
|
||||
// Incremental weight sum update for warmup
|
||||
currentWeightSum += weights[period - count];
|
||||
}
|
||||
|
||||
double sum = 0;
|
||||
|
||||
if (count == period)
|
||||
{
|
||||
// Buffer is full. bufferIdx points to the oldest element (next write position)
|
||||
// Split the dot product to handle circular buffer wrap-around
|
||||
|
||||
int part1Len = period - bufferIdx;
|
||||
|
||||
// Part 1: Oldest data (at bufferIdx..End) * Start of Weights
|
||||
sum += buffer.Slice(bufferIdx, part1Len).DotProduct(weights.Slice(0, part1Len));
|
||||
|
||||
// Part 2: Newest data (at 0..bufferIdx) * End of Weights
|
||||
sum += buffer.Slice(0, bufferIdx).DotProduct(weights.Slice(part1Len));
|
||||
|
||||
output[i] = sum * invWeightSum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Partial buffer
|
||||
int startIdx = (bufferIdx - count + period) % period;
|
||||
int weightOffset = period - count;
|
||||
|
||||
if (startIdx + count <= period)
|
||||
{
|
||||
// Contiguous in buffer
|
||||
sum = buffer.Slice(startIdx, count).DotProduct(weights.Slice(weightOffset, count));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wrapped in buffer
|
||||
int part1Len = period - startIdx;
|
||||
int part2Len = count - part1Len;
|
||||
|
||||
sum = buffer.Slice(startIdx, part1Len).DotProduct(weights.Slice(weightOffset, part1Len));
|
||||
sum += buffer.Slice(0, part2Len).DotProduct(weights.Slice(weightOffset + part1Len, part2Len));
|
||||
}
|
||||
|
||||
output[i] = currentWeightSum > 0 ? sum / currentWeightSum : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (weightsArray != null) ArrayPool<double>.Shared.Return(weightsArray);
|
||||
if (bufferArray != null) ArrayPool<double>.Shared.Return(bufferArray);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_state.LastValidValue = double.NaN;
|
||||
_p_state = _state;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
# HAMMA: Hamming-Weighted Moving Average
|
||||
|
||||
> "Julius von Hann picked his window function to suppress spectral leakage; we're just using it to smooth price data. Same math, different trading floor."
|
||||
|
||||
HAMMA is a Finite Impulse Response (FIR) filter that applies a Hamming window to price data. The Hamming window is a raised cosine with specific coefficients (0.54 and 0.46) chosen to minimize the amplitude of the first side lobe in the frequency domain. This makes it particularly effective at separating the signal (trend) from nearby noise frequencies.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Richard Hamming developed his eponymous window function at Bell Labs in 1977, though it built on earlier work by Julius von Hann (the "Hanning" window, often confused with Hamming). The Hamming window was designed specifically to address spectral leakage in discrete Fourier transforms.
|
||||
|
||||
The key insight was that by tweaking the coefficients of the raised cosine window, you could minimize the first side lobe amplitude at the cost of slightly wider main lobe. The result is a window that's excellent at isolating a signal from nearby interfering frequencies—exactly what traders want when separating trend from noise.
|
||||
|
||||
In trading applications, HAMMA provides smoother output than SMA while maintaining good responsiveness. Its symmetric weighting gives equal consideration to recent and older prices around the center of the window.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
HAMMA is a weighted moving average where weights follow the Hamming function:
|
||||
|
||||
$$ w_i = 0.54 - 0.46 \cdot \cos\left(\frac{2\pi i}{N-1}\right) $$
|
||||
|
||||
The physics of HAMMA reveal several key properties:
|
||||
|
||||
* **Symmetric weighting**: Center weight is 1.0, edge weights are 0.08
|
||||
* **First side lobe at -43 dB**: Much better side lobe suppression than rectangular (SMA) or Hanning windows
|
||||
* **Moderate main lobe width**: Trades some frequency resolution for side lobe suppression
|
||||
* **Zero phase distortion**: Symmetric filter means no group delay asymmetry
|
||||
|
||||
The 0.54/0.46 coefficients are specifically chosen to cancel the first side lobe. Other windows (like Hanning with 0.5/0.5) don't achieve this cancellation, resulting in higher side lobes.
|
||||
|
||||
### The Compute Challenge
|
||||
|
||||
Like other FIR filters, naive implementations recalculate weights on every tick. QuanTAlib precomputes the weight vector $\mathbf{W}$ upon initialization. Runtime becomes a dot product of the price buffer and weight vector.
|
||||
|
||||
$$ \text{Runtime Cost} = O(N) \text{ multiplications} $$
|
||||
|
||||
The memory locality of arrays enables SIMD vectorization, making the O(N) cost negligible for typical window sizes.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
The weight calculation uses the Hamming window formula:
|
||||
|
||||
### 1. Weight Generation
|
||||
|
||||
For each index $i$ from $0$ to $L-1$:
|
||||
|
||||
$$ w_i = 0.54 - 0.46 \cdot \cos\left(\frac{2\pi i}{L-1}\right) $$
|
||||
|
||||
Where $L$ is the lookback period.
|
||||
|
||||
### 2. Weight Properties
|
||||
|
||||
The Hamming coefficients produce these characteristic values:
|
||||
|
||||
| Position | Weight |
|
||||
|----------|--------|
|
||||
| Edge (i=0, i=L-1) | 0.08 |
|
||||
| Center (i=(L-1)/2) | 1.00 |
|
||||
|
||||
### 3. Normalization
|
||||
|
||||
The final HAMMA value is the weighted sum divided by the total sum of weights $W_{sum}$:
|
||||
|
||||
$$ \text{HAMMA}_t = \frac{\sum_{i=0}^{L-1} P_{t-L+1+i} \cdot w_i}{W_{sum}} $$
|
||||
|
||||
### Example Calculation
|
||||
|
||||
For period=5:
|
||||
|
||||
| Index | cos(2πi/4) | Weight |
|
||||
|-------|------------|--------|
|
||||
| 0 | cos(0) = 1.0 | 0.54 - 0.46(1.0) = 0.08 |
|
||||
| 1 | cos(π/2) = 0.0 | 0.54 - 0.46(0.0) = 0.54 |
|
||||
| 2 | cos(π) = -1.0 | 0.54 - 0.46(-1.0) = 1.00 |
|
||||
| 3 | cos(3π/2) = 0.0 | 0.54 - 0.46(0.0) = 0.54 |
|
||||
| 4 | cos(2π) = 1.0 | 0.54 - 0.46(1.0) = 0.08 |
|
||||
|
||||
Note the symmetry around the center (index 2) with characteristic edge weights of 0.08.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
HAMMA trades CPU cycles for excellent side lobe suppression.
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
Per-bar cost for period $L$ (weights precomputed at construction):
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| MUL | L | 3 | 3L |
|
||||
| ADD | L | 1 | L |
|
||||
| MUL (normalize) | 1 | 3 | 3 |
|
||||
| **Total** | **2L+1** | — | **~4L+3 cycles** |
|
||||
|
||||
For a typical period of 14:
|
||||
- **Total**: ~59 cycles per bar
|
||||
|
||||
**Constructor cost** (one-time): ~80L cycles (L cosines at ~80 cycles each + L additions)
|
||||
|
||||
**Complexity**: O(L) per bar — linear with period. Weights precomputed, runtime is pure dot product.
|
||||
|
||||
### Batch Mode (SIMD/FMA Analysis)
|
||||
|
||||
HAMMA's dot product structure enables efficient SIMD vectorization:
|
||||
|
||||
| Operation | Scalar Ops | SIMD Ops (AVX2) | Speedup |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| MUL+ADD (FMA) | 2L | L/4 (FMA256) | 8× |
|
||||
| Final normalize | 1 | 1 | 1× |
|
||||
|
||||
**Batch efficiency (512 bars, L=14):**
|
||||
|
||||
| Mode | Cycles/bar | Total (512 bars) | Improvement |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Scalar streaming | 59 | 30,208 | — |
|
||||
| SIMD batch (FMA) | ~10 | ~5,120 | **~83%** |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Matches Hamming definition to double precision |
|
||||
| **Timeliness** | 7/10 | Centered filter has inherent lag of (L-1)/2 bars |
|
||||
| **Overshoot** | 10/10 | Symmetric window prevents overshoot entirely |
|
||||
| **Smoothness** | 9/10 | Excellent noise suppression from -43 dB side lobes |
|
||||
|
||||
### Implementation Details
|
||||
|
||||
```csharp
|
||||
// Precomputation (Constructor)
|
||||
double twoPI_N1 = 2.0 * Math.PI / (period - 1);
|
||||
double wSum = 0;
|
||||
|
||||
for (int i = 0; i < period; i++) {
|
||||
double weight = 0.54 - 0.46 * Math.Cos(i * twoPI_N1);
|
||||
_weights[i] = weight;
|
||||
wSum += weight;
|
||||
}
|
||||
_invWeightSum = 1.0 / wSum;
|
||||
|
||||
// Runtime (Update)
|
||||
double sum = _buffer.DotProduct(_weights);
|
||||
return sum * _invWeightSum;
|
||||
```
|
||||
|
||||
## Comparison: Window Functions
|
||||
|
||||
| Window | Edge Weight | First Side Lobe | Main Lobe Width | Best For |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| Rectangular (SMA) | 1.0 | -13 dB | Narrowest | Maximum frequency resolution |
|
||||
| Hanning | 0.0 | -31 dB | Medium | General purpose smoothing |
|
||||
| **Hamming** | **0.08** | **-43 dB** | Medium | Side lobe suppression |
|
||||
| Blackman | 0.0 | -58 dB | Widest | Maximum side lobe suppression |
|
||||
| Gaussian | Variable | -43 dB typical | Variable | Optimal time-frequency tradeoff |
|
||||
|
||||
Choose HAMMA when you need better side lobe suppression than Hanning but don't want the wider main lobe of Blackman.
|
||||
|
||||
## Validation
|
||||
|
||||
QuanTAlib validates HAMMA against its mathematical definition and internal consistency checks.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Validated against math definition. |
|
||||
| **PineScript** | ✅ | Reference implementation matches. |
|
||||
| **TA-Lib** | ❌ | Not included in standard C distribution. |
|
||||
| **Skender** | ❌ | Not included. |
|
||||
| **Tulip** | ❌ | Not included. |
|
||||
| **Ooples** | ❌ | Not included. |
|
||||
|
||||
### C# Implementation Considerations
|
||||
|
||||
The QuanTAlib HAMMA implementation optimizes Hamming window convolution through precomputation and SIMD-accelerated dot products:
|
||||
|
||||
**Precomputed Weights with Inverse Sum**
|
||||
```csharp
|
||||
ComputeWeights(_weights, period, out _invWeightSum);
|
||||
// ...
|
||||
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
double w = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * i);
|
||||
weights[i] = w;
|
||||
sum += w;
|
||||
}
|
||||
invWeightSum = 1.0 / sum;
|
||||
```
|
||||
Trigonometric operations computed once at construction. Normalization uses multiplication by precomputed inverse rather than division per tick.
|
||||
|
||||
**State Record Struct**
|
||||
```csharp
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double LastValidValue, bool IsInitialized);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
```
|
||||
Compiler optimizes field layout. The `IsInitialized` flag tracks whether valid data has been seen for proper NaN handling.
|
||||
|
||||
**SIMD-Accelerated Circular Buffer Dot Product**
|
||||
```csharp
|
||||
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;
|
||||
```
|
||||
Full buffer splits into two `DotProduct` calls to handle circular wrap. The extension leverages AVX2/FMA intrinsics when available.
|
||||
|
||||
**Dual Allocation Strategy for Batch**
|
||||
```csharp
|
||||
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
|
||||
Span<double> weights = period <= 256
|
||||
? stackalloc double[period]
|
||||
: weightsArray!.AsSpan(0, period);
|
||||
```
|
||||
Small periods use stack allocation; large periods use `ArrayPool` to avoid heap pressure while respecting stack limits.
|
||||
|
||||
**Incremental Weight Sum During Warmup**
|
||||
```csharp
|
||||
if (count < period)
|
||||
{
|
||||
count++;
|
||||
currentWeightSum += weights[period - count];
|
||||
}
|
||||
```
|
||||
Partial buffer normalization accumulates weight sum incrementally rather than recalculating each tick.
|
||||
|
||||
**Memory Layout**
|
||||
|
||||
| Field | Type | Size | Notes |
|
||||
|:------|:-----|-----:|:------|
|
||||
| `_period` | int | 4B | Window length |
|
||||
| `_weights` | double[] | 8B + L×8B | Hamming coefficients |
|
||||
| `_invWeightSum` | double | 8B | Precomputed 1/Σw |
|
||||
| `_buffer` | RingBuffer | ~40B + L×8B | Circular data buffer |
|
||||
| `_state` | State | 16B | Last valid + initialized flag |
|
||||
| `_p_state` | State | 16B | Previous state for rollback |
|
||||
| **Total** | | ~92B + 2L×8B | Plus object overhead |
|
||||
|
||||
For a typical 14-period: ~92 + 224 ≈ **316 bytes** per instance.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Confusing Hamming and Hanning**: Hamming uses 0.54/0.46 coefficients with edge weights of 0.08. Hanning uses 0.5/0.5 with edge weights of 0.0. They're different windows with different properties.
|
||||
|
||||
2. **Lag Acceptance**: HAMMA has inherent lag of approximately $(L-1)/2$ bars. This is the price of symmetric smoothing. If you need faster response, consider asymmetric windows like ALMA.
|
||||
|
||||
3. **Cold Start**: HAMMA requires a full window ($L$) to be mathematically valid. First $L-1$ bars are convergence noise.
|
||||
|
||||
4. **Small Periods**: With very small periods (e.g., 3), the window shape degenerates. The edge-center-edge pattern becomes less meaningful. Consider period >= 5 for meaningful Hamming characteristics.
|
||||
|
||||
5. **Side Lobe Trade-off**: The -43 dB first side lobe comes at the cost of slightly wider main lobe than Hanning. If frequency resolution matters more than side lobe suppression, consider other windows.
|
||||
@@ -0,0 +1,43 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Hamming Moving Average (HAMMA)", "HAMMA", overlay=true)
|
||||
|
||||
//@function Calculates HAMMA using Hamming window weighting
|
||||
//@param source Series to calculate HAMMA from
|
||||
//@param period Lookback period - FIR window size
|
||||
//@returns HAMMA value, calculates from first bar using available data
|
||||
//@optimized Uses Hamming window coefficients with O(n) complexity per bar due to lookback loop
|
||||
hamma(series float source, simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
int p = math.min(bar_index + 1, period)
|
||||
var array<float> weights = array.new_float(1, 1.0)
|
||||
var int last_p = 1
|
||||
if last_p != p
|
||||
weights := array.new_float(p, 0.0)
|
||||
for i = 0 to p - 1
|
||||
float w = 0.54 - 0.46 * math.cos(2.0 * math.pi * i / (p - 1))
|
||||
array.set(weights, i, w)
|
||||
last_p := p
|
||||
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_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
hamma_value = hamma(i_source, i_period)
|
||||
|
||||
// Plot
|
||||
plot(hamma_value, "HAMMA", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user