From 0c9db680c30aab0dd2738499a8b92967bbb281c9 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Sat, 9 Dec 2023 19:00:45 -0800 Subject: [PATCH] APO Strategy is added. --- asset/README.md | 12 +- asset/snapshot.go | 8 + helper/README.md | 34 ++-- helper/buffered.go | 20 +++ helper/buffered_test.go | 23 +++ helper/check.go | 2 +- helper/operate.go | 8 +- helper/pipe.go | 2 +- helper/shift.go | 2 +- helper/skip.go | 2 +- pre-commit.sh | 2 +- strategy/README.md | 77 +++++++++ strategy/action.go | 25 +++ strategy/apo_strategy.go | 53 ++++++ strategy/apo_strategy_test.go | 38 +++++ strategy/testdata/apo_strategy.csv | 252 +++++++++++++++++++++++++++++ trend/apo.go | 1 + trend/apo_test.go | 33 ++-- trend/testdata/apo.csv | 252 +++++++++++++++++++++++++++++ 19 files changed, 810 insertions(+), 36 deletions(-) create mode 100644 helper/buffered.go create mode 100644 helper/buffered_test.go create mode 100644 strategy/README.md create mode 100644 strategy/action.go create mode 100644 strategy/apo_strategy.go create mode 100644 strategy/apo_strategy_test.go create mode 100644 strategy/testdata/apo_strategy.csv create mode 100644 trend/testdata/apo.csv diff --git a/asset/README.md b/asset/README.md index c68c129..5ecb7e9 100644 --- a/asset/README.md +++ b/asset/README.md @@ -24,11 +24,21 @@ The information provided on this project is strictly for informational purposes ## Index +- [func CloseOnly\(s \<\-chan \*Snapshot\) \<\-chan float64](<#CloseOnly>) - [type Snapshot](<#Snapshot>) + +## func [CloseOnly]() + +```go +func CloseOnly(s <-chan *Snapshot) <-chan float64 +``` + +CloseOnly filters the given snapshot channel and returns a new channel containing only the closing values. + -## type [Snapshot]() +## type [Snapshot]() Snapshot captures a single observation of an asset's price at a specific moment. diff --git a/asset/snapshot.go b/asset/snapshot.go index 4f90925..aa91506 100644 --- a/asset/snapshot.go +++ b/asset/snapshot.go @@ -6,6 +6,8 @@ package asset import ( "time" + + "github.com/cinar/indicator/helper" ) // Snapshot captures a single observation of an asset's price @@ -34,3 +36,9 @@ type Snapshot struct { // the asset during the snapshot period. Volume float64 } + +// CloseOnly filters the given snapshot channel and returns a new +// channel containing only the closing values. +func CloseOnly(s <-chan *Snapshot) <-chan float64 { + return helper.Map(s, func(s *Snapshot) float64 { return s.Close }) +} diff --git a/helper/README.md b/helper/README.md index 8c717e7..17a9438 100644 --- a/helper/README.md +++ b/helper/README.md @@ -28,10 +28,11 @@ The information provided on this project is strictly for informational purposes - [func Abs\[T Number\]\(c \<\-chan T\) \<\-chan T](<#Abs>) - [func Add\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Add>) - [func Apply\[T Number\]\(c \<\-chan T, f func\(T\) T\) \<\-chan T](<#Apply>) +- [func Buffered\[T any\]\(c \<\-chan T, size int\) \<\-chan T](<#Buffered>) - [func ChanToSlice\[T any\]\(c \<\-chan T\) \[\]T](<#ChanToSlice>) - [func Change\[T Number\]\(c \<\-chan T, before int\) \<\-chan T](<#Change>) - [func ChangePercent\[T Number\]\(c \<\-chan T, before int\) \<\-chan T](<#ChangePercent>) -- [func CheckEquals\[T Number\]\(inputs ...\<\-chan T\) error](<#CheckEquals>) +- [func CheckEquals\[T comparable\]\(inputs ...\<\-chan T\) error](<#CheckEquals>) - [func DecrementBy\[T Number\]\(c \<\-chan T, d T\) \<\-chan T](<#DecrementBy>) - [func Divide\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Divide>) - [func DivideBy\[T Number\]\(c \<\-chan T, d T\) \<\-chan T](<#DivideBy>) @@ -46,17 +47,17 @@ 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\]\(ac, bc \<\-chan T, o func\(T, T\) T\) \<\-chan T](<#Operate>) -- [func Pipe\[T Number\]\(f \<\-chan T, t chan\<\- T\)](<#Pipe>) +- [func Operate\[T Number, R any\]\(ac, bc \<\-chan T, o func\(T, T\) 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>) - [func RoundDigit\[T Number\]\(n T, d int\) T](<#RoundDigit>) - [func RoundDigits\[T Number\]\(c \<\-chan T, d int\) \<\-chan T](<#RoundDigits>) - [func Seq\[T Number\]\(from, to, increment T\) \<\-chan T](<#Seq>) -- [func Shift\[T Number\]\(c \<\-chan T, count int, fill T\) \<\-chan T](<#Shift>) +- [func Shift\[T any\]\(c \<\-chan T, count int, fill T\) \<\-chan T](<#Shift>) - [func Sign\[T Number\]\(c \<\-chan T\) \<\-chan T](<#Sign>) - [func Since\[T Number\]\(c \<\-chan T\) \<\-chan T](<#Since>) -- [func Skip\[T Number\]\(c \<\-chan T, count int\) \<\-chan T](<#Skip>) +- [func Skip\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#Skip>) - [func SliceToChan\[T any\]\(slice \[\]T\) \<\-chan T](<#SliceToChan>) - [func Sqrt\[T Number\]\(c \<\-chan T\) \<\-chan T](<#Sqrt>) - [func Subtract\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Subtract>) @@ -158,6 +159,17 @@ timesTwo := helper.Apply(c, func(n int) int { }) ``` + +## func [Buffered]() + +```go +func Buffered[T any](c <-chan T, size int) <-chan T +``` + +Buffered takes a channel of any type and returns a new channel of the same type with a buffer of the specified size. This allows the original channel to continue sending data even if the receiving end is temporarily unavailable. + +Example: + ## func [ChanToSlice]() @@ -218,7 +230,7 @@ fmt.Println(helper.ChanToSlice(actual)) // [400, 150, 60, -60, -87.5, -50, 200, ## func [CheckEquals]() ```go -func CheckEquals[T Number](inputs ...<-chan T) error +func CheckEquals[T comparable](inputs ...<-chan T) error ``` CheckEquals determines whether the two channels are equal. @@ -456,10 +468,10 @@ fmt.Println(helper.ChanToSlice(twoTimes)) // [2, 4, 6, 8] ## func [Operate]() ```go -func Operate[T Number](ac, bc <-chan T, o func(T, T) T) <-chan T +func Operate[T Number, R any](ac, bc <-chan T, o func(T, T) R) <-chan R ``` -Operate applies the provided operate function to corresponding values from two input channels of float64 and sends the resulting values to an output channel. +Operate applies the provided operate function to corresponding values from two numeric input channels and sends the resulting values to an output channel. Example: @@ -473,7 +485,7 @@ add := helper.Operate(ac, bc, func(a, b int) int { ## func [Pipe]() ```go -func Pipe[T Number](f <-chan T, t chan<- T) +func Pipe[T any](f <-chan T, t chan<- T) ``` Pipe function takes an input channel and an output channel and copies all elements from the input channel into the output channel. @@ -571,7 +583,7 @@ fmt.Println(<- s) // 4 ## func [Shift]() ```go -func Shift[T Number](c <-chan T, count int, fill T) <-chan T +func Shift[T any](c <-chan T, count int, fill T) <-chan T ``` Shift takes a channel of numbers, shifts them to the right by the specified count, and fills in any missing values with the provided fill value. @@ -614,7 +626,7 @@ Since counts the number of periods since the last change of value in a channel o ## func [Skip]() ```go -func Skip[T Number](c <-chan T, count int) <-chan T +func Skip[T any](c <-chan T, count int) <-chan T ``` Skip skips the specified number of elements from the given channel of float64. diff --git a/helper/buffered.go b/helper/buffered.go new file mode 100644 index 0000000..623a2f3 --- /dev/null +++ b/helper/buffered.go @@ -0,0 +1,20 @@ +// Copyright (c) 2023 Onur Cinar. All Rights Reserved. +// The source code is provided under MIT License. +// https://github.com/cinar/indicator + +package helper + +// Buffered takes a channel of any type and returns a new channel of the same type with +// a buffer of the specified size. This allows the original channel to continue sending +// data even if the receiving end is temporarily unavailable. +// +// Example: +func Buffered[T any](c <-chan T, size int) <-chan T { + result := make(chan T, size) + + go func() { + Pipe(c, result) + }() + + return result +} diff --git a/helper/buffered_test.go b/helper/buffered_test.go new file mode 100644 index 0000000..74d0021 --- /dev/null +++ b/helper/buffered_test.go @@ -0,0 +1,23 @@ +// Copyright (c) 2023 Onur Cinar. All Rights Reserved. +// The source code is provided under MIT License. +// https://github.com/cinar/indicator + +package helper_test + +import ( + "testing" + + "github.com/cinar/indicator/helper" +) + +func TestBuffered(t *testing.T) { + c := make(chan int, 1) + b := helper.Buffered(c, 4) + + c <- 1 + c <- 2 + c <- 3 + c <- 4 + + helper.Drain(b) +} diff --git a/helper/check.go b/helper/check.go index 1b9d414..b273fba 100644 --- a/helper/check.go +++ b/helper/check.go @@ -10,7 +10,7 @@ import ( ) // CheckEquals determines whether the two channels are equal. -func CheckEquals[T Number](inputs ...<-chan T) error { +func CheckEquals[T comparable](inputs ...<-chan T) error { if len(inputs)%2 != 0 { return errors.New("not pairs") } diff --git a/helper/operate.go b/helper/operate.go index d008fbc..40ffa51 100644 --- a/helper/operate.go +++ b/helper/operate.go @@ -5,15 +5,15 @@ package helper // Operate applies the provided operate function to corresponding values from two -// input channels of float64 and sends the resulting values to an output channel. +// numeric input channels and sends the resulting values to an output channel. // // Example: // // add := helper.Operate(ac, bc, func(a, b int) int { // return a + b // }) -func Operate[T Number](ac, bc <-chan T, o func(T, T) T) <-chan T { - oc := make(chan T) +func Operate[T Number, R any](ac, bc <-chan T, o func(T, T) R) <-chan R { + oc := make(chan R) go func() { defer close(oc) @@ -21,11 +21,13 @@ func Operate[T Number](ac, bc <-chan T, o func(T, T) T) <-chan T { for { an, ok := <-ac if !ok { + Drain(bc) break } bn, ok := <-bc if !ok { + Drain(ac) break } diff --git a/helper/pipe.go b/helper/pipe.go index 584ed73..cd940f5 100644 --- a/helper/pipe.go +++ b/helper/pipe.go @@ -13,7 +13,7 @@ package helper // output := make(chan int) // helper.Pipe(input, output) // fmt.println(helper.ChanToSlice(output)) // [2, 4, 6, 8] -func Pipe[T Number](f <-chan T, t chan<- T) { +func Pipe[T any](f <-chan T, t chan<- T) { go func() { defer close(t) for n := range f { diff --git a/helper/shift.go b/helper/shift.go index c70d028..c618b22 100644 --- a/helper/shift.go +++ b/helper/shift.go @@ -12,7 +12,7 @@ package helper // input := helper.SliceToChan([]int{2, 4, 6, 8}) // output := helper.ChanToSlice(input, 4, 0)) // fmt.Println(helper.ChanToSlice(output)) // [0, 0, 0, 0, 2, 4, 6, 8] -func Shift[T Number](c <-chan T, count int, fill T) <-chan T { +func Shift[T any](c <-chan T, count int, fill T) <-chan T { result := make(chan T, cap(c)+count) go func() { diff --git a/helper/skip.go b/helper/skip.go index f479888..c29bbd7 100644 --- a/helper/skip.go +++ b/helper/skip.go @@ -12,7 +12,7 @@ package helper // c := helper.SliceToChan([]int{2, 4, 6, 8}) // actual := helper.Skip(c, 2) // fmt.Println(helper.ChanToSlice(actual)) // [6, 8] -func Skip[T Number](c <-chan T, count int) <-chan T { +func Skip[T any](c <-chan T, count int) <-chan T { result := make(chan T, cap(c)) go func() { diff --git a/pre-commit.sh b/pre-commit.sh index cf4cf93..7ccb172 100755 --- a/pre-commit.sh +++ b/pre-commit.sh @@ -10,7 +10,7 @@ go test -cover ./... revive -config=revive.toml ./... staticcheck ./... -for package in asset helper trend; +for package in asset helper strategy trend; do echo Package $package gomarkdoc --repository.default-branch v2 --output $package/README.md ./$package diff --git a/strategy/README.md b/strategy/README.md new file mode 100644 index 0000000..d0a7fba --- /dev/null +++ b/strategy/README.md @@ -0,0 +1,77 @@ + + +# strategy + +```go +import "github.com/cinar/indicator/strategy" +``` + +## Index + +- [type Action](<#Action>) +- [type ApoStrategy](<#ApoStrategy>) + - [func NewApoStrategy\[T helper.Number\]\(\) \*ApoStrategy\[T\]](<#NewApoStrategy>) + - [func \(a \*ApoStrategy\[T\]\) Compute\(c \<\-chan T\) \<\-chan Action](<#ApoStrategy[T].Compute>) + + + +## type [Action]() + +ActionType represents the different action categories that a strategy can recommend. + +```go +type Action int +``` + + + +```go +const ( + // Hold suggests maintaining the current position and not + // taking any actions on the asset. + Hold Action = 0 + + // Sell suggests disposing of the asset and exiting the current position. + // This recommendation typically indicates that the strategy believes the + // asset's price has reached its peak or is likely to decline. + Sell Action = -1 + + // Buy suggests acquiring the asset and entering a new position. This + // recommendation usually implies that the strategy believes the + // asset's price is undervalued. + Buy Action = 1 +) +``` + + +## type [ApoStrategy]() + +ApoStrategy represents the configuration parameters for calculating the APO strategy. An APO value crossing above zero suggests a bullish trend, while crossing below zero indicates a bearish trend. Positive APO values signify an upward trend, while negative values signify a downward trend. + +```go +type ApoStrategy[T helper.Number] struct { + // Apo represents the configuration parameters for calculating the + // Absolute Price Oscillator (APO). + Apo *trend.Apo[T] +} +``` + + +### func [NewApoStrategy]() + +```go +func NewApoStrategy[T helper.Number]() *ApoStrategy[T] +``` + +NewApo function initializes a new APO strategy instance with the default parameters. + + +### func \(\*ApoStrategy\[T\]\) [Compute]() + +```go +func (a *ApoStrategy[T]) Compute(c <-chan T) <-chan Action +``` + +Compute uses the given values as input and returns a channel of action recommendations. + +Generated by [gomarkdoc]() diff --git a/strategy/action.go b/strategy/action.go new file mode 100644 index 0000000..72bab2e --- /dev/null +++ b/strategy/action.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 + +// ActionType represents the different action categories that a +// strategy can recommend. +type Action int + +const ( + // Hold suggests maintaining the current position and not + // taking any actions on the asset. + Hold Action = 0 + + // Sell suggests disposing of the asset and exiting the current position. + // This recommendation typically indicates that the strategy believes the + // asset's price has reached its peak or is likely to decline. + Sell Action = -1 + + // Buy suggests acquiring the asset and entering a new position. This + // recommendation usually implies that the strategy believes the + // asset's price is undervalued. + Buy Action = 1 +) diff --git a/strategy/apo_strategy.go b/strategy/apo_strategy.go new file mode 100644 index 0000000..2a7527b --- /dev/null +++ b/strategy/apo_strategy.go @@ -0,0 +1,53 @@ +// 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" + "github.com/cinar/indicator/trend" +) + +// ApoStrategy represents the configuration parameters for calculating the APO strategy. +// An APO value crossing above zero suggests a bullish trend, while crossing below zero +// indicates a bearish trend. Positive APO values signify an upward trend, while +// negative values signify a downward trend. +type ApoStrategy[T helper.Number] struct { + // Apo represents the configuration parameters for calculating the + // Absolute Price Oscillator (APO). + Apo *trend.Apo[T] +} + +// NewApo function initializes a new APO strategy instance with the default parameters. +func NewApoStrategy[T helper.Number]() *ApoStrategy[T] { + return &ApoStrategy[T]{ + Apo: trend.NewApo[T](), + } +} + +// Compute uses the given values as input and returns a channel of +// action recommendations. +func (a *ApoStrategy[T]) Compute(c <-chan T) <-chan Action { + apo := a.Apo.Compute(c) + apo = helper.Buffered(apo, 2) + + inputs := helper.Duplicate(apo, 2) + + // Ship the first value + inputs[1] = helper.Skip(inputs[1], 1) + + return helper.Shift(helper.Operate(inputs[0], inputs[1], func(b, c T) Action { + // An APO value crossing above zero suggests a bullish trend. + if c >= 0 && b < 0 { + return Buy + } + + // An APO value crossing below zero indicates a bearish trend. + if c <= 0 && b > 0 { + return Sell + } + + return Hold + }), 1, Hold) +} diff --git a/strategy/apo_strategy_test.go b/strategy/apo_strategy_test.go new file mode 100644 index 0000000..5da40b2 --- /dev/null +++ b/strategy/apo_strategy_test.go @@ -0,0 +1,38 @@ +// 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 TestApoStrategy(t *testing.T) { + type ApoData struct { + Close float64 + Action strategy.Action + } + + input, err := helper.ReadFromCsvFile[ApoData]("testdata/apo_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + inputs := helper.Duplicate(input, 2) + closing := helper.Map(inputs[0], func(a *ApoData) float64 { return a.Close }) + expected := helper.Map(inputs[1], func(a *ApoData) strategy.Action { return a.Action }) + + apo := strategy.NewApoStrategy[float64]() + expected = helper.Skip(expected, apo.Apo.SlowPeriod-1) + + actual := apo.Compute(closing) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/testdata/apo_strategy.csv b/strategy/testdata/apo_strategy.csv new file mode 100644 index 0000000..b66dd93 --- /dev/null +++ b/strategy/testdata/apo_strategy.csv @@ -0,0 +1,252 @@ +Close,Action +318.600006,0 +315.839996,0 +316.149994,0 +310.570007,0 +307.779999,0 +305.820007,0 +305.98999,0 +306.390015,0 +311.450012,0 +312.329987,0 +309.290009,0 +301.910004,0 +300,0 +300.029999,0 +302,0 +307.820007,0 +302.690002,0 +306.48999,0 +305.549988,0 +303.429993,0 +309.059998,0 +308.899994,0 +309.910004,0 +314.549988,0 +312.899994,0 +318.690002,0 +315.529999,0 +316.350006,0 +320.369995,0 +318.929993,0 +317.640015,0 +314.859985,0 +308.299988,0 +305.230011,0 +309.869995,0 +310.420013,0 +311.299988,0 +311.899994,0 +310.950012,0 +309.170013,0 +307.329987,0 +311.519989,1 +310.570007,0 +311.859985,0 +308.51001,0 +308.429993,0 +312.970001,0 +308.480011,0 +307.209991,0 +309.890015,0 +313.73999,0 +310.790009,0 +309.630005,0 +308.179993,0 +308.23999,0 +302.720001,0 +303.160004,0 +303.070007,0 +304.019989,0 +304.660004,0 +305.179993,0 +304.619995,0 +307.75,0 +312.450012,0 +316.970001,0 +311.119995,0 +311.369995,0 +304.820007,0 +303.630005,0 +302.880005,0 +305.329987,0 +297.880005,0 +302.01001,0 +293.51001,0 +301.059998,0 +303.850006,0 +299.730011,0 +298.369995,0 +298.920013,0 +302.140015,0 +302.320007,0 +305.299988,0 +305.079987,0 +308.769989,0 +310.309998,0 +309.070007,0 +310.390015,0 +312.51001,-1 +312.619995,0 +313.700012,0 +314.549988,0 +318.049988,0 +319.73999,0 +323.790009,0 +324.630005,0 +323.089996,0 +323.820007,0 +324.329987,0 +326.049988,0 +324.339996,0 +320.529999,0 +326.230011,0 +328.549988,0 +330.170013,0 +325.859985,0 +323.220001,0 +320,0 +323.880005,0 +326.140015,0 +324.869995,0 +322.98999,0 +322.640015,0 +322.48999,0 +323.529999,0 +323.75,0 +327.390015,0 +329.76001,0 +330.390015,0 +329.130005,0 +323.109985,1 +320.200012,0 +319.019989,0 +320.600006,0 +322.190002,0 +321.079987,0 +323.119995,0 +329.480011,0 +328.579987,0 +333.410004,-1 +335.420013,0 +335.950012,0 +335.290009,0 +333.600006,0 +336.390015,0 +335.899994,0 +339.820007,0 +338.309998,0 +338.670013,0 +338.609985,0 +336.959991,0 +335.25,0 +334.119995,0 +335.339996,0 +334.149994,0 +336.910004,0 +341,0 +342,0 +341.559998,0 +341.459991,0 +340.899994,0 +341.130005,0 +343.369995,0 +345.350006,0 +343.540009,0 +341.089996,0 +344.25,0 +345.339996,0 +342.429993,0 +346.609985,0 +345.76001,0 +349.630005,0 +347.579987,0 +349.799988,0 +349.309998,0 +349.809998,0 +351.959991,0 +352.26001,0 +351.190002,0 +353.809998,0 +349.98999,0 +362.579987,0 +363.730011,0 +358.019989,0 +356.980011,0 +358.350006,0 +358.480011,0 +354.5,0 +354.109985,0 +353.190002,0 +352.559998,0 +352.089996,0 +350.570007,0 +354.26001,0 +354.299988,0 +355.929993,0 +355.549988,0 +358.290009,0 +361.059998,0 +360.200012,1 +362.459991,-1 +360.470001,1 +361.670013,0 +361.799988,-1 +363.149994,0 +365.519989,0 +367.779999,0 +367.820007,0 +369.5,0 +367.859985,0 +370.429993,0 +370.480011,0 +366.820007,0 +363.279999,0 +360.160004,0 +361.709991,0 +359.420013,0 +357.779999,0 +357.059998,0 +350.299988,0 +348.079987,1 +343.040009,0 +343.690002,0 +345.059998,0 +346.339996,0 +345.450012,0 +348.559998,0 +348.429993,0 +345.660004,0 +345.089996,0 +346.230011,0 +345.390015,0 +340.890015,0 +338.660004,0 +335.859985,0 +336.839996,0 +338.630005,0 +336.899994,0 +336.160004,0 +331.709991,0 +337.410004,0 +341.329987,0 +343.75,0 +349.019989,0 +351.809998,0 +346.630005,0 +346.170013,0 +346.299988,0 +348.179993,0 +350.559998,0 +350.01001,-1 +354.25,0 +356.790009,0 +359.859985,0 +358.929993,0 +361.329987,0 +361,0 +361.799988,0 +362.679993,0 +361.339996,0 +360.049988,0 +358.690002,0 \ No newline at end of file diff --git a/trend/apo.go b/trend/apo.go index fbf4cc8..08ccbee 100644 --- a/trend/apo.go +++ b/trend/apo.go @@ -66,6 +66,7 @@ func NewApo[T helper.Number]() *Apo[T] { // Compute function takes a channel of numbers and computes the APO // over the specified period. func (apo *Apo[T]) Compute(c <-chan T) <-chan T { + c = helper.Buffered(c, apo.SlowPeriod) cs := helper.Duplicate(c, 2) fastEma := NewEma[T]() diff --git a/trend/apo_test.go b/trend/apo_test.go index 1e79389..9ad1897 100644 --- a/trend/apo_test.go +++ b/trend/apo_test.go @@ -5,7 +5,6 @@ package trend_test import ( - "reflect" "testing" "github.com/cinar/indicator/helper" @@ -13,24 +12,26 @@ import ( ) func TestApo(t *testing.T) { - input := helper.SliceToChan([]float64{ - 22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, - 22.29, 22.15, 22.39, 22.38, 22.61, 23.36, 24.05, 23.75, 23.83, - 23.95, 23.63, 23.82, 23.87, 23.65, 23.19, 23.10, 23.33, 22.68, - 23.10, 22.40, 22.17, - }) - - expected := []float64{ - -0.67, -0.63, -0.59, -0.39, -0.10, + type ApoData struct { + Close float64 + Apo float64 } - apo := trend.NewApo[float64]() - apo.FastPeriod = 12 - apo.SlowPeriod = 26 + input, err := helper.ReadFromCsvFile[ApoData]("testdata/apo.csv", true) + if err != nil { + t.Fatal(err) + } - actual := helper.ChanToSlice(helper.RoundDigits(apo.Compute(input), 2)) + inputs := helper.Duplicate(input, 2) + closing := helper.Map(inputs[0], func(a *ApoData) float64 { return a.Close }) + expected := helper.Map(inputs[1], func(a *ApoData) float64 { return a.Apo }) + + apo := trend.NewApo[float64]() + actual := helper.RoundDigits(apo.Compute(closing), 2) + expected = helper.Skip(expected, apo.SlowPeriod-1) - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("actual %v expected %v", actual, expected) + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) } } diff --git a/trend/testdata/apo.csv b/trend/testdata/apo.csv new file mode 100644 index 0000000..bbd730f --- /dev/null +++ b/trend/testdata/apo.csv @@ -0,0 +1,252 @@ +Close,Apo +318.600006,0 +315.839996,0 +316.149994,0 +310.570007,0 +307.779999,0 +305.820007,0 +305.98999,0 +306.390015,0 +311.450012,0 +312.329987,0 +309.290009,0 +301.910004,0 +300,0 +300.029999,0 +302,0 +307.820007,0 +302.690002,0 +306.48999,0 +305.549988,0 +303.429993,0 +309.059998,0 +308.899994,0 +309.910004,0 +314.549988,0 +312.899994,0 +318.690002,0 +315.529999,0 +316.350006,0 +320.369995,0 +318.929993,-1.12 +317.640015,-2.52 +314.859985,-2.81 +308.299988,-3.35 +305.230011,-3.09 +309.869995,-3.28 +310.420013,-3.75 +311.299988,-3.47 +311.899994,-3.29 +310.950012,-2.95 +309.170013,-1.92 +307.329987,-1.13 +311.519989,0.08 +310.570007,0.75 +311.859985,1.37 +308.51001,2.64 +308.429993,3.57 +312.970001,3.91 +308.480011,4.12 +307.209991,3.51 +309.890015,2.41 +313.73999,1.84 +310.790009,1.58 +309.630005,1.55 +308.179993,1.7 +308.23999,1.71 +302.720001,1.84 +303.160004,1.72 +303.070007,2.2 +304.019989,2.46 +304.660004,2.83 +305.179993,2.7 +304.619995,2.61 +307.75,2.96 +312.450012,2.36 +316.970001,1.36 +311.119995,1.19 +311.369995,1.52 +304.820007,1.83 +303.630005,2.04 +302.880005,2.1 +305.329987,2.02 +297.880005,1.71 +302.01001,1.28 +293.51001,1.47 +301.059998,1.33 +303.850006,1.13 +299.730011,1.31 +298.369995,1.5 +298.920013,2.07 +302.140015,3.02 +302.320007,4.44 +305.299988,4.7 +305.079987,4.98 +308.769989,4.1 +310.309998,3.06 +309.070007,2.11 +310.390015,1.52 +312.51001,-0.15 +312.619995,-1.09 +313.700012,-3.13 +314.549988,-3.99 +318.049988,-4.61 +319.73999,-5.86 +323.790009,-7.44 +324.630005,-8.85 +323.089996,-9.61 +323.820007,-10.35 +324.329987,-10.68 +326.049988,-11.16 +324.339996,-11.03 +320.529999,-10.52 +326.230011,-10.63 +328.549988,-10.75 +330.170013,-10.73 +325.859985,-10.49 +323.220001,-10 +320,-9.27 +323.880005,-8.44 +326.140015,-7.66 +324.869995,-6.39 +322.98999,-5.09 +322.640015,-4.16 +322.48999,-3.25 +323.529999,-2.48 +323.75,-1.61 +327.390015,-1.34 +329.76001,-1.79 +330.390015,-1.5 +329.130005,-0.9 +323.109985,0.2 +320.200012,0.76 +319.019989,0.98 +320.600006,0.65 +322.190002,0.79 +321.079987,1.29 +323.119995,1.42 +329.480011,0.87 +328.579987,0.37 +333.410004,-0.42 +335.420013,-1.14 +335.950012,-1.82 +335.290009,-1.94 +333.600006,-1.66 +336.390015,-1.54 +335.899994,-1.63 +339.820007,-2.8 +338.309998,-4.15 +338.670013,-5.55 +338.609985,-6.59 +336.959991,-7.22 +335.25,-7.83 +334.119995,-8.03 +335.339996,-7.45 +334.149994,-7.01 +336.910004,-6.18 +341,-5.47 +342,-4.9 +341.559998,-4.5 +341.459991,-4.41 +340.899994,-3.96 +341.130005,-3.68 +343.369995,-3.09 +345.350006,-2.94 +343.540009,-2.7 +341.089996,-2.36 +344.25,-2.52 +345.339996,-2.98 +342.429993,-3.38 +346.609985,-3.86 +345.76001,-4.41 +349.630005,-4.8 +347.579987,-4.51 +349.799988,-4.3 +349.309998,-4.19 +349.809998,-4.17 +351.959991,-4.41 +352.26001,-4.65 +351.190002,-4.53 +353.809998,-4.37 +349.98999,-4.27 +362.579987,-5.34 +363.730011,-6 +358.019989,-6.14 +356.980011,-6.63 +358.350006,-6.63 +358.480011,-6.79 +354.5,-6.2 +354.109985,-5.96 +353.190002,-5.42 +352.559998,-4.98 +352.089996,-4.52 +350.570007,-3.74 +354.26001,-3.26 +354.299988,-3 +355.929993,-2.55 +355.549988,-2.67 +358.290009,-1.29 +361.059998,-0.14 +360.200012,0.1 +362.459991,0 +360.470001,0.17 +361.670013,0.24 +361.799988,-0.28 +363.149994,-0.89 +365.519989,-1.74 +367.779999,-2.75 +367.820007,-3.74 +369.5,-4.95 +367.859985,-5.46 +370.429993,-6.1 +370.480011,-6.5 +366.820007,-6.7 +363.279999,-6.31 +360.160004,-5.41 +361.709991,-4.84 +359.420013,-3.91 +357.779999,-3.24 +357.059998,-2.45 +350.299988,-1.29 +348.079987,0.08 +343.040009,1.96 +343.690002,3.92 +345.059998,5.6 +346.339996,7.25 +345.450012,8.57 +348.559998,9.9 +348.429993,11.09 +345.660004,11.84 +345.089996,12.1 +346.230011,11.87 +345.390015,11.96 +340.890015,12.06 +338.660004,12.12 +335.859985,12.31 +336.839996,11.58 +338.630005,10.59 +336.899994,9.22 +336.160004,8.22 +331.709991,7.87 +337.410004,7.43 +341.329987,6.72 +343.75,6.38 +349.019989,5.74 +351.809998,4.61 +346.630005,3.86 +346.170013,3.39 +346.299988,2.86 +348.179993,1.67 +350.559998,0.18 +350.01001,-1.47 +354.25,-3.05 +356.790009,-4.39 +359.859985,-6.02 +358.929993,-7.53 +361.329987,-9.63 +361,-10.72 +361.799988,-11.25 +362.679993,-11.49 +361.339996,-10.96 +360.049988,-10.09 +358.690002,-9.96 \ No newline at end of file