Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE:exit trailing stop add callback by high profit #1793

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading