From 773f3fbbfe6d560d9eec3fe04b11be6ba5857392 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Tue, 8 Nov 2022 18:26:04 -0800 Subject: [PATCH] =?UTF-8?q?Bollinger=20Bands=C2=AE=20with=20validation=20t?= =?UTF-8?q?ests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/Basics/Abstracts.cs | 2 +- Source/Indicators/BBANDS_Series.cs | 73 ++++++++++++++++++++++++++++++ Source/QuanTAlib.csproj | 2 +- Tests/MovingAvg/BBANDS_Test.cs | 58 ++++++++++++++++++++++++ Tests/Validations/Skender_Stock.cs | 16 +++++++ Tests/Validations/TA_LIB.cs | 17 +++++++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 Source/Indicators/BBANDS_Series.cs create mode 100644 Tests/MovingAvg/BBANDS_Test.cs diff --git a/Source/Basics/Abstracts.cs b/Source/Basics/Abstracts.cs index 28b08ffb..d20be702 100644 --- a/Source/Basics/Abstracts.cs +++ b/Source/Basics/Abstracts.cs @@ -158,7 +158,7 @@ public abstract class Single_TBars_Indicator : TSeries for (int i = 0; i < data.Count; i++) { base.Add(TValue: data[i], update: false); } } -public new void Add((System.DateTime t, double o, double h, double l, double c, double v) TBar) +public void Add((System.DateTime t, double o, double h, double l, double c, double v) TBar) => this.Add(TBar: TBar, update: false); public void Add(bool update) => this.Add(TBar: this._bars[this._bars.Count - 1], update: update); diff --git a/Source/Indicators/BBANDS_Series.cs b/Source/Indicators/BBANDS_Series.cs new file mode 100644 index 00000000..169f8f35 --- /dev/null +++ b/Source/Indicators/BBANDS_Series.cs @@ -0,0 +1,73 @@ +namespace QuanTAlib; +using System; + +/* +BBANDS: Bollinger Bands® + Price channels created by John Bollinger, depict volatility as standard deviation boundary + line range from a moving average of price. The bands automatically widen when volatility + increases and contract when volatility decreases. Their dynamic nature allows them to be + used on different securities with the standard settings. + + Mid Band = simple moving average (SMA) + Upper Band = SMA + (standard deviation of price x multiplier) + Lower Band = SMA - (standard deviation of price x multiplier) + Bandwidth = Width of the channel: (Upper-Lower)/SMA + %B = The location of the data point within the channel: (Price-Lower)/(Upper/Lower) + Z-Score = number of standard deviations of the data point from SMA + +Sources: + https://www.investopedia.com/terms/b/bollingerbands.asp + https://school.stockcharts.com/doku.php?id=technical_indicators:bollinger_bands + +Note: + Bollinger Bands® is a registered trademark of John A. Bollinger. + + */ + +public class BBANDS_Series : Single_TSeries_Indicator +{ + public SMA_Series Mid { get; } + public ADD_Series Upper { get; } + public SUB_Series Lower { get; } + public DIV_Series PercentB { get; } + public DIV_Series Bandwidth { get; } + public DIV_Series Zscore { get; } + + private readonly SDEV_Series _sdev; + private readonly MUL_Series _mulsdev; + private readonly SUB_Series _pbdnd; + private readonly SUB_Series _pbdvr; + private readonly SUB_Series _zdnd; + + public BBANDS_Series(TSeries source, int period = 26, double multiplier = 2.0, bool useNaN = false) + : base(source, period: 0, useNaN) + { + this.Mid = new(source: source, period: period, useNaN: useNaN); + + _sdev = new(source, period, useNaN: useNaN); + _mulsdev = new(_sdev, multiplier); + this.Upper = new(Mid, _mulsdev); + this.Lower = new(Mid, _mulsdev); + + _pbdnd = new(source, Lower); + _pbdvr = new(Upper, Lower); + + this.PercentB = new(_pbdnd, _pbdvr); + this.Bandwidth = new(_pbdvr, Mid); + + _zdnd = new(source, Mid); + this.Zscore = new(_zdnd, _sdev); + + if (source.Count > 0) + { base.Add(this.Bandwidth); } + } + public override void Add((System.DateTime t, double v) TValue, bool update) + { + double _bbandwidth; + if (update) + { _sdev.Add(TValue, true); } + _bbandwidth = this.Bandwidth[(this.Count < this.Bandwidth.Count) ? this.Count : this.Bandwidth.Count - 1].v; + var result = (TValue.t, _bbandwidth); + base.Add(result, update); + } +} \ No newline at end of file diff --git a/Source/QuanTAlib.csproj b/Source/QuanTAlib.csproj index f8c323d7..1bf85a74 100644 --- a/Source/QuanTAlib.csproj +++ b/Source/QuanTAlib.csproj @@ -1,7 +1,7 @@  - 0.1.15 + 0.1.16 QuanTAlib diff --git a/Tests/MovingAvg/BBANDS_Test.cs b/Tests/MovingAvg/BBANDS_Test.cs new file mode 100644 index 00000000..cd59b273 --- /dev/null +++ b/Tests/MovingAvg/BBANDS_Test.cs @@ -0,0 +1,58 @@ +using Xunit; +using System; +using QuanTAlib; + +namespace MovingAvg; +public class BBANDS_Test +{ + [Fact] + public void Add_Test() + { + TSeries a = new() { 0, 1, 2, 3, 4, 5 }; + BBANDS_Series c = new(a, 4,2.5); + Assert.Equal(6, c.Count); + a.Add(5); + Assert.Equal(a.Count, c.Count); + Assert.Equal(a.Count, c.Mid.Count); + Assert.Equal(a.Count, c.Upper.Count); + Assert.Equal(a.Count, c.Lower.Count); + Assert.Equal(a.Count, c.PercentB.Count); + Assert.Equal(a.Count, c.Zscore.Count); + Assert.Equal(a.Count, c.Bandwidth.Count); + + a.Add(0, update: true); + Assert.Equal(a.Count, c.Count); + Assert.Equal(a.Count, c.Mid.Count); + Assert.Equal(a.Count, c.Upper.Count); + Assert.Equal(a.Count, c.Lower.Count); + Assert.Equal(a.Count, c.PercentB.Count); + Assert.Equal(a.Count, c.Zscore.Count); + Assert.Equal(a.Count, c.Bandwidth.Count); + } + + [Fact] + public void Edge_Test() + { + TSeries a = new() { double.NaN, double.Epsilon, double.PositiveInfinity, double.MaxValue }; + BBANDS_Series c = new(a, 4, 2.5); + Assert.Equal(a.Count, c.Count); + a.Add(double.NaN); + Assert.Equal(a.Count, c.Count); + Assert.Equal(a.Count, c.Mid.Count); + Assert.Equal(a.Count, c.Upper.Count); + Assert.Equal(a.Count, c.Lower.Count); + Assert.Equal(a.Count, c.PercentB.Count); + Assert.Equal(a.Count, c.Zscore.Count); + Assert.Equal(a.Count, c.Bandwidth.Count); + a.Add(double.PositiveInfinity); + Assert.Equal(a.Count, c.Count); + Assert.Equal(a.Count, c.Mid.Count); + Assert.Equal(a.Count, c.Upper.Count); + Assert.Equal(a.Count, c.Lower.Count); + Assert.Equal(a.Count, c.PercentB.Count); + Assert.Equal(a.Count, c.Zscore.Count); + Assert.Equal(a.Count, c.Bandwidth.Count); + + } + +} diff --git a/Tests/Validations/Skender_Stock.cs b/Tests/Validations/Skender_Stock.cs index 7b383598..2ee8329d 100644 --- a/Tests/Validations/Skender_Stock.cs +++ b/Tests/Validations/Skender_Stock.cs @@ -160,9 +160,25 @@ public class Skender_Stock var SK = this.quotes.GetMacd(12,26,9); Assert.Equal(Math.Round((double)SK.Last().Macd!, 8), Math.Round(QL.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().Signal!, 8), Math.Round(QL.Signal.Last().v, 8)); } [Fact] + public void BBANDS() + { + BBANDS_Series QL = new(this.bars.Close, this.period, 2.0, useNaN: false); + var SK = this.quotes.GetBollingerBands(this.period, 2.0); + + Assert.Equal(Math.Round((double)SK.Last().Sma!, 8), Math.Round(QL.Mid.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().UpperBand!, 8), Math.Round(QL.Upper.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().LowerBand!, 8), Math.Round(QL.Lower.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().Width!, 8), Math.Round(QL.Bandwidth.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().PercentB!, 8), Math.Round(QL.PercentB.Last().v, 8)); + Assert.Equal(Math.Round((double)SK.Last().ZScore!, 8), Math.Round(QL.Zscore.Last().v, 8)); + } + + + [Fact] public void RSI() { RSI_Series QL = new(this.bars.Close, this.period, useNaN: false); diff --git a/Tests/Validations/TA_LIB.cs b/Tests/Validations/TA_LIB.cs index 7c7ca807..e4f97f60 100644 --- a/Tests/Validations/TA_LIB.cs +++ b/Tests/Validations/TA_LIB.cs @@ -164,8 +164,25 @@ public class TA_LIB MACD_Series QL = new(this.bars.Close, slow: 26, fast: 12, signal: 9, false); Core.Macd(this.inclose, 0, this.bars.Count - 1, outMacd: this.TALIB, outMacdSignal: macdSignal, outMacdHist: macdHist, out int outBegIdx, out _); Assert.Equal(Math.Round(this.TALIB[this.TALIB.Length - outBegIdx - 1], 8), Math.Round(QL.Last().v, 8)); + Assert.Equal(Math.Round(macdSignal[macdSignal.Length - outBegIdx - 1], 8), Math.Round(QL.Signal.Last().v, 8)); } + [Fact] + public void BBANDS() + { + double[] outMiddle = new double[this.bars.Count]; + double[] outUpper = new double[this.bars.Count]; + double[] outLower = new double[this.bars.Count]; + BBANDS_Series QL = new(this.bars.Close, period:26, multiplier:2.0, false); + Core.Bbands(this.inclose, 0, this.bars.Count - 1, outRealUpperBand: outUpper, outRealMiddleBand: outMiddle, outRealLowerBand: outLower, out int outBegIdx, out _, optInTimePeriod:26, optInNbDevUp:2.0, optInNbDevDn:2.0); + Assert.Equal(Math.Round(outUpper[outUpper.Length - outBegIdx - 1], 8), Math.Round(QL.Upper.Last().v, 8)); + Assert.Equal(Math.Round(outMiddle[outMiddle.Length - outBegIdx - 1], 8), Math.Round(QL.Mid.Last().v, 8)); + Assert.Equal(Math.Round(outLower[outLower.Length - outBegIdx - 1], 8), Math.Round(QL.Lower.Last().v, 8)); + + } + + + [Fact] public void HL2() {