From 58694a9600d7edad2bd9938b1b7e2cf000b6235b Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Sat, 26 Nov 2022 22:04:26 -0800 Subject: [PATCH] refactoring --- .gitignore | 1 + Quantower/Quantower.csproj | 2 + Source/Basics/Single_TSeries_Abstract.cs | 91 +-- Source/Trends/EMA_Series.cs | 9 +- Source/Trends/SMA_Series.cs | 55 +- Source/Volatility/ADOSC_Series.cs | 6 +- Tests/Tests.csproj | 4 + Tests/Validations/DEMA_Test.cs | 166 ++++ Tests/Validations/EMA_Test.cs | 166 ++++ Tests/Validations/SMA_Test.cs | 160 ++++ Tests/Validations/{ => Trends}/Pandas_TA.cs | 712 +++++++++--------- .../{Skender_Stock.cs => Trends/Skender.cs} | 120 +-- Tests/Validations/{ => Trends}/TA_LIB.cs | 69 +- Tests/Validations/Trends/Tulip.cs | 158 ++++ docs/SMA.md | 40 + docs/comparing trends.ipynb | 137 ---- docs/crossovers.ipynb | 149 ---- docs/ema study.ipynb | 249 ------ docs/ema_study.png | Bin 90947 -> 0 bytes docs/getting_started.ipynb | 194 +++-- docs/index.html | 20 +- docs/readme.md | 75 +- 22 files changed, 1397 insertions(+), 1186 deletions(-) create mode 100644 Tests/Validations/DEMA_Test.cs create mode 100644 Tests/Validations/EMA_Test.cs create mode 100644 Tests/Validations/SMA_Test.cs rename Tests/Validations/{ => Trends}/Pandas_TA.cs (84%) rename Tests/Validations/{Skender_Stock.cs => Trends/Skender.cs} (88%) rename Tests/Validations/{ => Trends}/TA_LIB.cs (88%) create mode 100644 Tests/Validations/Trends/Tulip.cs create mode 100644 docs/SMA.md delete mode 100644 docs/comparing trends.ipynb delete mode 100644 docs/crossovers.ipynb delete mode 100644 docs/ema study.ipynb delete mode 100644 docs/ema_study.png diff --git a/.gitignore b/.gitignore index 2c6aac33..6327d78f 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ dotCover.Output.dcvr +/Tests/GlobalSuppressions.cs diff --git a/Quantower/Quantower.csproj b/Quantower/Quantower.csproj index 9cc46c29..0a13f65e 100644 --- a/Quantower/Quantower.csproj +++ b/Quantower/Quantower.csproj @@ -34,9 +34,11 @@ QuanTAlib\%(RecursiveDir)%(Filename)%(Extension) + diff --git a/Source/Basics/Single_TSeries_Abstract.cs b/Source/Basics/Single_TSeries_Abstract.cs index 3725f8e9..1e96b5ca 100644 --- a/Source/Basics/Single_TSeries_Abstract.cs +++ b/Source/Basics/Single_TSeries_Abstract.cs @@ -1,6 +1,7 @@ namespace QuanTAlib; using System; using System.Collections.Generic; +using System.Linq; /* Abstract classes with all scaffolding required to build indicators. @@ -17,50 +18,54 @@ Abstract classes with all scaffolding required to build indicators. */ public abstract class Single_TSeries_Indicator : TSeries { - protected readonly int _period; - protected readonly bool _NaN; - protected readonly TSeries _data; - protected int _p; + protected readonly int _period; + protected readonly bool _NaN; + protected readonly TSeries _data; + protected int _p; - // Chainable Constructor - add it at the end of primary constructor :base(source: source, period: period, useNaN: useNaN) - protected Single_TSeries_Indicator(TSeries source, int period, bool useNaN) + // Chainable Constructor - add it at the end of primary constructor :base(source: source, period: period, useNaN: useNaN) + protected Single_TSeries_Indicator(TSeries source, int period, bool useNaN) + { + this._data = source; + this._period = period; + this._p = _period; + this._NaN = useNaN; + this._data.Pub += this.Sub; + } + + // overridable Add() method to add/update a single item at the end of the list + + public virtual void Add((System.DateTime t, double v) TValue, bool update, bool useNaN) + { + if (_period == 0) { _p = this.Length; } + var res = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : TValue.v); + base.Add(res, update); + } + public new virtual void Add((System.DateTime t, double v) TValue, bool update) => base.Add(TValue, update); + + // potentially overridable Add() method for the whole series (could be replaced with faster bulk algo) + public virtual void Add(TSeries data) { for (int i = 0; i < data.Count; i++) { this.Add(TValue: data[i], update: false); } } + + public new void Add((System.DateTime t, double v) TValue) => this.Add(TValue: TValue, update: false); + public void Add(bool update) => this.Add(TValue: this._data[this._data.Count - 1], update: update); + public void Add() => this.Add(TValue: this._data[this._data.Count - 1], update: false); + public new void Sub(object source, TSeriesEventArgs e) => this.Add(TValue: this._data[this._data.Count - 1], update: e.update); + + protected static void Add_Replace(List l, double v, bool update) + { + if (update) + { l[l.Count - 1] = v; } + else + { l.Add(v); } + } + protected static double Add_Replace_Trim(List l, double v, int p, bool update) + { + Add_Replace(l, v, update); + double ret = (l.Count > 0) ? l.First() : 0; + if (l.Count > p && p != 0) { - this._data = source; - this._period = period; - this._p = _period; - this._NaN = useNaN; - this._data.Pub += this.Sub; - } - - // overridable Add() method to add/update a single item at the end of the list - - public virtual void Add((System.DateTime t, double v) TValue, bool update, bool useNaN) - { - if (_period == 0) { _p = this.Length; } - var res = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : TValue.v); - base.Add(res, update); - } - public new virtual void Add((System.DateTime t, double v) TValue, bool update) => base.Add(TValue, update); - - // potentially overridable Add() method for the whole series (could be replaced with faster bulk algo) - public virtual void Add(TSeries data) { for (int i = 0; i < data.Count; i++) { this.Add(TValue: data[i], update: false); }} - - public new void Add((System.DateTime t, double v) TValue) => this.Add(TValue: TValue, update: false); - public void Add(bool update) => this.Add(TValue: this._data[this._data.Count - 1], update: update); - public void Add() => this.Add(TValue: this._data[this._data.Count - 1], update: false); - public new void Sub(object source, TSeriesEventArgs e) => this.Add(TValue: this._data[this._data.Count - 1], update: e.update); - - protected static void Add_Replace(List l, double v, bool update) - { - if (update) - { l[l.Count - 1] = v; } - else - { l.Add(v); } - } - protected static void Add_Replace_Trim(List l, double v, int p, bool update) - { - Add_Replace(l, v, update); - if (l.Count > p && p!=0) - { l.RemoveAt(0); } + l.RemoveAt(0); } + return ret; + } } diff --git a/Source/Trends/EMA_Series.cs b/Source/Trends/EMA_Series.cs index 62406c43..c3fdc19e 100644 --- a/Source/Trends/EMA_Series.cs +++ b/Source/Trends/EMA_Series.cs @@ -25,12 +25,14 @@ public class EMA_Series : Single_TSeries_Indicator private readonly System.Collections.Generic.List _buffer = new(); private readonly double _k, _k1m; private double _lastema, _lastlastema; + private bool _useSMA; - public EMA_Series(TSeries source, int period, bool useNaN = false) : base(source, period, useNaN) + public EMA_Series(TSeries source, int period, bool useNaN = false, bool useSMA = true) : base(source, period, useNaN) { this._k = 2.0 / (this._p + 1); this._k1m = 1.0 - this._k; - this._lastema = this._lastlastema = double.NaN; + this._lastema = this._lastlastema = 0; + _useSMA = useSMA; if (this._data.Count > 0) { base.Add(this._data); } } @@ -38,8 +40,9 @@ public class EMA_Series : Single_TSeries_Indicator { double _ema; if (update) { this._lastema = this._lastlastema; } + if (this.Count == 0) { _lastema = TValue.v; } - if (this.Count < this._p) + if (this.Count < this._p && _useSMA) { Add_Replace(_buffer, TValue.v, update); _ema = 0; diff --git a/Source/Trends/SMA_Series.cs b/Source/Trends/SMA_Series.cs index f779e746..d7a635a1 100644 --- a/Source/Trends/SMA_Series.cs +++ b/Source/Trends/SMA_Series.cs @@ -1,6 +1,5 @@ namespace QuanTAlib; using System; -using System.Linq; /* SMA: Simple Moving Average @@ -19,19 +18,45 @@ Remark: public class SMA_Series : Single_TSeries_Indicator { - public SMA_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(); + private readonly System.Collections.Generic.List _buffer = new(); + private double _sma, _oldsma; + private double _topv, _oldtopv; + public SMA_Series(TSeries source, int period, bool useNaN = false) : base(source, period, useNaN) + { + if (base._data.Count > 0) + { base.Add(base._data); } + } + public override void Add((System.DateTime t, double v) TValue, bool update) + { + _topv = Add_Replace_Trim(_buffer, TValue.v, _p, update); - public override void Add((System.DateTime t, double v) TValue, bool update) - { - Add_Replace_Trim(_buffer, TValue.v, _p, update); - double _sma = 0; - for (int i=0; i<_buffer.Count; i++) { _sma+= _buffer[i]; } - _sma /= _buffer.Count; + // rolling back if update, storing data for potential future update + if (update) + { + _sma = _oldsma; + _topv = _oldtopv; + } + else + { + _oldsma = _sma; + _oldtopv = _topv; + } - base.Add((TValue.t, _sma), update, _NaN); - } -} + // main additive calculation of SMA - for data points that are larger than _p period + // this.Count > _p + if (this.Count > _p) + { + _sma += (TValue.v - _topv) / _p; + } + else + { + // calculate SMA the traditional way (sum all, divide with _p) for data points within _p period + _sma = 0; + for (int i = 0; i < _buffer.Count; i++) + { _sma += _buffer[i]; } + _sma /= _buffer.Count; + } + + base.Add((TValue.t, _sma), update, _NaN); + } +} \ No newline at end of file diff --git a/Source/Volatility/ADOSC_Series.cs b/Source/Volatility/ADOSC_Series.cs index a021bb01..e5b279bb 100644 --- a/Source/Volatility/ADOSC_Series.cs +++ b/Source/Volatility/ADOSC_Series.cs @@ -20,10 +20,10 @@ public class ADOSC_Series : Single_TBars_Indicator private double _lastema1, _lastlastema1, _lastema2, _lastlastema2; private double _lastadl, _lastlastadl; - public ADOSC_Series(TBars source, bool useNaN = false) : base(source, period: 0, useNaN) + public ADOSC_Series(TBars source, int shortPeriod = 3, int longPeriod =10, bool useNaN = false) : base(source, period: 0, useNaN) { - _k1 = 2.0 / (3 + 1); - _k2 = 2.0 / (10 + 1); + _k1 = 2.0 / (shortPeriod + 1); + _k2 = 2.0 / (longPeriod + 1); _lastadl = _lastlastadl = _lastema1 = _lastlastema1 = _lastema2 = _lastlastema2 = 0; if (_bars.Count > 0) { base.Add(_bars); } } diff --git a/Tests/Tests.csproj b/Tests/Tests.csproj index c584f538..19c1f9e1 100644 --- a/Tests/Tests.csproj +++ b/Tests/Tests.csproj @@ -19,6 +19,8 @@ + + @@ -28,5 +30,7 @@ + + diff --git a/Tests/Validations/DEMA_Test.cs b/Tests/Validations/DEMA_Test.cs new file mode 100644 index 00000000..7a393154 --- /dev/null +++ b/Tests/Validations/DEMA_Test.cs @@ -0,0 +1,166 @@ +using System; +using QuanTAlib; +using Skender.Stock.Indicators; +using Tulip; +using Python.Runtime; +using Python.Included; +using TALib; +using Validations; +using Xunit; + +namespace One.by.one; +public class Dema : IDisposable +{ + private readonly GBM_Feed bars; + private readonly Random rnd = new(); + private readonly int period, skip; + private readonly double precision; + + private readonly IEnumerable quotes; + private readonly double[] outdata; + private readonly double[] inopen; + private readonly double[] inhigh; + private readonly double[] inlow; + private readonly double[] inclose; + private readonly double[] involume; + + private readonly string OStype; + private readonly dynamic np; + private readonly dynamic ta; + private readonly dynamic df; + + public void Dispose() { + PythonEngine.Shutdown(); + GC.SuppressFinalize(this); + } + public Dema() + { + bars = new(Bars: 1000, Volatility: 0.5, Drift: 0.0); + period = rnd.Next(30) + 5; + skip = period*8; + precision = 1e-6; + + quotes = bars.Select(q => new Quote + { + Date = q.t, + Open = (decimal)q.o, + High = (decimal)q.h, + Low = (decimal)q.l, + Close = (decimal)q.c, + Volume = (decimal)q.v + }); + + outdata = new double[bars.Count]; + inopen = bars.Open.v.ToArray(); + inhigh = bars.High.v.ToArray(); + inlow = bars.Low.v.ToArray(); + inclose = bars.Close.v.ToArray(); + involume = bars.Volume.v.ToArray(); + + // 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(path: "."); + Installer.SetupPython().Wait(); + Installer.TryInstallPip(); + Installer.PipInstallModule(module_name: "pandas-ta"); + Runtime.PythonDLL = OStype; + PythonEngine.Initialize(); + np = Py.Import(name: "numpy"); + ta = Py.Import(name: "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)); + } + [Fact] + public void WeirdData() { + DEMA_Series QL = new(source: bars.Close, period); + var lastData = bars.Close.Last(); + var lastCalc = QL.Last(); + QL.Add((DateTime.Today.AddDays(1), double.NaN), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + + QL.Add((DateTime.Today.AddDays(-1), double.NegativeInfinity), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + + QL.Add((new DateTime(), double.Epsilon), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + } + [Fact] + public void Updating() { + DEMA_Series QL = new(source: bars.Close, period); + var lastData = bars.Close.Last(); + var lastCalc = QL.Last(); + int lastLen = QL.Count; + QL.Add((DateTime.Today, 0), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastLen, QL.Count); // same size + Assert.Equal(lastCalc, QL.Last()); // same data + } + [Fact] + public void Skender_Test() + { + DEMA_Series QL = new(bars.Close, period, false); + var SK = quotes.GetDema(period).Select(i => i.Dema.Null2NaN()!); + for (int i = QL.Length; i > skip; i--) + { + double QL_item = QL[i - 1].v; + double SK_item = SK.ElementAt(i - 1); + Assert.InRange(SK_item! - QL_item, -precision, precision); + + } + } + [Fact] + public void TALIB_Test() + { + DEMA_Series QL = new(bars.Close, period, false); + Core.Dema(inclose, 0, bars.Count - 1, outdata, out int outBegIdx, out _, period); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = QL[i].v; + double TA_item = outdata[i - outBegIdx]; + Assert.InRange(TA_item! - QL_item, -precision, precision); + } + } + [Fact] + public void Tulip_Test() { + double[][] arrin = { inclose }; + double[][] arrout = { outdata }; + DEMA_Series QL = new(bars.Close, period: period, useNaN: false); + Tulip.Indicators.dema.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip*3; i--) { + double QL_item = QL[i].v; + double TU_item = arrout[0][i]; + Assert.InRange(TU_item! - QL_item, -precision, precision); + } + } + [Fact] + void PandasTA_Test() { + DEMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.dema(close: df.close, length: period); + for (int i = QL.Length; i > skip; i--) { + double QL_item = QL[i - 1].v; + double PanTA_item = (double)pta[i - 1]; + Assert.InRange(PanTA_item! - QL_item, -1e-5, 1e-5); + } + } +} diff --git a/Tests/Validations/EMA_Test.cs b/Tests/Validations/EMA_Test.cs new file mode 100644 index 00000000..cc20f75a --- /dev/null +++ b/Tests/Validations/EMA_Test.cs @@ -0,0 +1,166 @@ +using System; +using QuanTAlib; +using Skender.Stock.Indicators; +using Tulip; +using Python.Runtime; +using Python.Included; +using TALib; +using Validations; +using Xunit; + +namespace One.by.one; +public class Ema : IDisposable +{ + private readonly GBM_Feed bars; + private readonly Random rnd = new(); + private readonly int period, skip; + private readonly double precision; + + private readonly IEnumerable quotes; + private readonly double[] outdata; + private readonly double[] inopen; + private readonly double[] inhigh; + private readonly double[] inlow; + private readonly double[] inclose; + private readonly double[] involume; + + private readonly string OStype; + private readonly dynamic np; + private readonly dynamic ta; + private readonly dynamic df; + + public void Dispose() { + PythonEngine.Shutdown(); + GC.SuppressFinalize(this); + } + public Ema() + { + bars = new(Bars: 1000, Volatility: 0.5, Drift: 0.0); + period = rnd.Next(30) + 5; + skip = period-1; + precision = 1e-8; + + quotes = bars.Select(q => new Quote + { + Date = q.t, + Open = (decimal)q.o, + High = (decimal)q.h, + Low = (decimal)q.l, + Close = (decimal)q.c, + Volume = (decimal)q.v + }); + + outdata = new double[bars.Count]; + inopen = bars.Open.v.ToArray(); + inhigh = bars.High.v.ToArray(); + inlow = bars.Low.v.ToArray(); + inclose = bars.Close.v.ToArray(); + involume = bars.Volume.v.ToArray(); + + // 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(path: "."); + Installer.SetupPython().Wait(); + Installer.TryInstallPip(); + Installer.PipInstallModule(module_name: "pandas-ta"); + Runtime.PythonDLL = OStype; + PythonEngine.Initialize(); + np = Py.Import(name: "numpy"); + ta = Py.Import(name: "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)); + } + [Fact] + public void WeirdData() { + EMA_Series QL = new(source: bars.Close, period); + var lastData = bars.Close.Last(); + var lastCalc = QL.Last(); + QL.Add((DateTime.Today.AddDays(1), double.NaN), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + + QL.Add((DateTime.Today.AddDays(-1), double.NegativeInfinity), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + + QL.Add((new DateTime(), double.Epsilon), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + } + [Fact] + public void Updating() { + EMA_Series QL = new(source: bars.Close, period); + var lastData = bars.Close.Last(); + var lastCalc = QL.Last(); + int lastLen = QL.Count; + QL.Add((DateTime.Today, 0), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastLen, QL.Count); // same size + Assert.Equal(lastCalc, QL.Last()); // same data + } + [Fact] + public void Skender_Test() + { + EMA_Series QL = new(bars.Close, period, false); + var SK = quotes.GetEma(period).Select(i => i.Ema.Null2NaN()!); + for (int i = QL.Length; i > skip; i--) + { + double QL_item = QL[i - 1].v; + double SK_item = SK.ElementAt(i - 1); + Assert.InRange(SK_item! - QL_item, -precision, precision); + + } + } + [Fact] + public void TALIB_Test() + { + EMA_Series QL = new(bars.Close, period, false); + Core.Ema(inclose, 0, bars.Count - 1, outdata, out int outBegIdx, out _, period); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = QL[i].v; + double TA_item = outdata[i - outBegIdx]; + Assert.InRange(TA_item! - QL_item, -precision, precision); + } + } + [Fact] + public void Tulip_Test() { + double[][] arrin = { inclose }; + double[][] arrout = { outdata }; + EMA_Series QL = new(bars.Close, period: period, useNaN: false, useSMA: false); + Tulip.Indicators.ema.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) { + double QL_item = QL[i].v; + double TU_item = arrout[0][i]; + Assert.InRange(TU_item! - QL_item, -precision, precision); + } + } + [Fact] + void PandasTA_Test() { + EMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.ema(close: df.close, length: period); + for (int i = QL.Length; i > skip; i--) { + double QL_item = QL[i - 1].v; + double PanTA_item = (double)pta[i - 1]; + Assert.InRange(PanTA_item! - QL_item, -1e-5, 1e-5); + } + } +} diff --git a/Tests/Validations/SMA_Test.cs b/Tests/Validations/SMA_Test.cs new file mode 100644 index 00000000..eaaf3d7c --- /dev/null +++ b/Tests/Validations/SMA_Test.cs @@ -0,0 +1,160 @@ +using System; +using QuanTAlib; +using Skender.Stock.Indicators; +using Tulip; +using Python.Runtime; +using Python.Included; +using TALib; +using Validations; +using Xunit; + +namespace One.by.one; +public class SMA : IDisposable { + private readonly GBM_Feed bars; + private readonly Random rnd = new(); + private readonly int period, skip; + private readonly double precision; + + private readonly IEnumerable quotes; + private readonly double[] outdata; + private readonly double[] inopen; + private readonly double[] inhigh; + private readonly double[] inlow; + private readonly double[] inclose; + private readonly double[] involume; + + private readonly string OStype; + private readonly dynamic np; + private readonly dynamic ta; + private readonly dynamic df; + + public void Dispose() { + PythonEngine.Shutdown(); + GC.SuppressFinalize(this); + } + public SMA() { + bars = new(Bars: 1000, Volatility: 0.5, Drift: 0.0); + period = rnd.Next(30) + 5; + precision = 1e-8; + skip = period-1; + + quotes = bars.Select(q => new Quote { + Date = q.t, + Open = (decimal)q.o, + High = (decimal)q.h, + Low = (decimal)q.l, + Close = (decimal)q.c, + Volume = (decimal)q.v + }); + + outdata = new double[bars.Count]; + inopen = bars.Open.v.ToArray(); + inhigh = bars.High.v.ToArray(); + inlow = bars.Low.v.ToArray(); + inclose = bars.Close.v.ToArray(); + involume = bars.Volume.v.ToArray(); + + // 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(path: "."); + Installer.SetupPython().Wait(); + Installer.TryInstallPip(); + Installer.PipInstallModule(module_name: "pandas-ta"); + Runtime.PythonDLL = OStype; + PythonEngine.Initialize(); + np = Py.Import(name: "numpy"); + ta = Py.Import(name: "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)); + } + [Fact] + public void WeirdData() { + SMA_Series QL = new(source: bars.Close, period); + var lastData = bars.Close.Last(); + var lastCalc = QL.Last(); + QL.Add((DateTime.Today.AddDays(1), double.NaN), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + + QL.Add((DateTime.Today.AddDays(-1), double.NegativeInfinity), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + + QL.Add((new DateTime(), double.Epsilon), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastCalc, QL.Last()); // back to the same data + } + [Fact] + public void Updating() { + SMA_Series QL = new(source: bars.Close, period); + var lastData = bars.Close.Last(); + var lastCalc = QL.Last(); + int lastLen = QL.Count; + QL.Add((DateTime.Today, 0), update: true); + Assert.NotEqual(lastCalc, QL.Last()); //value changed + QL.Add(lastData, update: true); + Assert.Equal(lastLen, QL.Count); // same size + Assert.Equal(lastCalc, QL.Last()); // same data + } + [Fact] + public void Skender_Test() { + SMA_Series QL = new(bars.Close, period, false); + var SK = quotes.GetSma(period).Select(i => i.Sma.Null2NaN()!); + for (int i = QL.Length; i > skip; i--) { + double QL_item = QL[i - 1].v; + double SK_item = SK.ElementAt(i - 1); + Assert.InRange(SK_item! - QL_item, -precision, precision); + } + } + [Fact] + public void TALIB_Test() { + SMA_Series QL = new(bars.Close, period, false); + Core.Sma(inclose, 0, bars.Count - 1, outdata, out int outBegIdx, out _, period); + for (int i = QL.Length - 1; i > skip; i--) { + double QL_item = QL[i].v; + double TA_item = outdata[i - outBegIdx]; + Assert.InRange(TA_item! - QL_item, -precision, precision); + + } + } + [Fact] + public void Tulip_Test() { + double[][] arrin = { inclose }; + double[][] arrout = { outdata }; + SMA_Series QL = new(bars.Close, period, false); + Tulip.Indicators.sma.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) { + double QL_item = QL[i].v; + double TU_item = arrout[0][i - period + 1]; + Assert.InRange(TU_item! - QL_item, -precision, precision); + + } + } + [Fact] + void PandasTA_Test() { + SMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.sma(close: df.close, length: period); + for (int i = QL.Length; i > skip; i--) { + double QL_item = QL[i - 1].v; + double PanTA_item = (double)pta[i - 1]; + Assert.InRange(PanTA_item! - QL_item, -precision, precision); + } + } +} diff --git a/Tests/Validations/Pandas_TA.cs b/Tests/Validations/Trends/Pandas_TA.cs similarity index 84% rename from Tests/Validations/Pandas_TA.cs rename to Tests/Validations/Trends/Pandas_TA.cs index 4225dea4..cfe24689 100644 --- a/Tests/Validations/Pandas_TA.cs +++ b/Tests/Validations/Trends/Pandas_TA.cs @@ -1,354 +1,360 @@ -using Xunit; -using System; -using QuanTAlib; -using Python.Runtime; -using Python.Included; - -namespace Validations; -public class PandasTA : IDisposable -{ - private readonly GBM_Feed bars; - private readonly Random rnd = new(); - private readonly int period; - private int digits; - private readonly string OStype; - private readonly dynamic np; - private readonly dynamic ta; - private readonly dynamic df; - - public PandasTA() { - bars = new(Bars: 5000, Volatility: 0.8, Drift: 0.0); - period = rnd.Next(maxValue: 28) + 3; - digits = 4; //minimizing rounding errors in type conversions - - // Checking the host OS and setting PythonDLL accordingly - OStype = Path.GetFullPath(path: ".") + @"\python-3.10.0-embed-amd64\python310.dll"; - - Installer.InstallPath = Path.GetFullPath(path: "."); - Installer.SetupPython().Wait(); - Installer.TryInstallPip(); - Installer.PipInstallModule(module_name: "pandas-ta"); - Runtime.PythonDLL = OStype; - PythonEngine.Initialize(); - np = Py.Import(name: "numpy"); - ta = Py.Import(name: "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(); - GC.SuppressFinalize(this); - } - - [Fact] void ADL() { - ADL_Series QL = new(bars); - var pta = df.ta.ad(high: df.high, low: df.low, close:df.close, volume:df.volume); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i-1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i-1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void ADOSC() { - ADOSC_Series QL = new(bars); - var pta = df.ta.adosc(high: df.high, low: df.low, close: df.close, volume: df.volume); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void ATR() { - ATR_Series QL = new(bars, period); - var pta = df.ta.atr(high: df.high, low: df.low, close: df.close, length: period); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void BIAS() { - BIAS_Series QL = new(bars.Close, period, false); - var pta = df.ta.bias(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void DEMA() { - DEMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.dema(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void EMA() { - EMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.ema(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void ENTROPY() { - ENTROPY_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.entropy(close: df.close, length: period); - for (int i = QL.Length; i > period+1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void HL2() { - var pta = df.ta.hl2(high: df.high, low: df.low); - for (int i = bars.HL2.Length; i > 0; i--) - { - double QL_item = Math.Round(bars.HL2[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void HLC3() { - var pta = df.ta.hlc3(high: df.high, low: df.low, close: df.close); - for (int i = bars.HLC3.Length; i > 0; i--) - { - double QL_item = Math.Round(bars.HLC3[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void HMA() { - HMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.hma(close: df.close, length: period); - for (int i = QL.Length; i > period+1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - - } - [Fact] void KAMA() { - KAMA_Series QL = new(bars.Close, period); - var pta = df.ta.kama(close: df.close, length: period); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void KURTOSIS() { - KURTOSIS_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.kurtosis(close: df.close, length: period); - for (int i = QL.Length; i > period+1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void MAD() - { - MAD_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.mad(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void MEDIAN() { - MEDIAN_Series QL = new(bars.Close, period); - var pta = df.ta.median(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void OBV() { - OBV_Series QL = new(bars); - var pta = df.ta.obv(close: df.close, volume: df.volume); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void OHLC4() { - var pta = df.ta.ohlc4(open: df.open, high: df.high, low: df.low, close: df.close); - for (int i = bars.OHLC4.Length; i > 0; i--) - { - double QL_item = Math.Round(bars.OHLC4[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void RMA() { - RMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.rma(close: df.close, length: period); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void RSI() { - RSI_Series QL = new(bars.Close, period); - var pta = df.ta.rsi(close: df.close, length: period); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void SDEV() { - SDEV_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.stdev(close: df.close, length: period, ddof: 0); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void SMA() { - SMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.sma(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void SSDEV() { - SSDEV_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.stdev(close: df.close, length: period, ddof: 1); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void SVARIANCE() { - SVAR_Series QL = new(bars.Close, period); - var pta = df.ta.variance(close: df.close, length: period, ddof: 1); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void T3() { - T3_Series QL = new(source: bars.Close, period: period, vfactor: 0.7, useNaN: false); - var pta = df.ta.t3(close: df.close, length: period, a: 0.7); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void TEMA() { - TEMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.tema(close: df.close, length: period); - for (int i = QL.Length; i > period; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void TR() { - TR_Series QL = new(bars); - var pta = df.ta.true_range(high: df.high, low: df.low, close: df.close); - for (int i = QL.Length; i > 1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void TRIMA() { - // TODO: return length to variable length (period) when Pandas-TA fixes trima to calculate even periods right - TRIMA_Series QL = new(bars.Close, 11); - var pta = df.ta.trima(close: df.close, length: 11); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void VARIANCE() { - VAR_Series QL = new(bars.Close, period); - var pta = df.ta.variance(close: df.close, length: period, ddof:0); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void WMA() { - WMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.wma(close: df.close, length: period); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void ZLEMA() { - ZLEMA_Series QL = new(bars.Close, period, false); - var pta = df.ta.zlma(close: df.close, length: period); - for (int i = QL.Length; i > 0; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - [Fact] void ZSCORE() { - ZSCORE_Series QL = new(bars.Close, period, useNaN: false); - var pta = df.ta.zscore(close: df.close, length: period, ddof: 0); - for (int i = QL.Length; i > period-1; i--) - { - double QL_item = Math.Round(QL[i - 1].v, digits: digits); - double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); - Assert.Equal(PanTA_item, QL_item); - } - } - +using Xunit; +using System; +using QuanTAlib; +using Python.Runtime; +using Python.Included; + +namespace Validations; +public class PandasTA : IDisposable +{ + private readonly GBM_Feed bars; + private readonly Random rnd = new(); + private readonly int period, sample; + private int digits; + private readonly string OStype; + private readonly dynamic np; + private readonly dynamic ta; + private readonly dynamic df; + + public PandasTA() { + bars = new(Bars: 5000, Volatility: 0.8, Drift: 0.0); + period = rnd.Next(maxValue: 28) + 3; + sample = 200; + digits = 3; //minimizing rounding errors in type conversions + + // 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(path: "."); + Installer.SetupPython().Wait(); + Installer.TryInstallPip(); + Installer.PipInstallModule(module_name: "pandas-ta"); + Runtime.PythonDLL = OStype; + PythonEngine.Initialize(); + np = Py.Import(name: "numpy"); + ta = Py.Import(name: "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(); + GC.SuppressFinalize(this); + } + + [Fact] void ADL() { + ADL_Series QL = new(bars); + var pta = df.ta.ad(high: df.high, low: df.low, close:df.close, volume:df.volume); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i-1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i-1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void ADOSC() { + ADOSC_Series QL = new(bars); + var pta = df.ta.adosc(high: df.high, low: df.low, close: df.close, volume: df.volume); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void ATR() { + ATR_Series QL = new(bars, period); + var pta = df.ta.atr(high: df.high, low: df.low, close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void BIAS() { + BIAS_Series QL = new(bars.Close, period, false); + var pta = df.ta.bias(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void DEMA() { + DEMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.dema(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void EMA() { + EMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.ema(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void ENTROPY() { + ENTROPY_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.entropy(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void HL2() { + var pta = df.ta.hl2(high: df.high, low: df.low); + for (int i = bars.HL2.Length; i > bars.HL2.Length-sample; i--) + { + double QL_item = Math.Round(bars.HL2[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void HLC3() { + var pta = df.ta.hlc3(high: df.high, low: df.low, close: df.close); + for (int i = bars.HLC3.Length; i > bars.HLC3.Length-sample; i--) + { + double QL_item = Math.Round(bars.HLC3[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void HMA() { + HMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.hma(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + + } + [Fact] void KAMA() { + KAMA_Series QL = new(bars.Close, period); + var pta = df.ta.kama(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void KURTOSIS() { + KURTOSIS_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.kurtosis(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void MAD() + { + MAD_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.mad(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void MEDIAN() { + MEDIAN_Series QL = new(bars.Close, period); + var pta = df.ta.median(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void OBV() { + OBV_Series QL = new(bars); + var pta = df.ta.obv(close: df.close, volume: df.volume); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void OHLC4() { + var pta = df.ta.ohlc4(open: df.open, high: df.high, low: df.low, close: df.close); + for (int i = bars.OHLC4.Length; i > bars.OHLC4.Length-sample; i--) + { + double QL_item = Math.Round(bars.OHLC4[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void RMA() { + RMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.rma(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void RSI() { + RSI_Series QL = new(bars.Close, period); + var pta = df.ta.rsi(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void SDEV() { + SDEV_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.stdev(close: df.close, length: period, ddof: 0); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void SMA() { + SMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.sma(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void SSDEV() { + SSDEV_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.stdev(close: df.close, length: period, ddof: 1); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + /* + [Fact] void SVARIANCE() { + SVAR_Series QL = new(bars.Close, period); + var pta = df.ta.variance(close: df.close, length: period, ddof: 1); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } +*/ + [Fact] void T3() { + T3_Series QL = new(source: bars.Close, period: period, vfactor: 0.7, useNaN: false); + var pta = df.ta.t3(close: df.close, length: period, a: 0.7); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void TEMA() { + TEMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.tema(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void TR() { + TR_Series QL = new(bars); + var pta = df.ta.true_range(high: df.high, low: df.low, close: df.close); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void TRIMA() { + // TODO: return length to variable length (period) when Pandas-TA fixes trima to calculate even periods right + TRIMA_Series QL = new(bars.Close, 11); + var pta = df.ta.trima(close: df.close, length: 11); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void VARIANCE() { + VAR_Series QL = new(bars.Close, period); + var pta = df.ta.variance(close: df.close, length: period, ddof:0); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void WMA() { + WMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.wma(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void ZLEMA() { + ZLEMA_Series QL = new(bars.Close, period, false); + var pta = df.ta.zlma(close: df.close, length: period); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + [Fact] void ZSCORE() { + ZSCORE_Series QL = new(bars.Close, period, useNaN: false); + var pta = df.ta.zscore(close: df.close, length: period, ddof: 0); + for (int i = QL.Length; i > QL.Length-sample; i--) + { + double QL_item = Math.Round(QL[i - 1].v, digits: digits); + double PanTA_item = Math.Round((double)pta[i - 1], digits: digits); + Assert.Equal(PanTA_item, QL_item); + } + } + } \ No newline at end of file diff --git a/Tests/Validations/Skender_Stock.cs b/Tests/Validations/Trends/Skender.cs similarity index 88% rename from Tests/Validations/Skender_Stock.cs rename to Tests/Validations/Trends/Skender.cs index 27837c89..b1796f86 100644 --- a/Tests/Validations/Skender_Stock.cs +++ b/Tests/Validations/Trends/Skender.cs @@ -1,21 +1,23 @@ -using System; -using QuanTAlib; -using Skender.Stock.Indicators; -using Xunit; - -namespace Validations; -public class Skender_Stock +using System; +using QuanTAlib; +using Skender.Stock.Indicators; +using Xunit; + +namespace Validations; +public class Skender { private readonly GBM_Feed bars; private readonly Random rnd = new(); - private readonly int period, digits; + private readonly int period, digits, skip; private readonly IEnumerable quotes; + - public Skender_Stock() + public Skender() { bars = new(Bars: 10000, Volatility: 0.5, Drift: 0.0, Precision: 2); period = rnd.Next(30) + 5; - digits = 4; //minimizing rounding errors in type conversions + digits = 2; //minimizing rounding errors in type conversions + skip = 300; quotes = bars.Select(q => new Quote { @@ -27,27 +29,26 @@ public class Skender_Stock Volume = (decimal)q.v }); } - /* + [Fact] public void ADL() { // TODO: check precision of ADL() ADL_Series QL = new(bars, false); var SK = quotes.GetAdl().Select(i => i.Adl); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1)!, digits: digits); Assert.Equal(SK_item!, QL_item); } } - */ [Fact] public void ALMA() { ALMA_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetAlma(period).Select(i => i.Alma.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); @@ -59,7 +60,7 @@ public class Skender_Stock { ATR_Series QL = new(bars, period, false); var SK = quotes.GetAtr(period).Select(i => i.Atr.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); @@ -71,7 +72,7 @@ public class Skender_Stock { ATRP_Series QL = new(bars, period, false); var SK = quotes.GetAtr(period).Select(i => i.Atrp.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); @@ -83,7 +84,7 @@ public class Skender_Stock { BBANDS_Series QL = new(bars.Close, period, 2.0, useNaN: false); var SK = quotes.GetBollingerBands(period, 2.0); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL.Mid[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1).Sma!.Value, digits: digits); @@ -110,7 +111,7 @@ public class Skender_Stock { CCI_Series QL = new(bars, period, false); var SK = quotes.GetCci(period).Select(i => i.Cci.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); @@ -122,20 +123,19 @@ public class Skender_Stock { CORR_Series QL = new(bars.High, bars.Low, period, false); var SK = quotes.Use(CandlePart.High).GetCorrelation(quotes.Use(CandlePart.Low), period).Select(i => i.Correlation.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - /* [Fact] public void COVAR() { COVAR_Series QL = new(bars.High, bars.Low, period, false); var SK = quotes.Use(CandlePart.High).GetCorrelation(quotes.Use(CandlePart.Low), period).Select(i => i.Covariance.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); @@ -147,33 +147,32 @@ public class Skender_Stock { DEMA_Series QL = new(bars.Close, period, false); var SK = quotes.GetDema(period).Select(i => i.Dema.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - */ [Fact] public void EMA() { EMA_Series QL = new(bars.Close, period, false); var SK = quotes.GetEma(period).Select(i => i.Ema.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - /* + /* [Fact] public void HL2() { TSeries QL = bars.HL2; var SK = quotes.GetBaseQuote(CandlePart.HL2).Select(i => i.Value); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1)!, digits: digits); @@ -185,33 +184,33 @@ public class Skender_Stock { TSeries QL = bars.HLC3; var SK = quotes.GetBaseQuote(CandlePart.HLC3).Select(i => i.Value); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1)!, digits: digits); Assert.Equal(SK_item!, QL_item); } } + */ [Fact] public void HMA() { HMA_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetHma(period).Select(i => i.Hma.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - */ [Fact] public void KAMA() { // TODO: check precision of KAMA() KAMA_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetKama(period).Select(i => i.Kama.Null2NaN()!); - for (int i = QL.Length; i > 600; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -223,7 +222,7 @@ public class Skender_Stock { LINREG_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetSlope(period); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1).Slope!, digits: digits); @@ -244,7 +243,7 @@ public class Skender_Stock { MACD_Series QL = new(bars.Close, 26, 12, 9, useNaN: false); var SK = quotes.GetMacd(12, 26, 9); - for (int i = QL.Length; i > 500; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1).Macd.Null2NaN()!, digits: digits); @@ -259,7 +258,7 @@ public class Skender_Stock { MAD_Series QL = new(bars.Close, period, false); var SK = quotes.GetSmaAnalysis(period).Select(i => i.Mad.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -271,7 +270,7 @@ public class Skender_Stock { MAMA_Series QL = new(bars.HL2, fastlimit: 0.5, slowlimit: 0.05); var SK = quotes.GetMama(fastLimit: 0.5, slowLimit: 0.05); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1).Mama.Null2NaN()!, digits: digits); @@ -286,7 +285,7 @@ public class Skender_Stock { MAPE_Series QL = new(bars.Close, period, false); var SK = quotes.GetSmaAnalysis(period).Select(i => i.Mape.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -298,7 +297,7 @@ public class Skender_Stock { MSE_Series QL = new(bars.Close, period, false); var SK = quotes.GetSmaAnalysis(period).Select(i => i.Mse.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -311,27 +310,32 @@ public class Skender_Stock OBV_Series QL = new(bars, period, false); var SK = quotes.GetObv(period).Select(i => i.Obv!); // adding volume[0] to OBV to pass the test and keep compatibility with TA-LIB - Assert.Equal(Math.Round(SK.Last()! + (double)quotes.First().Volume!, digits: digits), Math.Round(QL.Last().v, digits: digits)); + for (int i = QL.Length; i > skip; i--) + { + double QL_item = Math.Round(QL.Last().v, digits: digits); + double SK_item = Math.Round(SK.Last()! + (double)quotes.First().Volume!, digits: digits); + Assert.Equal(SK_item!, QL_item); + } } - /* + /* [Fact] public void OC2() { TSeries QL = bars.OC2; var SK = quotes.GetBaseQuote(CandlePart.OC2).Select(i => i.Value); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1)!, digits: digits); Assert.Equal(SK_item!, QL_item); } } - [Fact] + [Fact] public void OHL3() { TSeries QL = bars.OHL3; var SK = quotes.GetBaseQuote(CandlePart.OHL3).Select(i => i.Value); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1)!, digits: digits); @@ -343,20 +347,20 @@ public class Skender_Stock { TSeries QL = bars.OHLC4; var SK = quotes.GetBaseQuote(CandlePart.OHLC4).Select(i => i.Value); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round((double)SK.ElementAt(i - 1)!, digits: digits); Assert.Equal(SK_item!, QL_item); } } - */ - [Fact] + */ + [Fact] public void RSI() { RSI_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetRsi(period).Select(i => i.Rsi.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -368,7 +372,7 @@ public class Skender_Stock { SDEV_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetStdDev(period).Select(i => i.StdDev.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -380,7 +384,7 @@ public class Skender_Stock { SMA_Series QL = new(bars.Close, period, false); var SK = quotes.GetSma(period).Select(i => i.Sma.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -392,33 +396,31 @@ public class Skender_Stock { SMMA_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetSmma(period).Select(i => i.Smma.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - /* [Fact] public void T3() { T3_Series QL = new(source: bars.Close, period: period, vfactor: 0.7, false); var SK = quotes.GetT3(lookbackPeriods: period, volumeFactor: 0.7).Select(i => i.T3.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - */ [Fact] public void TEMA() { TEMA_Series QL = new(bars.Close, period, false); var SK = quotes.GetTema(period).Select(i => i.Tema.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); @@ -430,38 +432,36 @@ public class Skender_Stock { TR_Series QL = new(bars, useNaN: false); var SK = quotes.GetTr().Select(i => i.Tr.Null2NaN()!); - for (int i = QL.Length; i > 1; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - /* [Fact] public void WMA() { WMA_Series QL = new(bars.Close, period, false); var SK = quotes.GetWma(period).Select(i => i.Wma.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } } - */ [Fact] public void ZSCORE() { ZSCORE_Series QL = new(bars.Close, period, useNaN: false); var SK = quotes.GetStdDev(period).Select(i => i.ZScore.Null2NaN()!); - for (int i = QL.Length; i > period; i--) + for (int i = QL.Length; i > skip; i--) { double QL_item = Math.Round(QL[i - 1].v, digits: digits); double SK_item = Math.Round(SK.ElementAt(i - 1), digits: digits); Assert.Equal(SK_item!, QL_item); } - } - -} + } + +} diff --git a/Tests/Validations/TA_LIB.cs b/Tests/Validations/Trends/TA_LIB.cs similarity index 88% rename from Tests/Validations/TA_LIB.cs rename to Tests/Validations/Trends/TA_LIB.cs index 06edb0e6..43b77fbb 100644 --- a/Tests/Validations/TA_LIB.cs +++ b/Tests/Validations/Trends/TA_LIB.cs @@ -8,7 +8,7 @@ public class Ta_Lib { private readonly GBM_Feed bars; private readonly Random rnd = new(); - private readonly int period, digits; + private readonly int period, digits, skip; private readonly double[] TALIB; private readonly double[] TALIB2; private readonly double[] inopen; @@ -21,6 +21,7 @@ public class Ta_Lib { bars = new(Bars: 5000, Volatility: 0.8, Drift: 0.0, Precision: 3); period = rnd.Next(28) + 3; + skip = 200; digits = 6; TALIB = new double[bars.Count]; @@ -37,7 +38,7 @@ public class Ta_Lib { ADD_Series QL = new(bars.Open, bars.Close); Core.Add(inopen, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -59,9 +60,9 @@ public class Ta_Lib [Fact] public void ADOSC() { - ADOSC_Series QL = new(bars, false); + ADOSC_Series QL = new(bars, 3, 10, false); Core.AdOsc(inhigh, inlow, inclose, involume, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -73,7 +74,7 @@ public class Ta_Lib { ATR_Series QL = new(bars, period, false); Core.Atr(inhigh, inlow, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx * 15; i--) + for (int i = QL.Length - 1; i > skip * 15; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -88,7 +89,7 @@ public class Ta_Lib double[] outLower = new double[bars.Count]; BBANDS_Series QL = new(bars.Close, period: 26, multiplier: 2.0, false); Core.Bbands(inclose, 0, bars.Count - 1, outRealUpperBand: outUpper, outRealMiddleBand: outMiddle, outRealLowerBand: outLower, out int outBegIdx, out _, optInTimePeriod: 26, optInNbDevUp: 2.0, optInNbDevDn: 2.0); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL.Upper[i].v, digits: digits); double TA_item = Math.Round(outUpper[i - outBegIdx], digits: digits); @@ -109,7 +110,7 @@ public class Ta_Lib { CCI_Series QL = new(bars, period, false); Core.Cci(inhigh, inlow, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -121,7 +122,7 @@ public class Ta_Lib { CORR_Series QL = new(bars.Open, bars.Close, period); Core.Correl(inopen, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, optInTimePeriod: period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -133,7 +134,7 @@ public class Ta_Lib { DEMA_Series QL = new(bars.Close, period, false); Core.Dema(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -145,7 +146,7 @@ public class Ta_Lib { DIV_Series QL = new(bars.Open, bars.Close); Core.Div(inopen, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -157,7 +158,7 @@ public class Ta_Lib { EMA_Series QL = new(bars.Close, period, false); Core.Ema(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -169,7 +170,7 @@ public class Ta_Lib { TSeries QL = bars.HL2; Core.MedPrice(inhigh, inlow, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -181,7 +182,7 @@ public class Ta_Lib { TSeries QL = bars.HLC3; Core.TypPrice(inhigh, inlow, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -193,7 +194,7 @@ public class Ta_Lib { TSeries QL = bars.HLCC4; Core.WclPrice(inhigh, inlow, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -207,7 +208,7 @@ public class Ta_Lib double[] macdHist = new double[bars.Count]; MACD_Series QL = new(bars.Close, slow: 26, fast: 12, signal: 9, false); Core.Macd(inclose, 0, bars.Count - 1, outMacd: TALIB, outMacdSignal: macdSignal, outMacdHist: macdHist, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx * 10; i--) + for (int i = QL.Length - 1; i > skip * 10; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -222,7 +223,7 @@ public class Ta_Lib { MAMA_Series QL = new(bars.Close, fastlimit: 0.5, slowlimit: 0.05); Core.Mama(inReal: inclose, startIdx: 0, endIdx: bars.Count - 1, outMama: TALIB, outFama: TALIB2, outBegIdx: out int outBegIdx, outNbElement: out _, optInFastLimit: 0.5, optInSlowLimit: 0.05); - for (int i = QL.Length - 1; i > outBegIdx * 15; i--) + for (int i = QL.Length - 1; i > skip * 15; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -234,7 +235,7 @@ public class Ta_Lib { MAX_Series QL = new(bars.Close, period, false); Core.Max(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -246,7 +247,7 @@ public class Ta_Lib { MIDPOINT_Series QL = new(bars.Close, period, false); Core.MidPoint(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -258,7 +259,7 @@ public class Ta_Lib { MIDPRICE_Series QL = new(bars, period, false); Core.MidPrice(inhigh, inlow, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -270,7 +271,7 @@ public class Ta_Lib { MIN_Series QL = new(bars.Close, period, false); Core.Min(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -282,7 +283,7 @@ public class Ta_Lib { MUL_Series QL = new(bars.Open, bars.Close); Core.Mult(inopen, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -294,7 +295,7 @@ public class Ta_Lib { OBV_Series QL = new(bars, period, false); Core.Obv(inclose, involume, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -306,7 +307,7 @@ public class Ta_Lib { TSeries QL = bars.OHLC4; Core.AvgPrice(inopen, inhigh, inlow, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -318,7 +319,7 @@ public class Ta_Lib { RSI_Series QL = new(bars.Close, period, false); Core.Rsi(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -330,7 +331,7 @@ public class Ta_Lib { SDEV_Series QL = new(bars.Close, period, false); Core.StdDev(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -342,7 +343,7 @@ public class Ta_Lib { SMA_Series QL = new(bars.Close, period, false); Core.Sma(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -354,7 +355,7 @@ public class Ta_Lib { SUB_Series QL = new(bars.Open, bars.Close); Core.Sub(inopen, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -366,7 +367,7 @@ public class Ta_Lib { SUM_Series QL = new(bars.Close, period, false); Core.Sum(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -378,7 +379,7 @@ public class Ta_Lib { T3_Series QL = new(source: bars.Close, period: period, vfactor: 0.7, useNaN: false); Core.T3(inReal: inclose, startIdx: 0, endIdx: bars.Count - 1, outReal: TALIB, outBegIdx: out int outBegIdx, outNbElement: out _, optInTimePeriod: period, optInVFactor: 0.7); - for (int i = QL.Length - 1; i > outBegIdx * 15; i--) + for (int i = QL.Length - 1; i > skip * 15; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -390,7 +391,7 @@ public class Ta_Lib { TEMA_Series QL = new(bars.Close, period, false); Core.Tema(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx * 15; i--) + for (int i = QL.Length - 1; i > skip * 15; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -402,7 +403,7 @@ public class Ta_Lib { TR_Series QL = new(bars, false); Core.TRange(inhigh, inlow, inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -414,7 +415,7 @@ public class Ta_Lib { TRIMA_Series QL = new(bars.Close, period, false); Core.Trima(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -426,7 +427,7 @@ public class Ta_Lib { VAR_Series QL = new(bars.Close, period, false); Core.Var(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx * 15; i--) + for (int i = QL.Length - 1; i > skip * 15; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); @@ -438,7 +439,7 @@ public class Ta_Lib { WMA_Series QL = new(bars.Close, period, false); Core.Wma(inclose, 0, bars.Count - 1, TALIB, out int outBegIdx, out _, period); - for (int i = QL.Length - 1; i > outBegIdx; i--) + for (int i = QL.Length - 1; i > skip; i--) { double QL_item = Math.Round(QL[i].v, digits: digits); double TA_item = Math.Round(TALIB[i - outBegIdx], digits: digits); diff --git a/Tests/Validations/Trends/Tulip.cs b/Tests/Validations/Trends/Tulip.cs new file mode 100644 index 00000000..fd36c4e8 --- /dev/null +++ b/Tests/Validations/Trends/Tulip.cs @@ -0,0 +1,158 @@ +using Xunit; +using System; +using Tulip; +using QuanTAlib; + +namespace Validations; +public class Tulip_Test +{ + private readonly GBM_Feed bars; + private readonly Random rnd = new(); + private readonly int period, digits, skip; + private readonly double[] outdata; + private readonly double[] inopen; + private readonly double[] inhigh; + private readonly double[] inlow; + private readonly double[] inclose; + private readonly double[] involume; + + public Tulip_Test() + { + bars = new(Bars: 5000, Volatility: 0.8, Drift: 0.0, Precision: 3); + period = rnd.Next(28) + 3; + skip = 600; + digits = 5; + + outdata = new double[bars.Count]; + inopen = bars.Open.v.ToArray(); + inhigh = bars.High.v.ToArray(); + inlow = bars.Low.v.ToArray(); + inclose = bars.Close.v.ToArray()!; + involume = bars.Volume.v.ToArray()!; + + } + [Fact] + public void AD() + { + double[][] arrin = {inhigh, inlow, inclose, involume }; + double[][] arrout = { outdata }; + ADL_Series QL = new(bars, false); + Tulip.Indicators.ad.Run(inputs: arrin, options: new double[] { }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void ADD() + { + double[][] arrin = { inhigh, inlow }; + double[][] arrout = { outdata }; + ADD_Series QL = new(bars.High, bars.Low); + Tulip.Indicators.add.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void ADOSC() + { + double[][] arrin = { inhigh, inlow, inclose, involume }; + double[][] arrout = { outdata }; + int s = 3; + ADOSC_Series QL = new(bars, s, period, false); + Tulip.Indicators.adosc.Run(inputs: arrin, options: new double[] { s, period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i-period+1], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void ATR() + { + double[][] arrin = { inhigh, inlow, inclose }; + double[][] arrout = { outdata }; + + ATR_Series QL = new(bars, period, false); + Tulip.Indicators.atr.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i - period + 1], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void BBANDS() + { + double[][] arrin = { inclose }; + double[] outmid = new double[bars.Count]; + double[] outlower = new double[bars.Count]; + double[] outupper = new double[bars.Count]; + double[][] arrout = { outlower, outmid, outupper}; + BBANDS_Series QL = new(bars.Close, period, 2, false); + Tulip.Indicators.bbands.Run(inputs: arrin, options: new double[] { period, 2 }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL.Lower[i].v, digits: digits); + double TU_item = Math.Round(outlower[i - period + 1], digits); + Assert.Equal(TU_item!, QL_item); + QL_item = Math.Round(QL.Mid[i].v, digits: digits); + TU_item = Math.Round(outmid[i - period + 1], digits); + Assert.Equal(TU_item!, QL_item); + QL_item = Math.Round(QL.Upper[i].v, digits: digits); + TU_item = Math.Round(outupper[i - period + 1], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void EMA() + { + double[][] arrin = { inclose }; + double[][] arrout = { outdata }; + EMA_Series QL = new(bars.Close, period, false); + Tulip.Indicators.ema.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void AVGPRICE() + { + double[][] arrin = { inopen, inhigh, inlow, inclose }; + double[][] arrout = { outdata }; + + TSeries QL = bars.OHLC4; + Tulip.Indicators.avgprice.Run(inputs: arrin, options: new double[] { }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i], digits); + Assert.Equal(TU_item!, QL_item); + } + } + [Fact] + public void SMA() + { + double[][] arrin = { inclose }; + double[][] arrout = { outdata }; + SMA_Series QL = new(bars.Close, period, false); + Tulip.Indicators.sma.Run(inputs: arrin, options: new double[] { period }, outputs: arrout); + for (int i = QL.Length - 1; i > skip; i--) + { + double QL_item = Math.Round(QL[i].v, digits: digits); + double TU_item = Math.Round(arrout[0][i-period+1], digits); + Assert.Equal(TU_item!, QL_item); + } + } +} diff --git a/docs/SMA.md b/docs/SMA.md new file mode 100644 index 00000000..3f5eca99 --- /dev/null +++ b/docs/SMA.md @@ -0,0 +1,40 @@ +# SMA: Simple Moving Average +SMA is one of the most basic trend-following indicators used in Technical Analysis. It is calculated as the *unweighted mean* of the previous $p$ (period) data-points. + + +## Calculation + +SMA is a rolling calculation looking backwards from the position ${n}$ and is denoted as ${SMA}_{p}{(data)}$ where $p$ represents the period and $data$ represents the list of data points: +$$ +SMA_p{(data)} = \frac{1}{p}\sum_{i=n-p+1}^{n} data_i +$$ +When calculating the value of next $SMA_{p,next}$ while knowing all previous SMA values, SMA calculation can be reduced to: +$$ +SMA_{p,next} = SMA_{p,prev}+\frac{1}{p}\left( data_{n+1}-data_{n+1-p}\right) +$$ + +## Implementation + +``` csharp +SMA_Series mean = new(source: data, period: p, useNaN: false); + +QuanTA fluent = data.SMA(period: p); +``` + +## Parameters + +- `TSeries source` - List of value tuples (DateTime, double) +- `int period` - Integer representing the period of SMA +- `bool useNaN` - if true, initial values from 1 to period-1 will be replaced with NaN. If false, the initial calculation will return values for SMA(length) instead of SMA(period) + +## Sample chart + +picture of SMA + +## Comparison & Validation + +Validation tests +Performance tests + +## References + - https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/simple-moving-average-sma/ \ No newline at end of file diff --git a/docs/comparing trends.ipynb b/docs/comparing trends.ipynb deleted file mode 100644 index ae2cbc6c..00000000 --- a/docs/comparing trends.ipynb +++ /dev/null @@ -1,137 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/plain": [ - "Loading extensions from `C:\\Users\\miha\\.nuget\\packages\\plotly.net.interactive\\3.0.2\\interactive-extensions\\dotnet\\Plotly.NET.Interactive.dll`" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "//#r \"nuget: QuanTAlib;\"\n", - "\n", - "#r \"nuget: Plotly.NET;\"\n", - "#r \"nuget: Plotly.NET.Interactive;\"\n", - "#r \"..\\Source\\bin\\Debug\\net6.0\\QuanTAlib.dll\"\n", - "\n", - "using QuanTAlib;\n", - "using Plotly.NET;\n", - "using Plotly.NET.LayoutObjects;" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - "
\r\n", - "\r\n", - "\n", - " \n", - "
\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "int period = 25;\n", - "GBM_Feed feed = new(50, Volatility: 1);\n", - "TSeries data = feed.OHLC4;\n", - "//List raw = new() { 0, 1, 1 ,1 ,1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0}; \n", - "//TSeries data = new();\n", - "//for (int i=0; i(data.t, data.v, false, data.GetType().Name).WithLineStyle(Width: 4, Color: Color.fromString(\"red\"));\n", - "var line1 = Chart2D.Chart.Line(calc1.t, calc1.v, false, \"EMA_SMA\").WithLineStyle(Width: 2, Color: Color.fromString(\"purple\"));\n", - "var line2 = Chart2D.Chart.Line(calc2.t, calc2.v, false, \"True EMA\").WithLineStyle(Width: 2, Color: Color.fromString(\"blue\"));\n", - "\n", - "var chart = Chart.Combine(new []{line0, line1, line2})\n", - " .WithSize(1200,600)\n", - " .WithMargin(Margin.init(30,10,40,30,1,false))\n", - " .WithXAxisRangeSlider(RangeSlider.init(Visible:false));\n", - "\n", - "chart" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".NET (C#)", - "language": "C#", - "name": ".net-csharp" - }, - "language_info": { - "file_extension": ".cs", - "mimetype": "text/x-csharp", - "name": "C#", - "pygments_lexer": "csharp", - "version": "9.0" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/crossovers.ipynb b/docs/crossovers.ipynb deleted file mode 100644 index 1a8821f6..00000000 --- a/docs/crossovers.ipynb +++ /dev/null @@ -1,149 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#r \"nuget: QuanTAlib;\"\n", - "#r \"nuget: Plotly.NET;\"\n", - "#r \"nuget: Plotly.NET.Interactive;\"\n", - "\n", - "using QuanTAlib;\n", - "using Plotly.NET;\n", - "using Plotly.NET.LayoutObjects;\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [], - "source": [ - "String Sym = \"IBM\";\n", - "Alphavantage_Feed data = new(Symbol: Sym);\n", - "ZLEMA_Series calc1 = new(data.OHLC4,20);\n", - "HMA_Series calc2 = new(data.OHLC4,20);\n", - "HEMA_Series calc3 = new(data.OHLC4,20);" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "
\n", - "
\r\n", - "\r\n", - "\n", - " \n", - "
\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "var layout = Layout.init( \n", - " PlotBGColor : Color.fromString(\"#1e1e1e\"),\n", - " PaperBGColor : Color.fromString(\"#1e1e1e\"),\n", - " Font:Font.init(Size:10, Color: Color.fromString(\"#ffffff\")));\n", - "\n", - "var yAxis = LinearAxis.init(\n", - " GridColor:Color.fromString(\"#252525\")); \n", - "\n", - "var candles = Chart2D.Chart.Candlestick(data.Open.v, data.High.v, data.Low.v, data.Close.v, data.Open.t, \"\");\n", - "var line1 = Chart2D.Chart.Line(calc1.t, calc1.v, false, calc1.GetType().Name).WithLineStyle(Width: 2, Color: Color.fromString(\"yellow\"));\n", - "var line2 = Chart2D.Chart.Line(calc2.t, calc2.v, false, calc2.GetType().Name).WithLineStyle(Width: 3, Color: Color.fromString(\"red\"));\n", - "var line3 = Chart2D.Chart.Line(calc3.t, calc3.v, false, calc3.GetType().Name).WithLineStyle(Width: 2, Color: Color.fromString(\"blue\"));\n", - "var chart = Chart.Combine(new []{candles, line1, line2, line3})\n", - " .WithSize(1200,600)\n", - " .WithMargin(Margin.init(30,10,40,30,1,false))\n", - " .WithXAxisRangeSlider(RangeSlider.init(Visible:false))\n", - " .WithYAxis(yAxis)\n", - " .WithXAxis(yAxis)\n", - " .WithTitle(Sym)\n", - " .WithLayout(layout);\n", - "\n", - "chart" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".NET (C#)", - "language": "C#", - "name": ".net-csharp" - }, - "language_info": { - "file_extension": ".cs", - "mimetype": "text/x-csharp", - "name": "C#", - "pygments_lexer": "csharp", - "version": "9.0" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/ema study.ipynb b/docs/ema study.ipynb deleted file mode 100644 index 49fde29c..00000000 --- a/docs/ema study.ipynb +++ /dev/null @@ -1,249 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 44, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [ - { - "data": { - "text/html": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "#r \"nuget: Plotly.NET;\"\n", - "#r \"nuget: Plotly.NET.ImageExport;\"\n", - "\n", - "using Plotly.NET;\n", - "using Plotly.NET.ImageExport;\n", - "\n", - "double ema(double data, double prev_ema, int period, double attenuator = 0.0) {\n", - " double k = 2.0 / (period + 1);\n", - " return ((data*k) + (prev_ema*(1-k)))/(1-attenuator);\n", - "}\n", - "\n", - "List data = new() { 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0}; \n", - "List x = new() { 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};\n", - "List es = new();\n", - "List e1 = new();\n", - "List e2 = new();\n", - "List e3 = new();" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": {}, - "source": [ - "EMA is a convergent moving average where we don't need to keep a list of past values around; all that is needed to calculate the next value of EMA is a previous value, factor k [ k = 2.0/(period+1) ] and new value.\n", - "\n", - "The core question is: how to START the EMA sequence?\n", - "\n", - "The most direct way is to start the EMA sequence with an arbitrary number - either zero or the first value are commonly accepted 'igniters' of the calculation. Except both starting values are wrong: \n", - "- starting EMA with the First Value creates overshooting EMA, requiring 50+ bars to converge to the correct series.\n", - "- starting EMA with a Zero undershoots the series, requiring 50+ bars to climb back to the correct series.\n", - "- starting EMA with a short SMA warming sequence at the beginning creates even more problems: transition from SMA to EMA is non-linear and results are not predictable.\n", - "\n", - "But there is a better way.\n", - "\n", - "I found an article on [David Owen's blog](https://blog.fugue88.ws/archives/2017-01/The-correct-way-to-start-an-Exponential-Moving-Average-EMA) that explains the math behind attenuation of early elements of EMA series.\n", - "\n", - "Below is the simple comparison of four approaches:\n", - "\n", - "- low EMA (series assumes that pre-value was 0)\n", - "- high EMA (series assumes that pre-value was same as the first value)\n", - "- SMA-EMA (series first calculates SMA for the duration of period and then uses the last SMA to calculate EMA)\n", - "- fixed EMA (uses diminishing attenuation to keep EMA between overshooting and undershooting EMA )\n" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "data\t low\t high\t s_ema fixed\t\r\n", - "1\t 0.222\t 1.000\t 1.000\t 1.000\t\r\n", - "0\t 0.173\t 0.778\t 0.500\t 0.560\t\r\n", - "0\t 0.134\t 0.605\t 0.333\t 0.393\t\r\n", - "0\t 0.105\t 0.471\t 0.250\t 0.294\t\r\n", - "0\t 0.081\t 0.366\t 0.200\t 0.226\t\r\n", - "0\t 0.063\t 0.285\t 0.167\t 0.175\t\r\n", - "1\t 0.271\t 0.444\t 0.286\t 0.358\t\r\n", - "1\t 0.433\t 0.567\t 0.375\t 0.501\t\r\n", - "1\t 0.559\t 0.663\t 0.514\t 0.611\t\r\n", - "1\t 0.657\t 0.738\t 0.622\t 0.698\t\r\n", - "1\t 0.733\t 0.796\t 0.706\t 0.765\t\r\n", - "0\t 0.570\t 0.619\t 0.549\t 0.595\t\r\n", - "0\t 0.444\t 0.482\t 0.427\t 0.463\t\r\n", - "0\t 0.345\t 0.375\t 0.332\t 0.360\t\r\n", - "0\t 0.268\t 0.291\t 0.258\t 0.280\t\r\n", - "0\t 0.209\t 0.227\t 0.201\t 0.218\t\r\n" - ] - } - ], - "source": [ - "int period = 18;\n", - "\n", - "double k = 2.0 / (period + 1);\n", - "double attenuator = ( 1 - k ) * 0.5;\n", - "double factor = 1;\n", - "double ema1 = 0; // start too low\n", - "double ema2 = data[0]; // start too high\n", - "double ema3 = data[0]*(1-k-attenuator)/(1-k); \n", - "double emas = data[0];\n", - "Console.WriteLine($\"data\\t low\\t high\\t s_ema fixed\\t\");\n", - "for (int i=0; i\n", - "
\r\n", - "\r\n", - "\n", - " \n", - " \n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "var d = Chart2D.Chart.Line(x, data, true, \"Data\").WithLineStyle(Width: 4, Color: Color.fromString(\"blue\"));\n", - "var le = Chart2D.Chart.Line(x, es, true, \"s_ema\").WithLineStyle(Width: 2, Color: Color.fromString(\"green\"));\n", - "var le1 = Chart2D.Chart.Line(x, e1, true, \"low\").WithLineStyle(Width: 2, Color: Color.fromString(\"red\"));\n", - "var le2 = Chart2D.Chart.Line(x, e2, true, \"high\").WithLineStyle(Width: 2, Color: Color.fromString(\"red\"));\n", - "var le3 = Chart2D.Chart.Line(x, e3, true, \"fixed\").WithLineStyle(Width: 3, Color: Color.fromString(\"purple\"));\n", - "var chart = Chart.Combine(new []{d,le,le1,le2,le3})\n", - " .WithSize(1200,600)\n", - " .WithMargin(Margin.init(30,10,40,30,1,false))\n", - " .WithXAxisRangeSlider(RangeSlider.init(Visible:false));\n", - "\n", - "chart.SavePNG(\"ema_study\");\n", - "chart" - ] - }, - { - "attachments": {}, - "cell_type": "markdown", - "metadata": { - "dotnet_interactive": { - "language": "csharp" - }, - "vscode": { - "languageId": "dotnet-interactive.csharp" - } - }, - "source": [ - "![EMA study chart](./ema_study.png)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".NET (C#)", - "language": "C#", - "name": ".net-csharp" - }, - "language_info": { - "file_extension": ".cs", - "mimetype": "text/x-csharp", - "name": "C#", - "pygments_lexer": "csharp", - "version": "9.0" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "aee8b7b246df8f9039afb4144a1f6fd8d2ca17a180786b69acc140d282b71a49" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/docs/ema_study.png b/docs/ema_study.png deleted file mode 100644 index d5feee0b1659e1eb4b0abeb42b425e30e41facb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90947 zcmeEtRZyMXvTazwCAclz-5r7icMlfa-Q5Z9x^Q=bTd?5n4uRkrBzSPSi{#(??tR|x z!>u|GDXJ(kXZPsQGQ0Z=SCp4Pf(OFCdGiKIN>Wt$%^QHm>pxg%@NYUyQ&+%0Ae@vX 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 z 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
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "ename": "Error", - "evalue": "(3,1): error CS0246: The type or namespace name 'Yahoo_Feed' could not be found (are you missing a using directive or an assembly reference?)\r\n(10,15): error CS0019: Operator '<' cannot be applied to operands of type 'int' and 'method group'", - "output_type": "error", - "traceback": [ - "(3,1): error CS0246: The type or namespace name 'Yahoo_Feed' could not be found (are you missing a using directive or an assembly reference?)\r\n", - "(10,15): error CS0019: Operator '<' cannot be applied to operands of type 'int' and 'method group'" - ] - } - ], + "outputs": [], "source": [ "#r \"nuget:QuanTAlib;\"\n", "using QuanTAlib;\n", @@ -63,7 +51,14 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, "source": [ "## Understanding QuanTAlib data model\n", "\n", @@ -72,26 +67,16 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
indexItem1Item2
02022-11-10 00:00:00Z
105.3
12022-11-10 15:47:46Z
293.1
22022-11-10 15:47:46Z
0
32022-11-07 15:47:46Z
10
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "var item1 = (DateTime.Today, 105.3); // (DateTime, Value) tuple\n", "double item2 = 293.1; // a simple double\n", @@ -107,66 +92,60 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, "source": [ "TSeries list can display only values (without timestamps) or only timestamps (without values) by using `.v` or `.t` properties" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
indexvalue
0
105.3
1
293.1
2
0
3
10
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "data.v" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, "source": [ "The last element on the list can be accessed by .Last() or by [^1] - and using `.t` (time) and `.v` (value) properties. Also, casting a TSeries into (double) will return the value of the last element" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
10
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "bool IsTheSame = data.Last().v == data[^1].v;\n", "double lastvalue = data;\n", @@ -176,33 +155,30 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, "source": [ "All indicators are just modified TSeries classes; they get all required input during class construction (source of the datafeed, period...) and they automatically subscribe to events of the datafeed. Whenever datafeed gets a new value, indicator will calculate its own value. Indicators are also event publishers, so other indicators can subscribe to their results, chaining indicators together:" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
indexvalue
0
Infinity
1
0.6666666666666666
2
0.3333333333333333
3
0.2
4
0.14285714285714285
5
0.1111111111111111
6
0.09090909090909091
7
0.07692307692307693
8
0.06666666666666667
9
0.058823529411764705
10
0.25
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "TSeries t1 = new() {0,1,2,3,4,5,6,7,8,9}; // t1 is loaded with data and activated as a publisher\n", "EMA_Series t2 = new(t1, 3); // t2 will auto-load all history of t1 and wait for events from t1\n", @@ -218,7 +194,14 @@ }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "dotnet_interactive": { + "language": "csharp" + }, + "polyglot_notebook": { + "kernelName": "csharp" + } + }, "source": [ "# MACD compounded indicator\n", "\n", @@ -227,26 +210,16 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "dotnet_interactive": { "language": "csharp" }, "vscode": { - "languageId": "dotnet-interactive.csharp" + "languageId": "polyglot-notebook" } }, - "outputs": [ - { - "data": { - "text/html": [ - "
indexvalue
0
0
1
0
2
0
3
0
4
0
5
0
6
0
7
0
8
0
9
0
10
0
11
0
12
0.13543589743590018
13
-0.03897954353340993
14
-0.17731008431411102
15
-0.24030671152304095
16
-0.08247055673614988
17
-0.47898448490240814
18
-0.9020715041856615
19
-1.3489730137363423
(51 more)
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "Yahoo_Feed aapl = new(\"AAPL\", 100);\n", "TSeries close = aapl.Close; // close will get data from history\n", @@ -266,14 +239,33 @@ "language": "C#", "name": ".net-csharp" }, - "language_info": { - "file_extension": ".cs", - "mimetype": "text/x-csharp", - "name": "C#", - "pygments_lexer": "csharp", - "version": "9.0" - }, - "orig_nbformat": 4 + "polyglot_notebook": { + "kernelInfo": { + "defaultKernelName": "csharp", + "items": [ + { + "aliases": [ + "c#", + "C#" + ], + "languageName": "C#", + "name": "csharp" + }, + { + "aliases": [ + "frontend" + ], + "languageName": null, + "name": "vscode" + }, + { + "aliases": [], + "languageName": "KQL", + "name": "kql" + } + ] + } + } }, "nbformat": 4, "nbformat_minor": 2 diff --git a/docs/index.html b/docs/index.html index 10426a54..1cd0381d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -6,20 +6,26 @@ - + +
- - - + + + + diff --git a/docs/readme.md b/docs/readme.md index 224e2f64..9903a83f 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -34,28 +34,31 @@ See [Getting Started](https://github.com/mihakralj/QuanTAlib/blob/main/Docs/gett ⛔= Not implemented (yet) -| **BASIC TRANSFORMS** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | -|--|:--:|:--:|:--:|:--:| +| **BASIC TRANSFORMS** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | **Tulip** | +|--|:--:|:--:|:--:|:--:|:--:| | ⭐ OC2 - (Open+Close)/2 |️ `.OC2` || CandlePart.OC2 || | ⭐ HL2 - Median Price | `.HL2` | MEDPRICE | CandlePart.HL2 | hl2 | | ⭐ HLC3 - Typical Price | `.HLC3` | TYPPRICE | CandlePart.HLC3 | hlc3 | | ⭐ OHL3 - (Open+High+Low)/3 | `.OHL3` || CandlePart.OHL3 || -| ⭐ OHLC4 - Average Price | `.OHLC4` | AVGPRICE |️ CandlePart.OHLC4 | ohlc4 | +| ⭐ OHLC4 - Average Price | `.OHLC4` | AVGPRICE |️ CandlePart.OHLC4 | ohlc4 | avgprice | | ⭐ HLCC4 - Weighted Price | `.HLCC4` | WCLPRICE | CandlePart.HLCC4 || | ⭐ MIDPOINT - Midpoint value | `MIDPOINT_Series` | MIDPOINT || midpoint | | ⭐ MIDPRICE - Midpoint price | `MIDPRICE_Series` | MIDPRICE || midprice | -| ⭐ MAX - Max value | `MAX_Series` | MAX ||| -| ⭐ MIN - Min value | `MIN_Series` | MIN ||| -| ⭐ SUM - Summation | `SUM_Series` | SUM ||| -| ⭐ ADD - Addition | `ADD_Series` | ADD ||| -| ⭐ SUB - Subtraction | `SUB_Series` | SUB ||| -| ⭐ MUL - Multiplication | `MUL_Series` | MUL ||| -| ⭐ DIV - Division | `DIV_Series` | DIV ||| +| ⭐ MAX - Max value | `MAX_Series` | MAX ||| max | +| ⭐ MIN - Min value | `MIN_Series` | MIN ||| min | +| ⭐ SUM - Summation | `SUM_Series` | SUM ||| sum | +| ⭐ ADD - Addition | `ADD_Series` | ADD ||| add | +| ⭐ SUB - Subtraction | `SUB_Series` | SUB ||| sub | +| ⭐ MUL - Multiplication | `MUL_Series` | MUL ||| mul | +| ⭐ DIV - Division | `DIV_Series` | DIV ||| div | ||||| -| **STATISTICS & NUMERICAL ANALYSIS** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | +| **STATISTICS & NUMERICAL ANALYSIS** | +|||||| | ⭐ BIAS - Bias | `BIAS_Series` ||| bias | | ⭐ CORR - Pearson's Correlation Coefficient | `CORR_Series` | CORREL | GetCorrelation || | ⭐ COVAR - Covariance | `COVAR_Series` || GetCorrelation || +| ⛔ DECAY - Linear Decay ||||| decay | +| ⛔ EDECAY - Exponential Decay ||||| edecay | | ⭐ ENTROPY - Entropy | `ENTROPY_Series` ||| entropy | | ⭐ KURTOSIS - Kurtosis | `KURT_Series` ||| kurtosis | | ⭐ LINREG - Linear Regression | `LINREG_Series` || GetSlope || @@ -73,22 +76,23 @@ See [Getting Started](https://github.com/mihakralj/QuanTAlib/blob/main/Docs/gett | ✔️ WMAPE - Weighted Mean Absolute Percent Error | `WMAPE_Series` |||| | ⭐ ZSCORE - Number of standard deviations from mean | `ZSCORE_Series` || GetStdDev | zscore | |||||| -| **TREND INDICATORS & AVERAGES** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | +| **TREND INDICATORS & AVERAGES** | +|||||| | ⛔ AFIRMA - Autoregressive Finite Impulse Response Moving Average ||||| | ⭐ ALMA - Arnaud Legoux Moving Average | `ALMA_Series` || GetAlma | alma | | ⛔ ARIMA - Autoregressive Integrated Moving Average ||||| -| ⭐ DEMA - Double EMA Average | `DEMA_Series` | DEMA | GetDema | dema | -| ⭐ EMA - Exponential Moving Average | `EMA_Series` | EMA | GetEma | ema | +| ⭐ DEMA - Double EMA Average | `DEMA_Series` | DEMA | GetDema | dema | dema | +| ⭐ EMA - Exponential Moving Average | `EMA_Series` | EMA | GetEma | ema | ema | | ⛔ EPMA - Endpoint Moving Average ||| GetEpma || | ⛔ FRAMA - Fractal Adaptive Moving Average ||||| | ⛔ FWMA - Fibonacci's Weighted Moving Average |||| fwma | | ⛔ HILO - Gann High-Low Activator |||| hilo | | ✔️ HEMA - Hull/EMA Average | `HEMA_Series` |||| | ⛔ Hilbert Transform Instantaneous Trendline || HT_TRENDLINE | GetHtTrendline || -| ⭐ HMA - Hull Moving Average | `HMA_Series` || GetHma | hma | +| ⭐ HMA - Hull Moving Average | `HMA_Series` || GetHma | hma | hma | | ⛔ HWMA - Holt-Winter Moving Average |||| hwma | | ✔️ JMA - Jurik Moving Average | `JMA_Series` ||| jma | -| ⭐ KAMA - Kaufman's Adaptive Moving Average | `KAMA_Series` | KAMA | GetKama | kama | +| ⭐ KAMA - Kaufman's Adaptive Moving Average | `KAMA_Series` | KAMA | GetKama | kama | kama | | ⛔ KDJ - KDJ Indicator (trend reversal) |||| kdj | | ⛔ LSMA - Least Squares Moving Average ||||| | ⭐ MACD - Moving Average Convergence/Divergence | `MACD_Series` | MACD | GetMacd | macd | @@ -113,17 +117,20 @@ See [Getting Started](https://github.com/mihakralj/QuanTAlib/blob/main/Docs/gett | ⭐ WMA - Weighted Moving Average | `WMA_Series` | WMA | GetWma | wma | | ⭐ ZLEMA - Zero Lag EMA Average | `ZLEMA_Series` ||| zlma | |||||| -| **VOLATILITY INDICATORS** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | -| ⭐ ADL - Chaikin Accumulation Distribution Line | `ADL_Series` | AD | GetAdl | ad | -| ⭐ ADOSC - Chaikin Accumulation Distribution Oscillator | `ADOSC_Series` | ADOSC| GetAdl | adosc | -| ⭐ ATR - Average True Range | `ATR_Series` | ATR | GetAtr | atr | +| **VOLATILITY INDICATORS** | +|||||| +| ⭐ ADL - Chaikin Accumulation Distribution Line | `ADL_Series` | AD | GetAdl | ad | ad | +| ⭐ ADOSC - Chaikin Accumulation Distribution Oscillator | `ADOSC_Series` | ADOSC| GetAdl | adosc | adosc | +| ⭐ ATR - Average True Range | `ATR_Series` | ATR | GetAtr | atr | atr | | ⭐ ATRP - Average True Range Percent | `ATRP_Series` || GetAtr || | ⛔ BETA - Beta coefficient || BETA | GetBeta || -| ⭐ BBANDS - Bollinger Bands® | `BBANDS_Series` | BBANDS | GetBollingerBands || +| ⭐ BBANDS - Bollinger Bands® | `BBANDS_Series` | BBANDS | GetBollingerBands || bbands | | ⛔ CHAND - Chandelier Exit ||| GetChandelier || | ⛔ CRSI - Connor RSI ||| GetConnorsRsi || +| ⛔ CVI - Chaikins Volatility ||||| cvi | | ⛔ DON - Donchian Channels ||| GetDonchian || | ⛔ FCB - Fractal Chaos Bands ||| GetFcb || +| ⛔ FISHER - Fisher Transform ||| GetFcb || fisher | | ⛔ HV - Historical Volatility ||||| | ⛔ ICH - Ichimoku ||| GetIchimoku || | ⛔ KEL - Keltner Channels ||| GetKeltner || @@ -137,23 +144,25 @@ See [Getting Started](https://github.com/mihakralj/QuanTAlib/blob/main/Docs/gett | ⛔ UI - Ulcer Index ||||| | ⛔ VSTOP - Volatility Stop ||||| |||||| -| **MOMENTUM INDICATORS & OSCILLATORS** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | +| **MOMENTUM INDICATORS & OSCILLATORS** | +|||||| | ⛔ AC - Acceleration Oscillator ||||| -| ⛔ ADX - Average Directional Movement Index || ADX | GetAdx || -| ⛔ ADXR - Average Directional Movement Index Rating || ADXR | GetAdx || -| ⛔ AO - Awesome Oscillator ||| GetAwesome || -| ⛔ APO - Absolute Price Oscillator || APO ||| -| ⛔ AROON - Aroon oscillator || AROON | GetAroon || -| ⛔ BOP - Balance of Power || BOP | GetBop || -| ⭐ CCI - Commodity Channel Index | `CCI_Series` | CCI | GetCci || +| ⛔ ADX - Average Directional Movement Index || ADX | GetAdx || adx | +| ⛔ ADXR - Average Directional Movement Index Rating || ADXR | GetAdx || adxr | +| ⛔ AO - Awesome Oscillator ||| GetAwesome || ao | +| ⛔ APO - Absolute Price Oscillator || APO ||| apo | +| ⛔ AROON - Aroon oscillator || AROON | GetAroon || aroon | +| ⛔ BOP - Balance of Power || BOP | GetBop || bop | +| ⭐ CCI - Commodity Channel Index | `CCI_Series` | CCI | GetCci || cci | | ⛔ CFO - Chande Forcast Oscillator ||||| -| ⛔ CMO - Chande Momentum Oscillator || CMO | GetCmo || +| ⛔ CMO - Chande Momentum Oscillator || CMO | GetCmo || cmo | | ⛔ COG - Center of Gravity ||||| | ⛔ COPPOCK - Coppock Curve ||||| | ⛔ CTI - Ehler's Correlation Trend Indicator ||||| | ⛔ DPO - Detrended Price Oscillator ||| GetDpo || | ⛔ DMI - Directional Movement Index || DX | GetAdx || | ⛔ EFI - Elder Ray's Force Index ||| GetElderRay || +| ⛔ FOSC - Forecast oscillator ||||| fosc | | ⛔ GAT - Alligator oscillator ||| GetGator || | ⛔ HURST - Hurst Exponent ||| GetHurst || | ⛔ KRI - Kairi Relative Index ||||| @@ -176,10 +185,12 @@ See [Getting Started](https://github.com/mihakralj/QuanTAlib/blob/main/Docs/gett | ⛔ WILLR - Larry Williams' %R || WILLR | GetWilliamsR || | ⛔ WGAT - Williams Alligator ||||| |||||| -| **VOLUME INDICATORS** | **QuanTAlib** | **TA-LIB** | **Skender** | **Pandas TA** | +| **VOLUME INDICATORS** | +|||||| | ⛔ AOBV - Archer On-Balance Volume ||||| | ⛔ CMF - Chaikin Money Flow ||||| -| ⛔ EOM - Ease of Movement ||||| +| ⛔ EOM - Ease of Movement ||||| emv | +| ⛔ KVO - Klinger Volume Oscilaltor ||||| kvo | | ⭐ OBV - On-Balance Volume | `OBV_Series` | OBV | GetObv || | ⛔ PRS - Price Relative Strength |||| | ⛔ PVOL - Price-Volume |||||