Skip to content

Commit

Permalink
Merge pull request #1184 from c9s/feature/v2indicator-macd
Browse files Browse the repository at this point in the history
FEATURE: [indicator] add v2 MACD, SMA
  • Loading branch information
c9s authored Jun 1, 2023
2 parents 9435478 + 3e94584 commit 668444b
Show file tree
Hide file tree
Showing 18 changed files with 202 additions and 135 deletions.
14 changes: 9 additions & 5 deletions pkg/datatype/floats/slice.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,18 @@ func (s Slice) Last(i int) float64 {
return s[length-1-i]
}

func (s Slice) Truncate(size int) Slice {
if size < 0 || len(s) <= size {
return s
}

return s[len(s)-size:]
}

// Index fetches the element from the end of the slice
// WARNING: it does not start from 0!!!
func (s Slice) Index(i int) float64 {
length := len(s)
if i < 0 || length-1-i < 0 {
return 0.0
}
return s[length-1-i]
return s.Last(i)
}

func (s Slice) Length() int {
Expand Down
8 changes: 8 additions & 0 deletions pkg/datatype/floats/slice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ func TestSub(t *testing.T) {
assert.Equal(t, 5, c.Length())
}

func TestTruncate(t *testing.T) {
a := New(1, 2, 3, 4, 5)
for i := 5; i > 0; i-- {
a = a.Truncate(i)
assert.Equal(t, i, a.Length())
}
}

func TestAdd(t *testing.T) {
a := New(1, 2, 3, 4, 5)
b := New(1, 2, 3, 4, 5)
Expand Down
34 changes: 33 additions & 1 deletion pkg/indicator/float64updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,41 @@ func (f *Float64Series) Last(i int) float64 {
}

func (f *Float64Series) Index(i int) float64 {
return f.slice.Last(i)
return f.Last(i)
}

func (f *Float64Series) Length() int {
return len(f.slice)
}

func (f *Float64Series) PushAndEmit(x float64) {
f.slice.Push(x)
f.EmitUpdate(x)
}

// Bind binds the source event to the target (Float64Calculator)
// A Float64Calculator should be able to calculate the float64 result from a single float64 argument input
func (f *Float64Series) Bind(source Float64Source, target Float64Calculator) {
var c func(x float64)

// optimize the truncation check
trc, canTruncate := target.(Float64Truncator)
if canTruncate {
c = func(x float64) {
y := target.Calculate(x)
target.PushAndEmit(y)
trc.Truncate()
}
} else {
c = func(x float64) {
y := target.Calculate(x)
target.PushAndEmit(y)
}
}

if sub, ok := source.(Float64Subscription); ok {
sub.AddSubscriber(c)
} else {
source.OnUpdate(c)
}
}
24 changes: 19 additions & 5 deletions pkg/indicator/klinestream.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,43 @@ type KLineStream struct {
kLines []types.KLine
}

func (s *KLineStream) Length() int {
return len(s.kLines)
}

func (s *KLineStream) Last(i int) *types.KLine {
l := len(s.kLines)
if i < 0 || l-1-i < 0 {
return nil
}

return &s.kLines[l-1-i]
}

// AddSubscriber adds the subscriber function and push historical data to the subscriber
func (s *KLineStream) AddSubscriber(f func(k types.KLine)) {
s.OnUpdate(f)

if len(s.kLines) > 0 {
// push historical klines to the subscriber
for _, k := range s.kLines {
f(k)
}
}
s.OnUpdate(f)
}

// KLines creates a KLine stream that pushes the klines to the subscribers
func KLines(source types.Stream) *KLineStream {
func KLines(source types.Stream, symbol string, interval types.Interval) *KLineStream {
s := &KLineStream{}

source.OnKLineClosed(func(k types.KLine) {
source.OnKLineClosed(types.KLineWith(symbol, interval, func(k types.KLine) {
s.kLines = append(s.kLines, k)
s.EmitUpdate(k)

if len(s.kLines) > MaxNumOfKLines {
s.kLines = s.kLines[len(s.kLines)-1-MaxNumOfKLines:]
}
s.EmitUpdate(k)
})
}))

return s
}
37 changes: 0 additions & 37 deletions pkg/indicator/low.go

This file was deleted.

15 changes: 0 additions & 15 deletions pkg/indicator/low_callbacks.go

This file was deleted.

10 changes: 3 additions & 7 deletions pkg/indicator/macd.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,8 @@ func (inc *MACDLegacy) Update(x float64) {
inc.EmitUpdate(macd, signal, histogram)
}

func (inc *MACDLegacy) Last(int) float64 {
if len(inc.Values) == 0 {
return 0.0
}

return inc.Values[len(inc.Values)-1]
func (inc *MACDLegacy) Last(i int) float64 {
return inc.Values.Last(i)
}

func (inc *MACDLegacy) Length() int {
Expand Down Expand Up @@ -111,7 +107,7 @@ func (inc *MACDValues) Last(i int) float64 {
}

func (inc *MACDValues) Index(i int) float64 {
return inc.Values.Last(i)
return inc.Last(i)
}

func (inc *MACDValues) Length() int {
Expand Down
19 changes: 0 additions & 19 deletions pkg/indicator/sma.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package indicator

import (
"fmt"
"time"

"github.com/c9s/bbgo/pkg/datatype/floats"
Expand Down Expand Up @@ -82,21 +81,3 @@ func (inc *SMA) LoadK(allKLines []types.KLine) {
inc.PushK(k)
}
}

func calculateSMA(kLines []types.KLine, window int, priceF KLineValueMapper) (float64, error) {
length := len(kLines)
if length == 0 || length < window {
return 0.0, fmt.Errorf("insufficient elements for calculating SMA with window = %d", window)
}
if length != window {
return 0.0, fmt.Errorf("too much klines passed in, requires only %d klines", window)
}

sum := 0.0
for _, k := range kLines {
sum += priceF(k)
}

avg := sum / float64(window)
return avg, nil
}
5 changes: 5 additions & 0 deletions pkg/indicator/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "github.com/c9s/bbgo/pkg/types"

type Float64Calculator interface {
Calculate(x float64) float64
PushAndEmit(x float64)
}

type Float64Source interface {
Expand All @@ -15,3 +16,7 @@ type Float64Subscription interface {
types.Series
AddSubscriber(f func(v float64))
}

type Float64Truncator interface {
Truncate()
}
2 changes: 1 addition & 1 deletion pkg/indicator/v2_atr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func Test_ATR2(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
stream := &types.StandardStream{}

kLines := KLines(stream)
kLines := KLines(stream, "", "")
atr := ATR2(kLines, tt.window)

for _, k := range tt.kLines {
Expand Down
16 changes: 2 additions & 14 deletions pkg/indicator/v2_ewma.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,11 @@ func EWMA2(source Float64Source, window int) *EWMAStream {
window: window,
multiplier: 2.0 / float64(1+window),
}

if sub, ok := source.(Float64Subscription); ok {
sub.AddSubscriber(s.calculateAndPush)
} else {
source.OnUpdate(s.calculateAndPush)
}

s.Bind(source, s)
return s
}

func (s *EWMAStream) calculateAndPush(v float64) {
v2 := s.calculate(v)
s.slice.Push(v2)
s.EmitUpdate(v2)
}

func (s *EWMAStream) calculate(v float64) float64 {
func (s *EWMAStream) Calculate(v float64) float64 {
last := s.slice.Last(0)
m := s.multiplier
return (1.0-m)*last + m*v
Expand Down
29 changes: 29 additions & 0 deletions pkg/indicator/v2_macd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package indicator

type MACDStream struct {
*SubtractStream

shortWindow, longWindow, signalWindow int

fastEWMA, slowEWMA, signal *EWMAStream
histogram *SubtractStream
}

func MACD2(source Float64Source, shortWindow, longWindow, signalWindow int) *MACDStream {
// bind and calculate these first
fastEWMA := EWMA2(source, shortWindow)
slowEWMA := EWMA2(source, longWindow)
macd := Subtract(fastEWMA, slowEWMA)
signal := EWMA2(macd, signalWindow)
histogram := Subtract(macd, signal)
return &MACDStream{
SubtractStream: macd,
shortWindow: shortWindow,
longWindow: longWindow,
signalWindow: signalWindow,
fastEWMA: fastEWMA,
slowEWMA: slowEWMA,
signal: signal,
histogram: histogram,
}
}
57 changes: 57 additions & 0 deletions pkg/indicator/v2_macd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package indicator

import (
"encoding/json"
"math"
"testing"

"github.com/stretchr/testify/assert"

"github.com/c9s/bbgo/pkg/fixedpoint"
"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_MACD2(t *testing.T) {
var randomPrices = []byte(`[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]`)
var input []fixedpoint.Value
err := json.Unmarshal(randomPrices, &input)
assert.NoError(t, err)

tests := []struct {
name string
kLines []types.KLine
want float64
}{
{
name: "random_case",
kLines: buildKLines(input),
want: 0.7967670223776384,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prices := &PriceStream{}
macd := MACD2(prices, 12, 26, 9)
for _, k := range tt.kLines {
prices.EmitUpdate(k.Close.Float64())
}

got := macd.Last(0)
diff := math.Trunc((got-tt.want)*100) / 100
if diff != 0 {
t.Errorf("MACD2() = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit 668444b

Please sign in to comment.