Skip to content

Commit

Permalink
exit trailing stop add callback by high profit
Browse files Browse the repository at this point in the history
  • Loading branch information
anywhy committed Oct 27, 2024
1 parent f4df9a0 commit 3219bd0
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 21 deletions.
78 changes: 57 additions & 21 deletions pkg/bbgo/exit_trailing_stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type TrailingStop2 struct {
// CallbackRate is the callback rate from the previous high price
CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"`

// CallbackProfitRate is the callback rate from the previous high profit
CallbackProfitRate fixedpoint.Value `json:"callbackProfitRate,omitempty"`

ActivationRatio fixedpoint.Value `json:"activationRatio,omitempty"`

// ClosePosition is a percentage of the position to be closed
Expand All @@ -31,7 +34,8 @@ type TrailingStop2 struct {

Side types.SideType `json:"side,omitempty"`

latestHigh fixedpoint.Value
latestHigh fixedpoint.Value
latestHighProfit fixedpoint.Value

// activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop
activated bool
Expand Down Expand Up @@ -140,32 +144,48 @@ func (s *TrailingStop2) checkStopPrice(price fixedpoint.Value, position *types.P
return nil
}

switch s.Side {
case types.SideTypeBuy:
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)
}
default:
if position.IsLong() {
change := s.latestHigh.Sub(price).Div(s.latestHigh)
if !s.CallbackRate.IsZero() {
switch s.Side {
case types.SideTypeBuy:
change := price.Sub(s.latestHigh).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
} else if position.IsShort() {
change := price.Sub(s.latestHigh).Div(s.latestHigh)
case types.SideTypeSell:
change := s.latestHigh.Sub(price).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
default:
if position.IsLong() {
change := s.latestHigh.Sub(price).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
} else if position.IsShort() {
change := price.Sub(s.latestHigh).Div(s.latestHigh)
if change.Compare(s.CallbackRate) >= 0 {
// submit order
return s.triggerStop(price)
}
}
}
}

if !s.CallbackProfitRate.IsZero() {
if s.latestHighProfit.IsZero() {
s.latestHighProfit = position.UnrealizedProfit(price)
} else {
profit := position.UnrealizedProfit(price)
change := s.latestHighProfit.Sub(profit).Div(s.latestHighProfit)
if change.Compare(s.CallbackProfitRate) >= 0 {
// submit order
return s.triggerStop(price)
}
s.latestHighProfit = fixedpoint.Max(profit, s.latestHighProfit)
}
}

Expand All @@ -177,16 +197,32 @@ func (s *TrailingStop2) triggerStop(price fixedpoint.Value) error {
defer func() {
s.activated = false
s.latestHigh = fixedpoint.Zero
s.latestHighProfit = fixedpoint.Zero
}()

Notify("[TrailingStop] %s %s tailingStop is triggered. price: %f callbackRate: %s", s.Symbol, s.ActivationRatio.Percentage(), price.Float64(), s.CallbackRate.Percentage())
message := fmt.Sprintf("[TrailingStop] %s %s tailingStop is triggered. price: %f ",
s.Symbol, s.ActivationRatio.Percentage(), price.Float64())
if !s.CallbackRate.IsZero() {
message += fmt.Sprintf(" callbackRate: %s", s.CallbackRate.Percentage())
}
if !s.CallbackProfitRate.IsZero() {
message += fmt.Sprintf(" callbackProfitRate: %s", s.CallbackProfitRate.Percentage())
}
Notify("%s", message)

ctx := context.Background()
p := fixedpoint.One
if !s.ClosePosition.IsZero() {
p = s.ClosePosition
}

tagName := fmt.Sprintf("trailingStop:activation=%s,callback=%s", s.ActivationRatio.Percentage(), s.CallbackRate.Percentage())
tagName := fmt.Sprintf("trailingStop:activation=%s", s.ActivationRatio.Percentage())
if !s.CallbackRate.IsZero() {
tagName += fmt.Sprintf(",callback=%s", s.CallbackRate.Percentage())
}
if !s.CallbackProfitRate.IsZero() {
tagName += fmt.Sprintf(",callbackProfit=%s", s.CallbackProfitRate.Percentage())
}

return s.orderExecutor.ClosePosition(ctx, p, tagName)
}
77 changes: 77 additions & 0 deletions pkg/bbgo/exit_trailing_stop_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,80 @@ func TestTrailingStop_LongPosition(t *testing.T) {
assert.False(t, stop.activated)
}
}

func TestTrailingStop_CallbackProfitRate(t *testing.T) {
market := getTestMarket()

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockEx := mocks.NewMockExchange(mockCtrl)
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
mockEx.EXPECT().SubmitOrder(gomock.Any(), types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeSell,
Type: types.OrderTypeMarket,
Market: market,
Quantity: fixedpoint.NewFromFloat(1.0),
Tag: "trailingStop:activation=1%,callbackProfit=10%",
MarginSideEffect: types.SideEffectTypeAutoRepay,
})

session := NewExchangeSession("test", mockEx)
assert.NotNil(t, session)

session.markets[market.Symbol] = market

position := types.NewPositionFromMarket(market)
position.AverageCost = fixedpoint.NewFromFloat(20000.0)
position.Base = fixedpoint.NewFromFloat(1.0)

orderExecutor := NewGeneralOrderExecutor(session, "BTCUSDT", "test", "test-01", position)

activationRatio := fixedpoint.NewFromFloat(0.01)
callbackProfitRatio := fixedpoint.NewFromFloat(0.1)
stop := &TrailingStop2{
Symbol: "BTCUSDT",
Interval: types.Interval1m,
Side: types.SideTypeSell,
CallbackProfitRate: callbackProfitRatio,
ActivationRatio: activationRatio,
}
stop.Bind(session, orderExecutor)

// the same price
currentPrice := fixedpoint.NewFromFloat(20000.0)
err := stop.checkStopPrice(currentPrice, position)
if assert.NoError(t, err) {
assert.False(t, stop.activated)
}

// 20000 + 1% = 20200
currentPrice = currentPrice.Mul(one.Add(activationRatio))
assert.Equal(t, fixedpoint.NewFromFloat(20200.0), currentPrice)

err = stop.checkStopPrice(currentPrice, position)
if assert.NoError(t, err) {
assert.True(t, stop.activated)
assert.Equal(t, fixedpoint.NewFromFloat(200), stop.latestHighProfit)
}

// 20200 + 1% = 20402
currentPrice = currentPrice.Mul(one.Add(activationRatio))
assert.Equal(t, fixedpoint.NewFromFloat(20402.0), currentPrice)

err = stop.checkStopPrice(currentPrice, position)
if assert.NoError(t, err) {
assert.Equal(t, fixedpoint.NewFromFloat(402), stop.latestHighProfit)
assert.True(t, stop.activated)
}

// 20402 - 402*(1-10%)
currentPrice = currentPrice.Sub(stop.latestHighProfit.Mul(one.Sub(callbackProfitRatio)))
assert.Equal(t, fixedpoint.NewFromFloat(20040.2), currentPrice)
err = stop.checkStopPrice(currentPrice, position)
if assert.NoError(t, err) {
assert.False(t, stop.activated)
assert.Equal(t, fixedpoint.Zero, stop.latestHighProfit)
}
}

0 comments on commit 3219bd0

Please sign in to comment.