@@ -10,6 +10,19 @@ import (
10
10
"github.com/c9s/bbgo/pkg/types"
11
11
)
12
12
13
+ type StopEMA struct {
14
+ types.IntervalWindow
15
+ Range fixedpoint.Value `json:"range"`
16
+ }
17
+
18
+ type TrendEMA struct {
19
+ types.IntervalWindow
20
+ }
21
+
22
+ type FakeBreakStop struct {
23
+ types.IntervalWindow
24
+ }
25
+
13
26
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
14
27
type BreakLow struct {
15
28
Symbol string
@@ -26,22 +39,28 @@ type BreakLow struct {
26
39
// limit sell price = breakLowPrice * (1 + BounceRatio)
27
40
BounceRatio fixedpoint.Value `json:"bounceRatio"`
28
41
29
- Leverage fixedpoint.Value `json:"leverage"`
30
- Quantity fixedpoint.Value `json:"quantity"`
31
- StopEMARange fixedpoint.Value `json:"stopEMARange"`
32
- StopEMA * types.IntervalWindow `json:"stopEMA"`
42
+ Leverage fixedpoint.Value `json:"leverage"`
43
+ Quantity fixedpoint.Value `json:"quantity"`
44
+
45
+ StopEMA * StopEMA `json:"stopEMA"`
46
+
47
+ TrendEMA * TrendEMA `json:"trendEMA"`
48
+
49
+ FakeBreakStop * FakeBreakStop `json:"fakeBreakStop"`
33
50
34
- TrendEMA * types.IntervalWindow `json:"trendEMA"`
51
+ lastLow fixedpoint.Value
52
+
53
+ // lastBreakLow is the low that the price just break
54
+ lastBreakLow fixedpoint.Value
55
+
56
+ pivotLow * indicator.PivotLow
57
+ pivotLowPrices []fixedpoint.Value
35
58
36
- lastLow fixedpoint.Value
37
- pivot * indicator.PivotLow
38
59
stopEWMA * indicator.EWMA
39
60
40
61
trendEWMA * indicator.EWMA
41
62
trendEWMALast , trendEWMACurrent float64
42
63
43
- pivotLowPrices []fixedpoint.Value
44
-
45
64
orderExecutor * bbgo.GeneralOrderExecutor
46
65
session * bbgo.ExchangeSession
47
66
}
@@ -57,6 +76,10 @@ func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
57
76
if s .TrendEMA != nil {
58
77
session .Subscribe (types .KLineChannel , s .Symbol , types.SubscribeOptions {Interval : s .TrendEMA .Interval })
59
78
}
79
+
80
+ if s .FakeBreakStop != nil {
81
+ session .Subscribe (types .KLineChannel , s .Symbol , types.SubscribeOptions {Interval : s .FakeBreakStop .Interval })
82
+ }
60
83
}
61
84
62
85
func (s * BreakLow ) Bind (session * bbgo.ExchangeSession , orderExecutor * bbgo.GeneralOrderExecutor ) {
@@ -69,14 +92,14 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
69
92
70
93
s .lastLow = fixedpoint .Zero
71
94
72
- s .pivot = standardIndicator .PivotLow (s .IntervalWindow )
95
+ s .pivotLow = standardIndicator .PivotLow (s .IntervalWindow )
73
96
74
97
if s .StopEMA != nil {
75
- s .stopEWMA = standardIndicator .EWMA (* s .StopEMA )
98
+ s .stopEWMA = standardIndicator .EWMA (s .StopEMA . IntervalWindow )
76
99
}
77
100
78
101
if s .TrendEMA != nil {
79
- s .trendEWMA = standardIndicator .EWMA (* s .TrendEMA )
102
+ s .trendEWMA = standardIndicator .EWMA (s .TrendEMA . IntervalWindow )
80
103
81
104
session .MarketDataStream .OnKLineClosed (types .KLineWith (s .Symbol , s .TrendEMA .Interval , func (kline types.KLine ) {
82
105
s .trendEWMALast = s .trendEWMACurrent
@@ -86,58 +109,52 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
86
109
87
110
// update pivot low data
88
111
session .MarketDataStream .OnStart (func () {
89
- lastLow := fixedpoint .NewFromFloat (s .pivot .Lows .Last ())
90
- if lastLow .IsZero () {
91
- return
92
- }
93
-
94
- if lastLow .Compare (s .lastLow ) != 0 {
95
- bbgo .Notify ("%s found new pivot low: %f" , s .Symbol , s .pivot .Lows .Last ())
96
- }
97
-
98
- s .lastLow = lastLow
99
- s .pivotLowPrices = append (s .pivotLowPrices , s .lastLow )
100
-
101
- log .Infof ("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f" ,
102
- s .lastLow .Float64 (),
103
- s .Quantity .Float64 (),
104
- s .Leverage .Float64 ())
105
-
106
- quantity , err := risk .CalculateBaseQuantity (s .session , s .Market , s .lastLow , s .Quantity , s .Leverage )
107
- if err != nil {
108
- log .WithError (err ).Errorf ("quantity calculation error" )
109
- }
110
-
111
- if quantity .IsZero () {
112
- log .WithError (err ).Errorf ("quantity is zero, can not submit order" )
113
- return
112
+ if s .updatePivotLow () {
113
+ bbgo .Notify ("%s new pivot low: %f" , s .Symbol , s .pivotLow .Last ())
114
114
}
115
115
116
- bbgo . Notify ( "%s %f quantity will be used for shorting" , s . Symbol , quantity . Float64 () )
116
+ s . pilotQuantityCalculation ( )
117
117
})
118
118
119
119
session .MarketDataStream .OnKLineClosed (types .KLineWith (symbol , s .Interval , func (kline types.KLine ) {
120
- lastLow := fixedpoint .NewFromFloat (s .pivot .Lows .Last ())
121
- if lastLow .IsZero () {
122
- return
123
- }
120
+ if s .updatePivotLow () {
121
+ // when position is opened, do not send pivot low notify
122
+ if position .IsOpened (kline .Close ) {
123
+ return
124
+ }
124
125
125
- if lastLow .Compare (s .lastLow ) == 0 {
126
- return
126
+ bbgo .Notify ("%s new pivot low: %f" , s .Symbol , s .pivotLow .Last ())
127
127
}
128
+ }))
128
129
129
- s .lastLow = lastLow
130
- s .pivotLowPrices = append (s .pivotLowPrices , s .lastLow )
130
+ if s .FakeBreakStop != nil {
131
+ // if the position is already opened, and we just break the low, this checks if the kline closed above the low,
132
+ // so that we can close the position earlier
133
+ session .MarketDataStream .OnKLineClosed (types .KLineWith (s .Symbol , s .FakeBreakStop .Interval , func (k types.KLine ) {
134
+ // make sure the position is opened, and it's a short position
135
+ if ! position .IsOpened (k .Close ) || ! position .IsShort () {
136
+ return
137
+ }
131
138
132
- // when position is opened, do not send pivot low notify
133
- if position . IsOpened ( kline . Close ) {
134
- return
135
- }
139
+ // make sure we recorded the last break low
140
+ if s . lastBreakLow . IsZero ( ) {
141
+ return
142
+ }
136
143
137
- bbgo .Notify ("%s new pivot low: %f" , s .Symbol , s .pivot .Lows .Last ())
138
- }))
144
+ // the kline opened below the last break low, and closed above the last break low
145
+ if k .Open .Compare (s .lastBreakLow ) < 0 && k .Close .Compare (s .lastBreakLow ) > 0 {
146
+ bbgo .Notify ("kLine closed above the last break low, triggering stop earlier" )
147
+ if err := s .orderExecutor .ClosePosition (context .Background (), one , "kLineClosedStop" ); err != nil {
148
+ log .WithError (err ).Error ("position close error" )
149
+ }
139
150
140
- session .MarketDataStream .OnKLineClosed (types .KLineWith (symbol , types .Interval1m , func (kline types.KLine ) {
151
+ // reset to zero
152
+ s .lastBreakLow = fixedpoint .Zero
153
+ }
154
+ }))
155
+ }
156
+
157
+ session .MarketDataStream .OnKLineClosed (types .KLineWith (s .Symbol , types .Interval1m , func (kline types.KLine ) {
141
158
if len (s .pivotLowPrices ) == 0 {
142
159
log .Infof ("currently there is no pivot low prices, can not check break low..." )
143
160
return
@@ -170,6 +187,10 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
170
187
171
188
log .Infof ("%s breakLow signal detected, closed price %f < breakPrice %f" , kline .Symbol , closePrice .Float64 (), breakPrice .Float64 ())
172
189
190
+ if s .lastBreakLow .IsZero () || previousLow .Compare (s .lastBreakLow ) < 0 {
191
+ s .lastBreakLow = previousLow
192
+ }
193
+
173
194
if position .IsOpened (kline .Close ) {
174
195
log .Infof ("position is already opened, skip short" )
175
196
return
@@ -193,9 +214,9 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
193
214
return
194
215
}
195
216
196
- emaStopShortPrice := ema .Mul (fixedpoint .One .Sub (s .StopEMARange ))
217
+ emaStopShortPrice := ema .Mul (fixedpoint .One .Sub (s .StopEMA . Range ))
197
218
if closePrice .Compare (emaStopShortPrice ) < 0 {
198
- log .Infof ("stopEMA protection: close price %f < EMA(%v) = %f" , closePrice .Float64 (), s .StopEMA , ema .Float64 ())
219
+ log .Infof ("stopEMA protection: close price %f < EMA(%v %f) * (1 - RANGE %f) = %f" , closePrice .Float64 (), s .StopEMA , ema . Float64 (), s . StopEMA . Range . Float64 (), emaStopShortPrice .Float64 ())
199
220
return
200
221
}
201
222
}
@@ -241,3 +262,33 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
241
262
}
242
263
}))
243
264
}
265
+
266
+ func (s * BreakLow ) pilotQuantityCalculation () {
267
+ log .Infof ("pilot calculation for max position: last low = %f, quantity = %f, leverage = %f" ,
268
+ s .lastLow .Float64 (),
269
+ s .Quantity .Float64 (),
270
+ s .Leverage .Float64 ())
271
+
272
+ quantity , err := risk .CalculateBaseQuantity (s .session , s .Market , s .lastLow , s .Quantity , s .Leverage )
273
+ if err != nil {
274
+ log .WithError (err ).Errorf ("quantity calculation error" )
275
+ }
276
+
277
+ if quantity .IsZero () {
278
+ log .WithError (err ).Errorf ("quantity is zero, can not submit order" )
279
+ return
280
+ }
281
+
282
+ bbgo .Notify ("%s %f quantity will be used for shorting" , s .Symbol , quantity .Float64 ())
283
+ }
284
+
285
+ func (s * BreakLow ) updatePivotLow () bool {
286
+ lastLow := fixedpoint .NewFromFloat (s .pivotLow .Last ())
287
+ if lastLow .IsZero () || lastLow .Compare (s .lastLow ) == 0 {
288
+ return false
289
+ }
290
+
291
+ s .lastLow = lastLow
292
+ s .pivotLowPrices = append (s .pivotLowPrices , lastLow )
293
+ return true
294
+ }
0 commit comments