Skip to content

Commit e86d913

Browse files
authored
Implement On-Balance Volume (OBV) Strategy. Fixes #342 (#355)
## Describe Request Implements the On-Balance Volume (OBV) trading strategy using the existing volume.Obv indicator. This PR also includes: - A fix for a calculation bug in the OBV indicator where it was comparing the current closing price to the previous OBV value instead of the previous closing price. - Updated test data for the OBV indicator. - 100% test coverage for the new strategy. - Updated documentation. Fixed #342 Signed-off-by: Onur Cinar <onur.cinar@gmail.com>
1 parent 2f2e411 commit e86d913

11 files changed

Lines changed: 890 additions & 262 deletions

File tree

asset/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,7 @@ type TiingoEndOfDay struct {
563563
Close float64 `json:"close"`
564564

565565
// Volume is the total volume.
566-
Volume int64 `json:"volume"`
566+
Volume float64 `json:"volume"`
567567

568568
// AdjOpen is the adjusted opening price.
569569
AdjOpen float64 `json:"adjOpen"`
@@ -578,7 +578,7 @@ type TiingoEndOfDay struct {
578578
AdjClose float64 `json:"adjClose"`
579579

580580
// AdjVolume is the adjusted total volume.
581-
AdjVolume int64 `json:"adjVolume"`
581+
AdjVolume float64 `json:"adjVolume"`
582582

583583
// Dividend is the dividend paid out.
584584
Dividend float64 `json:"divCash"`

strategy/momentum/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ The information provided on this project is strictly for informational purposes
5555
- [func \(t \*TripleRsiStrategy\) IdlePeriod\(\) int](<#TripleRsiStrategy.IdlePeriod>)
5656
- [func \(t \*TripleRsiStrategy\) Name\(\) string](<#TripleRsiStrategy.Name>)
5757
- [func \(t \*TripleRsiStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#TripleRsiStrategy.Report>)
58+
- [type WilliamsRStrategy](<#WilliamsRStrategy>)
59+
- [func NewWilliamsRStrategy\(\) \*WilliamsRStrategy](<#NewWilliamsRStrategy>)
60+
- [func NewWilliamsRStrategyWith\(buyAt, sellAt float64\) \*WilliamsRStrategy](<#NewWilliamsRStrategyWith>)
61+
- [func \(r \*WilliamsRStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#WilliamsRStrategy.Compute>)
62+
- [func \(r \*WilliamsRStrategy\) Name\(\) string](<#WilliamsRStrategy.Name>)
63+
- [func \(r \*WilliamsRStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#WilliamsRStrategy.Report>)
5864

5965

6066
## Constants
@@ -107,6 +113,18 @@ const (
107113
)
108114
```
109115

116+
<a name="DefaultWilliamsRStrategyBuyAt"></a>
117+
118+
```go
119+
const (
120+
// DefaultWilliamsRStrategyBuyAt defines the default Williams R level at which a Buy action is generated.
121+
DefaultWilliamsRStrategyBuyAt = -80.0
122+
123+
// DefaultWilliamsRStrategySellAt defines the default Williams R level at which a Sell action is generated.
124+
DefaultWilliamsRStrategySellAt = -20.0
125+
)
126+
```
127+
110128
<a name="AllStrategies"></a>
111129
## func [AllStrategies](<https://github.com/cinar/indicator/blob/master/strategy/momentum/momentum.go#L24>)
112130

@@ -425,4 +443,67 @@ func (t *TripleRsiStrategy) Report(c <-chan *asset.Snapshot) *helper.Report
425443

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

446+
<a name="WilliamsRStrategy"></a>
447+
## type [WilliamsRStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/williams_r_strategy.go#L25-L34>)
448+
449+
WilliamsRStrategy represents the configuration parameters for calculating the Williams R strategy.
450+
451+
```go
452+
type WilliamsRStrategy struct {
453+
// WilliamsR represents the configuration parameters for calculating the Williams %R.
454+
WilliamsR *momentum.WilliamsR[float64]
455+
456+
// BuyAt defines the Williams R level at which a Buy action is generated.
457+
BuyAt float64
458+
459+
// SellAt defines the Williams R level at which a Sell action is generated.
460+
SellAt float64
461+
}
462+
```
463+
464+
<a name="NewWilliamsRStrategy"></a>
465+
### func [NewWilliamsRStrategy](<https://github.com/cinar/indicator/blob/master/strategy/momentum/williams_r_strategy.go#L37>)
466+
467+
```go
468+
func NewWilliamsRStrategy() *WilliamsRStrategy
469+
```
470+
471+
NewWilliamsRStrategy function initializes a new Williams R strategy instance with the default parameters.
472+
473+
<a name="NewWilliamsRStrategyWith"></a>
474+
### func [NewWilliamsRStrategyWith](<https://github.com/cinar/indicator/blob/master/strategy/momentum/williams_r_strategy.go#L45>)
475+
476+
```go
477+
func NewWilliamsRStrategyWith(buyAt, sellAt float64) *WilliamsRStrategy
478+
```
479+
480+
NewWilliamsRStrategyWith function initializes a new Williams R strategy instance with the given parameters.
481+
482+
<a name="WilliamsRStrategy.Compute"></a>
483+
### func \(\*WilliamsRStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/momentum/williams_r_strategy.go#L59>)
484+
485+
```go
486+
func (r *WilliamsRStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
487+
```
488+
489+
Compute processes the provided asset snapshots and generates a stream of actionable recommendations.
490+
491+
<a name="WilliamsRStrategy.Name"></a>
492+
### func \(\*WilliamsRStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/momentum/williams_r_strategy.go#L54>)
493+
494+
```go
495+
func (r *WilliamsRStrategy) Name() string
496+
```
497+
498+
Name returns the name of the strategy.
499+
500+
<a name="WilliamsRStrategy.Report"></a>
501+
### func \(\*WilliamsRStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/momentum/williams_r_strategy.go#L87>)
502+
503+
```go
504+
func (r *WilliamsRStrategy) Report(c <-chan *asset.Snapshot) *helper.Report
505+
```
506+
507+
Report processes the provided asset snapshots and generates a report annotated with the recommended actions.
508+
428509
Generated by [gomarkdoc](<https://github.com/princjef/gomarkdoc>)

strategy/volume/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ The information provided on this project is strictly for informational purposes
5656
- [func \(n \*NegativeVolumeIndexStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#NegativeVolumeIndexStrategy.Compute>)
5757
- [func \(n \*NegativeVolumeIndexStrategy\) Name\(\) string](<#NegativeVolumeIndexStrategy.Name>)
5858
- [func \(n \*NegativeVolumeIndexStrategy\) Report\(c \<\-chan \*asset.Snapshot\) \*helper.Report](<#NegativeVolumeIndexStrategy.Report>)
59+
- [type ObvStrategy](<#ObvStrategy>)
60+
- [func NewObvStrategy\(\) \*ObvStrategy](<#NewObvStrategy>)
61+
- [func NewObvStrategyWith\(period int\) \*ObvStrategy](<#NewObvStrategyWith>)
62+
- [func \(s \*ObvStrategy\) Compute\(snapshots \<\-chan \*asset.Snapshot\) \<\-chan strategy.Action](<#ObvStrategy.Compute>)
63+
- [func \(s \*ObvStrategy\) Name\(\) string](<#ObvStrategy.Name>)
64+
- [func \(s \*ObvStrategy\) Report\(snapshots \<\-chan \*asset.Snapshot\) \*helper.Report](<#ObvStrategy.Report>)
5965
- [type PercentBandMFIStrategy](<#PercentBandMFIStrategy>)
6066
- [func NewPercentBandMFIStrategy\(\) \*PercentBandMFIStrategy](<#NewPercentBandMFIStrategy>)
6167
- [func NewPercentBandMFIStrategyWith\(sellPercentBAt, buyPercentBAt, sellMfiAt, buyMfiAt float64\) \*PercentBandMFIStrategy](<#NewPercentBandMFIStrategyWith>)
@@ -111,6 +117,15 @@ const (
111117
)
112118
```
113119

120+
<a name="DefaultObvStrategyPeriod"></a>
121+
122+
```go
123+
const (
124+
// DefaultObvStrategyPeriod is the default OBV strategy period.
125+
DefaultObvStrategyPeriod = 10
126+
)
127+
```
128+
114129
<a name="AllStrategies"></a>
115130
## func [AllStrategies](<https://github.com/cinar/indicator/blob/master/strategy/volume/volume.go#L26>)
116131

@@ -414,6 +429,66 @@ func (n *NegativeVolumeIndexStrategy) Report(c <-chan *asset.Snapshot) *helper.R
414429

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

432+
<a name="ObvStrategy"></a>
433+
## type [ObvStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/obv_strategy.go#L24-L30>)
434+
435+
ObvStrategy represents the configuration parameters for calculating the On\-Balance Volume \(OBV\) strategy. Recommends a Buy action when OBV crosses above its SMA, and recommends a Sell action when OBV crosses below its SMA.
436+
437+
```go
438+
type ObvStrategy struct {
439+
// Obv is the OBV indicator instance.
440+
Obv *volume.Obv[float64]
441+
442+
// Sma is the SMA indicator instance.
443+
Sma *trend.Sma[float64]
444+
}
445+
```
446+
447+
<a name="NewObvStrategy"></a>
448+
### func [NewObvStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/obv_strategy.go#L33>)
449+
450+
```go
451+
func NewObvStrategy() *ObvStrategy
452+
```
453+
454+
NewObvStrategy function initializes a new OBV strategy instance with the default parameters.
455+
456+
<a name="NewObvStrategyWith"></a>
457+
### func [NewObvStrategyWith](<https://github.com/cinar/indicator/blob/master/strategy/volume/obv_strategy.go#L40>)
458+
459+
```go
460+
func NewObvStrategyWith(period int) *ObvStrategy
461+
```
462+
463+
NewObvStrategyWith function initializes a new OBV strategy instance with the given period.
464+
465+
<a name="ObvStrategy.Compute"></a>
466+
### func \(\*ObvStrategy\) [Compute](<https://github.com/cinar/indicator/blob/master/strategy/volume/obv_strategy.go#L53>)
467+
468+
```go
469+
func (s *ObvStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action
470+
```
471+
472+
Compute function processes the provided asset snapshots and generates a stream of actionable recommendations.
473+
474+
<a name="ObvStrategy.Name"></a>
475+
### func \(\*ObvStrategy\) [Name](<https://github.com/cinar/indicator/blob/master/strategy/volume/obv_strategy.go#L48>)
476+
477+
```go
478+
func (s *ObvStrategy) Name() string
479+
```
480+
481+
Name function returns the name of the strategy.
482+
483+
<a name="ObvStrategy.Report"></a>
484+
### func \(\*ObvStrategy\) [Report](<https://github.com/cinar/indicator/blob/master/strategy/volume/obv_strategy.go#L86>)
485+
486+
```go
487+
func (s *ObvStrategy) Report(snapshots <-chan *asset.Snapshot) *helper.Report
488+
```
489+
490+
Report function processes the provided asset snapshots and generates a report annotated with the recommended actions.
491+
417492
<a name="PercentBandMFIStrategy"></a>
418493
## type [PercentBandMFIStrategy](<https://github.com/cinar/indicator/blob/master/strategy/volume/percent_b_and_mfi_strategy.go#L34-L52>)
419494

strategy/volume/obv_strategy.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) 2021-2026 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package volume
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/cinar/indicator/v2/asset"
11+
"github.com/cinar/indicator/v2/helper"
12+
"github.com/cinar/indicator/v2/strategy"
13+
"github.com/cinar/indicator/v2/trend"
14+
"github.com/cinar/indicator/v2/volume"
15+
)
16+
17+
const (
18+
// DefaultObvStrategyPeriod is the default OBV strategy period.
19+
DefaultObvStrategyPeriod = 10
20+
)
21+
22+
// ObvStrategy represents the configuration parameters for calculating the On-Balance Volume (OBV) strategy.
23+
// Recommends a Buy action when OBV crosses above its SMA, and recommends a Sell action when OBV crosses below its SMA.
24+
type ObvStrategy struct {
25+
// Obv is the OBV indicator instance.
26+
Obv *volume.Obv[float64]
27+
28+
// Sma is the SMA indicator instance.
29+
Sma *trend.Sma[float64]
30+
}
31+
32+
// NewObvStrategy function initializes a new OBV strategy instance with the default parameters.
33+
func NewObvStrategy() *ObvStrategy {
34+
return NewObvStrategyWith(
35+
DefaultObvStrategyPeriod,
36+
)
37+
}
38+
39+
// NewObvStrategyWith function initializes a new OBV strategy instance with the given period.
40+
func NewObvStrategyWith(period int) *ObvStrategy {
41+
return &ObvStrategy{
42+
Obv: volume.NewObv[float64](),
43+
Sma: trend.NewSmaWithPeriod[float64](period),
44+
}
45+
}
46+
47+
// Name function returns the name of the strategy.
48+
func (s *ObvStrategy) Name() string {
49+
return fmt.Sprintf("OBV Strategy (%d)", s.Sma.Period)
50+
}
51+
52+
// Compute function processes the provided asset snapshots and generates a stream of actionable recommendations.
53+
func (s *ObvStrategy) Compute(snapshots <-chan *asset.Snapshot) <-chan strategy.Action {
54+
snapshotsSplice := helper.Duplicate(snapshots, 2)
55+
56+
closings := asset.SnapshotsAsClosings(snapshotsSplice[0])
57+
volumes := asset.SnapshotsAsVolumes(snapshotsSplice[1])
58+
59+
obvValues := s.Obv.Compute(closings, volumes)
60+
obvSplice := helper.Duplicate(obvValues, 2)
61+
62+
smaValues := s.Sma.Compute(obvSplice[0])
63+
64+
// Align OBV with SMA
65+
obvValuesAligned := helper.Skip(obvSplice[1], s.Sma.IdlePeriod())
66+
67+
actions := helper.Operate(obvValuesAligned, smaValues, func(obv, sma float64) strategy.Action {
68+
if obv > sma {
69+
return strategy.Buy
70+
}
71+
72+
if obv < sma {
73+
return strategy.Sell
74+
}
75+
76+
return strategy.Hold
77+
})
78+
79+
// OBV starts after its idle period (0), but SMA starts after its idle period.
80+
actions = helper.Shift(actions, s.Sma.IdlePeriod(), strategy.Hold)
81+
82+
return actions
83+
}
84+
85+
// Report function processes the provided asset snapshots and generates a report annotated with the recommended actions.
86+
func (s *ObvStrategy) Report(snapshots <-chan *asset.Snapshot) *helper.Report {
87+
//
88+
// snapshots[0] -> dates
89+
// snapshots[1] -> closings (for report)
90+
// snapshots[2] -> closings (for obv)
91+
// snapshots[3] -> volumes (for obv)
92+
// snapshots[4] -> actions / outcomes
93+
//
94+
snapshotsSplice := helper.Duplicate(snapshots, 5)
95+
96+
dates := helper.Skip(
97+
asset.SnapshotsAsDates(snapshotsSplice[0]),
98+
s.Sma.IdlePeriod(),
99+
)
100+
101+
closingsForReport := helper.Duplicate(
102+
helper.Skip(
103+
asset.SnapshotsAsClosings(snapshotsSplice[1]),
104+
s.Sma.IdlePeriod(),
105+
),
106+
2,
107+
)
108+
109+
closingsForObv := asset.SnapshotsAsClosings(snapshotsSplice[2])
110+
volumesForObv := asset.SnapshotsAsVolumes(snapshotsSplice[3])
111+
112+
obvValues := s.Obv.Compute(closingsForObv, volumesForObv)
113+
obvSplice := helper.Duplicate(obvValues, 2)
114+
115+
smaValues := s.Sma.Compute(obvSplice[0])
116+
obvValuesAligned := helper.Skip(obvSplice[1], s.Sma.IdlePeriod())
117+
118+
actions, outcomes := strategy.ComputeWithOutcome(s, snapshotsSplice[4])
119+
actions = helper.Skip(actions, s.Sma.IdlePeriod())
120+
outcomes = helper.Skip(outcomes, s.Sma.IdlePeriod())
121+
122+
annotations := strategy.ActionsToAnnotations(actions)
123+
outcomes = helper.MultiplyBy(outcomes, 100)
124+
125+
report := helper.NewReport(s.Name(), dates)
126+
report.AddChart()
127+
report.AddChart()
128+
129+
report.AddColumn(helper.NewNumericReportColumn("Close", closingsForReport[0]))
130+
131+
report.AddColumn(helper.NewNumericReportColumn("Close", closingsForReport[1]), 1)
132+
report.AddColumn(helper.NewNumericReportColumn("OBV", obvValuesAligned), 1)
133+
report.AddColumn(helper.NewNumericReportColumn("SMA", smaValues), 1)
134+
135+
report.AddColumn(helper.NewAnnotationReportColumn(annotations), 0, 1)
136+
137+
report.AddColumn(helper.NewNumericReportColumn("Outcome", outcomes), 2)
138+
139+
return report
140+
}

0 commit comments

Comments
 (0)