Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: [indicator] add v2 MACD, SMA #1184

Merged
merged 9 commits into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 len(s) < size {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be <=.

return s
}

return s[len(s)-size:]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if size is less than or equal to 0?

}

// 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)
}
}
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) {
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused

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()
}
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)
}
})
}
}
18 changes: 3 additions & 15 deletions pkg/indicator/v2_rma.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,11 @@ func RMA2(source Float64Source, window int, adjust bool) *RMAStream {
Adjust: adjust,
}

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

s.Bind(source, s)
return s
}

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

func (s *RMAStream) calculate(x float64) float64 {
func (s *RMAStream) Calculate(x float64) float64 {
lambda := 1 / float64(s.window)
tmp := 0.0
if s.counter == 0 {
Expand All @@ -62,7 +50,7 @@ func (s *RMAStream) calculate(x float64) float64 {
return tmp
}

func (s *RMAStream) truncate() {
func (s *RMAStream) Truncate() {
if len(s.slice) > MaxNumOfRMA {
s.slice = s.slice[MaxNumOfRMATruncateSize-1:]
}
Expand Down
16 changes: 2 additions & 14 deletions pkg/indicator/v2_rsi.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,11 @@ func RSI2(source Float64Source, window int) *RSIStream {
Float64Series: NewFloat64Series(),
window: window,
}

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

s.Bind(source, s)
return s
}

func (s *RSIStream) calculate(_ float64) float64 {
func (s *RSIStream) Calculate(_ float64) float64 {
var gainSum, lossSum float64
var sourceLen = s.source.Length()
var limit = min(s.window, sourceLen)
Expand All @@ -48,9 +42,3 @@ func (s *RSIStream) calculate(_ float64) float64 {
rsi := 100.0 - (100.0 / (1.0 + rs))
return rsi
}

func (s *RSIStream) calculateAndPush(x float64) {
rsi := s.calculate(x)
s.slice.Push(rsi)
s.EmitUpdate(rsi)
}
29 changes: 29 additions & 0 deletions pkg/indicator/v2_sma.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package indicator

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

type SMAStream struct {
Float64Series
window int
rawValues *types.Queue
}

func SMA2(source Float64Source, window int) *SMAStream {
s := &SMAStream{
Float64Series: NewFloat64Series(),
window: window,
rawValues: types.NewQueue(window),
}
s.Bind(source, s)
return s
}

func (s *SMAStream) Calculate(v float64) float64 {
s.rawValues.Update(v)
sma := s.rawValues.Mean(s.window)
return sma
}

func (s *SMAStream) Truncate() {
s.slice.Truncate(MaxNumOfSMA)
}