Skip to content

Commit

Permalink
Double Exponential Moving Average (DEMA) added.
Browse files Browse the repository at this point in the history
  • Loading branch information
cinar committed Dec 26, 2023
1 parent e085aa4 commit d1edec2
Show file tree
Hide file tree
Showing 16 changed files with 718 additions and 32 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions helper/abs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
14 changes: 7 additions & 7 deletions helper/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions helper/change.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
1 change: 1 addition & 0 deletions helper/change_percent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
12 changes: 6 additions & 6 deletions helper/change_percent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
12 changes: 6 additions & 6 deletions helper/change_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
12 changes: 7 additions & 5 deletions helper/slice_to_chan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
75 changes: 75 additions & 0 deletions strategy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>)
Expand Down Expand Up @@ -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

<a name="DefaultDemaStrategyPeriod1"></a>

```go
const (
// DefaultDemaStrategyPeriod1 is the first DEMA period.
DefaultDemaStrategyPeriod1 = 5

// DefaultDemaStrategyPeriod2 is the second DEMA period.
DefaultDemaStrategyPeriod2 = 35
)
```

<a name="ActionsToAnnotations"></a>
## func [ActionsToAnnotations](<https://github.com/cinar/indicator/blob/v2/strategy/action.go#L46>)

Expand Down Expand Up @@ -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.

<a name="DemaStrategy"></a>
## type [DemaStrategy](<https://github.com/cinar/indicator/blob/v2/strategy/dema_strategy.go#L26-L36>)

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]
}
```

<a name="NewDemaStrategy"></a>
### func [NewDemaStrategy](<https://github.com/cinar/indicator/blob/v2/strategy/dema_strategy.go#L40>)

```go
func NewDemaStrategy() *DemaStrategy
```

NewDemaStrategy function initializes a new DEMA strategy instance with the default parameters.

<a name="DemaStrategy.Compute"></a>
### func \(\*DemaStrategy\) [Compute](<https://github.com/cinar/indicator/blob/v2/strategy/dema_strategy.go#L62>)

```go
func (d *DemaStrategy) Compute(c <-chan *asset.Snapshot) <-chan Action
```

Compute processes the provided asset snapshots and generates a stream of actionable recommendations.

<a name="DemaStrategy.Name"></a>
### func \(\*DemaStrategy\) [Name](<https://github.com/cinar/indicator/blob/v2/strategy/dema_strategy.go#L56>)

```go
func (*DemaStrategy) Name() string
```

Name returns the name of the strategy.

<a name="DemaStrategy.Report"></a>
### func \(\*DemaStrategy\) [Report](<https://github.com/cinar/indicator/blob/v2/strategy/dema_strategy.go#L92>)

```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.

<a name="Strategy"></a>
## type [Strategy](<https://github.com/cinar/indicator/blob/v2/strategy/strategy.go#L27-L38>)

Expand Down
3 changes: 2 additions & 1 deletion strategy/backtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ func (b *Backtest) allStrategies() []Strategy {
return []Strategy{
NewApoStrategy(),
NewAroonStrategy(),
NewBuyAndHoldStrategy(),
NewBopStrategy(),
NewBuyAndHoldStrategy(),
NewDemaStrategy(),
}
}

Expand Down
128 changes: 128 additions & 0 deletions strategy/dema_strategy.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit d1edec2

Please sign in to comment.