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)); + } }