Skip to content

Commit a3e7cbd

Browse files
authored
Add momentum indicator ROC (#277)
# Describe Request Add momentum indicator ROC Fixed #57 # Change Type What is the type of this change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a Rate of Change (ROC) indicator with a configurable period (default: 9), streaming computation and an automatic warm-up (idle) period before outputs appear. * ROC exposes a readable string representation showing the configured period. * **Tests** * Added unit tests validating ROC on simple sequences and CSV reference data. * Tests cover rounding/alignment, default-period fallback, and string representation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 8252ac7 commit a3e7cbd

File tree

3 files changed

+163
-0
lines changed

3 files changed

+163
-0
lines changed

trend/roc.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package trend
6+
7+
import (
8+
"fmt"
9+
10+
"github.com/cinar/indicator/v2/helper"
11+
)
12+
13+
const (
14+
// DefaultRocPeriod is the default ROC period.
15+
DefaultRocPeriod = 9
16+
)
17+
18+
// Roc represents the configuration parameters for calculating the Rate Of Change (ROC) indicator.
19+
//
20+
// ROC = (Current Price - Price n periods ago) / Price n periods ago
21+
type Roc[T helper.Float] struct {
22+
// Time period.
23+
Period int
24+
}
25+
26+
// NewRoc function initializes a new Roc instance with the default parameters.
27+
func NewRoc[T helper.Float]() *Roc[T] {
28+
return NewRocWithPeriod[T](DefaultRocPeriod)
29+
}
30+
31+
// NewRocWithPeriod function initializes a new Roc instance with the given parameters.
32+
func NewRocWithPeriod[T helper.Float](period int) *Roc[T] {
33+
if period <= 0 {
34+
period = DefaultRocPeriod
35+
}
36+
return &Roc[T]{
37+
Period: period,
38+
}
39+
}
40+
41+
// Compute function takes a channel of numbers and computes the ROC and the signal line.
42+
func (r *Roc[T]) Compute(values <-chan T) <-chan T {
43+
window := helper.NewRing[T](r.Period)
44+
45+
rocs := helper.Map(values, func(value T) T {
46+
var result T
47+
48+
if window.IsFull() {
49+
p, ok := window.Get()
50+
if ok && p != 0 {
51+
result = (value - p) / p
52+
}
53+
}
54+
window.Put(value)
55+
56+
return result
57+
})
58+
59+
rocs = helper.Skip(rocs, r.IdlePeriod())
60+
61+
return rocs
62+
}
63+
64+
// IdlePeriod is the initial period that ROC won't yield any results.
65+
func (r *Roc[T]) IdlePeriod() int {
66+
return r.Period
67+
}
68+
69+
// String is the string representation of the ROC.
70+
func (r *Roc[T]) String() string {
71+
return fmt.Sprintf("ROC(%d)", r.Period)
72+
}

trend/roc_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package trend
2+
3+
import (
4+
"testing"
5+
6+
"github.com/cinar/indicator/v2/helper"
7+
)
8+
9+
func TestRocSimple(t *testing.T) {
10+
closing := helper.SliceToChan([]float64{2, 4, 6, 8, 9, 7})
11+
expected := helper.SliceToChan([]float64{0, 0, 0, (8 - 2) / 2, (9.0 - 4) / 4, (7.0 - 6) / 6})
12+
13+
roc := NewRoc[float64]()
14+
roc.Period = 3
15+
16+
actual := roc.Compute(closing)
17+
expected = helper.Skip(expected, roc.IdlePeriod())
18+
19+
err := helper.CheckEquals(actual, expected)
20+
if err != nil {
21+
t.Fatal(err)
22+
}
23+
}
24+
25+
func TestRocTestdata(t *testing.T) {
26+
type Data struct {
27+
Close float64
28+
Roc float64
29+
}
30+
31+
input, err := helper.ReadFromCsvFile[Data]("testdata/roc.csv", true)
32+
if err != nil {
33+
t.Fatal(err)
34+
}
35+
36+
inputs := helper.Duplicate(input, 2)
37+
closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close })
38+
expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Roc })
39+
40+
roc := NewRoc[float64]()
41+
42+
actual := roc.Compute(closing)
43+
actual = helper.RoundDigits(actual, 2)
44+
45+
expected = helper.Skip(expected, roc.IdlePeriod())
46+
47+
err = helper.CheckEquals(actual, expected)
48+
if err != nil {
49+
t.Fatal(err)
50+
}
51+
}
52+
53+
func TestRocFallbackPeriod(t *testing.T) {
54+
roc := NewRocWithPeriod[float64](-17)
55+
56+
if roc.Period != DefaultRocPeriod {
57+
t.Fatal("expected period to be fallback to default value")
58+
}
59+
}
60+
61+
func TestRocToStringAndIdlePeriod(t *testing.T) {
62+
roc := NewRocWithPeriod[float64](0)
63+
if roc.IdlePeriod() != DefaultRocPeriod {
64+
t.Fatalf("unexpected IdlePeriod: %d", roc.IdlePeriod())
65+
}
66+
roc.Period = 3
67+
if s := roc.String(); s != "ROC(3)" {
68+
t.Fatalf("unexpected String(): %s", s)
69+
}
70+
}

trend/testdata/roc.csv

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
Close,Roc
2+
11045.27,0
3+
11167.32,0
4+
11008.61,0
5+
11151.83,0
6+
10926.77,0
7+
10868.12,0
8+
10520.32,0
9+
10380.43,0
10+
10785.14,0
11+
10748.26,-0.03
12+
10896.91,-0.02
13+
10782.95,-0.02
14+
10620.16,-0.05
15+
10625.83,-0.03
16+
10510.95,-0.03
17+
10444.37,-0.01
18+
10068.01,-0.03
19+
10193.39,-0.05
20+
10066.57,-0.06
21+
10043.75,-0.08

0 commit comments

Comments
 (0)