diff --git a/README.md b/README.md index e141b44c..68c635d0 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ The following list of strategies are currently supported by this package: - [Double Exponential Moving Average (DEMA) Strategy](strategy/trend/README.md#DemaStrategy) - [Envelope Strategy](strategy/trend/README.md#EnvelopeStrategy) - [Golden Cross Strategy](strategy/trend/README.md#GoldenCrossStrategy) +- [Hull Moving Average (HMA) Strategy](strategy/trend/README.md#HmaStrategy) - [Kaufman's Adaptive Moving Average (KAMA) Strategy](strategy/trend/README.md#KamaStrategy) - [Moving Average Convergence Divergence (MACD) Strategy](strategy/trend/README.md#MacdStrategy) - [Qstick Strategy](strategy/trend/README.md#QstickStrategy) diff --git a/strategy/trend/README.md b/strategy/trend/README.md index 83575f56..246563c6 100644 --- a/strategy/trend/README.md +++ b/strategy/trend/README.md @@ -74,6 +74,12 @@ The information provided on this project is strictly for informational purposes - [func \(t \*GoldenCrossStrategy\) Compute\(c \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#GoldenCrossStrategy.Compute>) - [func \(\*GoldenCrossStrategy\) Name\(\) string](<#GoldenCrossStrategy.Name>) - [func \(t \*GoldenCrossStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#GoldenCrossStrategy.Report>) +- [type HmaStrategy](<#HmaStrategy>) + - [func NewHmaStrategy\(\) \*HmaStrategy](<#NewHmaStrategy>) + - [func NewHmaStrategyWith\(period int\) \*HmaStrategy](<#NewHmaStrategyWith>) + - [func \(h \*HmaStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#HmaStrategy.Compute>) + - [func \(h \*HmaStrategy\) Name\(\) string](<#HmaStrategy.Name>) + - [func \(h \*HmaStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#HmaStrategy.Report>) - [type KamaStrategy](<#KamaStrategy>) - [func NewKamaStrategy\(\) \*KamaStrategy](<#NewKamaStrategy>) - [func NewKamaStrategyWith\(erPeriod, fastScPeriod, slowScPeriod int\) \*KamaStrategy](<#NewKamaStrategyWith>) @@ -218,6 +224,15 @@ const ( ) ``` + + +```go +const ( + // DefaultHmaStrategyPeriod is the default period for the HMA strategy. + DefaultHmaStrategyPeriod = 9 +) +``` + ```go @@ -730,6 +745,63 @@ func (t *GoldenCrossStrategy) Report(c <-chan *asset.Snapshot) *helper.Report Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + +## type [HmaStrategy]() + +HmaStrategy represents the configuration parameters for calculating the HMA strategy. A closing price crossing above the HMA suggests a bullish trend, while crossing below the HMA indicates a bearish trend. + +```go +type HmaStrategy struct { + // Hma represents the configuration parameters for calculating the Hull Moving Average. + Hma *trend.Hma[float64] +} +``` + + +### func [NewHmaStrategy]() + +```go +func NewHmaStrategy() *HmaStrategy +``` + +NewHmaStrategy function initializes a new HMA strategy instance with the default parameters. + + +### func [NewHmaStrategyWith]() + +```go +func NewHmaStrategyWith(period int) *HmaStrategy +``` + +NewHmaStrategyWith function initializes a new HMA strategy instance with the given period. + + +### func \(\*HmaStrategy\) [Compute]() + +```go +func (h *HmaStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action +``` + +Compute processes the provided asset snapshots and generates a stream of actionable recommendations. + + +### func \(\*HmaStrategy\) [Name]() + +```go +func (h *HmaStrategy) Name() string +``` + +Name returns the name of the strategy. + + +### func \(\*HmaStrategy\) [Report]() + +```go +func (h *HmaStrategy) Report(c <-chan *asset.Snapshot) *helper.Report +``` + +Report processes the provided asset snapshots and generates a report annotated with the recommended actions. + ## type [KamaStrategy]() diff --git a/strategy/trend/hma_strategy.go b/strategy/trend/hma_strategy.go new file mode 100644 index 00000000..f29013cc --- /dev/null +++ b/strategy/trend/hma_strategy.go @@ -0,0 +1,99 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package trend + +import ( + "github.com/cinar/indicator/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/strategy" + "github.com/cinar/indicator/v2/trend" +) + +const ( + // DefaultHmaStrategyPeriod is the default period for the HMA strategy. + DefaultHmaStrategyPeriod = 9 +) + +// HmaStrategy represents the configuration parameters for calculating the HMA strategy. A closing price crossing +// above the HMA suggests a bullish trend, while crossing below the HMA indicates a bearish trend. +type HmaStrategy struct { + // Hma represents the configuration parameters for calculating the Hull Moving Average. + Hma *trend.Hma[float64] +} + +// NewHmaStrategy function initializes a new HMA strategy instance with the default parameters. +func NewHmaStrategy() *HmaStrategy { + return NewHmaStrategyWith(DefaultHmaStrategyPeriod) +} + +// NewHmaStrategyWith function initializes a new HMA strategy instance with the given period. +func NewHmaStrategyWith(period int) *HmaStrategy { + return &HmaStrategy{ + Hma: trend.NewHmaWithPeriod[float64](period), + } +} + +// Name returns the name of the strategy. +func (h *HmaStrategy) Name() string { + return h.Hma.String() + " Strategy" +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (h *HmaStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + closingsSplice := helper.Duplicate(asset.SnapshotsAsClosings(snapshots), 2) + closingsSplice[1] = helper.Skip(closingsSplice[1], h.Hma.IdlePeriod()) + + hmas := h.Hma.Compute(closingsSplice[0]) + + actions := helper.Operate(hmas, closingsSplice[1], func(hma, closing float64) strategy.Action { + if closing > hma { + return strategy.Buy + } + + if closing < hma { + return strategy.Sell + } + + return strategy.Hold + }) + + // HMA starts only after a full period. + actions = helper.Shift(actions, h.Hma.IdlePeriod(), strategy.Hold) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (h *HmaStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> closings[0] -> closings + // closings[1] -> hma + // snapshots[2] -> actions -> annotations + // -> outcomes + // + snapshotsSplice := helper.Duplicate(c, 3) + + dates := asset.SnapshotsAsDates(snapshotsSplice[0]) + closingsSplice := helper.Duplicate(asset.SnapshotsAsClosings(snapshotsSplice[1]), 2) + + hmas := h.Hma.Compute(closingsSplice[0]) + hmas = helper.Shift(hmas, h.Hma.IdlePeriod(), 0) + + actions, outcomes := strategy.ComputeWithOutcome(h, snapshotsSplice[2]) + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(h.Name(), dates) + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closingsSplice[1])) + report.AddColumn(helper.NewNumericReportColumn("HMA", hmas)) + report.AddColumn(helper.NewAnnotationReportColumn(annotations)) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 1) + + return report +} diff --git a/strategy/trend/hma_strategy_test.go b/strategy/trend/hma_strategy_test.go new file mode 100644 index 00000000..212b855c --- /dev/null +++ b/strategy/trend/hma_strategy_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2021-2026 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/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/strategy" + "github.com/cinar/indicator/v2/strategy/trend" +) + +func TestHmaStrategy(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv") + if err != nil { + t.Fatal(err) + } + + results, err := helper.ReadFromCsvFile[strategy.Result]("testdata/hma_strategy.csv") + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + h := trend.NewHmaStrategy() + actual := h.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestHmaStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv") + if err != nil { + t.Fatal(err) + } + + h := trend.NewHmaStrategy() + + report := h.Report(snapshots) + + fileName := "hma_strategy.html" + defer helper.Remove(t, fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +} + +func TestHmaStrategyHold(t *testing.T) { + snapshots := helper.SliceToChan([]*asset.Snapshot{ + {Close: 10}, + {Close: 10}, + {Close: 10}, + }) + + h := trend.NewHmaStrategyWith(2) + actions := h.Compute(snapshots) + + for action := range actions { + if action != strategy.Hold { + t.Fatalf("expected Hold action, got %v", action) + } + } +} + diff --git a/strategy/trend/testdata/hma_strategy.csv b/strategy/trend/testdata/hma_strategy.csv new file mode 100644 index 00000000..0b32f52b --- /dev/null +++ b/strategy/trend/testdata/hma_strategy.csv @@ -0,0 +1,252 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +-1 +1 +1 +1 +1 +1 +1 +1 +-1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +-1 +-1 +-1 +1 +-1 +-1 +1 +-1 +-1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +-1 +-1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +1 +1 +-1 +1 +1 +-1 +1 +1 +1 +1 +1 +1 +-1 +1 +1 +-1 +1 +-1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +1 +1 +1 +-1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +1 +1 +1 +1 +1 +1 +-1 +-1 +-1 +-1 +-1 +-1 diff --git a/strategy/trend/trend.go b/strategy/trend/trend.go index ad6c95e0..d42e195f 100644 --- a/strategy/trend/trend.go +++ b/strategy/trend/trend.go @@ -31,6 +31,7 @@ func AllStrategies() []strategy.Strategy { NewCfoStrategy(), NewDemaStrategy(), NewGoldenCrossStrategy(), + NewHmaStrategy(), NewKamaStrategy(), NewKdjStrategy(), NewMacdStrategy(), diff --git a/strategy/trend/trend_test.go b/strategy/trend/trend_test.go new file mode 100644 index 00000000..3438b724 --- /dev/null +++ b/strategy/trend/trend_test.go @@ -0,0 +1,18 @@ +// Copyright (c) 2021-2026 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/v2/strategy/trend" +) + +func TestAllStrategies(t *testing.T) { + strategies := trend.AllStrategies() + if len(strategies) != 19 { + t.Fatalf("expected 19 strategies, got %d", len(strategies)) + } +}