diff --git a/Docs/coverage.md b/Docs/coverage.md index f8de0937..aff5dc6c 100644 --- a/Docs/coverage.md +++ b/Docs/coverage.md @@ -20,7 +20,7 @@ | BIAS - Bias |✔️|||✔️| | ENTR - Entropy |✔️|||✔️| | KUR - Kurtosis |✔️|||✔️| -| LINREG - Linear Regression ||✔️|✔️|| +| LINREG - Linear Regression |✔️|✔️|✔️|| | MAD - Mean Absolute Deviation |✔️||✔️|✔️| | MAPE - Mean Absolute Percent Error |✔️||✔️|| | MAX - Max value |✔️|✔️||| @@ -30,9 +30,7 @@ | PSDEV - Population Standard Deviation |✔️|||| | PVAR - Population Variance |✔️|||| | QUANTILE ||||✔️| -| RS - R-Squared Coefficient |||✔️|| | SKEW - Skewness ||||✔️| -| SLOPE - Slope |||✔️|| | SMAPE - Symmetric Mean Absolute Percent Error |✔️|||| | SDEV - Sample Standard Deviation |✔️|✔️|✔️|✔️| | VAR - Sample Variance |✔️|||✔️| diff --git a/Source/Statistics/LINREG_Series.cs b/Source/Statistics/LINREG_Series.cs new file mode 100644 index 00000000..9c7aeff0 --- /dev/null +++ b/Source/Statistics/LINREG_Series.cs @@ -0,0 +1,93 @@ +namespace QuanTAlib; +using System; + +/* +LINREG: Linear Regression (using Least Square Method) + Linear Regression provides a slope of a straight line that is the best approximation of the given set of data. + The method of least squares is a standard approach in linear regression analysis to approximate the solution + by minimizing the sum of the squares of the residuals made in the results of each individual equation. + +Additional outputs provided by LINREG: + .Intercept - y-intercept point of the best fit line + .RSquared - R-Squared (R²), Coefficient of Determination + .StdDev - Standard Deviation of data over given periods + + y = Slope * x + Intercept + +Sources: + https://en.wikipedia.org/wiki/Least_squares + + */ + +public class LINREG_Series : Single_TSeries_Indicator +{ + public readonly TSeries Intercept = new(); + public readonly TSeries RSquared = new(); + public readonly TSeries StdDev = new(); + private readonly System.Collections.Generic.List _buffer = new(); + + public LINREG_Series(TSeries source, int period, bool useNaN = false) + : base(source, period, useNaN) + { + if (this._data.Count > 0) { base.Add(this._data); } + } + + public override void Add((System.DateTime t, double v) TValue, bool update) + { + if (update) { this._buffer[this._buffer.Count - 1] = TValue.v; } + else { this._buffer.Add(TValue.v); } + if (this._buffer.Count > this._p) { this._buffer.RemoveAt(0); } + + int _len = this._buffer.Count; + + // get averages for period + double sumX = 0; + double sumY = 0; + + for (int p = 0; p < _len; p++) + { + sumX += this.Count - _len + 2 + p; + sumY += _buffer[p]; + } + double avgX = sumX / _len; + double avgY = sumY / _len; + + // least squares method + double sumSqX = 0; + double sumSqY = 0; + double sumSqXY = 0; + + for (int p = 0; p < _len; p++) + { + double devX = this.Count - _len + 2 + p - avgX; + double devY = _buffer[p] - avgY; + + sumSqX += devX * devX; + sumSqY += devY * devY; + sumSqXY += devX * devY; + } + + double _slope = sumSqXY / sumSqX; + double _intercept = avgY - (_slope * avgX); + + // calculate Standard Deviation and R-Squared + double stdDevX = Math.Sqrt((double)sumSqX / _len); + double stdDevY = Math.Sqrt((double)sumSqY / _len); + double _StdDev = stdDevY; + + double arrr = (stdDevX * stdDevY != 0) ? (double)sumSqXY / (stdDevX * stdDevY) / _len : 0; + double _RSquared = arrr * arrr; + + var ret = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _slope); + base.Add(ret, update); + + ret = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _intercept); + Intercept.Add(ret, update); + + ret = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _StdDev); + StdDev.Add(ret, update); + + ret = (TValue.t, this.Count < this._p - 1 && this._NaN ? double.NaN : _RSquared); + RSquared.Add(ret, update); + } +} \ No newline at end of file diff --git a/Tests/Statistics/LINREG_Test.cs b/Tests/Statistics/LINREG_Test.cs new file mode 100644 index 00000000..c0370af1 --- /dev/null +++ b/Tests/Statistics/LINREG_Test.cs @@ -0,0 +1,33 @@ +using Xunit; +using System; +using QuanTAlib; + +namespace Statistics; +public class LINREG_Test +{ + [Fact] + public void Add_Test() + { + TSeries a = new() { 0, 1, 2, 3, 4, 5 }; + LINREG_Series c = new(a, 3); + Assert.Equal(6, c.Count); + a.Add(5); + Assert.Equal(a.Count, c.Count); + a.Add(0, update: true); + Assert.Equal(a.Count, c.Count); + } + + [Fact] + public void Edge_Test() + { + TSeries a = new() { double.NaN, double.Epsilon, double.PositiveInfinity, double.MaxValue }; + LINREG_Series c = new(a, 3); + Assert.Equal(a.Count, c.Count); + a.Add(double.NaN); + Assert.Equal(a.Count, c.Count); + a.Add(double.PositiveInfinity); + Assert.Equal(a.Count, c.Count); + + } + +} diff --git a/Tests/Validations/Skender_Stock.cs b/Tests/Validations/Skender_Stock.cs index e2924779..c45517bf 100644 --- a/Tests/Validations/Skender_Stock.cs +++ b/Tests/Validations/Skender_Stock.cs @@ -161,4 +161,16 @@ public class Skender_Stock Assert.Equal(Math.Round((double)SK.Last().Alma!, 8), Math.Round(QL.Last().v, 8)); } + + [Fact] + public void LINREG() + { + LINREG_Series QL = new(this.bars.Close, this.period, useNaN: false); + var SK = this.quotes.GetSlope(this.period); + + Assert.Equal(Math.Round((double)SK.Last().Slope!, 8), Math.Round(QL.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().Intercept!, 8), Math.Round(QL.Intercept.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().RSquared!, 8), Math.Round(QL.RSquared.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().StdDev!, 8), Math.Round(QL.StdDev.Last().v, 8)); + } }