Skip to content

Commit

Permalink
KDJ indicator is added.
Browse files Browse the repository at this point in the history
  • Loading branch information
cinar committed Dec 28, 2023
1 parent 208ecd1 commit 4199188
Show file tree
Hide file tree
Showing 5 changed files with 510 additions and 4 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ The following list of indicators are currently supported by this package:
- [Exponential Moving Average (EMA)](trend/README.md#type-ema)
- [Mass Index (MI)](trend/README.md#type-massindex)
- [Moving Average Convergence Divergence (MACD)](trend/README.md#type-macd)
- [Moving Max](trend/README.md#func-movingmax)
- [Moving Min](trend/README.md#func-movingmin)
- [Moving Sum](trend/README.md#func-movingsum)
- [Moving Max](trend/README.md#type-movingmax)
- [Moving Min](trend/README.md#type-movingmin)
- [Moving Sum](trend/README.md#type-movingsum)
- Parabolic SAR
- Random Index (KDJ)
- [Random Index (KDJ)](trend/README.md#type-kdj)
- Rolling Moving Average (RMA)
- [Simple Moving Average (SMA)](trend/README.md#type-sma)
- [Since Change](helper/README.md#func-since)
Expand Down
86 changes: 86 additions & 0 deletions trend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ The information provided on this project is strictly for informational purposes
- [type Ema](<#Ema>)
- [func NewEma\[T helper.Number\]\(\) \*Ema\[T\]](<#NewEma>)
- [func \(ema \*Ema\[T\]\) Compute\(c \<\-chan T\) \<\-chan T](<#Ema[T].Compute>)
- [type Kdj](<#Kdj>)
- [func NewKdj\[T helper.Number\]\(\) \*Kdj\[T\]](<#NewKdj>)
- [func \(kdj \*Kdj\[T\]\) Compute\(high, low, closing \<\-chan T\) \(\<\-chan T, \<\-chan T, \<\-chan T\)](<#Kdj[T].Compute>)
- [func \(kdj \*Kdj\[T\]\) IdlePeriod\(\) int](<#Kdj[T].IdlePeriod>)
- [type Macd](<#Macd>)
- [func NewMacd\[T helper.Number\]\(\) \*Macd\[T\]](<#NewMacd>)
- [func \(m \*Macd\[T\]\) Compute\(c \<\-chan T\) \(\<\-chan T, \<\-chan T\)](<#Macd[T].Compute>)
Expand Down Expand Up @@ -95,6 +99,22 @@ const (
)
```

<a name="DefaultKdjMinMaxPeriod"></a>

```go
const (
// DefaultKdjMinMaxPeriod is the default period for moving min
// of low, and moving max of high.
DefaultKdjMinMaxPeriod = 9

// DefaultKdjSma1Period is the default period for SMA of RSV.
DefaultKdjSma1Period = 3

// DefaultKdjSma2Period is the default period for SMA of K.
DefaultKdjSma2Period = 3
)
```

<a name="DefaultMacdPeriod1"></a>

```go
Expand Down Expand Up @@ -374,6 +394,72 @@ func (ema *Ema[T]) Compute(c <-chan T) <-chan T

Compute function takes a channel of numbers and computes the EMA over the specified period.

<a name="Kdj"></a>
## type [Kdj](<https://github.com/cinar/indicator/blob/v2/trend/kdj.go#L41-L53>)

Kdj represents the configuration parameters for calculating the KDJ, also known as the Random Index. KDJ is calculated similar to the Stochastic Oscillator with the difference of having the J line. It is used to analyze the trend and entry points.

The K and D lines show if the asset is overbought when they crosses above 80%, and oversold when they crosses below 20%. The J line represents the divergence.

```
RSV = ((Closing - Min(Low, rPeriod))
/ (Max(High, rPeriod) - Min(Low, rPeriod))) * 100

K = Sma(RSV, kPeriod)
D = Sma(K, dPeriod)
J = (3 * K) - (2 * D)
```

Example:

```
kdj := NewKdj[float64]()
values := kdj.Compute(highs, lows, closings)
```

```go
type Kdj[T helper.Number] struct {
// MovingMax is the highest high.
MovingMax *MovingMax[T]

// MovingMin is the lowest low.
MovingMin *MovingMin[T]

// Sma1 is the SMA of RSV.
Sma1 *Sma[T]

// Sma2 is the SMA of K.
Sma2 *Sma[T]
}
```

<a name="NewKdj"></a>
### func [NewKdj](<https://github.com/cinar/indicator/blob/v2/trend/kdj.go#L56>)

```go
func NewKdj[T helper.Number]() *Kdj[T]
```

NewKdj function initializes a new Kdj instance with the default parameters

<a name="Kdj[T].Compute"></a>
### func \(\*Kdj\[T\]\) [Compute](<https://github.com/cinar/indicator/blob/v2/trend/kdj.go#L74>)

```go
func (kdj *Kdj[T]) Compute(high, low, closing <-chan T) (<-chan T, <-chan T, <-chan T)
```

Compute function takes a channel of numbers and computes the KDJ over the specified period. Returns K, D, J.

<a name="Kdj[T].IdlePeriod"></a>
### func \(\*Kdj\[T\]\) [IdlePeriod](<https://github.com/cinar/indicator/blob/v2/trend/kdj.go#L113>)

```go
func (kdj *Kdj[T]) IdlePeriod() int
```

IdlePeriod is the initial period that KDJ won't yield any results.

<a name="Macd"></a>
## type [Macd](<https://github.com/cinar/indicator/blob/v2/trend/macd.go#L29-L33>)

Expand Down
115 changes: 115 additions & 0 deletions trend/kdj.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) 2021-2023 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package trend

import "github.com/cinar/indicator/helper"

const (
// DefaultKdjMinMaxPeriod is the default period for moving min
// of low, and moving max of high.
DefaultKdjMinMaxPeriod = 9

// DefaultKdjSma1Period is the default period for SMA of RSV.
DefaultKdjSma1Period = 3

// DefaultKdjSma2Period is the default period for SMA of K.
DefaultKdjSma2Period = 3
)

// Kdj represents the configuration parameters for calculating the
// KDJ, also known as the Random Index. KDJ is calculated similar
// to the Stochastic Oscillator with the difference of having the
// J line. It is used to analyze the trend and entry points.
//
// The K and D lines show if the asset is overbought when they
// crosses above 80%, and oversold when they crosses below
// 20%. The J line represents the divergence.
//
// RSV = ((Closing - Min(Low, rPeriod))
// / (Max(High, rPeriod) - Min(Low, rPeriod))) * 100
//
// K = Sma(RSV, kPeriod)
// D = Sma(K, dPeriod)
// J = (3 * K) - (2 * D)
//
// Example:
//
// kdj := NewKdj[float64]()
// values := kdj.Compute(highs, lows, closings)
type Kdj[T helper.Number] struct {
// MovingMax is the highest high.
MovingMax *MovingMax[T]

// MovingMin is the lowest low.
MovingMin *MovingMin[T]

// Sma1 is the SMA of RSV.
Sma1 *Sma[T]

// Sma2 is the SMA of K.
Sma2 *Sma[T]
}

// NewKdj function initializes a new Kdj instance with the default parameters
func NewKdj[T helper.Number]() *Kdj[T] {
kdj := &Kdj[T]{
MovingMax: NewMovingMax[T](),
MovingMin: NewMovingMin[T](),
Sma1: NewSma[T](),
Sma2: NewSma[T](),
}

kdj.MovingMax.Period = DefaultKdjMinMaxPeriod
kdj.MovingMin.Period = DefaultKdjMinMaxPeriod
kdj.Sma1.Period = DefaultKdjSma1Period
kdj.Sma2.Period = DefaultKdjSma2Period

return kdj
}

// Compute function takes a channel of numbers and computes the KDJ
// over the specified period. Returns K, D, J.
func (kdj *Kdj[T]) Compute(high, low, closing <-chan T) (<-chan T, <-chan T, <-chan T) {
highest := kdj.MovingMax.Compute(high)
lowests := helper.Duplicate(
kdj.MovingMin.Compute(low),
2,
)

closing = helper.Skip(closing, kdj.MovingMax.Period-1)

rsv := helper.MultiplyBy(
helper.Divide(
helper.Subtract(closing, lowests[0]),
helper.Subtract(highest, lowests[1]),
),
100,
)

ks := helper.Duplicate(
kdj.Sma1.Compute(rsv),
3,
)

ds := helper.Duplicate(
kdj.Sma2.Compute(ks[0]),
2,
)

ks[1] = helper.Skip(ks[1], kdj.Sma2.Period-1)
ks[2] = helper.Skip(ks[2], kdj.Sma2.Period-1)

j := helper.Subtract(
helper.MultiplyBy(ks[1], 3),
helper.MultiplyBy(ds[0], 2),
)

return ks[2], ds[1], j
}

// IdlePeriod is the initial period that KDJ won't yield any results.
func (kdj *Kdj[T]) IdlePeriod() int {
return kdj.MovingMax.Period + kdj.Sma1.Period + kdj.Sma2.Period - 3
}
53 changes: 53 additions & 0 deletions trend/kdj_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2021-2023 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package trend_test

import (
"testing"

"github.com/cinar/indicator/helper"
"github.com/cinar/indicator/trend"
)

func TestKdj(t *testing.T) {
type Data struct {
High float64
Low float64
Close float64
K float64
D float64
J float64
}

input, err := helper.ReadFromCsvFile[Data]("testdata/kdj.csv", true)
if err != nil {
t.Fatal(err)
}

inputs := helper.Duplicate(input, 6)
high := helper.Map(inputs[0], func(d *Data) float64 { return d.High })
low := helper.Map(inputs[1], func(d *Data) float64 { return d.Low })
closing := helper.Map(inputs[2], func(d *Data) float64 { return d.Close })
expectedK := helper.Map(inputs[3], func(d *Data) float64 { return d.K })
expectedD := helper.Map(inputs[4], func(d *Data) float64 { return d.D })
expectedJ := helper.Map(inputs[5], func(d *Data) float64 { return d.J })

kdj := trend.NewKdj[float64]()
actualK, actualD, actualJ := kdj.Compute(high, low, closing)

actualK = helper.RoundDigits(actualK, 2)
actualK = helper.Shift(actualK, kdj.IdlePeriod(), 0)

actualD = helper.RoundDigits(actualD, 2)
actualD = helper.Shift(actualD, kdj.IdlePeriod(), 0)

actualJ = helper.RoundDigits(actualJ, 2)
actualJ = helper.Shift(actualJ, kdj.IdlePeriod(), 0)

err = helper.CheckEquals(actualK, expectedK, actualD, expectedD, actualJ, expectedJ)
if err != nil {
t.Fatal(err)
}
}
Loading

0 comments on commit 4199188

Please sign in to comment.