diff --git a/README.md b/README.md index ef3a3d6..93ce0fa 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The following list of indicators are currently supported by this package: - [Balance of Power (BoP)](trend/README.md#type-bop) - Chande Forecast Oscillator (CFO) - Community Channel Index (CMI) -- Double Exponential Moving Average (DEMA) +- [Double Exponential Moving Average (DEMA)](trend/README.md#type-dema) - [Exponential Moving Average (EMA)](trend/README.md#type-ema) - Mass Index (MI) - Moving Average Convergence Divergence (MACD) @@ -104,6 +104,7 @@ The following list of strategies are currently supported by this package: - [Absolute Price Oscillator (APO) Strategy](strategy/README.md#type-apostrategy) - [Aroon Strategy](strategy/README.md#type-aroonstrategy) - [Balance of Power (BoP) Strategy](strategy/README.md#type-bopstrategy) +- [Double Exponential Moving Average (DEMA) Strategy](strategy/README.md#type-demastrategy) - Chande Forecast Oscillator Strategy - KDJ Strategy - MACD Strategy diff --git a/helper/abs_test.go b/helper/abs_test.go index 26fe4bb..2afd985 100644 --- a/helper/abs_test.go +++ b/helper/abs_test.go @@ -5,19 +5,19 @@ package helper_test import ( - "reflect" "testing" "github.com/cinar/indicator/helper" ) func TestAbs(t *testing.T) { - input := []int{-10, 20, -4, -5} - expected := []int{10, 20, 4, 5} + input := helper.SliceToChan([]int{-10, 20, -4, -5}) + expected := helper.SliceToChan([]int{10, 20, 4, 5}) - actual := helper.ChanToSlice(helper.Abs(helper.SliceToChan(input))) + actual := helper.Abs(input) - 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/helper/apply_test.go b/helper/apply_test.go index aaebeb5..80a0294 100644 --- a/helper/apply_test.go +++ b/helper/apply_test.go @@ -5,21 +5,21 @@ package helper_test import ( - "reflect" "testing" "github.com/cinar/indicator/helper" ) func TestApply(t *testing.T) { - input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} - expected := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20} + input := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) + expected := helper.SliceToChan([]int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}) - actual := helper.ChanToSlice(helper.Apply(helper.SliceToChan(input), func(n int) int { + actual := helper.Apply(input, func(n int) int { return n * 2 - })) + }) - 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/helper/change.go b/helper/change.go index 836d214..eaad685 100644 --- a/helper/change.go +++ b/helper/change.go @@ -13,6 +13,7 @@ package helper // fmt.Println(helper.ChanToSlice(output)) // [4, 3, 3, -3, -7, -1, 2, 3] func Change[T Number](c <-chan T, before int) <-chan T { cs := Duplicate(c, 2) + cs[0] = Buffered(cs[0], before) cs[1] = Skip(cs[1], before) return Subtract(cs[1], cs[0]) diff --git a/helper/change_percent.go b/helper/change_percent.go index d52ea32..8a1b942 100644 --- a/helper/change_percent.go +++ b/helper/change_percent.go @@ -14,5 +14,6 @@ package helper // fmt.Println(helper.ChanToSlice(actual)) // [400, 150, 60, -60, -87.5, -50, 200, 300] func ChangePercent[T Number](c <-chan T, before int) <-chan T { cs := Duplicate(c, 2) + cs[1] = Buffered(cs[1], before) return MultiplyBy(Divide(Change(cs[0], before), cs[1]), 100) } diff --git a/helper/change_percent_test.go b/helper/change_percent_test.go index 86dc18e..6bfd3f1 100644 --- a/helper/change_percent_test.go +++ b/helper/change_percent_test.go @@ -5,19 +5,19 @@ package helper_test import ( - "reflect" "testing" "github.com/cinar/indicator/helper" ) func TestChangePercent(t *testing.T) { - input := []float64{1, 2, 5, 5, 8, 2, 1, 1, 3, 4} - expected := []float64{400, 150, 60, -60, -87.5, -50, 200, 300} + input := helper.SliceToChan([]float64{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) + expected := helper.SliceToChan([]float64{400, 150, 60, -60, -87.5, -50, 200, 300}) - actual := helper.ChanToSlice(helper.ChangePercent(helper.SliceToChan(input), 2)) + actual := helper.ChangePercent(input, 2) - 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/helper/change_test.go b/helper/change_test.go index c18f484..c6d0f4c 100644 --- a/helper/change_test.go +++ b/helper/change_test.go @@ -5,19 +5,19 @@ package helper_test import ( - "reflect" "testing" "github.com/cinar/indicator/helper" ) func TestChange(t *testing.T) { - input := []int{1, 2, 5, 5, 8, 2, 1, 1, 3, 4} - expected := []int{4, 3, 3, -3, -7, -1, 2, 3} + input := helper.SliceToChan([]int{1, 2, 5, 5, 8, 2, 1, 1, 3, 4}) + expected := helper.SliceToChan([]int{4, 3, 3, -3, -7, -1, 2, 3}) - actual := helper.ChanToSlice(helper.Change(helper.SliceToChan(input), 2)) + actual := helper.Change(input, 2) - 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/helper/slice_to_chan.go b/helper/slice_to_chan.go index 1532e6c..5b7dfe2 100644 --- a/helper/slice_to_chan.go +++ b/helper/slice_to_chan.go @@ -15,13 +15,15 @@ package helper // fmt.Println(<- c) // 6 // fmt.Println(<- c) // 8 func SliceToChan[T any](slice []T) <-chan T { - c := make(chan T, len(slice)) + c := make(chan T) - for _, n := range slice { - c <- n - } + go func() { + defer close(c) - close(c) + for _, n := range slice { + c <- n + } + }() return c } diff --git a/strategy/README.md b/strategy/README.md index e56ce11..87a4a51 100644 --- a/strategy/README.md +++ b/strategy/README.md @@ -24,6 +24,7 @@ The information provided on this project is strictly for informational purposes ## Index +- [Constants](<#constants>) - [func ActionsToAnnotations\(ac \<\-chan Action\) \<\-chan string](<#ActionsToAnnotations>) - [func ComputeWithOutcome\(s Strategy, c \<\-chan \*asset.Snapshot\) \(\<\-chan Action, \<\-chan float64\)](<#ComputeWithOutcome>) - [func NormalizeActions\(ac \<\-chan Action\) \<\-chan Action](<#NormalizeActions>) @@ -54,9 +55,28 @@ The information provided on this project is strictly for informational purposes - [func \(b \*BuyAndHoldStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#BuyAndHoldStrategy.Compute>) - [func \(b \*BuyAndHoldStrategy\) Name\(\) string](<#BuyAndHoldStrategy.Name>) - [func \(b \*BuyAndHoldStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#BuyAndHoldStrategy.Report>) +- [type DemaStrategy](<#DemaStrategy>) + - [func NewDemaStrategy\(\) \*DemaStrategy](<#NewDemaStrategy>) + - [func \(d \*DemaStrategy\) Compute\(c \<\-chan \*asset.Snapshot\) \<\-chan Action](<#DemaStrategy.Compute>) + - [func \(\*DemaStrategy\) Name\(\) string](<#DemaStrategy.Name>) + - [func \(d \*DemaStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#DemaStrategy.Report>) - [type Strategy](<#Strategy>) +## Constants + + + +```go +const ( + // DefaultDemaStrategyPeriod1 is the first DEMA period. + DefaultDemaStrategyPeriod1 = 5 + + // DefaultDemaStrategyPeriod2 is the second DEMA period. + DefaultDemaStrategyPeriod2 = 35 +) +``` + ## func [ActionsToAnnotations]() @@ -375,6 +395,61 @@ func (b *BuyAndHoldStrategy) Report(c <-chan *asset.Snapshot) *helper.Report Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + +## type [DemaStrategy]() + +DemaStrategy represents the configuration parameters for calculating the DEMA strategy. A bullish cross occurs when DEMA with 5 days period moves above DEMA with 35 days period. A bearish cross occurs when DEMA with 35 days period moves above DEMA With 5 days period. + +```go +type DemaStrategy struct { + Strategy + + // Dema1 represents the configuration parameters for + // calculating the first DEMA. + Dema1 *trend.Dema[float64] + + // Dema2 represents the configuration parameters for + // calculating the second DEMA. + Dema2 *trend.Dema[float64] +} +``` + + +### func [NewDemaStrategy]() + +```go +func NewDemaStrategy() *DemaStrategy +``` + +NewDemaStrategy function initializes a new DEMA strategy instance with the default parameters. + + +### func \(\*DemaStrategy\) [Compute]() + +```go +func (d *DemaStrategy) Compute(c <-chan *asset.Snapshot) <-chan Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*DemaStrategy\) [Name]() + +```go +func (*DemaStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*DemaStrategy\) [Report]() + +```go +func (d *DemaStrategy) Report(c <-chan *asset.Snapshot) *helper.Report +``` + +Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + ## type [Strategy]() diff --git a/strategy/backtest.go b/strategy/backtest.go index 8d2a603..5ec5847 100644 --- a/strategy/backtest.go +++ b/strategy/backtest.go @@ -111,8 +111,9 @@ func (b *Backtest) allStrategies() []Strategy { return []Strategy{ NewApoStrategy(), NewAroonStrategy(), - NewBuyAndHoldStrategy(), NewBopStrategy(), + NewBuyAndHoldStrategy(), + NewDemaStrategy(), } } diff --git a/strategy/dema_strategy.go b/strategy/dema_strategy.go new file mode 100644 index 0000000..7d91d75 --- /dev/null +++ b/strategy/dema_strategy.go @@ -0,0 +1,128 @@ +// Copyright (c) 2021-2023 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package strategy + +import ( + "fmt" + + "github.com/cinar/indicator/asset" + "github.com/cinar/indicator/helper" + "github.com/cinar/indicator/trend" +) + +const ( + // DefaultDemaStrategyPeriod1 is the first DEMA period. + DefaultDemaStrategyPeriod1 = 5 + + // DefaultDemaStrategyPeriod2 is the second DEMA period. + DefaultDemaStrategyPeriod2 = 35 +) + +// DemaStrategy represents the configuration parameters for calculating the DEMA strategy. +// A bullish cross occurs when DEMA with 5 days period moves above DEMA with 35 days period. +// A bearish cross occurs when DEMA with 35 days period moves above DEMA With 5 days period. +type DemaStrategy struct { + Strategy + + // Dema1 represents the configuration parameters for + // calculating the first DEMA. + Dema1 *trend.Dema[float64] + + // Dema2 represents the configuration parameters for + // calculating the second DEMA. + Dema2 *trend.Dema[float64] +} + +// NewDemaStrategy function initializes a new DEMA strategy instance +// with the default parameters. +func NewDemaStrategy() *DemaStrategy { + dema1 := trend.NewDema[float64]() + dema1.Ema1.Period = DefaultDemaStrategyPeriod1 + dema1.Ema2.Period = DefaultDemaStrategyPeriod1 + + dema2 := trend.NewDema[float64]() + dema2.Ema1.Period = DefaultDemaStrategyPeriod2 + dema2.Ema2.Period = DefaultDemaStrategyPeriod2 + + return &DemaStrategy{ + Dema1: dema1, + Dema2: dema2, + } +} + +// Name returns the name of the strategy. +func (*DemaStrategy) Name() string { + return "DEMA Strategy" +} + +// Compute processes the provided asset snapshots and generates a +// stream of actionable recommendations. +func (d *DemaStrategy) Compute(c <-chan *asset.Snapshot) <-chan Action { + closings := helper.Duplicate(asset.SnapshotsAsClosings(c), 2) + + demas1 := d.Dema1.Compute(closings[0]) + demas1 = helper.Shift(demas1, d.Dema1.IdlePeriod(), 0) + + demas2 := d.Dema2.Compute(closings[1]) + demas2 = helper.Shift(demas2, d.Dema2.IdlePeriod(), 0) + + actions := NormalizeActions(helper.Operate(demas1, demas2, func(dema1, dema2 float64) Action { + if dema1 > dema2 { + return Buy + } + + if dema2 > dema1 { + return Sell + } + + return Hold + })) + + // DEMA starts only after the a full periods for each EMA used. + actions = helper.Skip(actions, d.Dema2.IdlePeriod()) + actions = helper.Shift(actions, d.Dema2.IdlePeriod(), Hold) + + return actions +} + +// Report processes the provided asset snapshots and generates a +// report annotated with the recommended actions. +func (d *DemaStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> closings[0] -> demas1 + // closings[1] -> demas2 + // closings[2] -> closings + // snapshots[2] -> actions -> annotations + // -> outcomes + // + snapshots := helper.Duplicate(c, 3) + + dates := asset.SnapshotsAsDates(snapshots[0]) + closings := helper.Duplicate(asset.SnapshotsAsClosings(snapshots[1]), 3) + + demas1 := d.Dema1.Compute(closings[0]) + demas1 = helper.Shift(demas1, d.Dema1.IdlePeriod(), 0) + + demas2 := d.Dema2.Compute(closings[1]) + demas2 = helper.Shift(demas2, d.Dema2.IdlePeriod(), 0) + + actions, outcomes := ComputeWithOutcome(d, snapshots[2]) + annotations := ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(d.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closings[2])) + report.AddColumn(helper.NewNumericReportColumn(fmt.Sprintf("Dema %d-day", d.Dema1.Ema1.Period), demas1), 1) + report.AddColumn(helper.NewNumericReportColumn(fmt.Sprintf("Dema %d-day", d.Dema2.Ema1.Period), demas2), 1) + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} diff --git a/strategy/dema_strategy_test.go b/strategy/dema_strategy_test.go new file mode 100644 index 0000000..88b94bb --- /dev/null +++ b/strategy/dema_strategy_test.go @@ -0,0 +1,68 @@ +// Copyright (c) 2021-2023 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package strategy_test + +import ( + "os" + "testing" + + "github.com/cinar/indicator/asset" + "github.com/cinar/indicator/helper" + "github.com/cinar/indicator/strategy" +) + +func TestDemaStrategy(t *testing.T) { + type Result struct { + Action strategy.Action + Outcome float64 + } + + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/repository/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + results, err := helper.ReadFromCsvFile[Result]("testdata/dema_strategy.csv", true) + if err != nil { + t.Fatal(err) + } + + dema := strategy.NewDemaStrategy() + actions, outcomes := strategy.ComputeWithOutcome(dema, snapshots) + + outcomes = helper.RoundDigits(outcomes, 2) + + for result := range results { + action := <-actions + outcome := <-outcomes + + if action != result.Action { + t.Fatalf("actual %v expected %v", action, result.Action) + } + + if outcome != result.Outcome { + t.Fatalf("actual %v expected %v", outcome, result.Outcome) + } + } +} + +func TestDemaStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/repository/brk-b.csv", true) + if err != nil { + t.Fatal(err) + } + + dema := strategy.NewDemaStrategy() + + report := dema.Report(snapshots) + + fileName := "dema_strategy.html" + defer os.Remove(fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} diff --git a/strategy/testdata/dema_strategy.csv b/strategy/testdata/dema_strategy.csv new file mode 100644 index 0000000..dce333c --- /dev/null +++ b/strategy/testdata/dema_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 +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 +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 +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 +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.01 +0,0 +0,-0.01 +0,0.01 +0,0.02 +0,0.02 +0,0.01 +0,0 +0,-0.01 +0,0 +0,0.01 +0,0.01 +0,-0 +0,-0 +0,-0 +0,0 +0,0 +0,0.01 +0,0.02 +0,0.02 +0,0.02 +0,0 +0,-0.01 +0,-0.01 +0,-0.01 +0,-0 +0,-0.01 +0,0 +0,0.02 +0,0.02 +0,0.03 +0,0.04 +0,0.04 +0,0.04 +0,0.03 +0,0.04 +0,0.04 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.04 +0,0.04 +0,0.03 +0,0.04 +0,0.03 +0,0.04 +0,0.06 +0,0.06 +0,0.06 +0,0.06 +0,0.06 +0,0.06 +0,0.06 +0,0.07 +0,0.06 +0,0.06 +0,0.07 +0,0.07 +0,0.06 +0,0.07 +0,0.07 +0,0.08 +0,0.08 +0,0.08 +0,0.08 +0,0.08 +0,0.09 +0,0.09 +0,0.09 +0,0.1 +0,0.08 +0,0.12 +0,0.13 +0,0.11 +0,0.1 +0,0.11 +0,0.11 +0,0.1 +0,0.1 +0,0.09 +0,0.09 +0,0.09 +0,0.09 +0,0.1 +0,0.1 +0,0.1 +0,0.1 +0,0.11 +0,0.12 +0,0.11 +0,0.12 +0,0.12 +0,0.12 +0,0.12 +0,0.12 +0,0.13 +0,0.14 +0,0.14 +0,0.14 +0,0.14 +0,0.15 +0,0.15 +0,0.14 +0,0.12 +0,0.11 +0,0.12 +0,0.11 +0,0.11 +0,0.11 +0,0.08 +0,0.08 +0,0.06 +0,0.06 +0,0.07 +0,0.07 +0,0.07 +0,0.08 +0,0.08 +0,0.07 +-1,0.07 +1,0.07 +0,0.07 +-1,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +0,0.05 +1,0.05 diff --git a/trend/README.md b/trend/README.md index 4f606bb..75921f3 100644 --- a/trend/README.md +++ b/trend/README.md @@ -37,6 +37,10 @@ The information provided on this project is strictly for informational purposes - [type Bop](<#Bop>) - [func NewBop\[T helper.Number\]\(\) \*Bop\[T\]](<#NewBop>) - [func \(\*Bop\[T\]\) Compute\(opening, high, low, closing \<\-chan T\) \<\-chan T](<#Bop[T].Compute>) +- [type Dema](<#Dema>) + - [func NewDema\[T helper.Number\]\(\) \*Dema\[T\]](<#NewDema>) + - [func \(d \*Dema\[T\]\) Compute\(c \<\-chan T\) \<\-chan T](<#Dema[T].Compute>) + - [func \(d \*Dema\[T\]\) IdlePeriod\(\) int](<#Dema[T].IdlePeriod>) - [type Ema](<#Ema>) - [func NewEma\[T helper.Number\]\(\) \*Ema\[T\]](<#NewEma>) - [func \(ema \*Ema\[T\]\) Compute\(c \<\-chan T\) \<\-chan T](<#Ema[T].Compute>) @@ -277,6 +281,64 @@ func (*Bop[T]) Compute(opening, high, low, closing <-chan T) <-chan T Compute processes a channel of open, high, low, and close values, computing the BOP for each entry. + +## type [Dema]() + +Dema represents the parameters for calculating the Double Exponential Moving Average \(DEMA\). A bullish cross occurs when DEMA with 5 days period moves above DEMA with 35 days period. A bearish cross occurs when DEMA with 35 days period moves above DEMA With 5 days period. + +``` +DEMA = (2 * EMA1(values)) - EMA2(EMA1(values)) +``` + +Example: + +``` +dema := trend.NewDema[float64]() +dema.Ema1.Period = 10 +dema.Ema2.Period = 16 + +result := dema.Compute(input) +``` + +```go +type Dema[T helper.Number] struct { + // Ema1 represents the configuration parameters for + // calculating the first EMA. + Ema1 *Ema[T] + + // Ema2 represents the configuration parameters for + // calculating the second EMA. + Ema2 *Ema[T] +} +``` + + +### func [NewDema]() + +```go +func NewDema[T helper.Number]() *Dema[T] +``` + +NewDema function initializes a new DEMA instance with the default parameters. + + +### func \(\*Dema\[T\]\) [Compute]() + +```go +func (d *Dema[T]) Compute(c <-chan T) <-chan T +``` + +Compute function takes a channel of numbers and computes the DEMA over the specified period. + + +### func \(\*Dema\[T\]\) [IdlePeriod]() + +```go +func (d *Dema[T]) IdlePeriod() int +``` + +IdlePeriod is the initial period that DEMA won't yield any results. + ## type [Ema]() diff --git a/trend/dema.go b/trend/dema.go new file mode 100644 index 0000000..6fd08e2 --- /dev/null +++ b/trend/dema.go @@ -0,0 +1,56 @@ +// Copyright (c) 2021-2023 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend + +import "github.com/cinar/indicator/helper" + +// Dema represents the parameters for calculating the Double Exponential Moving Average (DEMA). +// A bullish cross occurs when DEMA with 5 days period moves above DEMA with 35 days period. +// A bearish cross occurs when DEMA with 35 days period moves above DEMA With 5 days period. +// +// DEMA = (2 * EMA1(values)) - EMA2(EMA1(values)) +// +// Example: +// +// dema := trend.NewDema[float64]() +// dema.Ema1.Period = 10 +// dema.Ema2.Period = 16 +// +// result := dema.Compute(input) +type Dema[T helper.Number] struct { + // Ema1 represents the configuration parameters for + // calculating the first EMA. + Ema1 *Ema[T] + + // Ema2 represents the configuration parameters for + // calculating the second EMA. + Ema2 *Ema[T] +} + +// NewDema function initializes a new DEMA instance +// with the default parameters. +func NewDema[T helper.Number]() *Dema[T] { + return &Dema[T]{ + Ema1: NewEma[T](), + Ema2: NewEma[T](), + } +} + +// Compute function takes a channel of numbers and computes the DEMA +// over the specified period. +func (d *Dema[T]) Compute(c <-chan T) <-chan T { + ema1 := helper.Duplicate(d.Ema1.Compute(c), 2) + ema2 := d.Ema2.Compute(ema1[1]) + + doubleEma1 := helper.MultiplyBy(ema1[0], 2) + doubleEma1 = helper.Buffered(doubleEma1, d.Ema2.Period) + + return helper.Subtract(doubleEma1, ema2) +} + +// IdlePeriod is the initial period that DEMA won't yield any results. +func (d *Dema[T]) IdlePeriod() int { + return d.Ema1.Period + d.Ema2.Period - 2 +} diff --git a/trend/dema_test.go b/trend/dema_test.go new file mode 100644 index 0000000..07bfc33 --- /dev/null +++ b/trend/dema_test.go @@ -0,0 +1,39 @@ +// Copyright (c) 2021-2023 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend_test + +import ( + "testing" + + "github.com/cinar/indicator/helper" + "github.com/cinar/indicator/trend" +) + +func TestDema(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, + 22.15, 22.39, 22.38, 22.61, 23.36, 24.05, 23.75, 23.83, 23.95, 23.63, + 22.27, 22.19, 22.08, 22.17, 22.18, 22.13, 22.23, 22.43, 22.24, 22.29, + 23.82, 23.87, 23.65, 23.19, 23.10, 23.33, 22.68, 23.10, 22.40, 22.17, + }) + + expected := helper.SliceToChan([]float64{ + 22.51, 22.7, 22.88, 23.01, 23.06, 23.08, 23.16, 23.11, 23.15, 23.05, + 22.92, 22.81, 22.74, 22.66, 22.63, 22.74, 22.97, 23.12, 23.27, 23.43, + 23.52, 23.34, + }) + + dema := trend.NewDema[float64]() + actual := dema.Compute(input) + + actual = helper.RoundDigits(actual, 2) + + err := helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +}