bbgo_origin/pkg/strategy/pivotshort/breaklow.go

287 lines
8.1 KiB
Go
Raw Normal View History

package pivotshort
import (
"context"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type FakeBreakStop struct {
2022-07-27 03:47:12 +00:00
types.IntervalWindow
}
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
type BreakLow struct {
Symbol string
Market types.Market
types.IntervalWindow
2022-09-12 15:26:40 +00:00
// 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 fixedpoint.Value `json:"ratio"`
bbgo.OpenPositionOptions
// BounceRatio is a ratio used for placing the limit order sell price
// limit sell price = breakLowPrice * (1 + BounceRatio)
BounceRatio fixedpoint.Value `json:"bounceRatio"`
StopEMA *bbgo.StopEMA `json:"stopEMA"`
TrendEMA *bbgo.TrendEMA `json:"trendEMA"`
FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"`
2022-07-27 03:47:12 +00:00
2022-08-31 08:42:45 +00:00
lastLow, lastFastLow fixedpoint.Value
2022-07-27 03:47:12 +00:00
lastLowInvalidated bool
2022-07-27 03:47:12 +00:00
// lastBreakLow is the low that the price just break
lastBreakLow fixedpoint.Value
2022-08-31 08:42:45 +00:00
pivotLow, fastPivotLow *indicator.PivotLow
pivotLowPrices []fixedpoint.Value
orderExecutor *bbgo.GeneralOrderExecutor
session *bbgo.ExchangeSession
// StrategyController
bbgo.StrategyController
}
2022-07-02 05:21:27 +00:00
func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
2022-07-12 09:13:32 +00:00
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m})
if s.StopEMA != nil {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.StopEMA.Interval})
}
if s.TrendEMA != nil {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval})
}
2022-07-27 03:47:12 +00:00
if s.FakeBreakStop != nil {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.FakeBreakStop.Interval})
2022-07-27 03:47:12 +00:00
}
2022-07-02 05:21:27 +00:00
}
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
2022-09-12 15:26:40 +00:00
if s.FastWindow == 0 {
s.FastWindow = 3
}
s.session = session
s.orderExecutor = orderExecutor
// StrategyController
s.Status = types.StrategyStatusRunning
position := orderExecutor.Position()
symbol := position.Symbol
standardIndicator := session.StandardIndicatorSet(s.Symbol)
s.lastLow = fixedpoint.Zero
s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow)
2022-08-31 08:42:45 +00:00
s.fastPivotLow = standardIndicator.PivotLow(types.IntervalWindow{
Interval: s.Interval,
2022-09-12 15:26:40 +00:00
Window: s.FastWindow, // make it faster
2022-08-31 08:42:45 +00:00
})
if s.StopEMA != nil {
s.StopEMA.Bind(session, orderExecutor)
}
2022-07-13 03:09:57 +00:00
if s.TrendEMA != nil {
s.TrendEMA.Bind(session, orderExecutor)
2022-07-13 03:09:57 +00:00
}
// update pivot low data
2022-07-14 10:35:58 +00:00
session.MarketDataStream.OnStart(func() {
if s.updatePivotLow() {
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last())
2022-07-14 10:35:58 +00:00
}
s.pilotQuantityCalculation()
2022-07-14 10:35:58 +00:00
})
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
if s.updatePivotLow() {
// when position is opened, do not send pivot low notify
if position.IsOpened(kline.Close) {
return
}
2022-07-26 08:50:45 +00:00
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last())
2022-07-26 08:50:45 +00:00
}
}))
if s.FakeBreakStop != nil {
2022-07-27 03:47:12 +00:00
// 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) {
2022-07-27 03:47:12 +00:00
// 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, "fakeBreakStop"); err != nil {
2022-07-27 03:47:12 +00:00
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) {
2022-08-31 08:42:45 +00:00
if len(s.pivotLowPrices) == 0 || s.lastLow.IsZero() {
2022-07-01 10:10:39 +00:00
log.Infof("currently there is no pivot low prices, can not check break low...")
return
}
if s.lastLowInvalidated {
log.Infof("the last low is invalidated, skip")
return
}
2022-08-31 08:42:45 +00:00
previousLow := s.lastLow
ratio := fixedpoint.One.Add(s.Ratio)
breakPrice := previousLow.Mul(ratio)
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}
openPrice := kline.Open
closePrice := kline.Close
// if the previous low is not break, or the kline is not strong enough to break it, skip
if closePrice.Compare(breakPrice) >= 0 {
return
}
// we need the price cross the break line, or we do nothing:
// 1) open > break price > close price
// 2) high > break price > open price and close price
// v2
if !((openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) ||
(kline.High.Compare(breakPrice) > 0 && openPrice.Compare(breakPrice) < 0 && closePrice.Compare(breakPrice) < 0)) {
return
}
// force direction to be down
if closePrice.Compare(openPrice) >= 0 {
bbgo.Notify("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64())
// skip UP klines
return
}
bbgo.Notify("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64(), kline)
2022-07-27 03:47:12 +00:00
if s.lastBreakLow.IsZero() || previousLow.Compare(s.lastBreakLow) < 0 {
s.lastBreakLow = previousLow
}
2022-07-13 03:09:57 +00:00
if position.IsOpened(kline.Close) {
bbgo.Notify("position is already opened, skip")
2022-07-13 03:09:57 +00:00
return
}
// trend EMA protection
2022-07-28 03:29:27 +00:00
if s.TrendEMA != nil && !s.TrendEMA.GradientAllowed() {
bbgo.Notify("trendEMA protection: close price %f, gradient %f", kline.Close.Float64(), s.TrendEMA.Gradient())
return
2022-07-13 03:09:57 +00:00
}
// stop EMA protection
if s.StopEMA != nil {
if !s.StopEMA.Allowed(closePrice) {
return
}
}
ctx := context.Background()
// graceful cancel all active orders
_ = orderExecutor.GracefulCancel(ctx)
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())
opts := s.OpenPositionOptions
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 err := s.orderExecutor.OpenPosition(ctx, opts); err != nil {
log.WithError(err).Errorf("failed to open short position")
}
}))
}
func (s *BreakLow) pilotQuantityCalculation() {
if s.lastLow.IsZero() {
return
}
log.Infof("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f",
s.lastLow.Float64(),
s.Quantity.Float64(),
s.Leverage.Float64())
2022-09-09 09:40:17 +00:00
quantity, err := bbgo.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 {
low := fixedpoint.NewFromFloat(s.pivotLow.Last())
if low.IsZero() {
return false
}
// if the last low is different
lastLowChanged := low.Compare(s.lastLow) != 0
2022-08-31 08:42:45 +00:00
if lastLowChanged {
s.lastLow = low
s.lastLowInvalidated = false
s.pivotLowPrices = append(s.pivotLowPrices, low)
2022-08-31 08:42:45 +00:00
}
fastLow := fixedpoint.NewFromFloat(s.fastPivotLow.Last())
if !fastLow.IsZero() {
if fastLow.Compare(s.lastLow) < 0 {
s.lastLowInvalidated = true
2022-08-31 08:42:45 +00:00
lastLowChanged = false
}
s.lastFastLow = fastLow
2022-08-31 08:42:45 +00:00
}
return lastLowChanged
}