Skip to content

Commit cddf357

Browse files
authored
Merge pull request #1104 from c9s/narumi/rebalance/balance
fix: rebalance: adjust max amount by balance
2 parents 60d7d20 + add9372 commit cddf357

File tree

3 files changed

+85
-20
lines changed

3 files changed

+85
-20
lines changed

config/rebalance.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ exchangeStrategies:
3939
maxAmount: 1_000 # max amount to buy or sell per order
4040
orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET
4141
dryRun: false
42+
onStart: false

pkg/strategy/rebalance/order_executor_map.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func (m GeneralOrderExecutorMap) SubmitOrders(ctx context.Context, submitOrders
5555
return nil, fmt.Errorf("order executor not found for symbol %s", submitOrder.Symbol)
5656
}
5757

58-
createdOrders, err := orderExecutor.SubmitOrders(ctx)
58+
createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder)
5959
if err != nil {
6060
return nil, err
6161
}

pkg/strategy/rebalance/strategy.go

+83-19
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Strategy struct {
3434
MaxAmount fixedpoint.Value `json:"maxAmount"` // max amount to buy or sell per order
3535
OrderType types.OrderType `json:"orderType"`
3636
DryRun bool `json:"dryRun"`
37+
OnStart bool `json:"onStart"` // rebalance on start
3738

3839
PositionMap PositionMap `persistence:"positionMap"`
3940
ProfitStatsMap ProfitStatsMap `persistence:"profitStatsMap"`
@@ -114,6 +115,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
114115
s.activeOrderBook = bbgo.NewActiveOrderBook("")
115116
s.activeOrderBook.BindStream(s.session.UserDataStream)
116117

118+
session.UserDataStream.OnStart(func() {
119+
if s.OnStart {
120+
s.rebalance(ctx)
121+
}
122+
})
123+
117124
s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
118125
s.rebalance(ctx)
119126
})
@@ -133,12 +140,17 @@ func (s *Strategy) rebalance(ctx context.Context) {
133140
log.WithError(err).Errorf("failed to cancel orders")
134141
}
135142

136-
submitOrders := s.generateSubmitOrders(ctx)
143+
submitOrders, err := s.generateSubmitOrders(ctx)
144+
if err != nil {
145+
log.WithError(err).Error("failed to generate submit orders")
146+
return
147+
}
137148
for _, order := range submitOrders {
138149
log.Infof("generated submit order: %s", order.String())
139150
}
140151

141152
if s.DryRun {
153+
log.Infof("dry run, not submitting orders")
142154
return
143155
}
144156

@@ -150,7 +162,7 @@ func (s *Strategy) rebalance(ctx context.Context) {
150162
s.activeOrderBook.Add(createdOrders...)
151163
}
152164

153-
func (s *Strategy) prices(ctx context.Context) types.ValueMap {
165+
func (s *Strategy) prices(ctx context.Context) (types.ValueMap, error) {
154166
m := make(types.ValueMap)
155167
for currency := range s.TargetWeights {
156168
if currency == s.QuoteCurrency {
@@ -160,29 +172,37 @@ func (s *Strategy) prices(ctx context.Context) types.ValueMap {
160172

161173
ticker, err := s.session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency)
162174
if err != nil {
163-
log.WithError(err).Error("failed to query tickers")
164-
return nil
175+
return nil, err
165176
}
166177

167-
m[currency] = ticker.Last
178+
m[currency] = ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0))
168179
}
169-
return m
180+
return m, nil
170181
}
171182

172-
func (s *Strategy) quantities() types.ValueMap {
173-
m := make(types.ValueMap)
174-
183+
func (s *Strategy) balances() (types.BalanceMap, error) {
184+
m := make(types.BalanceMap)
175185
balances := s.session.GetAccount().Balances()
176186
for currency := range s.TargetWeights {
177-
m[currency] = balances[currency].Total()
187+
balance, ok := balances[currency]
188+
if !ok {
189+
return nil, fmt.Errorf("no balance for %s", currency)
190+
}
191+
m[currency] = balance
178192
}
179-
180-
return m
193+
return m, nil
181194
}
182195

183-
func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder) {
184-
prices := s.prices(ctx)
185-
marketValues := prices.Mul(s.quantities())
196+
func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder, err error) {
197+
prices, err := s.prices(ctx)
198+
if err != nil {
199+
return nil, err
200+
}
201+
balances, err := s.balances()
202+
if err != nil {
203+
return nil, err
204+
}
205+
marketValues := prices.Mul(balanceToTotal(balances))
186206
currentWeights := marketValues.Normalize()
187207

188208
for currency, targetWeight := range s.TargetWeights {
@@ -221,8 +241,9 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ
221241
quantity = quantity.Abs()
222242
}
223243

224-
if s.MaxAmount.Sign() > 0 {
225-
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, s.MaxAmount)
244+
maxAmount := s.adjustMaxAmountByBalance(side, currency, currentPrice, balances)
245+
if maxAmount.Sign() > 0 {
246+
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, maxAmount)
226247
log.Infof("adjust the quantity %v (%s %s @ %v) by max amount %v",
227248
quantity,
228249
symbol,
@@ -241,10 +262,12 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ
241262
Price: currentPrice,
242263
}
243264

244-
submitOrders = append(submitOrders, order)
265+
if ok := s.checkMinimalOrderQuantity(order); ok {
266+
submitOrders = append(submitOrders, order)
267+
}
245268
}
246269

247-
return submitOrders
270+
return submitOrders, err
248271
}
249272

250273
func (s *Strategy) symbols() (symbols []string) {
@@ -268,3 +291,44 @@ func (s *Strategy) markets() ([]types.Market, error) {
268291
}
269292
return markets, nil
270293
}
294+
295+
func (s *Strategy) adjustMaxAmountByBalance(side types.SideType, currency string, currentPrice fixedpoint.Value, balances types.BalanceMap) fixedpoint.Value {
296+
var maxAmount fixedpoint.Value
297+
298+
switch side {
299+
case types.SideTypeBuy:
300+
maxAmount = balances[s.QuoteCurrency].Available
301+
case types.SideTypeSell:
302+
maxAmount = balances[currency].Available.Mul(currentPrice)
303+
default:
304+
log.Errorf("unknown side type: %s", side)
305+
return fixedpoint.Zero
306+
}
307+
308+
if s.MaxAmount.Sign() > 0 {
309+
maxAmount = fixedpoint.Min(s.MaxAmount, maxAmount)
310+
}
311+
312+
return maxAmount
313+
}
314+
315+
func (s *Strategy) checkMinimalOrderQuantity(order types.SubmitOrder) bool {
316+
if order.Quantity.Compare(order.Market.MinQuantity) < 0 {
317+
log.Infof("order quantity is too small: %f < %f", order.Quantity.Float64(), order.Market.MinQuantity.Float64())
318+
return false
319+
}
320+
321+
if order.Quantity.Mul(order.Price).Compare(order.Market.MinNotional) < 0 {
322+
log.Infof("order min notional is too small: %f < %f", order.Quantity.Mul(order.Price).Float64(), order.Market.MinNotional.Float64())
323+
return false
324+
}
325+
return true
326+
}
327+
328+
func balanceToTotal(balances types.BalanceMap) types.ValueMap {
329+
m := make(types.ValueMap)
330+
for _, b := range balances {
331+
m[b.Currency] = b.Total()
332+
}
333+
return m
334+
}

0 commit comments

Comments
 (0)