From 94e27688b042c0278d4ab50aaf28bd6e8f6c77b7 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Thu, 14 Dec 2023 18:19:22 -0800 Subject: [PATCH] Outcome added. --- README.md | 20 +- helper/README.md | 4 +- helper/operate.go | 2 +- strategy/README.md | 30 + strategy/action.go | 32 + strategy/action_test.go | 38 + strategy/apo_strategy.go | 12 +- strategy/apo_strategy.html | 3065 +++++++++++++++++++++++++++++++++ strategy/apo_strategy_test.go | 3 +- strategy/outcome.go | 25 + strategy/outcome_test.go | 36 + 11 files changed, 3250 insertions(+), 17 deletions(-) create mode 100644 strategy/apo_strategy.html create mode 100644 strategy/outcome.go create mode 100644 strategy/outcome_test.go diff --git a/README.md b/README.md index b256b87..905c8b9 100644 --- a/README.md +++ b/README.md @@ -91,14 +91,11 @@ The following list of indicators are currently supported by this package: ## Strategies Provided -Strategies relies on the following: +The following list of strategies are currently supported by this package: -- [Action](strategy/README.md#type-action) -- [Snapshot](asset/README.md#type-snapshot) -- Strategy Function -- Buy and Hold Strategy +### Base Strategies -The following list of strategies are currently supported by this package: +- Buy and Hold Strategy ### Trend Strategies @@ -137,14 +134,17 @@ The following list of strategies are currently supported by this package: - Separate Strategies - MACD and RSI Strategy +### Strategy Helpers + +- [Action](strategy/README.md#type-action) +- [Outcome](strategy/README.md#func-outcome) +- [Snapshot](asset/README.md#type-snapshot) + ## Backtest Backtesting is the method for seeing how well a strategy would have done. The following backtesting functions are provided for evaluating strategies. - Apply Actions -- Count Transactions -- Normalize Actions -- Normalize Gains ## Usage @@ -164,7 +164,7 @@ import ( ## Contributing to the Project -Anyone can contribute to Checkers library. Please make sure to read our [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md) guide first. Follow the [How to Contribute to Checker](./CONTRIBUTING.md) to contribute. +Anyone can contribute to Indicator library. Please make sure to read our [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md) guide first. Follow the [How to Contribute to Checker](./CONTRIBUTING.md) to contribute. ## Disclaimer diff --git a/helper/README.md b/helper/README.md index 1398526..ed3683f 100644 --- a/helper/README.md +++ b/helper/README.md @@ -47,7 +47,7 @@ The information provided on this project is strictly for informational purposes - [func Map\[F, T any\]\(c \<\-chan F, f func\(F\) T\) \<\-chan T](<#Map>) - [func Multiply\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Multiply>) - [func MultiplyBy\[T Number\]\(c \<\-chan T, m T\) \<\-chan T](<#MultiplyBy>) -- [func Operate\[T Number, R any\]\(ac, bc \<\-chan T, o func\(T, T\) R\) \<\-chan R](<#Operate>) +- [func Operate\[A any, B any, R any\]\(ac \<\-chan A, bc \<\-chan B, o func\(A, B\) R\) \<\-chan R](<#Operate>) - [func Pipe\[T any\]\(f \<\-chan T, t chan\<\- T\)](<#Pipe>) - [func Pow\[T Number\]\(c \<\-chan T, y T\) \<\-chan T](<#Pow>) - [func ReadFromCsvFile\[T any\]\(fileName string, hasHeader bool\) \(\<\-chan \*T, error\)](<#ReadFromCsvFile>) @@ -470,7 +470,7 @@ fmt.Println(helper.ChanToSlice(twoTimes)) // [2, 4, 6, 8] ## func [Operate]() ```go -func Operate[T Number, R any](ac, bc <-chan T, o func(T, T) R) <-chan R +func Operate[A any, B any, R any](ac <-chan A, bc <-chan B, o func(A, B) R) <-chan R ``` Operate applies the provided operate function to corresponding values from two numeric input channels and sends the resulting values to an output channel. diff --git a/helper/operate.go b/helper/operate.go index 40ffa51..7b23c64 100644 --- a/helper/operate.go +++ b/helper/operate.go @@ -12,7 +12,7 @@ package helper // add := helper.Operate(ac, bc, func(a, b int) int { // return a + b // }) -func Operate[T Number, R any](ac, bc <-chan T, o func(T, T) R) <-chan R { +func Operate[A any, B any, R any](ac <-chan A, bc <-chan B, o func(A, B) R) <-chan R { oc := make(chan R) go func() { diff --git a/strategy/README.md b/strategy/README.md index 5d931b2..56cb014 100644 --- a/strategy/README.md +++ b/strategy/README.md @@ -25,6 +25,9 @@ The information provided on this project is strictly for informational purposes ## Index - [func ActionsToAnnotations\(ac \<\-chan Action\) \<\-chan string](<#ActionsToAnnotations>) +- [func NormalizeActions\(ac \<\-chan Action\) \<\-chan Action](<#NormalizeActions>) +- [func Outcome\(values \<\-chan float64, actions \<\-chan Action\) \<\-chan float64](<#Outcome>) +- [func SpreadActions\(ac \<\-chan Action\) \<\-chan Action](<#SpreadActions>) - [type Action](<#Action>) - [func \(a Action\) Annotation\(\) string](<#Action.Annotation>) - [type ApoStrategy](<#ApoStrategy>) @@ -42,6 +45,33 @@ func ActionsToAnnotations(ac <-chan Action) <-chan string ActionsToAnnotations takes a channel of action recommendations and returns a new channel containing corresponding annotations for those actions. + +## func [NormalizeActions]() + +```go +func NormalizeActions(ac <-chan Action) <-chan Action +``` + +NormalizeActions transforms the given channel of actions to ensure a consistent and predictable sequence. It eliminates consecutive occurrences of the same action \(Buy/Sell\), ensuring the order follows a pattern of Hold, Buy, Hold, Sell. + + +## func [Outcome]() + +```go +func Outcome(values <-chan float64, actions <-chan Action) <-chan float64 +``` + +Outcome simulates the potential result of executing the given actions based on the provided values. + + +## func [SpreadActions]() + +```go +func SpreadActions(ac <-chan Action) <-chan Action +``` + +SpreadActions simplifies the representation of the action sequence and facilitates subsequent processing by transforming the given channel of actions. It retains Hold actions until the first Buy or Sell action appears. Subsequently, it replaces all remaining Hold actions with the preceding Buy or Sell action, effectively merging consecutive actions. + ## type [Action]() diff --git a/strategy/action.go b/strategy/action.go index 695cc63..578a3fe 100644 --- a/strategy/action.go +++ b/strategy/action.go @@ -48,3 +48,35 @@ func ActionsToAnnotations(ac <-chan Action) <-chan string { return a.Annotation() }) } + +// NormalizeActions transforms the given channel of actions to ensure a consistent and +// predictable sequence. It eliminates consecutive occurrences of the same action +// (Buy/Sell), ensuring the order follows a pattern of Hold, Buy, Hold, Sell. +func NormalizeActions(ac <-chan Action) <-chan Action { + last := Sell + + return helper.Map(ac, func(a Action) Action { + if a != Hold && a != last { + last = a + return a + } + + return Hold + }) +} + +// SpreadActions simplifies the representation of the action sequence and facilitates subsequent +// processing by transforming the given channel of actions. It retains Hold actions until the +// first Buy or Sell action appears. Subsequently, it replaces all remaining Hold actions with +// the preceding Buy or Sell action, effectively merging consecutive actions. +func SpreadActions(ac <-chan Action) <-chan Action { + last := Hold + + return helper.Map(ac, func(a Action) Action { + if a != Hold && a != last { + last = a + } + + return last + }) +} diff --git a/strategy/action_test.go b/strategy/action_test.go index 55407be..e235575 100644 --- a/strategy/action_test.go +++ b/strategy/action_test.go @@ -36,3 +36,41 @@ func TestActionsToAnnotations(t *testing.T) { t.Fatal(err) } } + +func TestNormalizeActions(t *testing.T) { + actions := helper.SliceToChan([]strategy.Action{ + strategy.Hold, strategy.Sell, strategy.Sell, strategy.Buy, strategy.Hold, + strategy.Buy, strategy.Buy, strategy.Sell, strategy.Sell, strategy.Buy, + }) + + expected := helper.SliceToChan([]strategy.Action{ + strategy.Hold, strategy.Hold, strategy.Hold, strategy.Buy, strategy.Hold, + strategy.Hold, strategy.Hold, strategy.Sell, strategy.Hold, strategy.Buy, + }) + + actual := strategy.NormalizeActions(actions) + + err := helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestSpreadActions(t *testing.T) { + actions := helper.SliceToChan([]strategy.Action{ + strategy.Hold, strategy.Hold, strategy.Hold, strategy.Buy, strategy.Hold, + strategy.Hold, strategy.Hold, strategy.Sell, strategy.Hold, strategy.Buy, + }) + + expected := helper.SliceToChan([]strategy.Action{ + strategy.Hold, strategy.Hold, strategy.Hold, strategy.Buy, strategy.Buy, + strategy.Buy, strategy.Buy, strategy.Sell, strategy.Sell, strategy.Buy, + }) + + actual := strategy.SpreadActions(actions) + + err := helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/apo_strategy.go b/strategy/apo_strategy.go index 330229f..d653a90 100644 --- a/strategy/apo_strategy.go +++ b/strategy/apo_strategy.go @@ -57,19 +57,27 @@ func (a *ApoStrategy[T]) Compute(c <-chan T) <-chan Action { // Report takes input channels containing dates and values, computes the recommended actions // based on the data, and generates a report annotated with those actions. func (a *ApoStrategy[T]) Report(dates <-chan time.Time, c <-chan T) *helper.Report { - cs := helper.Duplicate(c, 3) + cs := helper.Duplicate(c, 4) dates = helper.Skip(dates, a.Apo.SlowPeriod-1) cs[0] = helper.Skip(cs[0], a.Apo.SlowPeriod-1) + cs[3] = helper.Skip(cs[3], a.Apo.SlowPeriod-1) apo := a.Apo.Compute(cs[1]) - annotations := ActionsToAnnotations(a.Compute(cs[2])) + + actions := helper.Duplicate(a.Compute(cs[2]), 2) + annotations := ActionsToAnnotations(actions[0]) + + outcome := Outcome(cs[3], actions[1]) report := helper.NewReport("APO Strategy", dates) report.AddChart() + report.AddChart() report.AddColumn(helper.NewNumericReportColumn("Close", cs[0])) report.AddColumn(helper.NewNumericReportColumn("APO", apo), 1) report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcome), 2) + return report } diff --git a/strategy/apo_strategy.html b/strategy/apo_strategy.html new file mode 100644 index 0000000..a412fab --- /dev/null +++ b/strategy/apo_strategy.html @@ -0,0 +1,3065 @@ + + + + + + + APO Strategy + + + + +
+
+

+ APO Strategy +

+ +
+ +
+ +
+ + +
+ + +
+ + +
+
+
+ + + + + + \ No newline at end of file diff --git a/strategy/apo_strategy_test.go b/strategy/apo_strategy_test.go index ec6cd5e..81a2abe 100644 --- a/strategy/apo_strategy_test.go +++ b/strategy/apo_strategy_test.go @@ -5,7 +5,6 @@ package strategy_test import ( - "os" "testing" "time" @@ -59,7 +58,7 @@ func TestApoStrategyReport(t *testing.T) { report := apo.Report(dates, closing) fileName := "apo_strategy.html" - defer os.Remove(fileName) + // defer os.Remove(fileName) err = report.WriteToFile(fileName) if err != nil { diff --git a/strategy/outcome.go b/strategy/outcome.go new file mode 100644 index 0000000..b664026 --- /dev/null +++ b/strategy/outcome.go @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Onur Cinar. All Rights Reserved. +// The source code is provided under MIT License. +// https://github.com/cinar/indicator + +package strategy + +import "github.com/cinar/indicator/helper" + +// Outcome simulates the potential result of executing the given actions based on the provided values. +func Outcome[T helper.Number](values <-chan T, actions <-chan Action) <-chan float64 { + balance := 1.0 + shares := 0.0 + + return helper.Operate(values, actions, func(value T, action Action) float64 { + if balance > 0 && action == Buy { + shares = balance / float64(value) + balance = 0 + } else if shares > 0 && action == Sell { + balance = shares * float64(value) + shares = 0 + } + + return balance + (shares * float64(value)) - 1.0 + }) +} diff --git a/strategy/outcome_test.go b/strategy/outcome_test.go new file mode 100644 index 0000000..bcd9e52 --- /dev/null +++ b/strategy/outcome_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Onur Cinar. All Rights Reserved. +// The source code is provided under MIT License. +// https://github.com/cinar/indicator + +package strategy_test + +import ( + "testing" + + "github.com/cinar/indicator/helper" + "github.com/cinar/indicator/strategy" +) + +func TestOutcome(t *testing.T) { + values := helper.SliceToChan([]float64{ + 10, 15, 12, 12, 18, + 20, 22, 25, 24, 20, + }) + + actions := helper.SliceToChan([]strategy.Action{ + strategy.Hold, strategy.Hold, strategy.Buy, strategy.Buy, strategy.Hold, + strategy.Hold, strategy.Hold, strategy.Sell, strategy.Hold, strategy.Hold, + }) + + expected := helper.SliceToChan([]float64{ + 0, 0, 0, 0, 0.5, + 0.67, 0.83, 1.08, 1.08, 1.08, + }) + + actual := helper.RoundDigits(strategy.Outcome(values, actions), 2) + + err := helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +}