forked from c9s/bbgo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
exit_protective_stop_loss.go
219 lines (185 loc) · 7.05 KB
/
exit_protective_stop_loss.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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 {
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
}
func (s *ProtectiveStopLoss) Subscribe(session *ExchangeSession) {
if s.Interval == "" {
s.Interval = types.Interval1m
}
// use kline to handle roi stop
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
}
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{
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))
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) {
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")
}
}
}