Skip to content

Commit

Permalink
Envelope trend indicator and Envelope strategy are added. (#233)
Browse files Browse the repository at this point in the history
# Describe Request

Envelope trend indicator and Envelope strategy are added.

Fixed #228 

# Change Type

New feature.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a new `Envelope` indicator and multiple new strategies,
enhancing the library's capabilities.
- Added dedicated test data in CSV format for better validation of
indicators and strategies.

- **Documentation**
- Added detailed documentation for the new `Envelope` type and its
methods.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
cinar authored Oct 12, 2024
1 parent ef7b65c commit 68c34da
Show file tree
Hide file tree
Showing 12 changed files with 1,262 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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 (CCI)](trend/README.md#type-cci)
- [Envelope](trend/README.md#type-envelope)
- [Hull Moving Average (HMA)](trend/README.md#type-hma)
- [Double Exponential Moving Average (DEMA)](trend/README.md#type-dema)
- [Exponential Moving Average (EMA)](trend/README.md#type-ema)
Expand Down
8 changes: 4 additions & 4 deletions strategy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The information provided on this project is strictly for informational purposes
- [type Action](<#Action>)
- [func \(a Action\) Annotation\(\) string](<#Action.Annotation>)
- [type AndStrategy](<#AndStrategy>)
- [func NewAndStrategy\(name string\) \*AndStrategy](<#NewAndStrategy>)
- [func NewAndStrategy\(name string, strategies ...Strategy\) \*AndStrategy](<#NewAndStrategy>)
- [func \(a \*AndStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#AndStrategy.Compute>)
- [func \(a \*AndStrategy\) Name\(\) string](<#AndStrategy.Name>)
- [func \(a \*AndStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#AndStrategy.Report>)
Expand All @@ -51,7 +51,7 @@ The information provided on this project is strictly for informational purposes
- [func \(a \*MajorityStrategy\) Name\(\) string](<#MajorityStrategy.Name>)
- [func \(a \*MajorityStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#MajorityStrategy.Report>)
- [type OrStrategy](<#OrStrategy>)
- [func NewOrStrategy\(name string\) \*OrStrategy](<#NewOrStrategy>)
- [func NewOrStrategy\(name string, strategies ...Strategy\) \*OrStrategy](<#NewOrStrategy>)
- [func \(a \*OrStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan Action](<#OrStrategy.Compute>)
- [func \(a \*OrStrategy\) Name\(\) string](<#OrStrategy.Name>)
- [func \(a \*OrStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#OrStrategy.Report>)
Expand Down Expand Up @@ -194,7 +194,7 @@ type AndStrategy struct {
### func [NewAndStrategy](<https://github.com/cinar/indicator/blob/master/strategy/and_strategy.go#L26>)

```go
func NewAndStrategy(name string) *AndStrategy
func NewAndStrategy(name string, strategies ...Strategy) *AndStrategy
```

NewAndStrategy function initializes an empty and strategies group with the given name.
Expand Down Expand Up @@ -347,7 +347,7 @@ type OrStrategy struct {
### func [NewOrStrategy](<https://github.com/cinar/indicator/blob/master/strategy/or_strategy.go#L23>)

```go
func NewOrStrategy(name string) *OrStrategy
func NewOrStrategy(name string, strategies ...Strategy) *OrStrategy
```

NewOrStrategy function initializes an empty or strategies group with the given name.
Expand Down
1 change: 1 addition & 0 deletions strategy/testdata/x
Original file line number Diff line number Diff line change
@@ -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 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 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
63 changes: 63 additions & 0 deletions strategy/trend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ The information provided on this project is strictly for informational purposes
- [func \(d \*DemaStrategy\) Compute\(c \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#DemaStrategy.Compute>)
- [func \(\*DemaStrategy\) Name\(\) string](<#DemaStrategy.Name>)
- [func \(d \*DemaStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#DemaStrategy.Report>)
- [type EnvelopeStrategy](<#EnvelopeStrategy>)
- [func NewEnvelopeStrategy\(\) \*EnvelopeStrategy](<#NewEnvelopeStrategy>)
- [func NewEnvelopeStrategyWith\(envelope \*trend.Envelope\[float64\]\) \*EnvelopeStrategy](<#NewEnvelopeStrategyWith>)
- [func \(e \*EnvelopeStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#EnvelopeStrategy.Compute>)
- [func \(e \*EnvelopeStrategy\) Name\(\) string](<#EnvelopeStrategy.Name>)
- [func \(e \*EnvelopeStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#EnvelopeStrategy.Report>)
- [type GoldenCrossStrategy](<#GoldenCrossStrategy>)
- [func NewGoldenCrossStrategy\(\) \*GoldenCrossStrategy](<#NewGoldenCrossStrategy>)
- [func NewGoldenCrossStrategyWith\(fastPeriod, slowPeriod int\) \*GoldenCrossStrategy](<#NewGoldenCrossStrategyWith>)
Expand Down Expand Up @@ -436,6 +442,63 @@ 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="EnvelopeStrategy"></a>
## type [EnvelopeStrategy](<https://github.com/cinar/indicator/blob/master/strategy/trend/envelope_strategy.go#L19-L22>)

EnvelopeStrategy represents the configuration parameters for calculating the Envelope strategy. When the closing is above the upper band suggests a Sell recommendation, and when the closing is below the lower band suggests a buy recommendation.

```go
type EnvelopeStrategy struct {
// Envelope is the envelope indicator instance.
Envelope *trend.Envelope[float64]
}
```

<a name="NewEnvelopeStrategy"></a>
### func [NewEnvelopeStrategy](<https://github.com/cinar/indicator/blob/master/strategy/trend/envelope_strategy.go#L25>)

```go
func NewEnvelopeStrategy() *EnvelopeStrategy
```

NewEnvelopeStrategy function initializes a new Envelope strategy with the default parameters.

<a name="NewEnvelopeStrategyWith"></a>
### func [NewEnvelopeStrategyWith](<https://github.com/cinar/indicator/blob/master/strategy/trend/envelope_strategy.go#L32>)

```go
func NewEnvelopeStrategyWith(envelope *trend.Envelope[float64]) *EnvelopeStrategy
```

NewEnvelopeStrategyWith function initializes a new Envelope strategy with the given Envelope instance.

<a name="EnvelopeStrategy.Compute"></a>
### func \(\*EnvelopeStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/trend/envelope_strategy.go#L44>)

```go
func (e *EnvelopeStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
```

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

<a name="EnvelopeStrategy.Name"></a>
### func \(\*EnvelopeStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/trend/envelope_strategy.go#L39>)

```go
func (e *EnvelopeStrategy) Name() string
```

Name returns the name of the strategy.

<a name="EnvelopeStrategy.Report"></a>
### func \(\*EnvelopeStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/trend/envelope_strategy.go#L76>)

```go
func (e *EnvelopeStrategy) Report(c <-chan *asset.Snapshot) *helper.Report
```

Report processes the provided asset snapshots and generates a report annotated with the recommended actions.

<a name="GoldenCrossStrategy"></a>
## type [GoldenCrossStrategy](<https://github.com/cinar/indicator/blob/master/strategy/trend/golden_cross_strategy.go#L27-L33>)

Expand Down
117 changes: 117 additions & 0 deletions strategy/trend/envelope_strategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2021-2024 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package trend

import (
"fmt"

"github.com/cinar/indicator/v2/asset"
"github.com/cinar/indicator/v2/helper"
"github.com/cinar/indicator/v2/strategy"
"github.com/cinar/indicator/v2/trend"
)

// EnvelopeStrategy represents the configuration parameters for calculating the Envelope strategy. When the closing
// is above the upper band suggests a Sell recommendation, and when the closing is below the lower band suggests a
// buy recommendation.
type EnvelopeStrategy struct {
// Envelope is the envelope indicator instance.
Envelope *trend.Envelope[float64]
}

// NewEnvelopeStrategy function initializes a new Envelope strategy with the default parameters.
func NewEnvelopeStrategy() *EnvelopeStrategy {
return NewEnvelopeStrategyWith(
trend.NewEnvelopeWithSma[float64](),
)
}

// NewEnvelopeStrategyWith function initializes a new Envelope strategy with the given Envelope instance.
func NewEnvelopeStrategyWith(envelope *trend.Envelope[float64]) *EnvelopeStrategy {
return &EnvelopeStrategy{
Envelope: envelope,
}
}

// Name returns the name of the strategy.
func (e *EnvelopeStrategy) Name() string {
return fmt.Sprintf("Envelope Strategy (%s)", e.Envelope.String())
}

// Compute processes the provided asset snapshots and generates a stream of actionable recommendations.
func (e *EnvelopeStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
closingsSplice := helper.Duplicate(
asset.SnapshotsAsClosings(snapshots),
2,
)

closingsSplice[1] = helper.Skip(closingsSplice[1], e.Envelope.IdlePeriod())

uppers, middles, lowers := e.Envelope.Compute(closingsSplice[0])
go helper.Drain(middles)

actions := helper.Operate3(uppers, lowers, closingsSplice[1], func(upper, lower, closing float64) strategy.Action {
// When the closing is below the lower band suggests a buy recommendation.
if closing < lower {
return strategy.Buy
}

// When the closing is above the upper band suggests a Sell recommendation.
if closing > upper {
return strategy.Sell
}

return strategy.Hold
})

// Envelope start only after a full period.
actions = helper.Shift(actions, e.Envelope.IdlePeriod(), strategy.Hold)

return actions
}

// Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
func (e *EnvelopeStrategy) Report(c <-chan *asset.Snapshot) *helper.Report {
//
// snapshots[0] -> dates
// snapshots[1] -> closings[0] -> closings
// closings[1] -> envelope -> upper
// -> middle
// -> lower
// snapshots[2] -> actions -> annotations
// -> outcomes
//
snapshotsSplice := helper.Duplicate(c, 3)

dates := helper.Skip(
asset.SnapshotsAsDates(snapshotsSplice[0]),
e.Envelope.IdlePeriod(),
)

closingsSplice := helper.Duplicate(asset.SnapshotsAsClosings(snapshotsSplice[1]), 2)
closingsSplice[0] = helper.Skip(closingsSplice[0], e.Envelope.IdlePeriod())

uppers, middles, lowers := e.Envelope.Compute(closingsSplice[1])

actions, outcomes := strategy.ComputeWithOutcome(e, snapshotsSplice[2])
actions = helper.Skip(actions, e.Envelope.IdlePeriod())
outcomes = helper.Skip(outcomes, e.Envelope.IdlePeriod())

annotations := strategy.ActionsToAnnotations(actions)
outcomes = helper.MultiplyBy(outcomes, 100)

report := helper.NewReport(e.Name(), dates)
report.AddChart()

report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[0]))
report.AddColumn(helper.NewNumericReportColumn("Upper", uppers))
report.AddColumn(helper.NewNumericReportColumn("Middle", middles))
report.AddColumn(helper.NewNumericReportColumn("Lower", lowers))
report.AddColumn(helper.NewAnnotationReportColumn(annotations))

report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 1)

return report
}
55 changes: 55 additions & 0 deletions strategy/trend/envelope_strategy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2021-2024 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package trend_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/trend"
)

func TestEnvelopeStrategy(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/envelope_strategy.csv", true)
if err != nil {
t.Fatal(err)
}

expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action })

envelope := trend.NewEnvelopeStrategy()
actual := envelope.Compute(snapshots)

err = helper.CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}

func TestEnvelopeStrategyReport(t *testing.T) {
snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv", true)
if err != nil {
t.Fatal(err)
}

envelope := trend.NewEnvelopeStrategy()
report := envelope.Report(snapshots)

fileName := "envelope_strategy.html"
defer os.Remove(fileName)

err = report.WriteToFile(fileName)
if err != nil {
t.Fatal(err)
}
}
Loading

0 comments on commit 68c34da

Please sign in to comment.