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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
@@ -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);
}
}
+58
View File
@@ -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);
}
}
+412
View File
@@ -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);
}
}
+394
View File
@@ -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;
}
}
+250
View File
@@ -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.
+43
View File
@@ -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)