Skip to content

Commit 3aeb691

Browse files
authored
Merge pull request #846 from c9s/strategy/pivotshort
strategy/pivotshort: refactor breaklow + add fake break stop
2 parents 4fd571d + 4c6fe11 commit 3aeb691

File tree

7 files changed

+125
-74
lines changed

7 files changed

+125
-74
lines changed

config/pivotshort.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ exchangeStrategies:
4242
# Notice: When marketOrder is set, bounceRatio will not be used.
4343
# bounceRatio: 0.1%
4444

45-
# stopEMARange is the price range we allow short.
45+
# stopEMA is the price range we allow short.
4646
# Short-allowed price range = [current price] > [EMA] * (1 - [stopEMARange])
4747
# Higher the stopEMARange than higher the chance to open a short
48-
stopEMARange: 2%
4948
stopEMA:
5049
interval: 1h
5150
window: 99
51+
range: 2%
5252

5353
trendEMA:
5454
interval: 1d

pkg/bbgo/exit_cumulated_volume_take_profit.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@ func (s *CumulatedVolumeTakeProfit) Bind(session *ExchangeSession, orderExecutor
7272
cqv.Float64(),
7373
s.MinQuoteVolume.Float64(), kline.Close.Float64())
7474

75-
_ = orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit")
75+
if err := orderExecutor.ClosePosition(context.Background(), fixedpoint.One, "cumulatedVolumeTakeProfit") ; err != nil {
76+
log.WithError(err).Errorf("close position error")
77+
}
7678
return
7779
}
7880
}))

pkg/bbgo/standard_indicator_set.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type StandardIndicatorSet struct {
2525
// interval -> window
2626
boll map[types.IntervalWindowBandWidth]*indicator.BOLL
2727
stoch map[types.IntervalWindow]*indicator.STOCH
28-
simples map[types.IntervalWindow]indicator.Simple
28+
simples map[types.IntervalWindow]indicator.KLinePusher
2929

3030
stream types.Stream
3131
store *MarketDataStore
@@ -36,7 +36,7 @@ func NewStandardIndicatorSet(symbol string, stream types.Stream, store *MarketDa
3636
Symbol: symbol,
3737
store: store,
3838
stream: stream,
39-
simples: make(map[types.IntervalWindow]indicator.Simple),
39+
simples: make(map[types.IntervalWindow]indicator.KLinePusher),
4040

4141
boll: make(map[types.IntervalWindowBandWidth]*indicator.BOLL),
4242
stoch: make(map[types.IntervalWindow]*indicator.STOCH),
@@ -53,7 +53,7 @@ func (s *StandardIndicatorSet) initAndBind(inc indicator.KLinePusher, iw types.I
5353
s.stream.OnKLineClosed(types.KLineWith(s.Symbol, iw.Interval, inc.PushK))
5454
}
5555

56-
func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.Simple, iw types.IntervalWindow) indicator.Simple {
56+
func (s *StandardIndicatorSet) allocateSimpleIndicator(t indicator.KLinePusher, iw types.IntervalWindow) indicator.KLinePusher {
5757
inc, ok := s.simples[iw]
5858
if ok {
5959
return inc

pkg/indicator/pivot_low.go

+7-10
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import (
1111

1212
//go:generate callbackgen -type PivotLow
1313
type PivotLow struct {
14-
types.IntervalWindow
1514
types.SeriesBase
1615

16+
types.IntervalWindow
17+
1718
Lows types.Float64Slice
1819
Values types.Float64Slice
1920
EndTime time.Time
@@ -71,15 +72,11 @@ func calculatePivotLow(lows types.Float64Slice, window int) (float64, error) {
7172
return 0., fmt.Errorf("insufficient elements for calculating with window = %d", window)
7273
}
7374

74-
var pv types.Float64Slice
75-
for _, low := range lows {
76-
pv.Push(low)
77-
}
78-
79-
pl := 0.
80-
if lows.Min() == lows.Index(int(window/2.)-1) {
81-
pl = lows.Min()
75+
end := length - 1
76+
min := lows[end-(window-1):].Min()
77+
if min == lows.Index(int(window/2.)-1) {
78+
return min, nil
8279
}
8380

84-
return pl, nil
81+
return 0., nil
8582
}

pkg/risk/account_value.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
164164
leverage = fixedpoint.NewFromInt(3)
165165
}
166166

167+
167168
baseBalance, _ := session.Account.Balance(market.BaseCurrency)
168169
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency)
169170

@@ -185,11 +186,11 @@ func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, p
185186
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings")
186187
}
187188

188-
// using leverage -- starts from here
189189
if !quantity.IsZero() {
190190
return quantity, nil
191191
}
192192

193+
// using leverage -- starts from here
193194
logrus.Infof("calculating available leveraged base quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance)
194195

195196
// calculate the quantity automatically

pkg/strategy/pivotshort/breaklow.go

+106-55
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ import (
1010
"github.com/c9s/bbgo/pkg/types"
1111
)
1212

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+
1326
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
1427
type BreakLow struct {
1528
Symbol string
@@ -26,22 +39,28 @@ type BreakLow struct {
2639
// limit sell price = breakLowPrice * (1 + BounceRatio)
2740
BounceRatio fixedpoint.Value `json:"bounceRatio"`
2841

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"`
3350

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
3558

36-
lastLow fixedpoint.Value
37-
pivot *indicator.PivotLow
3859
stopEWMA *indicator.EWMA
3960

4061
trendEWMA *indicator.EWMA
4162
trendEWMALast, trendEWMACurrent float64
4263

43-
pivotLowPrices []fixedpoint.Value
44-
4564
orderExecutor *bbgo.GeneralOrderExecutor
4665
session *bbgo.ExchangeSession
4766
}
@@ -57,6 +76,10 @@ func (s *BreakLow) Subscribe(session *bbgo.ExchangeSession) {
5776
if s.TrendEMA != nil {
5877
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval})
5978
}
79+
80+
if s.FakeBreakStop != nil {
81+
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.FakeBreakStop.Interval})
82+
}
6083
}
6184

6285
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
@@ -69,14 +92,14 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
6992

7093
s.lastLow = fixedpoint.Zero
7194

72-
s.pivot = standardIndicator.PivotLow(s.IntervalWindow)
95+
s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow)
7396

7497
if s.StopEMA != nil {
75-
s.stopEWMA = standardIndicator.EWMA(*s.StopEMA)
98+
s.stopEWMA = standardIndicator.EWMA(s.StopEMA.IntervalWindow)
7699
}
77100

78101
if s.TrendEMA != nil {
79-
s.trendEWMA = standardIndicator.EWMA(*s.TrendEMA)
102+
s.trendEWMA = standardIndicator.EWMA(s.TrendEMA.IntervalWindow)
80103

81104
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.TrendEMA.Interval, func(kline types.KLine) {
82105
s.trendEWMALast = s.trendEWMACurrent
@@ -86,58 +109,52 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
86109

87110
// update pivot low data
88111
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())
114114
}
115115

116-
bbgo.Notify("%s %f quantity will be used for shorting", s.Symbol, quantity.Float64())
116+
s.pilotQuantityCalculation()
117117
})
118118

119119
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+
}
124125

125-
if lastLow.Compare(s.lastLow) == 0 {
126-
return
126+
bbgo.Notify("%s new pivot low: %f", s.Symbol, s.pivotLow.Last())
127127
}
128+
}))
128129

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+
}
131138

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+
}
136143

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+
}
139150

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) {
141158
if len(s.pivotLowPrices) == 0 {
142159
log.Infof("currently there is no pivot low prices, can not check break low...")
143160
return
@@ -170,6 +187,10 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
170187

171188
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
172189

190+
if s.lastBreakLow.IsZero() || previousLow.Compare(s.lastBreakLow) < 0 {
191+
s.lastBreakLow = previousLow
192+
}
193+
173194
if position.IsOpened(kline.Close) {
174195
log.Infof("position is already opened, skip short")
175196
return
@@ -193,9 +214,9 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
193214
return
194215
}
195216

196-
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange))
217+
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMA.Range))
197218
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())
199220
return
200221
}
201222
}
@@ -241,3 +262,33 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
241262
}
242263
}))
243264
}
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+
}

pkg/strategy/pivotshort/resistance.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
5050
s.resistancePivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow)
5151

5252
// use the last kline from the history before we get the next closed kline
53-
s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Lows.Last()))
53+
s.updateResistanceOrders(fixedpoint.NewFromFloat(s.resistancePivot.Last()))
5454

5555
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
5656
position := s.orderExecutor.Position()
@@ -77,7 +77,7 @@ func tail(arr []float64, length int) []float64 {
7777
func (s *ResistanceShort) updateCurrentResistancePrice(closePrice fixedpoint.Value) bool {
7878
minDistance := s.MinDistance.Float64()
7979
groupDistance := s.GroupDistance.Float64()
80-
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, tail(s.resistancePivot.Lows, 6))
80+
resistancePrices := findPossibleResistancePrices(closePrice.Float64()*(1.0+minDistance), groupDistance, s.resistancePivot.Values.Tail(6))
8181
if len(resistancePrices) == 0 {
8282
return false
8383
}

0 commit comments

Comments
 (0)