diff --git a/Docs/coverage.md b/Docs/coverage.md
index 7e3a3c51..f8de0937 100644
--- a/Docs/coverage.md
+++ b/Docs/coverage.md
@@ -41,7 +41,7 @@
||||||
| **Moving Averages** |||||
| AFIRMA - Autoregressive Finite Impulse Response Moving Average |||||
-| ALMA - Arnaud Legoux Moving Average |||✔️|✔️|
+| ALMA - Arnaud Legoux Moving Average |✔️||✔️|✔️|
| ARIMA - Autoregressive Integrated Moving Average |||||
| ATR - Average True Range |✔️|✔️|✔️|✔️|
| ATRP - Average True Range Percent |✔️||✔️||
diff --git a/Source/Basics/Abstracts.cs b/Source/Basics/Abstracts.cs
index 85701a21..2deb84e4 100644
--- a/Source/Basics/Abstracts.cs
+++ b/Source/Basics/Abstracts.cs
@@ -141,7 +141,7 @@ public abstract class Single_TBars_Indicator : TSeries
this._p = period;
this._bars = source;
this._NaN = useNaN;
- this._bars.Close.Pub += this.Sub;
+ this._bars.Pub += this.Sub;
}
// overridable Add() method to add/update a single item at the end of the list
diff --git a/Source/Basics/TBars.cs b/Source/Basics/TBars.cs
index 5502253d..81a6db25 100644
--- a/Source/Basics/TBars.cs
+++ b/Source/Basics/TBars.cs
@@ -108,5 +108,22 @@ public class TBars : System.Collections.Generic.List<(DateTime t, double o, doub
_ohlc4.Add((t, (o + h + l + c) * 0.25));
_hlcc4.Add((t, (h + l + c + c) * 0.25));
}
+ this.OnEvent(update);
}
+
+ // delegate used by event handler + event handler (Pub == publisher)
+ public delegate
+ void NewDataEventHandler(object source, TSeriesEventArgs args);
+ public event NewDataEventHandler Pub;
+
+ // Broadcast handler - only to valid targets
+ protected virtual void OnEvent(bool update = false)
+ {
+ if (Pub != null && Pub.Target != this)
+ {
+ Pub(this, new TSeriesEventArgs { update = update });
+ }
+ }
+
+
}
diff --git a/Source/Indicators/ALMA_Series.cs b/Source/Indicators/ALMA_Series.cs
new file mode 100644
index 00000000..fde42118
--- /dev/null
+++ b/Source/Indicators/ALMA_Series.cs
@@ -0,0 +1,66 @@
+namespace QuanTAlib;
+using System;
+
+/*
+ALMA: Arnaud Legoux Moving Average
+ The ALMA moving average uses the curve of the Normal (Gauss) distribution, which
+ can be shifted from 0 to 1. This allows regulating the smoothness and high
+ sensitivity of the indicator. Sigma is another parameter that is responsible for
+ the shape of the curve coefficients. This moving average reduces lag of the data
+ in conjunction with smoothing to reduce noise.
+
+
+Sources:
+ https://phemex.com/academy/what-is-arnaud-legoux-moving-averages
+ https://www.prorealcode.com/prorealtime-indicators/alma-arnaud-legoux-moving-average/
+
+ */
+
+public class ALMA_Series : Single_TSeries_Indicator
+{
+ private readonly System.Collections.Generic.List _buffer = new();
+ private readonly double[] _weight;
+ private double _norm;
+ private readonly double _offset, _sigma;
+
+ public ALMA_Series(TSeries source, int period, double offset = 0.85, double sigma = 6.0, bool useNaN = false)
+ : base(source, period, useNaN)
+ {
+ _offset = offset;
+ _sigma = sigma;
+ _weight = new double[period];
+
+ if (this._data.Count > 0) { base.Add(this._data); }
+ }
+
+ public override void Add((System.DateTime t, double v) TValue, bool update)
+ {
+ if (update) { this._buffer[this._buffer.Count - 1] = TValue.v; }
+ else { this._buffer.Add(TValue.v); }
+ if (this._buffer.Count > this._p) { this._buffer.RemoveAt(0); }
+
+ if (this._buffer.Count <= _p) { calc_weights(); }
+
+ double _weightedSum = 0;
+ for (int i = 0; i < this._buffer.Count; i++) { _weightedSum += _weight[i] * _buffer[i]; }
+ double _alma = _weightedSum / _norm;
+
+ var ret = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _alma);
+ base.Add(ret, update);
+ }
+
+ private void calc_weights()
+ {
+ int _len = this._buffer.Count;
+ _norm = 0;
+ double _m = _offset * (_len - 1);
+ double _s = _len / _sigma;
+ for (int i = 0; i < _len; i++)
+ {
+ double _wt = Math.Exp(-((i - _m) * (i - _m)) / (2 * _s * _s));
+ _weight[i] = _wt;
+ _norm += _wt;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Tests/MovingAvg/ALMA_Test.cs b/Tests/MovingAvg/ALMA_Test.cs
new file mode 100644
index 00000000..fcf15738
--- /dev/null
+++ b/Tests/MovingAvg/ALMA_Test.cs
@@ -0,0 +1,33 @@
+using Xunit;
+using System;
+using QuanTAlib;
+
+namespace MovingAvg;
+public class ALMA_Test
+{
+ [Fact]
+ public void Add_Test()
+ {
+ TSeries a = new() { 0, 1, 2, 3, 4, 5 };
+ ALMA_Series c = new(a, 4);
+ Assert.Equal(6, c.Count);
+ a.Add(5);
+ Assert.Equal(a.Count, c.Count);
+ a.Add(10, update: true);
+ Assert.Equal(a.Count, c.Count);
+ }
+
+ [Fact]
+ public void Edge_Test()
+ {
+ TSeries a = new() { double.NaN, double.Epsilon, double.PositiveInfinity, double.MaxValue };
+ ALMA_Series c = new(a, 3);
+ Assert.Equal(a.Count, c.Count);
+ a.Add(double.NaN);
+ Assert.Equal(a.Count, c.Count);
+ a.Add(double.PositiveInfinity);
+ Assert.Equal(a.Count, c.Count);
+
+ }
+
+}
diff --git a/Tests/Validations/Skender_Stock.cs b/Tests/Validations/Skender_Stock.cs
index 13037492..e2924779 100644
--- a/Tests/Validations/Skender_Stock.cs
+++ b/Tests/Validations/Skender_Stock.cs
@@ -152,4 +152,13 @@ public class Skender_Stock
Assert.Equal(Math.Round((double)SK.Last().Rsi!, 8), Math.Round(QL.Last().v, 8));
}
+
+ [Fact]
+ public void ALMA()
+ {
+ ALMA_Series QL = new(this.bars.Close, this.period, useNaN: false);
+ var SK = this.quotes.GetAlma(this.period);
+
+ Assert.Equal(Math.Round((double)SK.Last().Alma!, 8), Math.Round(QL.Last().v, 8));
+ }
}