Skip to content

Commit f2ba901

Browse files
authored
Merge pull request #791 from c9s/strategy/pivotshort
strategy: pivotshort: refactor breaklow logics
2 parents 0057fe5 + 9a11fd5 commit f2ba901

File tree

6 files changed

+241
-259
lines changed

6 files changed

+241
-259
lines changed

pkg/bbgo/order_executor_general.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ func (e *GeneralOrderExecutor) BindProfitStats(profitStats *types.ProfitStats) {
6565
}
6666

6767
profitStats.AddProfit(*profit)
68-
Notify(&profitStats)
68+
Notify(profitStats)
6969
})
7070
}
7171

pkg/strategy/bollmaker/strategy.go

+2-30
Original file line numberDiff line numberDiff line change
@@ -215,18 +215,6 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu
215215
return s.orderExecutor.ClosePosition(ctx, percentage)
216216
}
217217

218-
// Deprecated: LoadState method is migrated to the persistence struct tag.
219-
func (s *Strategy) LoadState() error {
220-
var state State
221-
222-
// load position
223-
if err := s.Persistence.Load(&state, ID, s.Symbol, stateKey); err == nil {
224-
s.state = &state
225-
}
226-
227-
return nil
228-
}
229-
230218
func (s *Strategy) getCurrentAllowedExposurePosition(bandPercentage float64) (fixedpoint.Value, error) {
231219
if s.DynamicExposurePositionScale != nil {
232220
v, err := s.DynamicExposurePositionScale.Scale(bandPercentage)
@@ -505,17 +493,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
505493

506494
// If position is nil, we need to allocate a new position for calculation
507495
if s.Position == nil {
508-
// restore state (legacy)
509-
if err := s.LoadState(); err != nil {
510-
return err
511-
}
512-
513-
// fallback to the legacy position struct in the state
514-
if s.state != nil && s.state.Position != nil && !s.state.Position.Base.IsZero() {
515-
s.Position = s.state.Position
516-
} else {
517-
s.Position = types.NewPositionFromMarket(s.Market)
518-
}
496+
s.Position = types.NewPositionFromMarket(s.Market)
519497
}
520498

521499
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
@@ -526,13 +504,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
526504
}
527505

528506
if s.ProfitStats == nil {
529-
if s.state != nil {
530-
// copy profit stats
531-
p2 := s.state.ProfitStats
532-
s.ProfitStats = &p2
533-
} else {
534-
s.ProfitStats = types.NewProfitStats(s.Market)
535-
}
507+
s.ProfitStats = types.NewProfitStats(s.Market)
536508
}
537509

538510
// Always update the position fields

pkg/strategy/pivotshort/breaklow.go

+193
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package pivotshort
2+
3+
import (
4+
"context"
5+
6+
"github.com/c9s/bbgo/pkg/bbgo"
7+
"github.com/c9s/bbgo/pkg/fixedpoint"
8+
"github.com/c9s/bbgo/pkg/indicator"
9+
"github.com/c9s/bbgo/pkg/types"
10+
)
11+
12+
// BreakLow -- when price breaks the previous pivot low, we set a trade entry
13+
type BreakLow struct {
14+
Symbol string
15+
Market types.Market
16+
types.IntervalWindow
17+
18+
// Ratio is a number less than 1.0, price * ratio will be the price triggers the short order.
19+
Ratio fixedpoint.Value `json:"ratio"`
20+
21+
// MarketOrder is the option to enable market order short.
22+
MarketOrder bool `json:"marketOrder"`
23+
24+
// BounceRatio is a ratio used for placing the limit order sell price
25+
// limit sell price = breakLowPrice * (1 + BounceRatio)
26+
BounceRatio fixedpoint.Value `json:"bounceRatio"`
27+
28+
Quantity fixedpoint.Value `json:"quantity"`
29+
StopEMARange fixedpoint.Value `json:"stopEMARange"`
30+
StopEMA *types.IntervalWindow `json:"stopEMA"`
31+
32+
lastLow fixedpoint.Value
33+
pivot *indicator.Pivot
34+
stopEWMA *indicator.EWMA
35+
pivotLowPrices []fixedpoint.Value
36+
37+
orderExecutor *bbgo.GeneralOrderExecutor
38+
session *bbgo.ExchangeSession
39+
}
40+
41+
func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
42+
s.session = session
43+
s.orderExecutor = orderExecutor
44+
45+
position := orderExecutor.Position()
46+
symbol := position.Symbol
47+
store, _ := session.MarketDataStore(symbol)
48+
standardIndicator, _ := session.StandardIndicatorSet(symbol)
49+
50+
s.lastLow = fixedpoint.Zero
51+
52+
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
53+
s.pivot.Bind(store)
54+
preloadPivot(s.pivot, store)
55+
56+
if s.StopEMA != nil {
57+
s.stopEWMA = standardIndicator.EWMA(*s.StopEMA)
58+
}
59+
60+
// update pivot low data
61+
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
62+
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
63+
if lastLow.IsZero() {
64+
return
65+
}
66+
67+
if lastLow.Compare(s.lastLow) != 0 {
68+
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
69+
}
70+
71+
s.lastLow = lastLow
72+
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
73+
}))
74+
75+
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, types.Interval1m, func(kline types.KLine) {
76+
if position.IsOpened(kline.Close) {
77+
return
78+
}
79+
80+
if len(s.pivotLowPrices) == 0 {
81+
log.Infof("currently there is no pivot low prices, skip placing orders...")
82+
return
83+
}
84+
85+
previousLow := s.pivotLowPrices[len(s.pivotLowPrices)-1]
86+
87+
// truncate the pivot low prices
88+
if len(s.pivotLowPrices) > 10 {
89+
s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:]
90+
}
91+
92+
ratio := fixedpoint.One.Add(s.Ratio)
93+
breakPrice := previousLow.Mul(ratio)
94+
95+
openPrice := kline.Open
96+
closePrice := kline.Close
97+
98+
// if previous low is not break, skip
99+
if closePrice.Compare(breakPrice) >= 0 {
100+
return
101+
}
102+
103+
// we need the price cross the break line or we do nothing
104+
if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) {
105+
log.Infof("%s kline is not between the break low price %f", kline.Symbol, breakPrice.Float64())
106+
return
107+
}
108+
109+
// force direction to be down
110+
if closePrice.Compare(openPrice) >= 0 {
111+
log.Infof("%s price %f is closed higher than the open price %f, skip this break", kline.Symbol, closePrice.Float64(), openPrice.Float64())
112+
// skip UP klines
113+
return
114+
}
115+
116+
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
117+
118+
// stop EMA protection
119+
if s.stopEWMA != nil {
120+
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
121+
if ema.IsZero() {
122+
return
123+
}
124+
125+
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMARange))
126+
if closePrice.Compare(emaStopShortPrice) < 0 {
127+
log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.StopEMA, ema.Float64())
128+
return
129+
}
130+
}
131+
132+
ctx := context.Background()
133+
134+
// graceful cancel all active orders
135+
_ = orderExecutor.GracefulCancel(ctx)
136+
137+
quantity := s.useQuantityOrBaseBalance(s.Quantity)
138+
if s.MarketOrder {
139+
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting market sell to open a short position", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64())
140+
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
141+
Symbol: s.Symbol,
142+
Side: types.SideTypeSell,
143+
Type: types.OrderTypeMarket,
144+
Quantity: quantity,
145+
MarginSideEffect: types.SideEffectTypeMarginBuy,
146+
Tag: "breakLowMarket",
147+
})
148+
149+
} else {
150+
sellPrice := previousLow.Mul(fixedpoint.One.Add(s.BounceRatio))
151+
152+
bbgo.Notify("%s price %f breaks the previous low %f with ratio %f, submitting limit sell @ %f", symbol, kline.Close.Float64(), previousLow.Float64(), s.Ratio.Float64(), sellPrice.Float64())
153+
_, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
154+
Symbol: kline.Symbol,
155+
Side: types.SideTypeSell,
156+
Type: types.OrderTypeLimit,
157+
Price: sellPrice,
158+
Quantity: quantity,
159+
MarginSideEffect: types.SideEffectTypeMarginBuy,
160+
Tag: "breakLowLimit",
161+
})
162+
}
163+
}))
164+
165+
if !bbgo.IsBackTesting {
166+
// use market trade to submit short order
167+
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
168+
169+
})
170+
}
171+
}
172+
173+
func (s *BreakLow) useQuantityOrBaseBalance(quantity fixedpoint.Value) fixedpoint.Value {
174+
if s.session.Margin || s.session.IsolatedMargin || s.session.Futures || s.session.IsolatedFutures {
175+
return quantity
176+
}
177+
178+
balance, hasBalance := s.session.Account.Balance(s.Market.BaseCurrency)
179+
if hasBalance {
180+
if quantity.IsZero() {
181+
bbgo.Notify("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String())
182+
quantity = balance.Available
183+
} else {
184+
quantity = fixedpoint.Min(quantity, balance.Available)
185+
}
186+
}
187+
188+
if quantity.IsZero() {
189+
log.Errorf("quantity is zero, can not submit sell order, please check settings")
190+
}
191+
192+
return quantity
193+
}

pkg/strategy/pivotshort/resistance.go

+32-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pivotshort
22

33
import (
44
"context"
5+
"sort"
56

67
"github.com/c9s/bbgo/pkg/bbgo"
78
"github.com/c9s/bbgo/pkg/fixedpoint"
@@ -120,12 +121,12 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
120121
log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64())
121122

122123
orderForms = append(orderForms, types.SubmitOrder{
123-
Symbol: s.Symbol,
124-
Side: types.SideTypeSell,
125-
Type: types.OrderTypeLimitMaker,
126-
Price: price,
127-
Quantity: quantity,
128-
Tag: "resistanceShort",
124+
Symbol: s.Symbol,
125+
Side: types.SideTypeSell,
126+
Type: types.OrderTypeLimitMaker,
127+
Price: price,
128+
Quantity: quantity,
129+
Tag: "resistanceShort",
129130
MarginSideEffect: types.SideEffectTypeMarginBuy,
130131
})
131132

@@ -144,3 +145,28 @@ func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistanceP
144145
}
145146
s.activeOrders.Add(createdOrders...)
146147
}
148+
149+
func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 {
150+
// sort float64 in increasing order
151+
// lower to higher prices
152+
sort.Float64s(lows)
153+
154+
var resistancePrices []float64
155+
for _, low := range lows {
156+
if low < closePrice {
157+
continue
158+
}
159+
160+
last := closePrice
161+
if len(resistancePrices) > 0 {
162+
last = resistancePrices[len(resistancePrices)-1]
163+
}
164+
165+
if (low / last) < (1.0 + minDistance) {
166+
continue
167+
}
168+
resistancePrices = append(resistancePrices, low)
169+
}
170+
171+
return resistancePrices
172+
}

0 commit comments

Comments
 (0)