Skip to content

Commit 7518d9d

Browse files
author
Don Johnson
committed
added kelner
1 parent 28a0b6c commit 7518d9d

File tree

3 files changed

+139
-1
lines changed

3 files changed

+139
-1
lines changed

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ A **Go (Golang) library** of popular technical analysis indicators. This is a **
1818
12. [MFI (Money Flow Index)](#12-mfi-money-flow-index)
1919
13. [Ultimate Oscillator (UO)](#13-ultimate-oscillator-uo)
2020
14. [Ichimoku Kinko Hyo (Ichimoku Cloud)](#14-ichimoku-kinko-hyo-ichimoku-cloud)
21-
15. [Parabolic SAR](#15-parabolic-sar)
21+
15. [Parabolic SAR](#15-parabolic-sar)
22+
16. [Keltner Channels](#16-keltner-channels)
2223

2324
---
2425

@@ -201,3 +202,18 @@ A **Go (Golang) library** of popular technical analysis indicators. This is a **
201202
- **Use Cases & Patterns**:
202203
- **Trend-following** with automated trailing stops.
203204
- Dots appear below price in an uptrend and above price in a downtrend; reversal triggers when price crosses the SAR level.
205+
206+
---
207+
208+
## 16. Keltner Channels
209+
210+
- **Origin**: Based on work by Chester Keltner in the 1960s, later modified/popularized by Linda Bradford Raschke.
211+
- **Description**: A volatility-based envelope indicator. The middle line is typically an EMA of typical price (High+Low+Close/3), and the upper/lower lines are offset by a multiple of ATR.
212+
- **Common Parameters**:
213+
- `emaPeriod` for the middle line (e.g., 20).
214+
- `atrPeriod` (e.g., 10).
215+
- `mult` as a multiplier for the ATR offset (commonly around 2.0).
216+
- **Use Cases & Patterns**:
217+
- **Volatility-based bands** that contract/expand with market moves.
218+
- Similar to Bollinger Bands but uses ATR instead of standard deviation.
219+
- Can signal breakouts when price strongly pierces the upper or lower channel.

indicators/keltner_channels.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package indicators
2+
3+
import (
4+
"errors"
5+
)
6+
7+
// KeltnerChannels holds the configuration for computing the middle line (EMA of typical price)
8+
// and bands offset by a multiple of ATR.
9+
type KeltnerChannels struct {
10+
EmaPeriod int // Period for the EMA of typical price (middle line)
11+
AtrPeriod int // Period for the ATR calculation
12+
Mult float64 // Multiplier for the ATR offset
13+
}
14+
15+
// NewKeltnerChannels creates a KeltnerChannels instance with a given EMA period, ATR period, and ATR multiplier.
16+
func NewKeltnerChannels(emaPeriod, atrPeriod int, multiplier float64) *KeltnerChannels {
17+
return &KeltnerChannels{
18+
EmaPeriod: emaPeriod,
19+
AtrPeriod: atrPeriod,
20+
Mult: multiplier,
21+
}
22+
}
23+
24+
// Calculate returns three slices (middle, upper, lower), each the same length as the inputs.
25+
// - middle line = EMA of typical price
26+
// - upper line = middle line + (mult * ATR)
27+
// - lower line = middle line - (mult * ATR)
28+
//
29+
// The function needs high, low, close arrays of equal length. (Volume is not used here.)
30+
func (kc *KeltnerChannels) Calculate(high, low, close []float64) ([]float64, []float64, []float64, error) {
31+
length := len(high)
32+
if length != len(low) || length != len(close) {
33+
return nil, nil, nil, errors.New("high, low, and close must have the same length")
34+
}
35+
if length == 0 {
36+
return nil, nil, nil, errors.New("no price data provided")
37+
}
38+
39+
// 1) Compute typical price: (High + Low + Close) / 3
40+
typicalPrices := make([]float64, length)
41+
for i := 0; i < length; i++ {
42+
typicalPrices[i] = (high[i] + low[i] + close[i]) / 3.0
43+
}
44+
45+
// 2) Compute EMA of typical price
46+
emaCalc := NewEMA(kc.EmaPeriod)
47+
emaValues, err := emaCalc.Calculate(typicalPrices)
48+
if err != nil {
49+
return nil, nil, nil, err
50+
}
51+
52+
// 3) Compute ATR for the same length
53+
// Note: We already have an ATR indicator that expects (high, low, close).
54+
// We'll re-use it here.
55+
atrCalc := NewATR(kc.AtrPeriod)
56+
atrValues, err := atrCalc.Calculate(high, low, close)
57+
if err != nil {
58+
return nil, nil, nil, err
59+
}
60+
61+
// 4) Build the final Keltner Channels
62+
middle := make([]float64, length)
63+
upper := make([]float64, length)
64+
lower := make([]float64, length)
65+
66+
for i := 0; i < length; i++ {
67+
m := emaValues[i]
68+
a := atrValues[i]
69+
middle[i] = m
70+
upper[i] = m + kc.Mult*a
71+
lower[i] = m - kc.Mult*a
72+
}
73+
74+
return middle, upper, lower, nil
75+
}

tests/keltner_channels_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package tests
2+
3+
import (
4+
"math"
5+
"testing"
6+
7+
"github.com/copyleftdev/indicator-libs/indicators"
8+
)
9+
10+
func TestKeltnerChannels(t *testing.T) {
11+
// Small sample data for demonstration.
12+
// In reality, more bars would give a more reliable test.
13+
highs := []float64{10, 10.5, 11, 11.2, 11.1, 11.3, 11.7, 12.0}
14+
lows := []float64{9, 10.0, 10.2, 10.6, 10.8, 10.9, 11.1, 11.6}
15+
closes := []float64{9.5, 10.2, 10.5, 11.0, 10.9, 11.1, 11.4, 11.8}
16+
17+
// Suppose we choose typical Keltner settings: EMA period=3, ATR period=3, multiplier=1.5
18+
// These are small for testing on a short data set.
19+
kc := indicators.NewKeltnerChannels(3, 3, 1.5)
20+
mid, up, low, err := kc.Calculate(highs, lows, closes)
21+
if err != nil {
22+
t.Fatalf("Calculate returned error: %v", err)
23+
}
24+
25+
// All outputs should match input length
26+
if len(mid) != len(highs) || len(up) != len(highs) || len(low) != len(highs) {
27+
t.Errorf("output slice lengths must match input: got mid=%d, up=%d, low=%d, want=%d",
28+
len(mid), len(up), len(low), len(highs))
29+
}
30+
31+
// Basic check: ensure no NaNs in final results (some early bars might be 0 or partial due to warm-up).
32+
for i := range mid {
33+
if math.IsNaN(mid[i]) {
34+
t.Errorf("mid[%d] is NaN, expected a valid float", i)
35+
}
36+
if math.IsNaN(up[i]) {
37+
t.Errorf("up[%d] is NaN, expected a valid float", i)
38+
}
39+
if math.IsNaN(low[i]) {
40+
t.Errorf("low[%d] is NaN, expected a valid float", i)
41+
}
42+
}
43+
44+
// If you want to compare to known references, place them here.
45+
t.Logf("Final Keltner Channels => mid=%.3f, up=%.3f, low=%.3f",
46+
mid[len(mid)-1], up[len(up)-1], low[len(low)-1])
47+
}

0 commit comments

Comments
 (0)