diff --git a/pkg/bbgo/doc.go b/pkg/bbgo/doc.go new file mode 100644 index 000000000..ceb21a00a --- /dev/null +++ b/pkg/bbgo/doc.go @@ -0,0 +1,3 @@ +// Package bbgo provides the core BBGO API for strategies + +package bbgo diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index 3b16061bf..09ef5d082 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -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 diff --git a/pkg/indicator/dema.go b/pkg/indicator/dema.go index bec582329..993d2c447 100644 --- a/pkg/indicator/dema.go +++ b/pkg/indicator/dema.go @@ -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) diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index 05136f4e4..50d0059ca 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -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) } diff --git a/pkg/indicator/ewma_test.go b/pkg/indicator/ewma_test.go index f781f251c..23bc81285 100644 --- a/pkg/indicator/ewma_test.go +++ b/pkg/indicator/ewma_test.go @@ -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 diff --git a/pkg/indicator/hull.go b/pkg/indicator/hull.go index 4352bbba0..df3e8c569 100644 --- a/pkg/indicator/hull.go +++ b/pkg/indicator/hull.go @@ -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) diff --git a/pkg/indicator/mapper.go b/pkg/indicator/mapper.go index b101e62ae..e169bef16 100644 --- a/pkg/indicator/mapper.go +++ b/pkg/indicator/mapper.go @@ -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)) } diff --git a/pkg/indicator/pivot.go b/pkg/indicator/pivot.go index e3912afea..e781475b2 100644 --- a/pkg/indicator/pivot.go +++ b/pkg/indicator/pivot.go @@ -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 { diff --git a/pkg/indicator/pivot_low.go b/pkg/indicator/pivot_low.go index 8f605dfe0..4603b1244 100644 --- a/pkg/indicator/pivot_low.go +++ b/pkg/indicator/pivot_low.go @@ -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 + }) } diff --git a/pkg/indicator/pivot_low_test.go b/pkg/indicator/pivot_low_test.go new file mode 100644 index 000000000..318df37a7 --- /dev/null +++ b/pkg/indicator/pivot_low_test.go @@ -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) + }) + +} diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index d2209714a..a6a93089c 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -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) diff --git a/pkg/indicator/tema.go b/pkg/indicator/tema.go index 37a9f78b0..60affd0e5 100644 --- a/pkg/indicator/tema.go +++ b/pkg/indicator/tema.go @@ -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() diff --git a/pkg/indicator/till.go b/pkg/indicator/till.go index 08d64130a..6c1860b2e 100644 --- a/pkg/indicator/till.go +++ b/pkg/indicator/till.go @@ -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 diff --git a/pkg/indicator/tma.go b/pkg/indicator/tma.go index 31600f2c4..97c5997d5 100644 --- a/pkg/indicator/tma.go +++ b/pkg/indicator/tma.go @@ -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) diff --git a/pkg/indicator/volatility.go b/pkg/indicator/volatility.go index 9d86c7f31..a87cc022a 100644 --- a/pkg/indicator/volatility.go +++ b/pkg/indicator/volatility.go @@ -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) diff --git a/pkg/indicator/vwap.go b/pkg/indicator/vwap.go index a95cee206..c040e866c 100644 --- a/pkg/indicator/vwap.go +++ b/pkg/indicator/vwap.go @@ -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()) diff --git a/pkg/indicator/vwma.go b/pkg/indicator/vwma.go index 08e45f4b2..263c5fb5a 100644 --- a/pkg/indicator/vwma.go +++ b/pkg/indicator/vwma.go @@ -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 diff --git a/pkg/indicator/zlema.go b/pkg/indicator/zlema.go index cb7dfbde3..482ecd09d 100644 --- a/pkg/indicator/zlema.go +++ b/pkg/indicator/zlema.go @@ -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) diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index a59aae43b..8d1f0d325 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -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 { diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 2809ab56c..8d3958afc 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -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 {