diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index a6e21c212..67142b902 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -42,13 +42,13 @@ exchangeStrategies: # Notice: When marketOrder is set, bounceRatio will not be used. # bounceRatio: 0.1% - # stopEMARange is the price range we allow short. + # stopEMA is the price range we allow short. # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange]) # Higher the stopEMARange than higher the chance to open a short - stopEMARange: 2% stopEMA: interval: 1h window: 99 + range: 2% trendEMA: interval: 1d diff --git a/pkg/bbgo/exit_cumulated_volume_take_profit.go b/pkg/bbgo/exit_cumulated_volume_take_profit.go index 8ba7ed65f..e75f6f1cc 100644 --- a/pkg/bbgo/exit_cumulated_volume_take_profit.go +++ b/pkg/bbgo/exit_cumulated_volume_take_profit.go @@ -72,7 +72,9 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor cqv.Float64(), s.MinQuoteVolume.Float64(), kline.Close.Float64()) - _ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") + if err := orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") ; err != nil { + log.WithError(err).Errorf("close position error") + } return } })) diff --git a/pkg/bbgo/standard_indicator_set.go b/pkg/bbgo/standard_indicator_set.go index a43286c30..3b16061bf 100644 --- a/pkg/bbgo/standard_indicator_set.go +++ b/pkg/bbgo/standard_indicator_set.go @@ -25,7 +25,7 @@ type StandardIndicatorSet struct { // interval -> window boll map[types.IntervalWindowBandWidth]*indicator.BOLL stoch map[types.IntervalWindow]*indicator.STOCH - simples map[types.IntervalWindow]indicator.Simple + simples map[types.IntervalWindow]indicator.KLinePusher stream types.Stream store *MarketDataStore @@ -36,7 +36,7 @@ func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDa Symbol: symbol, store: store, stream: stream, - simples: make(map[types.IntervalWindow]indicator.Simple), + simples: make(map[types.IntervalWindow]indicator.KLinePusher), boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL), stoch: make(map[types.IntervalWindow]*indicator.STOCH), @@ -53,7 +53,7 @@ func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, iw types.I s.stream.OnKLineClosed(types.KLineWith(s.Symbol, iw.Interval, inc.PushK)) } -func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.Simple, iw types.IntervalWindow) indicator.Simple { +func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow) indicator.KLinePusher { inc, ok := s.simples[iw] if ok { return inc diff --git a/pkg/indicator/pivot_low.go b/pkg/indicator/pivot_low.go index cdcbb4890..8f605dfe0 100644 --- a/pkg/indicator/pivot_low.go +++ b/pkg/indicator/pivot_low.go @@ -11,9 +11,10 @@ import ( //go:generate callbackgen -type PivotLow type PivotLow struct { - types.IntervalWindow types.SeriesBase + types.IntervalWindow + Lows types.Float64Slice Values types.Float64Slice EndTime time.Time @@ -71,15 +72,11 @@ func calculatePivotLow(lows types.Float64Slice, window int) (float64, error) { return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window) } - var pv types.Float64Slice - for _, low := range lows { - pv.Push(low) + end := length - 1 + min := lows[end-(window-1):].Min() + if min == lows.Index(int(window/2.)-1) { + return min, nil } - pl := 0. - if lows.Min() == lows.Index(int(window/2.)-1) { - pl = lows.Min() - } - - return pl, nil + return 0., nil } diff --git a/pkg/risk/account_value.go b/pkg/risk/account_value.go index e7c881627..66070f4fb 100644 --- a/pkg/risk/account_value.go +++ b/pkg/risk/account_value.go @@ -164,6 +164,7 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p leverage = fixedpoint.NewFromInt(3) } + baseBalance, _ := session.Account.Balance(market.BaseCurrency) quoteBalance, _ := session.Account.Balance(market.QuoteCurrency) @@ -185,11 +186,11 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings") } - // using leverage -- starts from here if !quantity.IsZero() { return quantity, nil } + // using leverage -- starts from here logrus.Infof("calculating available leveraged base quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance) // calculate the quantity automatically diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 565c8336f..d0d744454 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -10,6 +10,19 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +type StopEMA struct { + types.IntervalWindow + Range fixedpoint.Value `json:"range"` +} + +type TrendEMA struct { + types.IntervalWindow +} + +type FakeBreakStop struct { + types.IntervalWindow +} + // BreakLow -- when price breaks the previous pivot low, we set a trade entry type BreakLow struct { Symbol string @@ -26,22 +39,28 @@ type BreakLow struct { // limit sell price = breakLowPrice * (1 + BounceRatio) BounceRatio fixedpoint.Value `json:"bounceRatio"` - Leverage fixedpoint.Value `json:"leverage"` - Quantity fixedpoint.Value `json:"quantity"` - StopEMARange fixedpoint.Value `json:"stopEMARange"` - StopEMA *types.IntervalWindow `json:"stopEMA"` + Leverage fixedpoint.Value `json:"leverage"` + Quantity fixedpoint.Value `json:"quantity"` - TrendEMA *types.IntervalWindow `json:"trendEMA"` + StopEMA *StopEMA `json:"stopEMA"` + + TrendEMA *TrendEMA `json:"trendEMA"` + + FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"` + + lastLow fixedpoint.Value + + // lastBreakLow is the low that the price just break + lastBreakLow fixedpoint.Value + + pivotLow *indicator.PivotLow + pivotLowPrices []fixedpoint.Value - lastLow fixedpoint.Value - pivot *indicator.PivotLow stopEWMA *indicator.EWMA trendEWMA *indicator.EWMA trendEWMALast, trendEWMACurrent float64 - pivotLowPrices []fixedpoint.Value - orderExecutor *bbgo.GeneralOrderExecutor session *bbgo.ExchangeSession } @@ -57,6 +76,10 @@ func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) { if s.TrendEMA != nil { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) } + + if s.FakeBreakStop != nil { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.FakeBreakStop.Interval}) + } } func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { @@ -69,14 +92,14 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener s.lastLow = fixedpoint.Zero - s.pivot = standardIndicator.PivotLow(s.IntervalWindow) + s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow) if s.StopEMA != nil { - s.stopEWMA = standardIndicator.EWMA(*s.StopEMA) + s.stopEWMA = standardIndicator.EWMA(s.StopEMA.IntervalWindow) } if s.TrendEMA != nil { - s.trendEWMA = standardIndicator.EWMA(*s.TrendEMA) + s.trendEWMA = standardIndicator.EWMA(s.TrendEMA.IntervalWindow) session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.TrendEMA.Interval, func(kline types.KLine) { s.trendEWMALast = s.trendEWMACurrent @@ -86,58 +109,52 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // update pivot low data session.MarketDataStream.OnStart(func() { - lastLow := fixedpoint.NewFromFloat(s.pivot.Lows.Last()) - if lastLow.IsZero() { - return + if s.updatePivotLow() { + bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last()) } - if lastLow.Compare(s.lastLow) != 0 { - bbgo.Notify("%s found new pivot low: %f", s.Symbol, s.pivot.Lows.Last()) - } - - s.lastLow = lastLow - s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) - - log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f", - s.lastLow.Float64(), - s.Quantity.Float64(), - s.Leverage.Float64()) - - quantity, err := risk.CalculateBaseQuantity(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage) - if err != nil { - log.WithError(err).Errorf("quantity calculation error") - } - - if quantity.IsZero() { - log.WithError(err).Errorf("quantity is zero, can not submit order") - return - } - - bbgo.Notify("%s %f quantity will be used for shorting", s.Symbol, quantity.Float64()) + s.pilotQuantityCalculation() }) session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { - lastLow := fixedpoint.NewFromFloat(s.pivot.Lows.Last()) - if lastLow.IsZero() { - return + if s.updatePivotLow() { + // when position is opened, do not send pivot low notify + if position.IsOpened(kline.Close) { + return + } + + bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last()) } - - if lastLow.Compare(s.lastLow) == 0 { - return - } - - s.lastLow = lastLow - s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) - - // when position is opened, do not send pivot low notify - if position.IsOpened(kline.Close) { - return - } - - bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivot.Lows.Last()) })) - session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, types.Interval1m, func(kline types.KLine) { + if s.FakeBreakStop != nil { + // if the position is already opened, and we just break the low, this checks if the kline closed above the low, + // so that we can close the position earlier + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.FakeBreakStop.Interval, func(k types.KLine) { + // make sure the position is opened, and it's a short position + if !position.IsOpened(k.Close) || !position.IsShort() { + return + } + + // make sure we recorded the last break low + if s.lastBreakLow.IsZero() { + return + } + + // the kline opened below the last break low, and closed above the last break low + if k.Open.Compare(s.lastBreakLow) < 0 && k.Close.Compare(s.lastBreakLow) > 0 { + bbgo.Notify("kLine closed above the last break low, triggering stop earlier") + if err := s.orderExecutor.ClosePosition(context.Background(), one, "kLineClosedStop"); err != nil { + log.WithError(err).Error("position close error") + } + + // reset to zero + s.lastBreakLow = fixedpoint.Zero + } + })) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { if len(s.pivotLowPrices) == 0 { log.Infof("currently there is no pivot low prices, can not check break low...") return @@ -170,6 +187,10 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64()) + if s.lastBreakLow.IsZero() || previousLow.Compare(s.lastBreakLow) < 0 { + s.lastBreakLow = previousLow + } + if position.IsOpened(kline.Close) { log.Infof("position is already opened, skip short") return @@ -193,9 +214,9 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener return } - emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange)) + emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMA.Range)) if closePrice.Compare(emaStopShortPrice) < 0 { - log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.StopEMA, ema.Float64()) + log.Infof("stopEMA protection: close price %f < EMA(%v %f) * (1 - RANGE %f) = %f", closePrice.Float64(), s.StopEMA, ema.Float64(), s.StopEMA.Range.Float64(), emaStopShortPrice.Float64()) return } } @@ -241,3 +262,33 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener } })) } + +func (s *BreakLow) pilotQuantityCalculation() { + log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f", + s.lastLow.Float64(), + s.Quantity.Float64(), + s.Leverage.Float64()) + + quantity, err := risk.CalculateBaseQuantity(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage) + if err != nil { + log.WithError(err).Errorf("quantity calculation error") + } + + if quantity.IsZero() { + log.WithError(err).Errorf("quantity is zero, can not submit order") + return + } + + bbgo.Notify("%s %f quantity will be used for shorting", s.Symbol, quantity.Float64()) +} + +func (s *BreakLow) updatePivotLow() bool { + lastLow := fixedpoint.NewFromFloat(s.pivotLow.Last()) + if lastLow.IsZero() || lastLow.Compare(s.lastLow) == 0 { + return false + } + + s.lastLow = lastLow + s.pivotLowPrices = append(s.pivotLowPrices, lastLow) + return true +} diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go index 2cf336f79..de054863e 100644 --- a/pkg/strategy/pivotshort/resistance.go +++ b/pkg/strategy/pivotshort/resistance.go @@ -50,7 +50,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow) // use the last kline from the history before we get the next closed kline - s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Lows.Last())) + s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Last())) session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { position := s.orderExecutor.Position() @@ -77,7 +77,7 @@ func tail(arr []float64, length int) []float64 { func (s *ResistanceShort) updateCurrentResistancePrice(closePrice fixedpoint.Value) bool { minDistance := s.MinDistance.Float64() groupDistance := s.GroupDistance.Float64() - resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6)) + resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, s.resistancePivot.Values.Tail(6)) if len(resistancePrices) == 0 { return false }