mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #892 from c9s/feature/pivot-right-window
feature: add pivot low right window support
This commit is contained in:
commit
066b0ca30e
3
pkg/bbgo/doc.go
Normal file
3
pkg/bbgo/doc.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
// Package bbgo provides the core BBGO API for strategies
|
||||
|
||||
package bbgo
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
51
pkg/indicator/pivot_low_test.go
Normal file
51
pkg/indicator/pivot_low_test.go
Normal 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)
|
||||
})
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user