diff --git a/README.md b/README.md
index 58be376..3f9b7b7 100644
--- a/README.md
+++ b/README.md
@@ -158,6 +158,7 @@ Decorator strategies offer a way to alter the recommendations of other strategie
- [Inverse Strategy](strategy/decorator/README.md#type-inversestrategy)
- [No Loss Strategy](strategy/decorator/README.md#type-nolossstrategy)
+- [Stop Loss Strategy](strategy/decorator/README.md#type-stoplossstrategy)
🗃 Repositories
--------------
diff --git a/strategy/decorator/README.md b/strategy/decorator/README.md
index ef7be3f..df78cca 100644
--- a/strategy/decorator/README.md
+++ b/strategy/decorator/README.md
@@ -34,6 +34,11 @@ The information provided on this project is strictly for informational purposes
- [func \(n \*NoLossStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#NoLossStrategy.Compute>)
- [func \(n \*NoLossStrategy\) Name\(\) string](<#NoLossStrategy.Name>)
- [func \(n \*NoLossStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#NoLossStrategy.Report>)
+- [type StopLossStrategy](<#StopLossStrategy>)
+ - [func NewStopLossStrategy\(innerStrategy strategy.Strategy, percentage float64\) \*StopLossStrategy](<#NewStopLossStrategy>)
+ - [func \(s \*StopLossStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#StopLossStrategy.Compute>)
+ - [func \(s \*StopLossStrategy\) Name\(\) string](<#StopLossStrategy.Name>)
+ - [func \(s \*StopLossStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#StopLossStrategy.Report>)
@@ -136,4 +141,57 @@ func (n *NoLossStrategy) Report(c <-chan *asset.Snapshot) *helper.Report
Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+
+## type [StopLossStrategy]()
+
+StopLossStrategy prevents a loss by recommending a sell action when the assets drops below the given threshold.
+
+```go
+type StopLossStrategy struct {
+ strategy.Strategy
+
+ // InnertStrategy is the inner strategy.
+ InnertStrategy strategy.Strategy
+
+ // Percentage is the loss threshold in percentage.
+ Percentage float64
+}
+```
+
+
+### func [NewStopLossStrategy]()
+
+```go
+func NewStopLossStrategy(innerStrategy strategy.Strategy, percentage float64) *StopLossStrategy
+```
+
+NewStopLossStrategy function initializes a new stop loss strategy instance.
+
+
+### func \(\*StopLossStrategy\) [Compute]()
+
+```go
+func (s *StopLossStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
+```
+
+Compute processes the provided asset snapshots and generates a stream of actionable recommendations.
+
+
+### func \(\*StopLossStrategy\) [Name]()
+
+```go
+func (s *StopLossStrategy) Name() string
+```
+
+Name returns the name of the strategy.
+
+
+### func \(\*StopLossStrategy\) [Report]()
+
+```go
+func (s *StopLossStrategy) 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]()
diff --git a/strategy/decorator/stop_loss_strategy.go b/strategy/decorator/stop_loss_strategy.go
new file mode 100644
index 0000000..79f4e96
--- /dev/null
+++ b/strategy/decorator/stop_loss_strategy.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2021-2024 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package decorator
+
+import (
+ "fmt"
+
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/helper"
+ "github.com/cinar/indicator/v2/strategy"
+)
+
+// StopLossStrategy prevents a loss by recommending a sell action when the assets drops below the given threshold.
+type StopLossStrategy struct {
+ strategy.Strategy
+
+ // InnertStrategy is the inner strategy.
+ InnertStrategy strategy.Strategy
+
+ // Percentage is the loss threshold in percentage.
+ Percentage float64
+}
+
+// NewStopLossStrategy function initializes a new stop loss strategy instance.
+func NewStopLossStrategy(innerStrategy strategy.Strategy, percentage float64) *StopLossStrategy {
+ return &StopLossStrategy{
+ InnertStrategy: innerStrategy,
+ Percentage: percentage,
+ }
+}
+
+// Name returns the name of the strategy.
+func (s *StopLossStrategy) Name() string {
+ return fmt.Sprintf("Stop Loss Strategy (%s)", s.InnertStrategy.Name())
+}
+
+// Compute processes the provided asset snapshots and generates a stream of actionable recommendations.
+func (s *StopLossStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
+ snapshotsSplice := helper.Duplicate(snapshots, 2)
+
+ innerActions := s.InnertStrategy.Compute(snapshotsSplice[0])
+ closings := asset.SnapshotsAsClosings(snapshotsSplice[1])
+ stopLossAt := 0.0
+
+ return helper.Operate(innerActions, closings, func(action strategy.Action, closing float64) strategy.Action {
+ // If action is Buy and the asset is not yet bought, buy it as recommended.
+ if action == strategy.Buy && stopLossAt == 0.0 {
+ stopLossAt = closing * (1 - s.Percentage)
+ return strategy.Buy
+ }
+
+ // If asset is bought and action is sell or closing is less than or equal to stop loss at, recommend sell.
+ if stopLossAt != 0 && (action == strategy.Sell || closing <= stopLossAt) {
+ stopLossAt = 0.0
+ return strategy.Sell
+ }
+
+ return strategy.Hold
+ })
+}
+
+// Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
+func (s *StopLossStrategy) Report(c <-chan *asset.Snapshot) *helper.Report {
+ snapshots := helper.Duplicate(c, 3)
+
+ dates := asset.SnapshotsAsDates(snapshots[0])
+ closings := asset.SnapshotsAsClosings(snapshots[1])
+
+ actions, outcomes := strategy.ComputeWithOutcome(s, snapshots[2])
+ annotations := strategy.ActionsToAnnotations(actions)
+ outcomes = helper.MultiplyBy(outcomes, 100)
+
+ report := helper.NewReport(s.Name(), dates)
+ report.AddChart()
+
+ report.AddColumn(helper.NewNumericReportColumn("Close", closings))
+ report.AddColumn(helper.NewAnnotationReportColumn(annotations))
+
+ report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 1)
+
+ return report
+}
diff --git a/strategy/decorator/stop_loss_strategy_test.go b/strategy/decorator/stop_loss_strategy_test.go
new file mode 100644
index 0000000..e7bcf3e
--- /dev/null
+++ b/strategy/decorator/stop_loss_strategy_test.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2021-2024 Onur Cinar.
+// The source code is provided under GNU AGPLv3 License.
+// https://github.com/cinar/indicator
+
+package decorator_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/cinar/indicator/v2/asset"
+ "github.com/cinar/indicator/v2/helper"
+ "github.com/cinar/indicator/v2/strategy"
+ "github.com/cinar/indicator/v2/strategy/decorator"
+ "github.com/cinar/indicator/v2/strategy/trend"
+)
+
+func TestStopLossStrategy(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/stop_loss_strategy.csv", true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action })
+
+ innerStrategy := trend.NewAroonStrategy()
+ strategy := decorator.NewStopLossStrategy(innerStrategy, 0.02)
+
+ actual := strategy.Compute(snapshots)
+
+ err = helper.CheckEquals(actual, expected)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestStopLossStrategyReport(t *testing.T) {
+ snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ innerStrategy := trend.NewAroonStrategy()
+ strategy := decorator.NewStopLossStrategy(innerStrategy, 0.02)
+
+ report := strategy.Report(snapshots)
+
+ fileName := "stop_loss_strategy.html"
+ defer os.Remove(fileName)
+
+ err = report.WriteToFile(fileName)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/strategy/decorator/testdata/stop_loss_strategy.csv b/strategy/decorator/testdata/stop_loss_strategy.csv
new file mode 100644
index 0000000..d9a8acd
--- /dev/null
+++ b/strategy/decorator/testdata/stop_loss_strategy.csv
@@ -0,0 +1,252 @@
+Action
+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
+0
+0
+0
+-1
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+1
+-1
+1
+0
+-1
+0
+0
+0
+0
+0
+1
+0
+0
+-1
+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
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+-1
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+1
+0
+-1
+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
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+-1
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+1
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+-1
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+1
+0
+0
+-1
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0
+0