diff --git a/Source/Basics/MIDPOINT_Series.cs b/Source/Basics/MIDPOINT_Series.cs new file mode 100644 index 00000000..ea5bdd0a --- /dev/null +++ b/Source/Basics/MIDPOINT_Series.cs @@ -0,0 +1,44 @@ +namespace QuanTAlib; +using System; + +/* +MIDPOINT: Midpoint value (max+min)/2 in the given period in the series. + If period = 0 => period = full length of the series + +Sources: + https://thefaqblog.com/what-is-the-midpoint-in-statistics/ + + */ + +public class MIDPOINT_Series : Single_TSeries_Indicator +{ + public MIDPOINT_Series(TSeries source, int period, bool useNaN = false) : base(source, period, useNaN) + { + if (base._data.Count > 0) + { base.Add(base._data); } + } + private readonly System.Collections.Generic.List _buffer = new(); + + public override void Add((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._p != 0) + { this._buffer.RemoveAt(0); } + + double _max = TValue.v; + double _min = TValue.v; + for (int i = 0; i < this._buffer.Count; i++) + { + _max = Math.Max(this._buffer[i], _max); + _min = Math.Min(this._buffer[i], _min); + } + double _mid = (_max + _min) * 0.5; + + var result = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _mid); + + base.Add(result, update); + } +} \ No newline at end of file diff --git a/Source/Basics/MIDPRICE_Series.cs b/Source/Basics/MIDPRICE_Series.cs new file mode 100644 index 00000000..f91a0f47 --- /dev/null +++ b/Source/Basics/MIDPRICE_Series.cs @@ -0,0 +1,50 @@ +namespace QuanTAlib; +using System; + +/* +MIDPRICE: Midpoint price (highhest high + lowest low)/2 in the given period in the series. + If period = 0 => period = full length of the series + + */ + +public class MIDPRICE_Series : Single_TBars_Indicator +{ + public MIDPRICE_Series(TBars source, int period, bool useNaN = false) : base(source, period, useNaN) + { + if (base._bars.Count > 0) + { base.Add(base._bars); } + } + private readonly System.Collections.Generic.List _bufferhi = new(); + private readonly System.Collections.Generic.List _bufferlo = new(); + + public override void Add((DateTime t, double o, double h, double l, double c, double v) TBar, bool update) + { + if (update) + { + this._bufferhi[this._bufferhi.Count - 1] = TBar.h; + this._bufferlo[this._bufferlo.Count - 1] = TBar.l; + } + else + { + this._bufferhi.Add(TBar.h); + this._bufferlo.Add(TBar.l); + } + if (this._bufferhi.Count > this._p && this._p != 0) + { this._bufferhi.RemoveAt(0); } + if (this._bufferlo.Count > this._p && this._p != 0) + { this._bufferlo.RemoveAt(0); } + + double _max = TBar.h; + double _min = TBar.l; + for (int i = 0; i < this._bufferhi.Count; i++) + { + _max = Math.Max(this._bufferhi[i], _max); + _min = Math.Min(this._bufferlo[i], _min); + } + double _mid = (_max + _min) * 0.5; + + var result = (TBar.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _mid); + + base.Add(result, update); + } +} \ No newline at end of file diff --git a/Source/Basics/SUM_Series.cs b/Source/Basics/SUM_Series.cs new file mode 100644 index 00000000..bb98134d --- /dev/null +++ b/Source/Basics/SUM_Series.cs @@ -0,0 +1,35 @@ +namespace QuanTAlib; +using System; + +/* +SUM: Cumulative Sum (aka Running Total) + SUM across a period provides a rolling sum of all values across the period. + If SUM values would be divided with period, the output would be SMA() + +Sources: + https://en.wikipedia.org/wiki/CUSUM + + */ + +public class SUM_Series : Single_TSeries_Indicator +{ + public SUM_Series(TSeries source, int period, bool useNaN = false) : base(source, period, useNaN) + { + if (base._data.Count > 0) { base.Add(base._data); } + } + private readonly System.Collections.Generic.List _buffer = new(); + + public override void Add((System.DateTime t, double v) TValue, bool update) + { + if (update) { _buffer[_buffer.Count - 1] = TValue.v; } + else { _buffer.Add(TValue.v); } + if (_buffer.Count > this._p && this._p != 0) { _buffer.RemoveAt(0); } + + double _sum = 0; + for (int i = 0; i < _buffer.Count; i++) { _sum += _buffer[i]; } + + var result = (TValue.t, (this.Count < this._p - 1 && this._NaN) ? double.NaN : _sum); + + base.Add(result, update); + } +} diff --git a/Source/Trends/ALMA_Series.cs b/Source/Trends/ALMA_Series.cs index 82072d68..122cc2d4 100644 --- a/Source/Trends/ALMA_Series.cs +++ b/Source/Trends/ALMA_Series.cs @@ -14,52 +14,54 @@ Sources: https://phemex.com/academy/what-is-arnaud-legoux-moving-averages https://www.prorealcode.com/prorealtime-indicators/alma-arnaud-legoux-moving-average/ +TODO: Discrepancy with Pandas-TA (but passes the validation with Skender.GetAlma) + */ 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; - } - } +{ + 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; + } + } } diff --git a/Tests/Validations/Pandas_TA.cs b/Tests/Validations/Pandas_TA.cs index f7d3b346..67b86791 100644 --- a/Tests/Validations/Pandas_TA.cs +++ b/Tests/Validations/Pandas_TA.cs @@ -6,123 +6,171 @@ using Python.Included; namespace Validations; public class PandasTA : IDisposable -{ - private GBM_Feed bars; - private Random rnd = new(); - private int period; - private string OStype; - private dynamic np; - private dynamic ta; - private dynamic df; - - public PandasTA() - { - bars = new(5000); - period = rnd.Next(28) + 3; - - // Checking the host OS and setting PythonDLL accordingly - OStype = Environment.OSVersion.ToString(); - if (OStype == "Unix 13.1.0") - OStype = @"/usr/local/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/libpython3.10.dylib"; - else OStype = Path.GetFullPath(".") + @"\python-3.10.0-embed-amd64\python310.dll"; - - Installer.InstallPath = Path.GetFullPath("."); - Installer.SetupPython().Wait(); - Installer.TryInstallPip(); - Installer.PipInstallModule("pandas-ta"); - - Runtime.PythonDLL = OStype; - PythonEngine.Initialize(); - np = Py.Import("numpy"); - ta = Py.Import("pandas_ta"); - - string[] cols = { "open", "high", "low", "close", "volume" }; - double[,] ary = new double[bars.Count, 5]; - for (int i = 0; i < bars.Count; i++) - { - ary[i, 0] = bars.Open[i].v; - ary[i, 1] = bars.High[i].v; - ary[i, 2] = bars.Low[i].v; - ary[i, 3] = bars.Close[i].v; - ary[i, 4] = bars.Volume[i].v; - } - df = ta.DataFrame(data: np.array(ary), index: np.array(bars.Close.t), columns: np.array(cols)); - } - - public void Dispose() - { - PythonEngine.Shutdown(); - } - - [Fact] - void SMA() - { - SMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.sma(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void EMA() - { - EMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.ema(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void TEMA() - { - TEMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.tema(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void ENTP() - { - ENTP_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.entropy(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void WMA() - { - WMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.wma(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void DEMA() - { - DEMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.dema(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void BIAS() - { - BIAS_Series QL = new(bars.Close, period, false); - var pta = df.ta.bias(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); - } - - [Fact] - void KURT() - { - KURT_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.kurtosis(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 4), Math.Round(QL.Last().v, 4)); - } - - [Fact] - void MAD() - { - MAD_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.mad(close: df.close, length: period); - Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); +{ + private GBM_Feed bars; + private Random rnd = new(); + private int period; + private string OStype; + private dynamic np; + private dynamic ta; + private dynamic df; + + public PandasTA() + { + bars = new(5000); + period = rnd.Next(28) + 3; + + // Checking the host OS and setting PythonDLL accordingly + OStype = Environment.OSVersion.ToString(); + if (OStype == "Unix 13.1.0") + OStype = @"/usr/local/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/libpython3.10.dylib"; + else OStype = Path.GetFullPath(".") + @"\python-3.10.0-embed-amd64\python310.dll"; + + Installer.InstallPath = Path.GetFullPath("."); + Installer.SetupPython().Wait(); + Installer.TryInstallPip(); + //Installer.PipInstallModule("pandas-ta"); + Installer.PipInstallModule("git+https://github.com/twopirllc/pandas-ta@development"); + + Runtime.PythonDLL = OStype; + PythonEngine.Initialize(); + np = Py.Import("numpy"); + ta = Py.Import("pandas_ta"); + + string[] cols = { "open", "high", "low", "close", "volume" }; + double[,] ary = new double[bars.Count, 5]; + for (int i = 0; i < bars.Count; i++) + { + ary[i, 0] = bars.Open[i].v; + ary[i, 1] = bars.High[i].v; + ary[i, 2] = bars.Low[i].v; + ary[i, 3] = bars.Close[i].v; + ary[i, 4] = bars.Volume[i].v; } + df = ta.DataFrame(data: np.array(ary), index: np.array(bars.Close.t), columns: np.array(cols)); + } + + public void Dispose() + { + PythonEngine.Shutdown(); + } + + [Fact] + void HL2() + { + var pta = df.ta.hl2(high: df.high, low: df.low); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(bars.HL2.Last().v, 7)); + } + + [Fact] + void HLC3() + { + var pta = df.ta.hlc3(high: df.high, low: df.low, close: df.close); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(bars.HLC3.Last().v, 7)); + } + + [Fact] + void OHLC4() + { + var pta = df.ta.ohlc4(open: df.open, high: df.high, low: df.low, close: df.close); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(bars.OHLC4.Last().v, 7)); + } + + [Fact] + void KAMA() + { + KAMA_Series QL = new(bars.Close, period); + var pta = df.ta.kama(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + /* + [Fact] + void ALMA() + { + ALMA_Series QL = new(bars.Close, period: period, offset: 0.85, sigma: 6.0, false); + var pta = df.ta.alma(close: df.close, length: period, distribution_offset: 0.85, sigma: 6.0); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + */ + + [Fact] + void HMA() + { + HMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.hma(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void SMA() + { + SMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.sma(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void EMA() + { + EMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.ema(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void TEMA() + { + TEMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.tema(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void ENTP() + { + ENTP_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.entropy(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void WMA() + { + WMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.wma(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void DEMA() + { + DEMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.dema(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void BIAS() + { + BIAS_Series QL = new(bars.Close, period, false); + var pta = df.ta.bias(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } + + [Fact] + void KURT() + { + KURT_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.kurtosis(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 4), Math.Round(QL.Last().v, 4)); + } + + [Fact] + void MAD() + { + MAD_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.mad(close: df.close, length: period); + Assert.Equal(Math.Round((double)pta.tail(1), 7), Math.Round(QL.Last().v, 7)); + } } \ No newline at end of file diff --git a/Tests/Validations/TA_LIB.cs b/Tests/Validations/TA_LIB.cs index 6776c783..84f92250 100644 --- a/Tests/Validations/TA_LIB.cs +++ b/Tests/Validations/TA_LIB.cs @@ -85,6 +85,33 @@ public class TA_LIB } [Fact] + public void SUM() + { + SUM_Series QL = new(bars.Close, period, false); + Core.Sum(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); + + Assert.Equal(Math.Round(TALIB[TALIB.Length - outBegIdx - 1], 6, MidpointRounding.AwayFromZero), Math.Round(QL.Last().v, 6, MidpointRounding.AwayFromZero)); + } + + [Fact] + public void MIDPRICE() + { + MIDPRICE_Series QL = new(bars, period, false); + Core.MidPrice(inhigh, inlow, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); + + Assert.Equal(Math.Round(TALIB[TALIB.Length - outBegIdx - 1], 6, MidpointRounding.AwayFromZero), Math.Round(QL.Last().v, 6, MidpointRounding.AwayFromZero)); + } + + [Fact] + public void MIDPOINT() + { + MIDPOINT_Series QL = new(bars.Close, period, false); + Core.MidPoint(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); + + Assert.Equal(Math.Round(TALIB[TALIB.Length - outBegIdx - 1], 6, MidpointRounding.AwayFromZero), Math.Round(QL.Last().v, 6, MidpointRounding.AwayFromZero)); + } + + [Fact] public void TRIMA() { TRIMA_Series QL = new(bars.Close, period, false); diff --git a/docs/readme.md b/docs/readme.md index e455846a..c6d0e743 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -43,11 +43,11 @@ See [Getting Started](https://github.com/mihakralj/QuanTAlib/blob/main/Docs/gett | ✔️ OHL3 - (Open+High+Low)/3 | `.OHL3` ||| | ⭐ OHLC4 - Average Price | `.OHLC4` | AVGPRICE |️ GetBaseQuote | | ⭐ HLCC4 - Weighted Price | `.HLCC4` | WCLPRICE || +| ⭐ MIDPOINT - Midpoint value | `MIDPOINT_Series` | MIDPOINT || +| ⭐ MIDPRICE - Midpoint price | `MIDPRICE_Series` | MIDPRICE || | ⭐ MAX - Max value | `MAX_Series` | MAX || | ⭐ MIN - Min value | `MIN_Series` | MIN || -| ⛔ MID - Midpoint value || MIDPOINT || -| ⛔ MIDP - Midpoint price || MIDPRICE || -| ⛔ SUM - Summation || SUM || +| ⭐ SUM - Summation | `SUM_Series` | SUM || | ⭐ ADD - Addition | `ADD_Series` | ADD || | ⭐ SUB - Subtraction | `SUB_Series` | SUB || | ⭐ MUL - Multiplication | `MUL_Series` | MUL ||