Merge pull request #940 from c9s/strategy/pivotshort

This commit is contained in:
Yo-An Lin 2022-09-13 10:57:31 +08:00 committed by GitHub
commit a6e01c6c2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 127 additions and 114 deletions

View File

@ -33,8 +33,19 @@ exchangeStrategies:
quantity: 10.0 quantity: 10.0
# marketOrder submits the market sell order when the closed price is lower than the previous pivot low. # marketOrder submits the market sell order when the closed price is lower than the previous pivot low.
# by default we will use market order
marketOrder: true marketOrder: true
# limitOrder place limit order to open the short position instead of using market order
# this is useful when your quantity or leverage is quiet large.
limitOrder: false
# limitOrderTakerRatio is the price ratio to adjust your limit order as a taker order. e.g., 0.1%
# for sell order, 0.1% ratio means your final price = price * (1 - 0.1%)
# for buy order, 0.1% ratio means your final price = price * (1 + 0.1%)
# this is only enabled when the limitOrder option set to true
limitOrderTakerRatio: 0
# bounceRatio is used for calculating the price of the limit sell order. # bounceRatio is used for calculating the price of the limit sell order.
# it's ratio of pivot low bounce when a new pivot low is detected. # it's ratio of pivot low bounce when a new pivot low is detected.
# Sometimes when the price breaks the previous low, the price might be pulled back to a higher price. # Sometimes when the price breaks the previous low, the price might be pulled back to a higher price.

View File

@ -129,34 +129,43 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
type OpenPositionOptions struct { type OpenPositionOptions struct {
// Long is for open a long position // Long is for open a long position
// Long or Short must be set // Long or Short must be set, avoid loading it from the config file
Long bool `json:"long"` // it should be set from the strategy code
Long bool `json:"-" yaml:"-"`
// Short is for open a short position // Short is for open a short position
// Long or Short must be set // Long or Short must be set
Short bool `json:"short"` Short bool `json:"-" yaml:"-"`
// Leverage is used for leveraged position and account // Leverage is used for leveraged position and account
// Leverage is not effected when using non-leverage spot account
Leverage fixedpoint.Value `json:"leverage,omitempty"` Leverage fixedpoint.Value `json:"leverage,omitempty"`
// Quantity will be used first, it will override the leverage if it's given. // Quantity will be used first, it will override the leverage if it's given
Quantity fixedpoint.Value `json:"quantity,omitempty"` Quantity fixedpoint.Value `json:"quantity,omitempty"`
// MarketOrder set to true to open a position with a market order // MarketOrder set to true to open a position with a market order
// default is MarketOrder = true
MarketOrder bool `json:"marketOrder,omitempty"` MarketOrder bool `json:"marketOrder,omitempty"`
// LimitOrder set to true to open a position with a limit order // LimitOrder set to true to open a position with a limit order
LimitOrder bool `json:"limitOrder,omitempty"` LimitOrder bool `json:"limitOrder,omitempty"`
// LimitTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio. // LimitOrderTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio.
// So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order. // So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order.
LimitTakerRatio fixedpoint.Value `json:"limitTakerRatio,omitempty"` //
CurrentPrice fixedpoint.Value `json:"currentPrice,omitempty"` // limitOrderTakerRatio is the price ratio to adjust your limit order as a taker order. e.g., 0.1%
Tags []string `json:"tags"` // for sell order, 0.1% ratio means your final price = price * (1 - 0.1%)
// for buy order, 0.1% ratio means your final price = price * (1 + 0.1%)
// this is only enabled when the limitOrder option set to true
LimitOrderTakerRatio fixedpoint.Value `json:"limitOrderTakerRatio,omitempty"`
Price fixedpoint.Value `json:"-" yaml:"-"`
Tags []string `json:"-" yaml:"-"`
} }
func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) error { func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) error {
price := options.CurrentPrice price := options.Price
submitOrder := types.SubmitOrder{ submitOrder := types.SubmitOrder{
Symbol: e.position.Symbol, Symbol: e.position.Symbol,
Type: types.OrderTypeMarket, Type: types.OrderTypeMarket,
@ -164,13 +173,13 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos
Tag: strings.Join(options.Tags, ","), Tag: strings.Join(options.Tags, ","),
} }
if !options.LimitTakerRatio.IsZero() { if !options.LimitOrderTakerRatio.IsZero() {
if options.Long { if options.Long {
// use higher price to buy (this ensures that our order will be filled) // use higher price to buy (this ensures that our order will be filled)
price = price.Mul(one.Add(options.LimitTakerRatio)) price = price.Mul(one.Add(options.LimitOrderTakerRatio))
} else if options.Short { } else if options.Short {
// use lower price to sell (this ensures that our order will be filled) // use lower price to sell (this ensures that our order will be filled)
price = price.Mul(one.Sub(options.LimitTakerRatio)) price = price.Mul(one.Sub(options.LimitOrderTakerRatio))
} }
} }

View File

@ -19,32 +19,40 @@ type BreakLow struct {
Market types.Market Market types.Market
types.IntervalWindow types.IntervalWindow
// FastWindow is used for fast pivot (this is to to filter the nearest high/low)
FastWindow int `json:"fastWindow"`
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
Ratio fixedpoint.Value `json:"ratio"` Ratio fixedpoint.Value `json:"ratio"`
// MarketOrder is the option to enable market order short. // MarketOrder is the option to enable market order short.
MarketOrder bool `json:"marketOrder"` MarketOrder bool `json:"marketOrder"`
// LimitOrder is the option to use limit order instead of market order to short
LimitOrder bool `json:"limitOrder"`
LimitTakerRatio fixedpoint.Value `json:"limitTakerRatio"`
Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"`
bbgo.OpenPositionOptions
// BounceRatio is a ratio used for placing the limit order sell price // BounceRatio is a ratio used for placing the limit order sell price
// limit sell price = breakLowPrice * (1 + BounceRatio) // limit sell price = breakLowPrice * (1 + BounceRatio)
BounceRatio fixedpoint.Value `json:"bounceRatio"` BounceRatio fixedpoint.Value `json:"bounceRatio"`
Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"`
StopEMA *bbgo.StopEMA `json:"stopEMA"` StopEMA *bbgo.StopEMA `json:"stopEMA"`
TrendEMA *bbgo.TrendEMA `json:"trendEMA"` TrendEMA *bbgo.TrendEMA `json:"trendEMA"`
FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"` FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"`
lastLow fixedpoint.Value lastLow, lastFastLow fixedpoint.Value
// lastBreakLow is the low that the price just break // lastBreakLow is the low that the price just break
lastBreakLow fixedpoint.Value lastBreakLow fixedpoint.Value
pivotLow *indicator.PivotLow pivotLow, fastPivotLow *indicator.PivotLow
pivotLowPrices []fixedpoint.Value pivotLowPrices []fixedpoint.Value
trendEWMALast, trendEWMACurrent float64 trendEWMALast, trendEWMACurrent float64
@ -73,6 +81,10 @@ func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
} }
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
if s.FastWindow == 0 {
s.FastWindow = 3
}
s.session = session s.session = session
s.orderExecutor = orderExecutor s.orderExecutor = orderExecutor
@ -84,8 +96,11 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
standardIndicator := session.StandardIndicatorSet(s.Symbol) standardIndicator := session.StandardIndicatorSet(s.Symbol)
s.lastLow = fixedpoint.Zero s.lastLow = fixedpoint.Zero
s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow) s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow)
s.fastPivotLow = standardIndicator.PivotLow(types.IntervalWindow{
Interval: s.Interval,
Window: s.FastWindow, // make it faster
})
if s.StopEMA != nil { if s.StopEMA != nil {
s.StopEMA.Bind(session, orderExecutor) s.StopEMA.Bind(session, orderExecutor)
@ -143,12 +158,12 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
} }
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, types.Interval1m, func(kline types.KLine) {
if len(s.pivotLowPrices) == 0 { if len(s.pivotLowPrices) == 0 || s.lastLow.IsZero() {
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
} }
previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1] previousLow := s.lastLow
ratio := fixedpoint.One.Add(s.Ratio) ratio := fixedpoint.One.Add(s.Ratio)
breakPrice := previousLow.Mul(ratio) breakPrice := previousLow.Mul(ratio)
@ -207,40 +222,17 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
// graceful cancel all active orders // graceful cancel all active orders
_ = orderExecutor.GracefulCancel(ctx) _ = orderExecutor.GracefulCancel(ctx)
quantity, err := bbgo.CalculateBaseQuantity(s.session, s.Market, closePrice, s.Quantity, s.Leverage) bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, opening short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64())
if err != nil { opts := s.OpenPositionOptions
log.WithError(err).Errorf("quantity calculation error") opts.Short = true
opts.Price = closePrice
opts.Tags = []string{"breakLowMarket"}
if opts.LimitOrder && !s.BounceRatio.IsZero() {
opts.Price = previousLow.Mul(fixedpoint.One.Add(s.BounceRatio))
} }
if quantity.IsZero() { if err := s.orderExecutor.OpenPosition(ctx, opts); err != nil {
log.Warn("quantity is zero, can not submit order, skip") log.WithError(err).Errorf("failed to open short position")
return
}
if s.MarketOrder {
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64())
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: "breakLowMarket",
})
} else {
sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BounceRatio))
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64(), sellPrice.Float64())
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: kline.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Price: sellPrice,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: "breakLowLimit",
})
} }
})) }))
} }
@ -265,12 +257,28 @@ func (s *BreakLow) pilotQuantityCalculation() {
} }
func (s *BreakLow) updatePivotLow() bool { func (s *BreakLow) updatePivotLow() bool {
lastLow := fixedpoint.NewFromFloat(s.pivotLow.Last()) low := fixedpoint.NewFromFloat(s.pivotLow.Last())
if lastLow.IsZero() || lastLow.Compare(s.lastLow) == 0 { if low.IsZero() {
return false return false
} }
s.lastLow = lastLow lastLowChanged := low.Compare(s.lastLow) != 0
s.pivotLowPrices = append(s.pivotLowPrices, lastLow) if lastLowChanged {
return true if s.lastFastLow.IsZero() || low.Compare(s.lastFastLow) > 0 {
s.lastLow = low
s.pivotLowPrices = append(s.pivotLowPrices, low)
}
}
fastLow := fixedpoint.NewFromFloat(s.fastPivotLow.Last())
if !fastLow.IsZero() {
if fastLow.Compare(s.lastLow) < 0 {
// invalidate the last low
s.lastLow = fixedpoint.Zero
lastLowChanged = false
}
s.lastFastLow = fastLow
}
return lastLowChanged
} }

View File

@ -17,6 +17,8 @@ type FailedBreakHigh struct {
// IntervalWindow is used for finding the pivot high // IntervalWindow is used for finding the pivot high
types.IntervalWindow types.IntervalWindow
bbgo.OpenPositionOptions
// BreakInterval is used for checking failed break // BreakInterval is used for checking failed break
BreakInterval types.Interval `json:"breakInterval"` BreakInterval types.Interval `json:"breakInterval"`
@ -25,23 +27,17 @@ type FailedBreakHigh struct {
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order. // Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
Ratio fixedpoint.Value `json:"ratio"` Ratio fixedpoint.Value `json:"ratio"`
// MarketOrder is the option to enable market order short.
MarketOrder bool `json:"marketOrder"`
Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"`
VWMA *types.IntervalWindow `json:"vwma"` VWMA *types.IntervalWindow `json:"vwma"`
StopEMA *bbgo.StopEMA `json:"stopEMA"` StopEMA *bbgo.StopEMA `json:"stopEMA"`
TrendEMA *bbgo.TrendEMA `json:"trendEMA"` TrendEMA *bbgo.TrendEMA `json:"trendEMA"`
lastFailedBreakHigh, lastHigh fixedpoint.Value lastFailedBreakHigh, lastHigh, lastFastHigh fixedpoint.Value
pivotHigh *indicator.PivotHigh pivotHigh, fastPivotHigh *indicator.PivotHigh
vwma *indicator.VWMA vwma *indicator.VWMA
PivotHighPrices []fixedpoint.Value pivotHighPrices []fixedpoint.Value
orderExecutor *bbgo.GeneralOrderExecutor orderExecutor *bbgo.GeneralOrderExecutor
session *bbgo.ExchangeSession session *bbgo.ExchangeSession
@ -82,6 +78,10 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
s.lastHigh = fixedpoint.Zero s.lastHigh = fixedpoint.Zero
s.pivotHigh = standardIndicator.PivotHigh(s.IntervalWindow) s.pivotHigh = standardIndicator.PivotHigh(s.IntervalWindow)
s.fastPivotHigh = standardIndicator.PivotHigh(types.IntervalWindow{
Interval: s.IntervalWindow.Interval,
Window: 3,
})
// StrategyController // StrategyController
s.Status = types.StrategyStatusRunning s.Status = types.StrategyStatusRunning
@ -156,7 +156,7 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
})) }))
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.BreakInterval, func(kline types.KLine) { session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.BreakInterval, func(kline types.KLine) {
if len(s.PivotHighPrices) == 0 || s.lastHigh.IsZero() { if len(s.pivotHighPrices) == 0 || s.lastHigh.IsZero() {
log.Infof("currently there is no pivot high prices, can not check failed break high...") log.Infof("currently there is no pivot high prices, can not check failed break high...")
return return
} }
@ -221,50 +221,21 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
ctx := context.Background() ctx := context.Background()
bbgo.Notify("%s price %f failed breaking the previous high %f with ratio %f, opening short position",
symbol,
kline.Close.Float64(),
previousHigh.Float64(),
s.Ratio.Float64())
// graceful cancel all active orders // graceful cancel all active orders
_ = orderExecutor.GracefulCancel(ctx) _ = orderExecutor.GracefulCancel(ctx)
quantity, err := bbgo.CalculateBaseQuantity(s.session, s.Market, closePrice, s.Quantity, s.Leverage) opts := s.OpenPositionOptions
if err != nil { opts.Short = true
log.WithError(err).Errorf("quantity calculation error") opts.Price = closePrice
} opts.Tags = []string{"FailedBreakHighMarket"}
if err := s.orderExecutor.OpenPosition(ctx, opts); err != nil {
if quantity.IsZero() { log.WithError(err).Errorf("failed to open short position")
log.Warn("quantity is zero, can not submit order, skip")
return
}
if s.MarketOrder {
bbgo.Notify("%s price %f failed breaking the previous high %f with ratio %f, submitting market sell %f to open a short position", symbol, kline.Close.Float64(), previousHigh.Float64(), s.Ratio.Float64(), quantity.Float64())
_, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: "FailedBreakHighMarket",
})
if err != nil {
bbgo.Notify(err.Error())
}
} else {
sellPrice := previousHigh
bbgo.Notify("%s price %f failed breaking the previous high %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousHigh.Float64(), s.Ratio.Float64(), sellPrice.Float64())
_, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: kline.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Price: sellPrice,
Quantity: quantity,
MarginSideEffect: types.SideEffectTypeMarginBuy,
Tag: "FailedBreakHighLimit",
})
if err != nil {
bbgo.Notify(err.Error())
}
} }
})) }))
} }
@ -290,11 +261,25 @@ func (s *FailedBreakHigh) pilotQuantityCalculation() {
func (s *FailedBreakHigh) updatePivotHigh() bool { func (s *FailedBreakHigh) updatePivotHigh() bool {
lastHigh := fixedpoint.NewFromFloat(s.pivotHigh.Last()) lastHigh := fixedpoint.NewFromFloat(s.pivotHigh.Last())
if lastHigh.IsZero() || lastHigh.Compare(s.lastHigh) == 0 { if lastHigh.IsZero() {
return false return false
} }
s.lastHigh = lastHigh lastHighChanged := lastHigh.Compare(s.lastHigh) != 0
s.PivotHighPrices = append(s.PivotHighPrices, lastHigh) if lastHighChanged {
return true s.lastHigh = lastHigh
s.pivotHighPrices = append(s.pivotHighPrices, lastHigh)
}
lastFastHigh := fixedpoint.NewFromFloat(s.fastPivotHigh.Last())
if !lastFastHigh.IsZero() {
if lastFastHigh.Compare(s.lastHigh) > 0 {
// invalidate the last low
s.lastHigh = fixedpoint.Zero
lastHighChanged = false
}
s.lastFastHigh = lastFastHigh
}
return lastHighChanged
} }