Merge pull request #892 from c9s/feature/pivot-right-window

feature: add pivot low right window support
This commit is contained in:
Yo-An Lin 2022-08-24 19:44:44 +08:00 committed by GitHub
commit 066b0ca30e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 154 additions and 69 deletions

3
pkg/bbgo/doc.go Normal file
View File

@ -0,0 +1,3 @@
// Package bbgo provides the core BBGO API for strategies
package bbgo

View File

@ -23,9 +23,8 @@ type StandardIndicatorSet struct {
// Standard indicators // Standard indicators
// interval -> window // interval -> window
boll map[types.IntervalWindowBandWidth]*indicator.BOLL iwbIndicators map[types.IntervalWindowBandWidth]*indicator.BOLL
stoch map[types.IntervalWindow]*indicator.STOCH iwIndicators map[types.IntervalWindow]indicator.KLinePusher
simples map[types.IntervalWindow]indicator.KLinePusher
stream types.Stream stream types.Stream
store *MarketDataStore store *MarketDataStore
@ -36,32 +35,30 @@ func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDa
Symbol: symbol, Symbol: symbol,
store: store, store: store,
stream: stream, stream: stream,
simples: make(map[types.IntervalWindow]indicator.KLinePusher), iwIndicators: make(map[types.IntervalWindow]indicator.KLinePusher),
iwbIndicators: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
} }
} }
func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, iw types.IntervalWindow) { func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, interval types.Interval) {
if klines, ok := s.store.KLinesOfInterval(iw.Interval); ok { if klines, ok := s.store.KLinesOfInterval(interval); ok {
for _, k := range *klines { for _, k := range *klines {
inc.PushK(k) inc.PushK(k)
} }
} }
s.stream.OnKLineClosed(types.KLineWith(s.Symbol, iw.Interval, inc.PushK)) s.stream.OnKLineClosed(types.KLineWith(s.Symbol, interval, inc.PushK))
} }
func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow) indicator.KLinePusher { func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow) indicator.KLinePusher {
inc, ok := s.simples[iw] inc, ok := s.iwIndicators[iw]
if ok { if ok {
return inc return inc
} }
inc = t inc = t
s.initAndBind(inc, iw) s.initAndBind(inc, iw.Interval)
s.simples[iw] = inc s.iwIndicators[iw] = inc
return t return t
} }
@ -77,6 +74,13 @@ func (s *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
return inc.(*indicator.EWMA) return inc.(*indicator.EWMA)
} }
// VWMA
func (s *StandardIndicatorSet) VWMA(iw types.IntervalWindow) *indicator.VWMA {
inc := s.allocateSimpleIndicator(&indicator.VWMA{IntervalWindow: iw}, iw)
return inc.(*indicator.VWMA)
}
func (s *StandardIndicatorSet) PivotLow(iw types.IntervalWindow) *indicator.PivotLow { func (s *StandardIndicatorSet) PivotLow(iw types.IntervalWindow) *indicator.PivotLow {
inc := s.allocateSimpleIndicator(&indicator.PivotLow{IntervalWindow: iw}, iw) inc := s.allocateSimpleIndicator(&indicator.PivotLow{IntervalWindow: iw}, iw)
return inc.(*indicator.PivotLow) return inc.(*indicator.PivotLow)
@ -108,30 +112,24 @@ func (s *StandardIndicatorSet) HULL(iw types.IntervalWindow) *indicator.HULL {
} }
func (s *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH { func (s *StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH {
inc, ok := s.stoch[iw] inc := s.allocateSimpleIndicator(&indicator.STOCH{IntervalWindow: iw}, iw)
if !ok { return inc.(*indicator.STOCH)
inc = &indicator.STOCH{IntervalWindow: iw}
s.initAndBind(inc, iw)
s.stoch[iw] = inc
}
return inc
} }
// BOLL returns the bollinger band indicator of the given interval, the window and bandwidth // BOLL returns the bollinger band indicator of the given interval, the window and bandwidth
func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL { func (s *StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandWidth float64) *indicator.BOLL {
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth} iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
inc, ok := s.boll[iwb] inc, ok := s.iwbIndicators[iwb]
if !ok { if !ok {
inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth} inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth}
s.initAndBind(inc, iw) s.initAndBind(inc, iw.Interval)
if debugBOLL { if debugBOLL {
inc.OnUpdate(func(sma float64, upBand float64, downBand float64) { inc.OnUpdate(func(sma float64, upBand float64, downBand float64) {
logrus.Infof("%s BOLL %s: sma=%f up=%f down=%f", s.Symbol, iw.String(), sma, upBand, downBand) logrus.Infof("%s BOLL %s: sma=%f up=%f down=%f", s.Symbol, iw.String(), sma, upBand, downBand)
}) })
} }
s.boll[iwb] = inc s.iwbIndicators[iwb] = inc
} }
return inc return inc

View File

@ -38,8 +38,8 @@ func (inc *DEMA) TestUpdate(value float64) *DEMA {
func (inc *DEMA) Update(value float64) { func (inc *DEMA) Update(value float64) {
if len(inc.Values) == 0 { if len(inc.Values) == 0 {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
inc.a1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.a1 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.a2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.a2 = &EWMA{IntervalWindow: inc.IntervalWindow}
} }
inc.a1.Update(value) inc.a1.Update(value)

View File

@ -83,7 +83,7 @@ func (inc *EWMA) PushK(k types.KLine) {
inc.EmitUpdate(inc.Last()) inc.EmitUpdate(inc.Last())
} }
func CalculateKLinesEMA(allKLines []types.KLine, priceF KLinePriceMapper, window int) float64 { func CalculateKLinesEMA(allKLines []types.KLine, priceF KLineValueMapper, window int) float64 {
var multiplier = 2.0 / (float64(window) + 1) var multiplier = 2.0 / (float64(window) + 1)
return ewma(MapKLinePrice(allKLines, priceF), multiplier) return ewma(MapKLinePrice(allKLines, priceF), multiplier)
} }

View File

@ -1027,7 +1027,7 @@ func buildKLines(prices []fixedpoint.Value) (klines []types.KLine) {
func Test_calculateEWMA(t *testing.T) { func Test_calculateEWMA(t *testing.T) {
type args struct { type args struct {
allKLines []types.KLine allKLines []types.KLine
priceF KLinePriceMapper priceF KLineValueMapper
window int window int
} }
var input []fixedpoint.Value var input []fixedpoint.Value

View File

@ -24,9 +24,9 @@ var _ types.SeriesExtend = &HULL{}
func (inc *HULL) Update(value float64) { func (inc *HULL) Update(value float64) {
if inc.result == nil { if inc.result == nil {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window / 2}} inc.ma1 = &EWMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window / 2}}
inc.ma2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.ma2 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.result = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, int(math.Sqrt(float64(inc.Window)))}} inc.result = &EWMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: int(math.Sqrt(float64(inc.Window)))}}
} }
inc.ma1.Update(value) inc.ma1.Update(value)
inc.ma2.Update(value) inc.ma2.Update(value)

View File

@ -2,7 +2,7 @@ package indicator
import "github.com/c9s/bbgo/pkg/types" import "github.com/c9s/bbgo/pkg/types"
type KLinePriceMapper func(k types.KLine) float64 type KLineValueMapper func(k types.KLine) float64
func KLineOpenPriceMapper(k types.KLine) float64 { func KLineOpenPriceMapper(k types.KLine) float64 {
return k.Open.Float64() return k.Open.Float64()
@ -24,7 +24,7 @@ func KLineVolumeMapper(k types.KLine) float64 {
return k.Volume.Float64() return k.Volume.Float64()
} }
func MapKLinePrice(kLines []types.KLine, f KLinePriceMapper) (prices []float64) { func MapKLinePrice(kLines []types.KLine, f KLineValueMapper) (prices []float64) {
for _, k := range kLines { for _, k := range kLines {
prices = append(prices, f(k)) prices = append(prices, f(k))
} }

View File

@ -9,7 +9,6 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
type KLineValueMapper func(k types.KLine) float64
//go:generate callbackgen -type Pivot //go:generate callbackgen -type Pivot
type Pivot struct { type Pivot struct {

View File

@ -1,11 +1,8 @@
package indicator package indicator
import ( import (
"fmt"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
@ -45,9 +42,8 @@ func (inc *PivotLow) Update(value float64) {
return return
} }
low, err := calculatePivotLow(inc.Lows, inc.Window) low, ok := calculatePivotLow(inc.Lows, inc.Window, inc.RightWindow)
if err != nil { if !ok {
log.WithError(err).Errorf("can not calculate pivot low")
return return
} }
@ -66,17 +62,43 @@ func (inc *PivotLow) PushK(k types.KLine) {
inc.EmitUpdate(inc.Last()) inc.EmitUpdate(inc.Last())
} }
func calculatePivotLow(lows types.Float64Slice, window int) (float64, error) { func calculatePivotF(values types.Float64Slice, left, right int, f func(a, pivot float64) bool) (float64, bool) {
length := len(lows) length := len(values)
if length == 0 || length < window {
return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) if right == 0 {
right = left
}
if length == 0 || length < left+right+1 {
return 0.0, false
} }
end := length - 1 end := length - 1
min := lows[end-(window-1):].Min() index := end - right
if min == lows.Index(int(window/2.)-1) { val := values[index]
return min, nil
for i := index - left; i <= index+right; i++ {
if i == index {
continue
} }
return 0., nil // return if we found lower value
if !f(values[i], val) {
return 0.0, false
}
}
return val, true
}
func calculatePivotHigh(highs types.Float64Slice, left, right int) (float64, bool) {
return calculatePivotF(highs, left, right, func(a, pivot float64) bool {
return a < pivot
})
}
func calculatePivotLow(lows types.Float64Slice, left, right int) (float64, bool) {
return calculatePivotF(lows, left, right, func(a, pivot float64) bool {
return a > pivot
})
} }

View File

@ -0,0 +1,51 @@
package indicator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_calculatePivotLow(t *testing.T) {
t.Run("normal", func(t *testing.T) {
low, ok := calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 2, 2)
// ^left ----- ^pivot ---- ^right
assert.True(t, ok)
assert.Equal(t, 10.0, low)
low, ok = calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 9.0}, 2, 2)
// ^left ----- ^pivot ---- ^right
assert.False(t, ok)
low, ok = calculatePivotLow([]float64{15.0, 9.0, 12.0, 10.0, 14.0, 15.0}, 2, 2)
// ^left ----- ^pivot ---- ^right
assert.False(t, ok)
})
t.Run("different left and right", func(t *testing.T) {
low, ok := calculatePivotLow([]float64{11.0, 12.0, 16.0, 15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 5, 2)
// ^left ---------------------- ^pivot ---- ^right
assert.True(t, ok)
assert.Equal(t, 10.0, low)
low, ok = calculatePivotLow([]float64{9.0, 8.0, 16.0, 15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 5, 2)
// ^left ---------------------- ^pivot ---- ^right
// 8.0 < 10.0
assert.False(t, ok)
assert.Equal(t, 0.0, low)
})
t.Run("right window 0", func(t *testing.T) {
low, ok := calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 2, 0)
assert.True(t, ok)
assert.Equal(t, 10.0, low)
})
t.Run("insufficient length", func(t *testing.T) {
low, ok := calculatePivotLow([]float64{15.0, 13.0, 12.0, 10.0, 14.0, 15.0}, 3, 3)
assert.False(t, ok)
assert.Equal(t, 0.0, low)
})
}

View File

@ -86,7 +86,7 @@ func (inc *SMA) LoadK(allKLines []types.KLine) {
} }
} }
func calculateSMA(kLines []types.KLine, window int, priceF KLinePriceMapper) (float64, error) { func calculateSMA(kLines []types.KLine, window int, priceF KLineValueMapper) (float64, error) {
length := len(kLines) length := len(kLines)
if length == 0 || length < window { if length == 0 || length < window {
return 0.0, fmt.Errorf("insufficient elements for calculating SMA with window = %d", window) return 0.0, fmt.Errorf("insufficient elements for calculating SMA with window = %d", window)

View File

@ -22,9 +22,9 @@ type TEMA struct {
func (inc *TEMA) Update(value float64) { func (inc *TEMA) Update(value float64) {
if len(inc.Values) == 0 { if len(inc.Values) == 0 {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
inc.A1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.A1 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.A2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.A2 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.A3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.A3 = &EWMA{IntervalWindow: inc.IntervalWindow}
} }
inc.A1.Update(value) inc.A1.Update(value)
a1 := inc.A1.Last() a1 := inc.A1.Last()

View File

@ -33,12 +33,12 @@ func (inc *TILL) Update(value float64) {
inc.VolumeFactor = defaultVolumeFactor inc.VolumeFactor = defaultVolumeFactor
} }
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
inc.e1 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e1 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.e2 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e2 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.e3 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e3 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.e4 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e4 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.e5 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e5 = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.e6 = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.e6 = &EWMA{IntervalWindow: inc.IntervalWindow}
square := inc.VolumeFactor * inc.VolumeFactor square := inc.VolumeFactor * inc.VolumeFactor
cube := inc.VolumeFactor * square cube := inc.VolumeFactor * square
inc.c1 = -cube inc.c1 = -cube

View File

@ -19,8 +19,8 @@ func (inc *TMA) Update(value float64) {
if inc.s1 == nil { if inc.s1 == nil {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
w := (inc.Window + 1) / 2 w := (inc.Window + 1) / 2
inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}} inc.s1 = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: w}}
inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{inc.Interval, w}} inc.s2 = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: w}}
} }
inc.s1.Update(value) inc.s1.Update(value)

View File

@ -91,7 +91,7 @@ func (inc *Volatility) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
} }
func calculateVOLATILITY(klines []types.KLine, window int, priceF KLinePriceMapper) (float64, error) { func calculateVOLATILITY(klines []types.KLine, window int, priceF KLineValueMapper) (float64, error) {
length := len(klines) length := len(klines)
if length == 0 || length < window { if length == 0 || length < window {
return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window)

View File

@ -100,7 +100,7 @@ func (inc *VWAP) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
} }
func calculateVWAP(klines []types.KLine, priceF KLinePriceMapper, window int) float64 { func calculateVWAP(klines []types.KLine, priceF KLineValueMapper, window int) float64 {
vwap := VWAP{IntervalWindow: types.IntervalWindow{Window: window}} vwap := VWAP{IntervalWindow: types.IntervalWindow{Window: window}}
for _, k := range klines { for _, k := range klines {
vwap.Update(priceF(k), k.Volume.Float64()) vwap.Update(priceF(k), k.Volume.Float64())

View File

@ -70,6 +70,15 @@ func (inc *VWMA) Update(price, volume float64) {
inc.Values.Push(vwma) inc.Values.Push(vwma)
} }
func (inc *VWMA) PushK(k types.KLine) {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
return
}
inc.Update(k.Close.Float64(), k.Volume.Float64())
}
func (inc *VWMA) CalculateAndUpdate(allKLines []types.KLine) { func (inc *VWMA) CalculateAndUpdate(allKLines []types.KLine) {
if len(allKLines) < inc.Window { if len(allKLines) < inc.Window {
return return

View File

@ -43,7 +43,7 @@ func (inc *ZLEMA) Length() int {
func (inc *ZLEMA) Update(value float64) { func (inc *ZLEMA) Update(value float64) {
if inc.lag == 0 || inc.zlema == nil { if inc.lag == 0 || inc.zlema == nil {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
inc.zlema = &EWMA{IntervalWindow: types.IntervalWindow{inc.Interval, inc.Window}} inc.zlema = &EWMA{IntervalWindow: inc.IntervalWindow}
inc.lag = int((float64(inc.Window)-1.)/2. + 0.5) inc.lag = int((float64(inc.Window)-1.)/2. + 0.5)
} }
inc.data.Push(value) inc.data.Push(value)

View File

@ -434,8 +434,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Setup dynamic spread // Setup dynamic spread
if s.DynamicSpread.Enabled { if s.DynamicSpread.Enabled {
s.DynamicSpread.DynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.DynamicSpread.Window}} s.DynamicSpread.DynamicBidSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.DynamicSpread.Window}}
s.DynamicSpread.DynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.DynamicSpread.Window}} s.DynamicSpread.DynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.DynamicSpread.Window}}
} }
if s.DisableShort { if s.DisableShort {

View File

@ -77,8 +77,11 @@ type IntervalWindow struct {
// The interval of kline // The interval of kline
Interval Interval `json:"interval"` Interval Interval `json:"interval"`
// The windows size of the indicator (EWMA and SMA) // The windows size of the indicator (for example, EWMA and SMA)
Window int `json:"window"` Window int `json:"window"`
// RightWindow is used by the pivot indicator
RightWindow int `json:"rightWindow"`
} }
type IntervalWindowBandWidth struct { type IntervalWindowBandWidth struct {