Skip to content

Commit ac76f5c

Browse files
authored
Choppiness Index (CHOP) added. (#333)
# Describe Request Choppiness Index (CHOP) added. Fixed #306 # Change Type New indicator.
1 parent cb9560c commit ac76f5c

4 files changed

Lines changed: 505 additions & 0 deletions

File tree

volatility/README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ The information provided on this project is strictly for informational purposes
4848
- [func NewChandelierExit\[T helper.Number\]\(\) \*ChandelierExit\[T\]](<#NewChandelierExit>)
4949
- [func \(c \*ChandelierExit\[T\]\) Compute\(highs, lows, closings \<\-chan T\) \(\<\-chan T, \<\-chan T\)](<#ChandelierExit[T].Compute>)
5050
- [func \(c \*ChandelierExit\[T\]\) IdlePeriod\(\) int](<#ChandelierExit[T].IdlePeriod>)
51+
- [type Chop](<#Chop>)
52+
- [func NewChop\[T helper.Number\]\(\) \*Chop\[T\]](<#NewChop>)
53+
- [func NewChopWithPeriod\[T helper.Number\]\(period int\) \*Chop\[T\]](<#NewChopWithPeriod>)
54+
- [func \(c \*Chop\[T\]\) Compute\(highs, lows, closings \<\-chan T\) \<\-chan T](<#Chop[T].Compute>)
55+
- [func \(c \*Chop\[T\]\) IdlePeriod\(\) int](<#Chop[T].IdlePeriod>)
56+
- [func \(c \*Chop\[T\]\) String\(\) string](<#Chop[T].String>)
5157
- [type DonchianChannel](<#DonchianChannel>)
5258
- [func NewDonchianChannel\[T helper.Number\]\(\) \*DonchianChannel\[T\]](<#NewDonchianChannel>)
5359
- [func NewDonchianChannelWithPeriod\[T helper.Number\]\(period int\) \*DonchianChannel\[T\]](<#NewDonchianChannelWithPeriod>)
@@ -139,6 +145,15 @@ const (
139145
)
140146
```
141147

148+
<a name="DefaultChopPeriod"></a>
149+
150+
```go
151+
const (
152+
// DefaultChopPeriod is the default period for the Choppiness Index (CHOP).
153+
DefaultChopPeriod = 14
154+
)
155+
```
156+
142157
<a name="DefaultDonchianChannelPeriod"></a>
143158

144159
```go
@@ -474,6 +489,67 @@ func (c *ChandelierExit[T]) IdlePeriod() int
474489

475490
IdlePeriod is the initial period that Chandelier Exit won't yield any results.
476491

492+
<a name="Chop"></a>
493+
## type [Chop](<https://github.com/cinar/indicator/blob/master/volatility/chop.go#L24-L27>)
494+
495+
Chop represents the configuration parameters for calculating the Choppiness Index \(CHOP\). It is a technical analysis indicator that measures the market's trendiness or choppiness.
496+
497+
```
498+
CHOP = 100 * LOG10( SUM(ATR(1), n) / (MAX(High, n) - MIN(Low, n)) ) / LOG10(n)
499+
```
500+
501+
```go
502+
type Chop[T helper.Number] struct {
503+
// Period is the period for the CHOP.
504+
Period int
505+
}
506+
```
507+
508+
<a name="NewChop"></a>
509+
### func [NewChop](<https://github.com/cinar/indicator/blob/master/volatility/chop.go#L30>)
510+
511+
```go
512+
func NewChop[T helper.Number]() *Chop[T]
513+
```
514+
515+
NewChop function initializes a new CHOP instance with the default parameters.
516+
517+
<a name="NewChopWithPeriod"></a>
518+
### func [NewChopWithPeriod](<https://github.com/cinar/indicator/blob/master/volatility/chop.go#L35>)
519+
520+
```go
521+
func NewChopWithPeriod[T helper.Number](period int) *Chop[T]
522+
```
523+
524+
NewChopWithPeriod function initializes a new CHOP instance with the given period.
525+
526+
<a name="Chop[T].Compute"></a>
527+
### func \(\*Chop\[T\]\) [Compute](<https://github.com/cinar/indicator/blob/master/volatility/chop.go#L42>)
528+
529+
```go
530+
func (c *Chop[T]) Compute(highs, lows, closings <-chan T) <-chan T
531+
```
532+
533+
Compute function takes channels of highs, lows, and closings, and computes the CHOP over the specified period.
534+
535+
<a name="Chop[T].IdlePeriod"></a>
536+
### func \(\*Chop\[T\]\) [IdlePeriod](<https://github.com/cinar/indicator/blob/master/volatility/chop.go#L81>)
537+
538+
```go
539+
func (c *Chop[T]) IdlePeriod() int
540+
```
541+
542+
IdlePeriod is the initial period that CHOP won't yield any results.
543+
544+
<a name="Chop[T].String"></a>
545+
### func \(\*Chop\[T\]\) [String](<https://github.com/cinar/indicator/blob/master/volatility/chop.go#L86>)
546+
547+
```go
548+
func (c *Chop[T]) String() string
549+
```
550+
551+
String function returns a string representation of the CHOP.
552+
477553
<a name="DonchianChannel"></a>
478554
## type [DonchianChannel](<https://github.com/cinar/indicator/blob/master/volatility/donchian_channel.go#L30-L36>)
479555

volatility/chop.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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 volatility
6+
7+
import (
8+
"fmt"
9+
"math"
10+
11+
"github.com/cinar/indicator/v2/helper"
12+
"github.com/cinar/indicator/v2/trend"
13+
)
14+
15+
const (
16+
// DefaultChopPeriod is the default period for the Choppiness Index (CHOP).
17+
DefaultChopPeriod = 14
18+
)
19+
20+
// Chop represents the configuration parameters for calculating the Choppiness Index (CHOP).
21+
// It is a technical analysis indicator that measures the market's trendiness or choppiness.
22+
//
23+
// CHOP = 100 * LOG10( SUM(ATR(1), n) / (MAX(High, n) - MIN(Low, n)) ) / LOG10(n)
24+
type Chop[T helper.Number] struct {
25+
// Period is the period for the CHOP.
26+
Period int
27+
}
28+
29+
// NewChop function initializes a new CHOP instance with the default parameters.
30+
func NewChop[T helper.Number]() *Chop[T] {
31+
return NewChopWithPeriod[T](DefaultChopPeriod)
32+
}
33+
34+
// NewChopWithPeriod function initializes a new CHOP instance with the given period.
35+
func NewChopWithPeriod[T helper.Number](period int) *Chop[T] {
36+
return &Chop[T]{
37+
Period: period,
38+
}
39+
}
40+
41+
// Compute function takes channels of highs, lows, and closings, and computes the CHOP over the specified period.
42+
func (c *Chop[T]) Compute(highs, lows, closings <-chan T) <-chan T {
43+
highs2 := helper.Duplicate(highs, 2)
44+
lows2 := helper.Duplicate(lows, 2)
45+
46+
// TR calculation
47+
// Use previous closing by skipping highs and lows by one.
48+
trHighs := helper.Skip(highs2[0], 1)
49+
trLows := helper.Skip(lows2[0], 1)
50+
51+
tr := helper.Operate3(trHighs, trLows, closings, func(high, low, closing T) T {
52+
return T(math.Max(float64(high-low), math.Max(float64(high-closing), float64(closing-low))))
53+
})
54+
55+
sumTr := trend.NewMovingSumWithPeriod[T](c.Period).Compute(tr)
56+
57+
// MAX(High, n) and MIN(Low, n)
58+
// They should be aligned with the TR bars (starting from bar 1).
59+
rangeHighs := helper.Skip(highs2[1], 1)
60+
rangeLows := helper.Skip(lows2[1], 1)
61+
62+
maxHigh := trend.NewMovingMaxWithPeriod[T](c.Period).Compute(rangeHighs)
63+
minLow := trend.NewMovingMinWithPeriod[T](c.Period).Compute(rangeLows)
64+
65+
log10n := math.Log10(float64(c.Period))
66+
67+
chop := helper.Operate3(sumTr, maxHigh, minLow, func(sum, max, min T) T {
68+
diff := float64(max - min)
69+
if diff == 0 {
70+
return 0
71+
}
72+
73+
val := 100 * math.Log10(float64(sum)/diff) / log10n
74+
return T(val)
75+
})
76+
77+
return chop
78+
}
79+
80+
// IdlePeriod is the initial period that CHOP won't yield any results.
81+
func (c *Chop[T]) IdlePeriod() int {
82+
return c.Period
83+
}
84+
85+
// String function returns a string representation of the CHOP.
86+
func (c *Chop[T]) String() string {
87+
return fmt.Sprintf("CHOP(%d)", c.Period)
88+
}

volatility/chop_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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 volatility_test
6+
7+
import (
8+
"testing"
9+
10+
"github.com/cinar/indicator/v2/helper"
11+
"github.com/cinar/indicator/v2/volatility"
12+
)
13+
14+
func TestChop(t *testing.T) {
15+
type Data struct {
16+
High float64 `header:"High"`
17+
Low float64 `header:"Low"`
18+
Close float64 `header:"Close"`
19+
Expected float64 `header:"Expected"`
20+
}
21+
22+
input, err := helper.ReadFromCsvFile[Data]("testdata/chop.csv")
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
27+
inputs := helper.Duplicate(input, 4)
28+
29+
highs := helper.Map(inputs[0], func(d *Data) float64 { return d.High })
30+
lows := helper.Map(inputs[1], func(d *Data) float64 { return d.Low })
31+
closings := helper.Map(inputs[2], func(d *Data) float64 { return d.Close })
32+
33+
chop := volatility.NewChop[float64]()
34+
actuals := chop.Compute(highs, lows, closings)
35+
actuals = helper.RoundDigits(actuals, 6)
36+
37+
inputs[3] = helper.Skip(inputs[3], chop.IdlePeriod())
38+
39+
for data := range inputs[3] {
40+
actual, ok := <-actuals
41+
if !ok {
42+
t.Fatal("actuals channel closed early")
43+
}
44+
if actual != data.Expected {
45+
t.Fatalf("actual %v expected %v for High=%v Low=%v Close=%v", actual, data.Expected, data.High, data.Low, data.Close)
46+
}
47+
}
48+
49+
if _, ok := <-actuals; ok {
50+
t.Fatal("actuals channel should be closed")
51+
}
52+
53+
if chop.String() != "CHOP(14)" {
54+
t.Fatalf("expected CHOP(14) but got %s", chop.String())
55+
}
56+
57+
if chop.IdlePeriod() != 14 {
58+
t.Fatalf("expected 14 but got %d", chop.IdlePeriod())
59+
}
60+
}
61+
62+
func TestNewChop(t *testing.T) {
63+
chop := volatility.NewChop[float64]()
64+
if chop == nil {
65+
t.Fatal("NewChop should not be nil")
66+
}
67+
}
68+
69+
func TestNewChopWithPeriod(t *testing.T) {
70+
chop := volatility.NewChopWithPeriod[float64](10)
71+
if chop.Period != 10 {
72+
t.Fatalf("expected 10 but got %d", chop.Period)
73+
}
74+
}
75+
76+
func TestChopDiffZero(t *testing.T) {
77+
highs := helper.SliceToChan([]float64{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10})
78+
lows := helper.SliceToChan([]float64{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10})
79+
closings := helper.SliceToChan([]float64{10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10})
80+
81+
chop := volatility.NewChopWithPeriod[float64](2)
82+
actuals := chop.Compute(highs, lows, closings)
83+
84+
for actual := range actuals {
85+
if actual != 0 {
86+
t.Fatalf("expected 0 but got %v", actual)
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)