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
// interval -> window
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
stoch map[types.IntervalWindow]*indicator.STOCH
simples map[types.IntervalWindow]indicator.KLinePusher
iwbIndicators map[types.IntervalWindowBandWidth]*indicator.BOLL
iwIndicators map[types.IntervalWindow]indicator.KLinePusher
stream types.Stream
store *MarketDataStore
@ -33,35 +32,33 @@ type StandardIndicatorSet struct {
func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDataStore) *StandardIndicatorSet {
return &StandardIndicatorSet{
Symbol: symbol,
store: store,
stream: stream,
simples: make(map[types.IntervalWindow]indicator.KLinePusher),
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
Symbol: symbol,
store: store,
stream: stream,
iwIndicators: make(map[types.IntervalWindow]indicator.KLinePusher),
iwbIndicators: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
}
}
func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, iw types.IntervalWindow) {
if klines, ok := s.store.KLinesOfInterval(iw.Interval); ok {
func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, interval types.Interval) {
if klines, ok := s.store.KLinesOfInterval(interval); ok {
for _, k := range *klines {
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 {
inc, ok := s.simples[iw]
inc, ok := s.iwIndicators[iw]
if ok {
return inc
}
inc = t
s.initAndBind(inc, iw)
s.simples[iw] = inc
s.initAndBind(inc, iw.Interval)
s.iwIndicators[iw] = inc
return t
}
@ -77,6 +74,13 @@ func (s *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *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 {
inc := s.allocateSimpleIndicator(&indicator.PivotLow{IntervalWindow: iw}, iw)
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 {
inc, ok := s.stoch[iw]
if !ok {
inc = &indicator.STOCH{IntervalWindow: iw}
s.initAndBind(inc, iw)
s.stoch[iw] = inc
}
return inc
inc := s.allocateSimpleIndicator(&indicator.STOCH{IntervalWindow: iw}, iw)
return inc.(*indicator.STOCH)
}
// 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 {
iwb := types.IntervalWindowBandWidth{IntervalWindow: iw, BandWidth: bandWidth}
inc, ok := s.boll[iwb]
inc, ok := s.iwbIndicators[iwb]
if !ok {
inc = &indicator.BOLL{IntervalWindow: iw, K: bandWidth}
s.initAndBind(inc, iw)
s.initAndBind(inc, iw.Interval)
if debugBOLL {
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)
})
}
s.boll[iwb] = inc
s.iwbIndicators[iwb] = inc
}
return inc

View File

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

View File

@ -83,7 +83,7 @@ func (inc *EWMA) PushK(k types.KLine) {
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)
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) {
type args struct {
allKLines []types.KLine
priceF KLinePriceMapper
priceF KLineValueMapper
window int
}
var input []fixedpoint.Value

View File

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

View File

@ -2,7 +2,7 @@ package indicator
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 {
return k.Open.Float64()
@ -24,7 +24,7 @@ func KLineVolumeMapper(k types.KLine) 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 {
prices = append(prices, f(k))
}

View File

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

View File

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

View File

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

View File

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

View File

@ -91,7 +91,7 @@ func (inc *Volatility) Bind(updater KLineWindowUpdater) {
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)
if length == 0 || length < 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)
}
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}}
for _, k := range klines {
vwap.Update(priceF(k), k.Volume.Float64())

View File

@ -70,6 +70,15 @@ func (inc *VWMA) Update(price, volume float64) {
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) {
if len(allKLines) < inc.Window {
return

View File

@ -43,7 +43,7 @@ func (inc *ZLEMA) Length() int {
func (inc *ZLEMA) Update(value float64) {
if inc.lag == 0 || inc.zlema == nil {
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.data.Push(value)

View File

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

View File

@ -77,8 +77,11 @@ type IntervalWindow struct {
// The interval of kline
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"`
// RightWindow is used by the pivot indicator
RightWindow int `json:"rightWindow"`
}
type IntervalWindowBandWidth struct {