bbgo_origin/pkg/bbgo/exit_trailing_stop.go

186 lines
5.2 KiB
Go
Raw Normal View History

2022-07-05 08:10:55 +00:00
package bbgo
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
2022-07-05 08:10:55 +00:00
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type TrailingStop2 struct {
Symbol string
// CallbackRate is the callback rate from the previous high price
CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"`
ActivationRatio fixedpoint.Value `json:"activationRatio,omitempty"`
2022-07-05 08:10:55 +00:00
// ClosePosition is a percentage of the position to be closed
ClosePosition fixedpoint.Value `json:"closePosition,omitempty"`
// MinProfit is the percentage of the minimum profit ratio.
// Stop order will be activated only when the price reaches above this threshold.
MinProfit fixedpoint.Value `json:"minProfit,omitempty"`
// Interval is the time resolution to update the stop order
// KLine per Interval will be used for updating the stop order
Interval types.Interval `json:"interval,omitempty"`
Side types.SideType `json:"side,omitempty"`
latestHigh fixedpoint.Value
// activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop
activated bool
2022-07-05 08:10:55 +00:00
// private fields
session *ExchangeSession
orderExecutor *GeneralOrderExecutor
}
func (s *TrailingStop2) Subscribe(session *ExchangeSession) {
// use 1m kline to handle roi stop
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
2022-07-05 08:10:55 +00:00
}
func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
s.latestHigh = fixedpoint.Zero
2022-07-05 08:10:55 +00:00
position := orderExecutor.Position()
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
if err := s.checkStopPrice(kline.Close, position); err != nil {
log.WithError(err).Errorf("error")
}
2022-07-05 08:10:55 +00:00
}))
2022-07-16 16:59:35 +00:00
if !IsBackTesting && enableMarketTradeStop {
2022-07-05 08:10:55 +00:00
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
if trade.Symbol != position.Symbol {
return
}
if err := s.checkStopPrice(trade.Price, position); err != nil {
log.WithError(err).Errorf("error")
}
2022-07-05 08:10:55 +00:00
})
}
}
2022-07-06 14:35:57 +00:00
// getRatio returns the ratio between the price and the average cost of the position
func (s *TrailingStop2) getRatio(price fixedpoint.Value, position *types.Position) (fixedpoint.Value, error) {
switch s.Side {
case types.SideTypeBuy:
2022-07-06 14:35:57 +00:00
// for short position, it's:
2022-08-11 08:39:16 +00:00
// (avg_cost - price) / avg_cost
return position.AverageCost.Sub(price).Div(position.AverageCost), nil
case types.SideTypeSell:
return price.Sub(position.AverageCost).Div(position.AverageCost), nil
2022-08-11 09:23:42 +00:00
default:
2022-08-11 08:39:16 +00:00
if position.IsLong() {
return price.Sub(position.AverageCost).Div(position.AverageCost), nil
} else if position.IsShort() {
return position.AverageCost.Sub(price).Div(position.AverageCost), nil
}
}
return fixedpoint.Zero, fmt.Errorf("unexpected side type: %v", s.Side)
}
func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.Position) error {
if position.IsClosed() || position.IsDust(price) {
return nil
}
2022-08-11 08:39:16 +00:00
if !s.MinProfit.IsZero() {
// check if we have the minimal profit
roi := position.ROI(price)
if roi.Compare(s.MinProfit) >= 0 {
2023-03-17 02:43:47 +00:00
Notify("[trailingStop] activated: %s ROI %s > minimal profit ratio %f", s.Symbol, roi.Percentage(), s.MinProfit.Float64())
2022-08-11 08:39:16 +00:00
s.activated = true
}
} else if !s.ActivationRatio.IsZero() {
ratio, err := s.getRatio(price, position)
if err != nil {
return err
}
2022-08-11 08:39:16 +00:00
if ratio.Compare(s.ActivationRatio) >= 0 {
s.activated = true
}
}
// update the latest high for the sell order, or the latest low for the buy order
if s.latestHigh.IsZero() {
s.latestHigh = price
} else {
switch s.Side {
case types.SideTypeBuy:
s.latestHigh = fixedpoint.Min(price, s.latestHigh)
case types.SideTypeSell:
s.latestHigh = fixedpoint.Max(price, s.latestHigh)
2022-08-11 09:23:42 +00:00
default:
2022-08-11 08:39:16 +00:00
if position.IsLong() {
s.latestHigh = fixedpoint.Max(price, s.latestHigh)
} else if position.IsShort() {
s.latestHigh = fixedpoint.Min(price, s.latestHigh)
}
}
2022-07-05 08:10:55 +00:00
}
if !s.activated {
return nil
2022-07-05 08:10:55 +00:00
}
switch s.Side {
case types.SideTypeBuy:
2022-08-11 08:39:16 +00:00
change := price.Sub(s.latestHigh).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
case types.SideTypeSell:
change := s.latestHigh.Sub(price).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
2022-08-11 09:23:42 +00:00
default:
2022-08-11 08:39:16 +00:00
if position.IsLong() {
change := s.latestHigh.Sub(price).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
2022-08-11 08:39:16 +00:00
} else if position.IsShort() {
change := price.Sub(s.latestHigh).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
}
}
return nil
}
func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error {
// reset activated flag
defer func() {
s.activated = false
s.latestHigh = fixedpoint.Zero
}()
Notify("[TrailingStop] %s %s stop loss triggered. price: %f callback rate: %f", s.Symbol, s, price.Float64(), s.CallbackRate.Float64())
ctx := context.Background()
p := fixedpoint.One
if !s.ClosePosition.IsZero() {
p = s.ClosePosition
}
return s.orderExecutor.ClosePosition(ctx, p, "trailingStop")
2022-07-05 08:10:55 +00:00
}