From f269d9732a08fa359dc5d1d46fbf46c5932fddf5 Mon Sep 17 00:00:00 2001
From: Onur Cinar <onur.cinar@gmail.com>
Date: Sat, 30 Dec 2023 14:55:30 -0800
Subject: [PATCH] Bollinger Bands strategy added.

---
 README.md                                     |   2 +-
 strategy/volatility/README.md                 |  94 +++++++
 .../volatility/bollinger_bands_strategy.go    | 118 ++++++++
 .../bollinger_bands_strategy_test.go          |  55 ++++
 .../testdata/bollinger_bands_strategy.csv     | 252 ++++++++++++++++++
 strategy/volatility/testdata/brk-b.csv        |   1 +
 strategy/volatility/volatility.go             |  28 ++
 7 files changed, 549 insertions(+), 1 deletion(-)
 create mode 100644 strategy/volatility/README.md
 create mode 100644 strategy/volatility/bollinger_bands_strategy.go
 create mode 100644 strategy/volatility/bollinger_bands_strategy_test.go
 create mode 100644 strategy/volatility/testdata/bollinger_bands_strategy.csv
 create mode 120000 strategy/volatility/testdata/brk-b.csv
 create mode 100644 strategy/volatility/volatility.go

diff --git a/README.md b/README.md
index 983ddc6..da7c7ad 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ The following list of strategies are currently supported by this package:
 
 ### 🎢 Volatility Strategies
 
-- Bollinger Bands Strategy
+- [Bollinger Bands Strategy](strategy/volatility/README.md#type-bollingerbandsstrategy)
 - Projection Oscillator Strategy
 
 ### 📢 Volume Strategies
diff --git a/strategy/volatility/README.md b/strategy/volatility/README.md
new file mode 100644
index 0000000..10ea811
--- /dev/null
+++ b/strategy/volatility/README.md
@@ -0,0 +1,94 @@
+<!-- Code generated by gomarkdoc. DO NOT EDIT -->
+
+# volatility
+
+```go
+import "github.com/cinar/indicator/strategy/volatility"
+```
+
+Package volatility contains the volatility strategy functions.
+
+This package belongs to the Indicator project. Indicator is a Golang module that supplies a variety of technical indicators, strategies, and a backtesting framework for analysis.
+
+### License
+
+```
+Copyright (c) 2021-2023 Onur Cinar.
+The source code is provided under GNU AGPLv3 License.
+https://github.com/cinar/indicator
+```
+
+### Disclaimer
+
+The information provided on this project is strictly for informational purposes and is not to be construed as advice or solicitation to buy or sell any security.
+
+## Index
+
+- [func AllStrategies\(\) \[\]strategy.Strategy](<#AllStrategies>)
+- [type BollingerBandsStrategy](<#BollingerBandsStrategy>)
+  - [func NewBollingerBandsStrategy\(\) \*BollingerBandsStrategy](<#NewBollingerBandsStrategy>)
+  - [func \(b \*BollingerBandsStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#BollingerBandsStrategy.Compute>)
+  - [func \(\*BollingerBandsStrategy\) Name\(\) string](<#BollingerBandsStrategy.Name>)
+  - [func \(b \*BollingerBandsStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#BollingerBandsStrategy.Report>)
+
+
+<a name="AllStrategies"></a>
+## func [AllStrategies](<https://github.com/cinar/indicator/blob/v2/strategy/volatility/volatility.go#L24>)
+
+```go
+func AllStrategies() []strategy.Strategy
+```
+
+AllStrategies returns a slice containing references to all available volatility strategies.
+
+<a name="BollingerBandsStrategy"></a>
+## type [BollingerBandsStrategy](<https://github.com/cinar/indicator/blob/v2/strategy/volatility/bollinger_bands_strategy.go#L17-L22>)
+
+BollingerBandsStrategy represents the configuration parameters for calculating the Bollinger Bands strategy. A closing value crossing above the upper band suggets a Buy signal, while crossing below the lower band indivates a Sell signal.
+
+```go
+type BollingerBandsStrategy struct {
+    strategy.Strategy
+
+    // BollingerBands represents the configuration parameters for calculating the Bollinger Bands.
+    BollingerBands *volatility.BollingerBands[float64]
+}
+```
+
+<a name="NewBollingerBandsStrategy"></a>
+### func [NewBollingerBandsStrategy](<https://github.com/cinar/indicator/blob/v2/strategy/volatility/bollinger_bands_strategy.go#L25>)
+
+```go
+func NewBollingerBandsStrategy() *BollingerBandsStrategy
+```
+
+NewBollingerBandsStrategy function initializes a new Bollinger Bands strategy instance.
+
+<a name="BollingerBandsStrategy.Compute"></a>
+### func \(\*BollingerBandsStrategy\) [Compute](<https://github.com/cinar/indicator/blob/v2/strategy/volatility/bollinger_bands_strategy.go#L37>)
+
+```go
+func (b *BollingerBandsStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
+```
+
+Compute processes the provided asset snapshots and generates a stream of actionable recommendations.
+
+<a name="BollingerBandsStrategy.Name"></a>
+### func \(\*BollingerBandsStrategy\) [Name](<https://github.com/cinar/indicator/blob/v2/strategy/volatility/bollinger_bands_strategy.go#L32>)
+
+```go
+func (*BollingerBandsStrategy) Name() string
+```
+
+Name returns the name of the strategy.
+
+<a name="BollingerBandsStrategy.Report"></a>
+### func \(\*BollingerBandsStrategy\) [Report](<https://github.com/cinar/indicator/blob/v2/strategy/volatility/bollinger_bands_strategy.go#L82>)
+
+```go
+func (b *BollingerBandsStrategy) Report(c <-chan *asset.Snapshot) *helper.Report
+```
+
+Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+
+Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)
diff --git a/strategy/volatility/bollinger_bands_strategy.go b/strategy/volatility/bollinger_bands_strategy.go
new file mode 100644
index 0000000..06a3fba
--- /dev/null
+++ b/strategy/volatility/bollinger_bands_strategy.go
@@ -0,0 +1,118 @@
+// Copyright (c) 2021-2023 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package volatility
+
+import (
+	"github.com/cinar/indicator/asset"
+	"github.com/cinar/indicator/helper"
+	"github.com/cinar/indicator/strategy"
+	"github.com/cinar/indicator/volatility"
+)
+
+// BollingerBandsStrategy represents the configuration parameters for calculating the Bollinger Bands strategy.
+// A closing value crossing above the upper band suggets a Buy signal, while crossing below the lower band
+// indivates a Sell signal.
+type BollingerBandsStrategy struct {
+	strategy.Strategy
+
+	// BollingerBands represents the configuration parameters for calculating the Bollinger Bands.
+	BollingerBands *volatility.BollingerBands[float64]
+}
+
+// NewBollingerBandsStrategy function initializes a new Bollinger Bands strategy instance.
+func NewBollingerBandsStrategy() *BollingerBandsStrategy {
+	return &BollingerBandsStrategy{
+		BollingerBands: volatility.NewBollingerBands[float64](),
+	}
+}
+
+// Name returns the name of the strategy.
+func (*BollingerBandsStrategy) Name() string {
+	return "Bollinger Bands Strategy"
+}
+
+// Compute processes the provided asset snapshots and generates a stream of actionable recommendations.
+func (b *BollingerBandsStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
+	closings := helper.Duplicate(
+		asset.SnapshotsAsClosings(snapshots),
+		3,
+	)
+
+	uppers, middles, lowers := b.BollingerBands.Compute(closings[0])
+
+	go helper.Drain(middles)
+
+	closings[1] = helper.Skip(closings[1], b.BollingerBands.IdlePeriod())
+
+	aboveUppers := helper.Subtract(
+		closings[1],
+		uppers,
+	)
+
+	closings[2] = helper.Skip(closings[2], b.BollingerBands.IdlePeriod())
+
+	belowLowers := helper.Subtract(
+		closings[2],
+		lowers,
+	)
+
+	actions := helper.Operate(aboveUppers, belowLowers, func(aboveUpper, belowLower float64) strategy.Action {
+		if aboveUpper > 0 {
+			return strategy.Buy
+		}
+
+		if belowLower < 0 {
+			return strategy.Sell
+		}
+
+		return strategy.Hold
+	})
+
+	actions = strategy.NormalizeActions(actions)
+
+	// Bollinger Bands starts only after a full period.
+	actions = helper.Shift(actions, b.BollingerBands.IdlePeriod(), strategy.Hold)
+
+	return actions
+}
+
+// Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+func (b *BollingerBandsStrategy) Report(c <-chan *asset.Snapshot) *helper.Report {
+	//
+	// snapshots[0] -> dates
+	// snapshots[1] -> closings[0] -> closings
+	//                 closings[1] -> upper
+	//                             -> middle
+	//                             -> lower
+	// snapshots[2] -> actions     -> annotations
+	//              -> outcomes
+	//
+	snapshots := helper.Duplicate(c, 3)
+
+	dates := asset.SnapshotsAsDates(snapshots[0])
+	closings := helper.Duplicate(asset.SnapshotsAsClosings(snapshots[1]), 2)
+
+	uppers, middles, lowers := b.BollingerBands.Compute(closings[0])
+	uppers = helper.Shift(uppers, b.BollingerBands.IdlePeriod(), 0)
+	middles = helper.Shift(middles, b.BollingerBands.IdlePeriod(), 0)
+	lowers = helper.Shift(lowers, b.BollingerBands.IdlePeriod(), 0)
+
+	actions, outcomes := strategy.ComputeWithOutcome(b, snapshots[2])
+	annotations := strategy.ActionsToAnnotations(actions)
+	outcomes = helper.MultiplyBy(outcomes, 100)
+
+	report := helper.NewReport(b.Name(), dates)
+	report.AddChart()
+
+	report.AddColumn(helper.NewNumericReportColumn("Close", closings[1]))
+	report.AddColumn(helper.NewNumericReportColumn("Upper", uppers))
+	report.AddColumn(helper.NewNumericReportColumn("Middle", middles))
+	report.AddColumn(helper.NewNumericReportColumn("Lower", lowers))
+	report.AddColumn(helper.NewAnnotationReportColumn(annotations))
+
+	report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 1)
+
+	return report
+}
diff --git a/strategy/volatility/bollinger_bands_strategy_test.go b/strategy/volatility/bollinger_bands_strategy_test.go
new file mode 100644
index 0000000..b18f97e
--- /dev/null
+++ b/strategy/volatility/bollinger_bands_strategy_test.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2021-2023 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package volatility_test
+
+import (
+	"os"
+	"testing"
+
+	"github.com/cinar/indicator/asset"
+	"github.com/cinar/indicator/helper"
+	"github.com/cinar/indicator/strategy"
+	"github.com/cinar/indicator/strategy/volatility"
+)
+
+func TestBollingerBandsStrategy(t *testing.T) {
+	snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/bollinger_bands_strategy.csv", true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	bb := volatility.NewBollingerBandsStrategy()
+	actions, outcomes := strategy.ComputeWithOutcome(bb, snapshots)
+	outcomes = helper.RoundDigits(outcomes, 2)
+
+	err = strategy.CheckResults(results, actions, outcomes)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestBollingerBandsStrategyReport(t *testing.T) {
+	snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	bb := volatility.NewBollingerBandsStrategy()
+
+	report := bb.Report(snapshots)
+
+	fileName := "bollinger_bands_strategy.html"
+	defer os.Remove(fileName)
+
+	err = report.WriteToFile(fileName)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/strategy/volatility/testdata/bollinger_bands_strategy.csv b/strategy/volatility/testdata/bollinger_bands_strategy.csv
new file mode 100644
index 0000000..630dfde
--- /dev/null
+++ b/strategy/volatility/testdata/bollinger_bands_strategy.csv
@@ -0,0 +1,252 @@
+Action,Outcome
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+0,0
+1,0
+0,-0.01
+0,0.01
+0,0
+0,0.01
+0,0.02
+0,0.01
+0,0.01
+0,0
+0,-0.02
+0,-0.03
+0,-0.01
+0,-0.01
+0,-0.01
+0,-0.01
+0,-0.01
+0,-0.02
+0,-0.02
+0,-0.01
+0,-0.01
+0,-0.01
+0,-0.02
+0,-0.02
+0,-0.01
+0,-0.02
+0,-0.02
+0,-0.01
+0,-0
+0,-0.01
+0,-0.02
+0,-0.02
+0,-0.02
+-1,-0.04
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.04
+1,-0.04
+0,-0.06
+0,-0.05
+0,-0.07
+0,-0.08
+0,-0.08
+0,-0.07
+-1,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+1,-0.1
+0,-0.09
+0,-0.1
+0,-0.1
+0,-0.09
+0,-0.09
+0,-0.09
+0,-0.1
+0,-0.09
+0,-0.08
+0,-0.08
+0,-0.09
+0,-0.1
+0,-0.11
+0,-0.1
+0,-0.09
+0,-0.09
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.09
+0,-0.08
+0,-0.08
+0,-0.08
+0,-0.1
+0,-0.11
+0,-0.11
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.1
+0,-0.08
+0,-0.08
+0,-0.07
+0,-0.06
+0,-0.06
+0,-0.06
+0,-0.07
+0,-0.06
+0,-0.06
+0,-0.05
+0,-0.06
+0,-0.05
+0,-0.05
+0,-0.06
+0,-0.06
+0,-0.07
+0,-0.06
+0,-0.07
+0,-0.06
+0,-0.05
+0,-0.04
+0,-0.05
+0,-0.05
+0,-0.05
+0,-0.05
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.05
+0,-0.04
+0,-0.04
+0,-0.04
+0,-0.03
+0,-0.03
+0,-0.02
+0,-0.03
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.01
+0,-0.02
+0,0.01
+0,0.02
+0,0
+0,-0
+0,0
+0,0
+0,-0.01
+0,-0.01
+0,-0.01
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.01
+0,-0.01
+0,-0.01
+0,-0.01
+0,0
+0,0.01
+0,0.01
+0,0.01
+0,0.01
+0,0.01
+0,0.01
+0,0.01
+0,0.02
+0,0.03
+0,0.03
+0,0.03
+0,0.03
+0,0.03
+0,0.03
+0,0.02
+0,0.01
+0,0.01
+0,0.01
+0,0
+0,-0
+0,-0
+-1,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
+0,-0.02
diff --git a/strategy/volatility/testdata/brk-b.csv b/strategy/volatility/testdata/brk-b.csv
new file mode 120000
index 0000000..53db519
--- /dev/null
+++ b/strategy/volatility/testdata/brk-b.csv
@@ -0,0 +1 @@
+../../../asset/testdata/repository/brk-b.csv
\ No newline at end of file
diff --git a/strategy/volatility/volatility.go b/strategy/volatility/volatility.go
new file mode 100644
index 0000000..0bea739
--- /dev/null
+++ b/strategy/volatility/volatility.go
@@ -0,0 +1,28 @@
+// Package volatility contains the volatility strategy functions.
+//
+// This package belongs to the Indicator project. Indicator is
+// a Golang module that supplies a variety of technical
+// indicators, strategies, and a backtesting framework
+// for analysis.
+//
+// # License
+//
+//	Copyright (c) 2021-2023 Onur Cinar.
+//	The source code is provided under GNU AGPLv3 License.
+//	https://github.com/cinar/indicator
+//
+// # Disclaimer
+//
+// The information provided on this project is strictly for
+// informational purposes and is not to be construed as
+// advice or solicitation to buy or sell any security.
+package volatility
+
+import "github.com/cinar/indicator/strategy"
+
+// AllStrategies returns a slice containing references to all available volatility strategies.
+func AllStrategies() []strategy.Strategy {
+	return []strategy.Strategy{
+		NewBollingerBandsStrategy(),
+	}
+}