-
-
Notifications
You must be signed in to change notification settings - Fork 274
/
exit_trailing_stop.go
185 lines (156 loc) · 5.21 KB
/
exit_trailing_stop.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
package bbgo
import (
"context"
"fmt"
log "github.com/sirupsen/logrus"
"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"`
// 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
// 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})
}
func (s *TrailingStop2) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
s.latestHigh = fixedpoint.Zero
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")
}
}))
if !IsBackTesting && enableMarketTradeStop {
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")
}
})
}
}
// 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:
// for short position, it's:
// (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
default:
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
}
if !s.MinProfit.IsZero() {
// check if we have the minimal profit
roi := position.ROI(price)
if roi.Compare(s.MinProfit) >= 0 {
Notify("[trailingStop] activated: ROI %f > minimal profit ratio %f", roi.Float64(), s.MinProfit.Float64())
s.activated = true
}
} else if !s.ActivationRatio.IsZero() {
ratio, err := s.getRatio(price, position)
if err != nil {
return err
}
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)
default:
if position.IsLong() {
s.latestHigh = fixedpoint.Max(price, s.latestHigh)
} else if position.IsShort() {
s.latestHigh = fixedpoint.Min(price, s.latestHigh)
}
}
}
if !s.activated {
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 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)
}
}
}
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 stop loss triggered. price: %f callback rate: %f", s.Symbol, 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")
}