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