Skip to content

Commit

Permalink
feature: add MACD indicator (c9s#219)
Browse files Browse the repository at this point in the history
  • Loading branch information
narumiruna authored May 8, 2021
1 parent 7de7228 commit 52248fc
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 1 deletion.
12 changes: 12 additions & 0 deletions pkg/indicator/ewma.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ type EWMA struct {
UpdateCallbacks []func(value float64)
}

func (inc *EWMA) Update(value float64) {
var multiplier = 2.0 / float64(1+inc.Window)

if len(inc.Values) == 0 {
inc.Values.Push(value)
return
}

ema := (1-multiplier)*inc.Last() + multiplier*value
inc.Values.Push(ema)
}

func (inc *EWMA) Last() float64 {
if len(inc.Values) == 0 {
return 0
Expand Down
94 changes: 94 additions & 0 deletions pkg/indicator/macd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package indicator

import (
"time"

"github.com/c9s/bbgo/pkg/types"
)

/*
macd implements moving average convergence divergence indicator
Moving Average Convergence Divergence (MACD)
- https://www.investopedia.com/terms/m/macd.asp
*/

//go:generate callbackgen -type MACD
type MACD struct {
types.IntervalWindow // 9
ShortPeriod int // 12
LongPeriod int // 26
Values Float64Slice
FastEWMA EWMA
SlowEWMA EWMA
SignalLine EWMA
Histogram Float64Slice

EndTime time.Time

UpdateCallbacks []func(value float64)
}

func (inc *MACD) calculateMACD(kLines []types.KLine, priceF KLinePriceMapper) float64 {
for _, kline := range kLines {
inc.update(kline, priceF)
}
return inc.Values[len(inc.Values)-1]
}

func (inc *MACD) update(kLine types.KLine, priceF KLinePriceMapper) {
if len(inc.Values) == 0 {
inc.FastEWMA = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.ShortPeriod}}
inc.SlowEWMA = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.LongPeriod}}
inc.SignalLine = EWMA{IntervalWindow: types.IntervalWindow{Window: inc.Window}}
}

price := priceF(kLine)

// update fast and slow ema
inc.FastEWMA.Update(price)
inc.SlowEWMA.Update(price)

// update macd
macd := inc.FastEWMA.Last() - inc.SlowEWMA.Last()
inc.Values.Push(macd)

// update signal line
inc.SignalLine.Update(macd)

// update histogram
inc.Histogram.Push(macd - inc.SignalLine.Last())
}

func (inc *MACD) calculateAndUpdate(kLines []types.KLine) {
if len(kLines) == 0 {
return
}

var priceF = KLineClosePriceMapper

var index = len(kLines) - 1
var kline = kLines[index]
if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) {
return
}

for i, kLine := range kLines {
inc.update(kLine, priceF)
inc.EmitUpdate(inc.Values[len(inc.Values)-1])
inc.EndTime = kLines[i].EndTime
}

}

func (inc *MACD) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if inc.Interval != interval {
return
}

inc.calculateAndUpdate(window)
}

func (inc *MACD) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
15 changes: 15 additions & 0 deletions pkg/indicator/macd_callbacks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions pkg/indicator/macd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package indicator

import (
"testing"

"github.com/c9s/bbgo/pkg/types"
)

/*
python:
import pandas as pd
s = pd.Series([0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9])
slow = s.ewm(span=26, adjust=False).mean()
fast = s.ewm(span=12, adjust=False).mean()
print(fast - slow)
*/

func Test_calculateMACD(t *testing.T) {
var randomPrices = []float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
tests := []struct {
name string
kLines []types.KLine
want float64
}{
{
name: "random_case",
kLines: buildKLines(randomPrices),
want: 0.7967670223776384,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iw := types.IntervalWindow{Window: 9}
macd := MACD{IntervalWindow: iw, ShortPeriod: 12, LongPeriod: 26}
priceF := KLineClosePriceMapper
got := macd.calculateMACD(tt.kLines, priceF)
if got != tt.want {
t.Errorf("calculateMACD() = %v, want %v", got, tt.want)
}
})
}
}
4 changes: 3 additions & 1 deletion pkg/indicator/vwap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
/*
vwap implements the volume weighted average price (VWAP) indicator:
The basics of VWAP
Volume Weighted Average Price (VWAP) Definition
- https://www.investopedia.com/terms/v/vwap.asp
Volume-Weighted Average Price (VWAP) Explained
- https://academy.binance.com/en/articles/volume-weighted-average-price-vwap-explained
*/
//go:generate callbackgen -type VWAP
Expand Down

0 comments on commit 52248fc

Please sign in to comment.