Merge pull request #1184 from c9s/feature/v2indicator-macd

FEATURE: [indicator] add v2 MACD, SMA
This commit is contained in:
Yo-An Lin 2023-06-01 12:47:41 +08:00 committed by GitHub
commit 668444b9df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 202 additions and 135 deletions

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -1,37 +0,0 @@
package indicator
import (
"time"
"github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/types"
)
//go:generate callbackgen -type Low
type Low struct {
types.IntervalWindow
types.SeriesBase
Values floats.Slice
EndTime time.Time
updateCallbacks []func(value float64)
}
func (inc *Low) Update(value float64) {
if len(inc.Values) == 0 {
inc.SeriesBase.Series = inc
}
inc.Values.Push(value)
}
func (inc *Low) PushK(k types.KLine) {
if k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(k.Low.Float64())
inc.EndTime = k.EndTime.Time()
inc.EmitUpdate(inc.Last(0))
}

View File

@ -1,15 +0,0 @@
// Code generated by "callbackgen -type Low"; DO NOT EDIT.
package indicator
import ()
func (inc *Low) OnUpdate(cb func(value float64)) {
inc.updateCallbacks = append(inc.updateCallbacks, cb)
}
func (inc *Low) EmitUpdate(value float64) {
for _, cb := range inc.updateCallbacks {
cb(value)
}
}

View File

@ -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 {
@ -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 {

View File

@ -1,7 +1,6 @@
package indicator
import (
"fmt"
"time"
"github.com/c9s/bbgo/pkg/datatype/floats"
@ -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
}

View File

@ -4,6 +4,7 @@ import "github.com/c9s/bbgo/pkg/types"
type Float64Calculator interface {
Calculate(x float64) float64
PushAndEmit(x float64)
}
type Float64Source interface {
@ -15,3 +16,7 @@ type Float64Subscription interface {
types.Series
AddSubscriber(f func(v float64))
}
type Float64Truncator interface {
Truncate()
}

View File

@ -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 {

View File

@ -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

29
pkg/indicator/v2_macd.go Normal file
View File

@ -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,
}
}

View File

@ -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)
}
})
}
}

View File

@ -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 {
@ -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:]
}

View File

@ -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)
@ -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
pkg/indicator/v2_sma.go Normal file
View File

@ -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 = s.slice.Truncate(MaxNumOfSMA)
}

View File

@ -11,7 +11,7 @@ import (
func Test_v2_Subtract(t *testing.T) {
stream := &types.StandardStream{}
kLines := KLines(stream)
kLines := KLines(stream, "", "")
closePrices := ClosePrices(kLines)
fastEMA := EWMA2(closePrices, 10)
slowEMA := EWMA2(closePrices, 25)

View File

@ -64,7 +64,7 @@ func Test_TR_and_RMA(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
stream := &types.StandardStream{}
kLines := KLines(stream)
kLines := KLines(stream, "", "")
atr := TR2(kLines)
rma := RMA2(atr, tt.window, true)