Skip to content

Commit 16f9654

Browse files
authored
add Keltner channel (ta.kc) (#54)
1 parent 2200f88 commit 16f9654

File tree

7 files changed

+262
-40
lines changed

7 files changed

+262
-40
lines changed

pine/ohlc_prop.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@ const (
2020
OHLCPropHLC3
2121
// OHLCPropTR is true range i.e. max(high - low, abs(high - close[1]), abs(low - close[1])).
2222
OHLCPropTR
23+
24+
// OHLCPropTR is true range i.e. na(highsrc[1])? highsrc-lowsrc : math.max(math.max(highsrc - lowsrc, math.abs(highsrc - closesrc[1])), math.abs(lowsrc - closesrc[1])).
25+
// If previous bar doesn't exist, it returns high - low
26+
OHLCPropTRHL
2327
)

pine/ohlcv_series_base.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -153,35 +153,39 @@ func (s *ohlcvBaseSeries) GetSeries(p OHLCProp) ValueSeries {
153153
if v == nil {
154154
break
155155
}
156-
var propVal float64
156+
var propVal *float64
157157
switch p {
158158
case OHLCPropClose:
159-
propVal = v.C
159+
propVal = NewFloat64(v.C)
160160
case OHLCPropOpen:
161-
propVal = v.O
161+
propVal = NewFloat64(v.O)
162162
case OHLCPropHigh:
163-
propVal = v.H
163+
propVal = NewFloat64(v.H)
164164
case OHLCPropLow:
165-
propVal = v.L
165+
propVal = NewFloat64(v.L)
166166
case OHLCPropVolume:
167-
propVal = v.V
168-
case OHLCPropTR:
167+
propVal = NewFloat64(v.V)
168+
case OHLCPropTR, OHLCPropTRHL:
169169
if v.prev != nil {
170170
p := v.prev
171-
propVal = math.Max(
172-
math.Abs(v.H-v.L),
173-
math.Max(
174-
math.Abs(v.H-p.C),
175-
math.Abs(v.L-p.C)))
176-
} else {
177-
propVal = math.Abs(v.H - v.L)
171+
v1 := math.Abs(v.H - v.L)
172+
v2 := math.Abs(v.H - p.C)
173+
v3 := math.Abs(v.L - p.C)
174+
v := math.Max(v1, math.Max(v2, v3))
175+
propVal = NewFloat64(v)
176+
}
177+
if p == OHLCPropTRHL && v.prev == nil {
178+
d := v.H - v.L
179+
propVal = &d
178180
}
179181
case OHLCPropHLC3:
180-
propVal = (v.H + v.L + v.C) / 3
182+
propVal = NewFloat64((v.H + v.L + v.C) / 3)
181183
default:
182184
continue
183185
}
184-
vs.Set(v.S, propVal)
186+
if propVal != nil {
187+
vs.Set(v.S, *propVal)
188+
}
185189
v = v.next
186190
}
187191

pine/ohlcv_series_test.go

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package pine
22

33
import (
4-
"math"
4+
"fmt"
55
"testing"
66
"time"
77
)
88

9-
func TestNewOHLCVSeries(t *testing.T) {
9+
func TestNewOHLCVGetSeries(t *testing.T) {
1010
start := time.Now()
1111
data := OHLCVTestData(start, 3, 5*60*1000)
1212

@@ -15,35 +15,21 @@ func TestNewOHLCVSeries(t *testing.T) {
1515
t.Fatal(err)
1616
}
1717

18-
tr1 := math.Abs(data[0].H - data[0].L)
19-
20-
tr2 := math.Max(
21-
math.Abs(data[1].H-data[1].L),
22-
math.Max(
23-
math.Abs(data[1].H-data[0].C),
24-
math.Abs(data[1].L-data[0].C)))
25-
26-
tr3 := math.Max(
27-
math.Abs(data[2].H-data[2].L),
28-
math.Max(
29-
math.Abs(data[2].H-data[1].C),
30-
math.Abs(data[2].L-data[1].C)))
31-
3218
testTable := []struct {
3319
prop []OHLCProp
3420
vals []float64
3521
}{
3622
{
37-
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR, OHLCPropHLC3},
38-
vals: []float64{data[0].C, data[0].H, data[0].L, data[0].O, tr1, (data[0].H + data[0].L + data[0].C) / 3},
23+
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropHLC3},
24+
vals: []float64{data[0].C, data[0].H, data[0].L, data[0].O, (data[0].H + data[0].L + data[0].C) / 3},
3925
},
4026
{
41-
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR},
42-
vals: []float64{data[1].C, data[1].H, data[1].L, data[1].O, tr2, (data[1].H + data[1].L + data[1].C) / 3},
27+
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropHLC3},
28+
vals: []float64{data[1].C, data[1].H, data[1].L, data[1].O, (data[1].H + data[1].L + data[1].C) / 3},
4329
},
4430
{
45-
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR},
46-
vals: []float64{data[2].C, data[2].H, data[2].L, data[2].O, tr3, (data[2].H + data[2].L + data[2].C) / 3},
31+
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropHLC3},
32+
vals: []float64{data[2].C, data[2].H, data[2].L, data[2].O, (data[2].H + data[2].L + data[2].C) / 3},
4733
},
4834
}
4935

@@ -66,6 +52,50 @@ func TestNewOHLCVSeries(t *testing.T) {
6652
}
6753
}
6854

55+
func TestNewOHLCVGetTrueRange(t *testing.T) {
56+
data := OHLCVStaticTestData()
57+
58+
s, err := NewOHLCVSeries(data)
59+
if err != nil {
60+
t.Fatal(err)
61+
}
62+
63+
expVals := []*float64{
64+
nil,
65+
NewFloat64(6.8),
66+
NewFloat64(8.5),
67+
NewFloat64(7.9),
68+
NewFloat64(8.3),
69+
NewFloat64(6.3),
70+
NewFloat64(6.6),
71+
NewFloat64(9.6),
72+
NewFloat64(8.0),
73+
NewFloat64(7.6),
74+
}
75+
76+
for i, v := range expVals {
77+
// move to next iteration
78+
s.Next()
79+
vals := s.GetSeries(OHLCPropTR)
80+
81+
if (vals.Val() == nil) != (v == nil) {
82+
t.Errorf("Expected %+v but got %+v for i: %d", v, vals.Val(), i)
83+
continue
84+
}
85+
if v == nil {
86+
continue
87+
}
88+
if fmt.Sprintf("%0.2f", *vals.Val()) != fmt.Sprintf("%0.2f", *v) {
89+
t.Errorf("expected %+v but got %+v", *v, *vals.Val())
90+
}
91+
}
92+
93+
// if this is last, return nil
94+
if v, _ := s.Next(); v != nil {
95+
t.Errorf("Expected to be nil but got %+v", v)
96+
}
97+
}
98+
6999
func TestNewOHLCVSeriesPush(t *testing.T) {
70100
start := time.Now()
71101
data := OHLCVTestData(start, 3, 5*60*1000)

pine/series_atr_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func TestSeriesATRIteration(t *testing.T) {
9898
for i, v := range testTable {
9999
series.Next()
100100

101-
prop := series.GetSeries(OHLCPropTR)
101+
prop := series.GetSeries(OHLCPropTRHL)
102102
atr, err := ATR(prop, 3)
103103
if err != nil {
104104
t.Fatal(errors.Wrap(err, "error ATR"))

pine/series_dmi.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func DMI(ohlcv OHLCVSeries, len, smoo int) (adx, dmip, dmim ValueSeries, err err
1818
}
1919

2020
l := ohlcv.GetSeries(OHLCPropLow)
21-
tr := ohlcv.GetSeries(OHLCPropTR)
21+
tr := ohlcv.GetSeries(OHLCPropTRHL)
2222

2323
up, err := Change(h, 1)
2424
if err != nil {

pine/series_kc.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package pine
2+
3+
import "github.com/pkg/errors"
4+
5+
// KC generates ValueSeries of ketler channel's middle, upper and lower in that order.
6+
func KC(src ValueSeries, o OHLCVSeries, l int64, mult float64, usetr bool) (middle, upper, lower ValueSeries, err error) {
7+
8+
lower = NewValueSeries()
9+
upper = NewValueSeries()
10+
middle = NewValueSeries()
11+
start := src.GetCurrent()
12+
13+
if start == nil {
14+
return middle, upper, lower, nil
15+
}
16+
17+
var span ValueSeries
18+
basis, err := EMA(src, l)
19+
if err != nil {
20+
return middle, upper, lower, errors.Wrap(err, "error EMA")
21+
}
22+
23+
if usetr {
24+
span = o.GetSeries(OHLCPropTR)
25+
} else {
26+
h := o.GetSeries(OHLCPropHigh)
27+
l := o.GetSeries(OHLCPropLow)
28+
span = h.Sub(l)
29+
}
30+
31+
rangeEma, err := EMA(span, l)
32+
if err != nil {
33+
return middle, upper, lower, errors.Wrap(err, "error EMA")
34+
}
35+
36+
middle = basis
37+
rangeEmaMul := rangeEma.MulConst(mult)
38+
upper = basis.Add(rangeEmaMul)
39+
lower = basis.Sub(rangeEmaMul)
40+
41+
middle.SetCurrent(start.t)
42+
upper.SetCurrent(start.t)
43+
lower.SetCurrent(start.t)
44+
45+
return middle, upper, lower, nil
46+
}

pine/series_kc_test.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package pine
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"testing"
7+
"time"
8+
9+
"github.com/pkg/errors"
10+
)
11+
12+
// TestSeriesKC tests no data scenario
13+
func TestSeriesKC(t *testing.T) {
14+
15+
data := OHLCVStaticTestData()
16+
17+
series, err := NewOHLCVSeries(data)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
close := series.GetSeries(OHLCPropClose)
22+
23+
m, u, l, err := KC(close, series, 3, 2.5, true)
24+
if err != nil {
25+
t.Fatal(errors.Wrap(err, "error KC"))
26+
}
27+
if m == nil {
28+
t.Error("Expected kc to be non nil but got nil")
29+
}
30+
if u == nil {
31+
t.Error("Expected kc to be non nil but got nil")
32+
}
33+
if l == nil {
34+
t.Error("Expected kc to be non nil but got nil")
35+
}
36+
}
37+
38+
// TestSeriesKCNoIteration tests this sceneario where there's no iteration yet
39+
func TestSeriesKCNoIteration(t *testing.T) {
40+
41+
data := OHLCVStaticTestData()
42+
series, err := NewOHLCVSeries(data)
43+
if err != nil {
44+
t.Fatal(err)
45+
}
46+
close := series.GetSeries(OHLCPropClose)
47+
48+
m, u, l, err := KC(close, series, 3, 2.5, true)
49+
if err != nil {
50+
t.Fatal(errors.Wrap(err, "error KC"))
51+
}
52+
if m == nil {
53+
t.Error("Expected kc to be non nil but got nil")
54+
}
55+
if u == nil {
56+
t.Error("Expected kc to be non nil but got nil")
57+
}
58+
if l == nil {
59+
t.Error("Expected kc to be non nil but got nil")
60+
}
61+
}
62+
63+
// TestSeriesKCIteration tests the output against TradingView's expected values
64+
func TestSeriesKCIteration(t *testing.T) {
65+
data := OHLCVStaticTestData()
66+
series, err := NewOHLCVSeries(data)
67+
if err != nil {
68+
t.Fatal(err)
69+
}
70+
// array in order of Middle, Upper, Lower
71+
tests := [][]*float64{
72+
nil,
73+
nil,
74+
nil,
75+
{NewFloat64(16.33), NewFloat64(36.2), NewFloat64(-3.55)},
76+
{NewFloat64(17.52), NewFloat64(37.74), NewFloat64(-2.71)},
77+
{NewFloat64(16.19), NewFloat64(34.62), NewFloat64(-2.25)},
78+
{NewFloat64(15.47), NewFloat64(33.13), NewFloat64(-2.19)},
79+
{NewFloat64(13.68), NewFloat64(33.88), NewFloat64(-6.51)},
80+
{NewFloat64(14.09), NewFloat64(32.81), NewFloat64(-4.63)},
81+
{NewFloat64(12.57), NewFloat64(31.41), NewFloat64(-6.26)},
82+
}
83+
84+
for i, v := range tests {
85+
series.Next()
86+
c := series.GetSeries(OHLCPropClose)
87+
m, u, l, err := KC(c, series, 4, 2.5, false)
88+
if err != nil {
89+
t.Fatal(errors.Wrap(err, "error dmi"))
90+
}
91+
92+
// list can be empty
93+
if v == nil {
94+
if m.Val() != nil || u.Val() != nil || l.Val() != nil {
95+
t.Errorf("Expected no values to be returned but got some at %d", i)
96+
}
97+
continue
98+
}
99+
100+
// Middle line
101+
if v[0] != nil && fmt.Sprintf("%.01f", *v[0]) != fmt.Sprintf("%.01f", *m.Val()) {
102+
t.Errorf("Expected middle to be %+v but got %+v for iteration: %d", *v[0], *m.Val(), i)
103+
}
104+
105+
// Upper line
106+
if v != nil && fmt.Sprintf("%.01f", *v[1]) != fmt.Sprintf("%.01f", *u.Val()) {
107+
t.Errorf("Expected upper to be %+v but got %+v for iteration: %d", *v[1], *u.Val(), i)
108+
}
109+
110+
// Lower line
111+
if v != nil && fmt.Sprintf("%.01f", *v[2]) != fmt.Sprintf("%.01f", *l.Val()) {
112+
t.Errorf("Expected lower to be %+v but got %+v for iteration: %d", *v[2], *l.Val(), i)
113+
}
114+
}
115+
}
116+
117+
func BenchmarkKC(b *testing.B) {
118+
// run the Fib function b.N times
119+
start := time.Now()
120+
data := OHLCVTestData(start, 10000, 5*60*1000)
121+
series, _ := NewOHLCVSeries(data)
122+
123+
for n := 0; n < b.N; n++ {
124+
series.Next()
125+
KC(series.GetSeries(OHLCPropClose), series, 4, 2.5, false)
126+
}
127+
}
128+
129+
func ExampleKC() {
130+
start := time.Now()
131+
data := OHLCVTestData(start, 10000, 5*60*1000)
132+
series, _ := NewOHLCVSeries(data)
133+
m, u, l, err := KC(series.GetSeries(OHLCPropClose), series, 4, 2.5, false)
134+
if err != nil {
135+
log.Fatal(errors.Wrap(err, "error KC"))
136+
}
137+
log.Printf("KC middle line: %+v, upper: %+v, lower: %+v", m.Val(), u.Val(), l.Val())
138+
}

0 commit comments

Comments
 (0)