diff --git a/strategy/momentum/momentum.go b/strategy/momentum/momentum.go index 8a17e7b..e0caa70 100644 --- a/strategy/momentum/momentum.go +++ b/strategy/momentum/momentum.go @@ -28,5 +28,6 @@ func AllStrategies() []strategy.Strategy { NewRsiStrategy(), NewStochasticRsiStrategy(), NewTripleRsiStrategy(), + NewWilliamsRStrategy(), } } diff --git a/strategy/momentum/testdata/williams_r_strategy.csv b/strategy/momentum/testdata/williams_r_strategy.csv new file mode 100644 index 0000000..f4bb45b --- /dev/null +++ b/strategy/momentum/testdata/williams_r_strategy.csv @@ -0,0 +1,252 @@ +Action +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +0 +0 +0 +0 +0 +0 +0 +0 +0 +-1 +0 +-1 +0 +-1 +-1 +-1 +-1 +0 +0 +1 +0 +0 +0 +0 +0 +0 +1 +0 +0 +0 +0 +0 +-1 +0 +1 +0 +-1 +0 +0 +0 +0 +1 +1 +0 +0 +0 +0 +0 +0 +-1 +-1 +0 +0 +0 +0 +1 +0 +1 +0 +1 +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 +0 +-1 +-1 +-1 +0 +0 +1 +0 +0 +0 +0 +0 +0 +0 +0 +0 +-1 +0 +0 +0 +1 +1 +1 +0 +0 +0 +0 +0 +-1 +-1 +-1 +-1 +0 +-1 +-1 +-1 +-1 +-1 +-1 +0 +0 +0 +0 +0 +0 +-1 +-1 +-1 +-1 +0 +0 +-1 +-1 +-1 +0 +-1 +-1 +0 +-1 +0 +-1 +0 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +0 +-1 +-1 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +0 +0 +0 +0 +0 +-1 +-1 +-1 +0 +0 +0 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +0 +0 +1 +0 +1 +1 +1 +1 +1 +1 +1 +1 +0 +1 +0 +0 +0 +0 +0 +0 +1 +1 +1 +0 +0 +0 +1 +1 +0 +0 +0 +-1 +-1 +0 +0 +0 +0 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +-1 +0 diff --git a/strategy/momentum/williams_r_strategy.go b/strategy/momentum/williams_r_strategy.go new file mode 100644 index 0000000..f0266a3 --- /dev/null +++ b/strategy/momentum/williams_r_strategy.go @@ -0,0 +1,121 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package momentum + +import ( + "fmt" + + "github.com/cinar/indicator/v2/asset" + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/momentum" + "github.com/cinar/indicator/v2/strategy" +) + +const ( + // DefaultWilliamsRStrategyBuyAt defines the default Williams R level at which a Buy action is generated. + DefaultWilliamsRStrategyBuyAt = -80.0 + + // DefaultWilliamsRStrategySellAt defines the default Williams R level at which a Sell action is generated. + DefaultWilliamsRStrategySellAt = -20.0 +) + +// WilliamsRStrategy represents the configuration parameters for calculating the Williams R strategy. +type WilliamsRStrategy struct { + // WilliamsR represents the configuration parameters for calculating the Williams %R. + WilliamsR *momentum.WilliamsR[float64] + + // BuyAt defines the Williams R level at which a Buy action is generated. + BuyAt float64 + + // SellAt defines the Williams R level at which a Sell action is generated. + SellAt float64 +} + +// NewWilliamsRStrategy function initializes a new Williams R strategy instance with the default parameters. +func NewWilliamsRStrategy() *WilliamsRStrategy { + return NewWilliamsRStrategyWith( + DefaultWilliamsRStrategyBuyAt, + DefaultWilliamsRStrategySellAt, + ) +} + +// NewWilliamsRStrategyWith function initializes a new Williams R strategy instance with the given parameters. +func NewWilliamsRStrategyWith(buyAt, sellAt float64) *WilliamsRStrategy { + return &WilliamsRStrategy{ + WilliamsR: momentum.NewWilliamsR[float64](), + BuyAt: buyAt, + SellAt: sellAt, + } +} + +// Name returns the name of the strategy. +func (r *WilliamsRStrategy) Name() string { + return fmt.Sprintf("Williams R Strategy (%.0f,%.0f)", r.BuyAt, r.SellAt) +} + +// Compute processes the provided asset snapshots and generates a stream of actionable recommendations. +func (r *WilliamsRStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action { + snapshotsSplice := helper.Duplicate(snapshots, 3) + + highs := asset.SnapshotsAsHighs(snapshotsSplice[0]) + lows := asset.SnapshotsAsLows(snapshotsSplice[1]) + closings := asset.SnapshotsAsClosings(snapshotsSplice[2]) + + wr := r.WilliamsR.Compute(highs, lows, closings) + + actions := helper.Map(wr, func(value float64) strategy.Action { + if value <= r.BuyAt { + return strategy.Buy + } + + if value >= r.SellAt { + return strategy.Sell + } + + return strategy.Hold + }) + + // Williams R starts only after the idle period. + actions = helper.Shift(actions, r.WilliamsR.IdlePeriod(), strategy.Hold) + + return actions +} + +// Report processes the provided asset snapshots and generates a report annotated with the recommended actions. +func (r *WilliamsRStrategy) Report(c <-chan *asset.Snapshot) *helper.Report { + // + // snapshots[0] -> dates + // snapshots[1] -> Compute -> actions -> annotations + // snapshots[2] -> closings -> close + // snapshots[3] -> highs -| + // snapshots[4] -> lows -+-> WilliamsR.Compute -> wr + // snapshots[5] -> closings-| + // + snapshots := helper.Duplicate(c, 6) + + dates := asset.SnapshotsAsDates(snapshots[0]) + closings := asset.SnapshotsAsClosings(snapshots[2]) + highs := asset.SnapshotsAsHighs(snapshots[3]) + lows := asset.SnapshotsAsLows(snapshots[4]) + closingsForWR := asset.SnapshotsAsClosings(snapshots[5]) + + wr := helper.Shift(r.WilliamsR.Compute(highs, lows, closingsForWR), r.WilliamsR.IdlePeriod(), 0) + + actions, outcomes := strategy.ComputeWithOutcome(r, snapshots[1]) + annotations := strategy.ActionsToAnnotations(actions) + outcomes = helper.MultiplyBy(outcomes, 100) + + report := helper.NewReport(r.Name(), dates) + report.AddChart() + report.AddChart() + + report.AddColumn(helper.NewNumericReportColumn("Close", closings)) + report.AddColumn(helper.NewNumericReportColumn("Williams R", wr), 1) + report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1) + + report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2) + + return report +} diff --git a/strategy/momentum/williams_r_strategy_test.go b/strategy/momentum/williams_r_strategy_test.go new file mode 100644 index 0000000..9751e21 --- /dev/null +++ b/strategy/momentum/williams_r_strategy_test.go @@ -0,0 +1,55 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package momentum_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/momentum" +) + +func TestWilliamsRStrategy(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/williams_r_strategy.csv") + if err != nil { + t.Fatal(err) + } + + expected := helper.Map(results, func(r *strategy.Result) strategy.Action { return r.Action }) + + wr := momentum.NewWilliamsRStrategy() + actual := wr.Compute(snapshots) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestWilliamsRStrategyReport(t *testing.T) { + snapshots, err := helper.ReadFromCsvFile[asset.Snapshot]("testdata/brk-b.csv") + if err != nil { + t.Fatal(err) + } + + wr := momentum.NewWilliamsRStrategy() + + report := wr.Report(snapshots) + + fileName := "williams_r_strategy.html" + defer helper.Remove(t, fileName) + + err = report.WriteToFile(fileName) + if err != nil { + t.Fatal(err) + } +}