Merge pull request #846 from c9s/strategy/pivotshort

strategy/pivotshort: refactor breaklow + add fake break stop
This commit is contained in:
Yo-An Lin 2022-07-27 12:18:50 +08:00 committed by GitHub
commit 3aeb6912c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 128 additions and 77 deletions

View File

@ -42,13 +42,13 @@ exchangeStrategies:
# Notice: When marketOrder is set, bounceRatio will not be used. # Notice: When marketOrder is set, bounceRatio will not be used.
# bounceRatio: 0.1% # 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]) # Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
# Higher the stopEMARange than higher the chance to open a short # Higher the stopEMARange than higher the chance to open a short
stopEMARange: 2%
stopEMA: stopEMA:
interval: 1h interval: 1h
window: 99 window: 99
range: 2%
trendEMA: trendEMA:
interval: 1d interval: 1d

View File

@ -72,7 +72,9 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
cqv.Float64(), cqv.Float64(),
s.MinQuoteVolume.Float64(), kline.Close.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 return
} }
})) }))

View File

@ -25,7 +25,7 @@ type StandardIndicatorSet struct {
// interval -> window // interval -> window
boll map[types.IntervalWindowBandWidth]*indicator.BOLL boll map[types.IntervalWindowBandWidth]*indicator.BOLL
stoch map[types.IntervalWindow]*indicator.STOCH stoch map[types.IntervalWindow]*indicator.STOCH
simples map[types.IntervalWindow]indicator.Simple simples map[types.IntervalWindow]indicator.KLinePusher
stream types.Stream stream types.Stream
store *MarketDataStore store *MarketDataStore
@ -36,7 +36,7 @@ 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.Simple), simples: make(map[types.IntervalWindow]indicator.KLinePusher),
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL), boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
stoch: make(map[types.IntervalWindow]*indicator.STOCH), 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)) 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] inc, ok := s.simples[iw]
if ok { if ok {
return inc return inc

View File

@ -11,9 +11,10 @@ import (
//go:generate callbackgen -type PivotLow //go:generate callbackgen -type PivotLow
type PivotLow struct { type PivotLow struct {
types.IntervalWindow
types.SeriesBase types.SeriesBase
types.IntervalWindow
Lows types.Float64Slice Lows types.Float64Slice
Values types.Float64Slice Values types.Float64Slice
EndTime time.Time 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) return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window)
} }
var pv types.Float64Slice end := length - 1
for _, low := range lows { min := lows[end-(window-1):].Min()
pv.Push(low) if min == lows.Index(int(window/2.)-1) {
return min, nil
} }
pl := 0. return 0., nil
if lows.Min() == lows.Index(int(window/2.)-1) {
pl = lows.Min()
}
return pl, nil
} }

View File

@ -164,6 +164,7 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
leverage = fixedpoint.NewFromInt(3) leverage = fixedpoint.NewFromInt(3)
} }
baseBalance, _ := session.Account.Balance(market.BaseCurrency) baseBalance, _ := session.Account.Balance(market.BaseCurrency)
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency) 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") 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() { if !quantity.IsZero() {
return quantity, nil return quantity, nil
} }
// using leverage -- starts from here
logrus.Infof("calculating available leveraged base quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance) logrus.Infof("calculating available leveraged base quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance)
// calculate the quantity automatically // calculate the quantity automatically

View File

@ -10,6 +10,19 @@ import (
"github.com/c9s/bbgo/pkg/types" "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 // BreakLow -- when price breaks the previous pivot low, we set a trade entry
type BreakLow struct { type BreakLow struct {
Symbol string Symbol string
@ -28,20 +41,26 @@ type BreakLow struct {
Leverage fixedpoint.Value `json:"leverage"` Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"` Quantity fixedpoint.Value `json:"quantity"`
StopEMARange fixedpoint.Value `json:"stopEMARange"`
StopEMA *types.IntervalWindow `json:"stopEMA"`
TrendEMA *types.IntervalWindow `json:"trendEMA"` StopEMA *StopEMA `json:"stopEMA"`
TrendEMA *TrendEMA `json:"trendEMA"`
FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"`
lastLow fixedpoint.Value lastLow fixedpoint.Value
pivot *indicator.PivotLow
// lastBreakLow is the low that the price just break
lastBreakLow fixedpoint.Value
pivotLow *indicator.PivotLow
pivotLowPrices []fixedpoint.Value
stopEWMA *indicator.EWMA stopEWMA *indicator.EWMA
trendEWMA *indicator.EWMA trendEWMA *indicator.EWMA
trendEWMALast, trendEWMACurrent float64 trendEWMALast, trendEWMACurrent float64
pivotLowPrices []fixedpoint.Value
orderExecutor *bbgo.GeneralOrderExecutor orderExecutor *bbgo.GeneralOrderExecutor
session *bbgo.ExchangeSession session *bbgo.ExchangeSession
} }
@ -57,6 +76,10 @@ func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
if s.TrendEMA != nil { if s.TrendEMA != nil {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval}) 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) { 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.lastLow = fixedpoint.Zero
s.pivot = standardIndicator.PivotLow(s.IntervalWindow) s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow)
if s.StopEMA != nil { if s.StopEMA != nil {
s.stopEWMA = standardIndicator.EWMA(*s.StopEMA) s.stopEWMA = standardIndicator.EWMA(s.StopEMA.IntervalWindow)
} }
if s.TrendEMA != nil { 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) { session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.TrendEMA.Interval, func(kline types.KLine) {
s.trendEWMALast = s.trendEWMACurrent s.trendEWMALast = s.trendEWMACurrent
@ -86,58 +109,52 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
// update pivot low data // update pivot low data
session.MarketDataStream.OnStart(func() { session.MarketDataStream.OnStart(func() {
lastLow := fixedpoint.NewFromFloat(s.pivot.Lows.Last()) if s.updatePivotLow() {
if lastLow.IsZero() { bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last())
return
} }
if lastLow.Compare(s.lastLow) != 0 { s.pilotQuantityCalculation()
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())
}) })
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
lastLow := fixedpoint.NewFromFloat(s.pivot.Lows.Last()) if s.updatePivotLow() {
if lastLow.IsZero() {
return
}
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 // when position is opened, do not send pivot low notify
if position.IsOpened(kline.Close) { if position.IsOpened(kline.Close) {
return return
} }
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivot.Lows.Last()) bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.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 { if len(s.pivotLowPrices) == 0 {
log.Infof("currently there is no pivot low prices, can not check break low...") log.Infof("currently there is no pivot low prices, can not check break low...")
return 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()) 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) { if position.IsOpened(kline.Close) {
log.Infof("position is already opened, skip short") log.Infof("position is already opened, skip short")
return return
@ -193,9 +214,9 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
return return
} }
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange)) emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMA.Range))
if closePrice.Compare(emaStopShortPrice) < 0 { 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 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
}

View File

@ -50,7 +50,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow) s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow)
// use the last kline from the history before we get the next closed kline // 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) { session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
position := s.orderExecutor.Position() position := s.orderExecutor.Position()
@ -77,7 +77,7 @@ func tail(arr []float64, length int) []float64 {
func (s *ResistanceShort) updateCurrentResistancePrice(closePrice fixedpoint.Value) bool { func (s *ResistanceShort) updateCurrentResistancePrice(closePrice fixedpoint.Value) bool {
minDistance := s.MinDistance.Float64() minDistance := s.MinDistance.Float64()
groupDistance := s.GroupDistance.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 { if len(resistancePrices) == 0 {
return false return false
} }