bbgo_origin/pkg/bbgo/exit_protective_stop_loss.go

220 lines
7.0 KiB
Go
Raw Permalink Normal View History

package bbgo
import (
"context"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
// ProtectiveStopLoss provides a way to protect your profit but also keep a room for the price volatility
// Set ActivationRatio to 1% means if the price is away from your average cost by 1%, we will activate the protective stop loss
// and the StopLossRatio is the minimal profit ratio you want to keep for your position.
// If you set StopLossRatio to 0.1% and ActivationRatio to 1%,
// when the price goes away from your average cost by 1% and then goes back to below your (average_cost * (1 - 0.1%))
// The stop will trigger.
type ProtectiveStopLoss struct {
2022-07-16 06:44:44 +00:00
Symbol string `json:"symbol"`
// ActivationRatio is the trigger condition of this ROI protection stop loss
// When the price goes lower (for short position) with the ratio, the protection stop will be activated.
// This number should be positive to protect the profit
ActivationRatio fixedpoint.Value `json:"activationRatio"`
// StopLossRatio is the ratio for stop loss. This number should be positive to protect the profit.
// negative ratio will cause loss.
StopLossRatio fixedpoint.Value `json:"stopLossRatio"`
// PlaceStopOrder places the stop order on exchange and lock the balance
PlaceStopOrder bool `json:"placeStopOrder"`
// 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"`
session *ExchangeSession
orderExecutor *GeneralOrderExecutor
stopLossPrice fixedpoint.Value
stopLossOrder *types.Order
}
2022-07-16 06:44:44 +00:00
func (s *ProtectiveStopLoss) Subscribe(session *ExchangeSession) {
2024-02-28 07:02:57 +00:00
if s.Interval == "" {
s.Interval = types.Interval1m
}
// use kline to handle roi stop
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
2022-07-16 06:44:44 +00:00
}
func (s *ProtectiveStopLoss) shouldActivate(position *types.Position, closePrice fixedpoint.Value) bool {
if position.IsLong() {
r := one.Add(s.ActivationRatio)
activationPrice := position.AverageCost.Mul(r)
return closePrice.Compare(activationPrice) > 0
} else if position.IsShort() {
r := one.Sub(s.ActivationRatio)
activationPrice := position.AverageCost.Mul(r)
// for short position, if the close price is less than the activation price then this is a profit position.
return closePrice.Compare(activationPrice) < 0
}
return false
}
func (s *ProtectiveStopLoss) placeStopOrder(ctx context.Context, position *types.Position, orderExecutor OrderExecutor) error {
if s.stopLossOrder != nil {
if err := orderExecutor.CancelOrders(ctx, *s.stopLossOrder); err != nil {
log.WithError(err).Errorf("failed to cancel stop limit order: %+v", s.stopLossOrder)
}
s.stopLossOrder = nil
}
createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
2022-09-19 19:39:13 +00:00
Symbol: position.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeStopLimit,
Quantity: position.GetQuantity(),
Price: s.stopLossPrice.Mul(one.Add(fixedpoint.NewFromFloat(0.005))), // +0.5% from the trigger price, slippage protection
StopPrice: s.stopLossPrice,
Market: position.Market,
Tag: "protectiveStopLoss",
MarginSideEffect: types.SideEffectTypeAutoRepay,
})
if len(createdOrders) > 0 {
s.stopLossOrder = &createdOrders[0]
}
return err
}
func (s *ProtectiveStopLoss) shouldStop(closePrice fixedpoint.Value, position *types.Position) bool {
if s.stopLossPrice.IsZero() {
return false
}
if position.IsShort() {
return closePrice.Compare(s.stopLossPrice) >= 0
} else if position.IsLong() {
return closePrice.Compare(s.stopLossPrice) <= 0
}
return false
}
func (s *ProtectiveStopLoss) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
if position.IsClosed() {
s.stopLossOrder = nil
s.stopLossPrice = fixedpoint.Zero
}
})
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if s.stopLossOrder == nil {
return
}
if order.OrderID == s.stopLossOrder.OrderID {
switch order.Status {
case types.OrderStatusFilled, types.OrderStatusCanceled:
s.stopLossOrder = nil
s.stopLossPrice = fixedpoint.Zero
}
}
})
position := orderExecutor.Position()
f := func(kline types.KLine) {
isPositionOpened := !position.IsClosed() && !position.IsDust(kline.Close)
if isPositionOpened {
s.handleChange(context.Background(), position, kline.Close, s.orderExecutor)
} else {
s.stopLossPrice = fixedpoint.Zero
}
}
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, f))
session.MarketDataStream.OnKLine(types.KLineWith(s.Symbol, s.Interval, f))
2022-07-16 16:59:35 +00:00
if !IsBackTesting && enableMarketTradeStop {
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
if trade.Symbol != position.Symbol {
return
}
if s.stopLossPrice.IsZero() || s.PlaceStopOrder {
return
}
s.checkStopPrice(trade.Price, position)
})
}
}
func (s *ProtectiveStopLoss) handleChange(ctx context.Context, position *types.Position, closePrice fixedpoint.Value, orderExecutor *GeneralOrderExecutor) {
if s.stopLossOrder != nil {
// use RESTful to query the order status
// orderQuery := orderExecutor.Session().Exchange.(types.ExchangeOrderQueryService)
// order, err := orderQuery.QueryOrder(ctx, types.OrderQuery{
// Symbol: s.stopLossOrder.Symbol,
// OrderID: strconv.FormatUint(s.stopLossOrder.OrderID, 10),
// })
// if err != nil {
// log.WithError(err).Errorf("query order failed")
// }
}
if s.stopLossPrice.IsZero() {
if s.shouldActivate(position, closePrice) {
// calculate stop loss price
if position.IsShort() {
s.stopLossPrice = position.AverageCost.Mul(one.Sub(s.StopLossRatio))
} else if position.IsLong() {
s.stopLossPrice = position.AverageCost.Mul(one.Add(s.StopLossRatio))
}
Notify("[ProtectiveStopLoss] %s protection (%s) stop loss activated, SL = %f, currentPrice = %f, averageCost = %f",
position.Symbol,
s.StopLossRatio.Percentage(),
s.stopLossPrice.Float64(),
closePrice.Float64(),
position.AverageCost.Float64())
if s.PlaceStopOrder {
if err := s.placeStopOrder(ctx, position, orderExecutor); err != nil {
log.WithError(err).Errorf("failed to place stop limit order")
}
return
}
} else {
// not activated, skip setup stop order
return
}
}
// check stop price
s.checkStopPrice(closePrice, position)
}
func (s *ProtectiveStopLoss) checkStopPrice(closePrice fixedpoint.Value, position *types.Position) {
2022-06-26 11:20:30 +00:00
if s.stopLossPrice.IsZero() {
return
}
if s.shouldStop(closePrice, position) {
Notify("[ProtectiveStopLoss] %s protection stop (%s) is triggered at price %f",
s.Symbol,
s.StopLossRatio.Percentage(),
closePrice.Float64(),
position)
if err := s.orderExecutor.ClosePosition(context.Background(), one, "protectiveStopLoss"); err != nil {
log.WithError(err).Errorf("failed to close position")
}
}
}