Merge pull request #1104 from c9s/narumi/rebalance/balance

fix: rebalance: adjust max amount by balance
This commit is contained in:
なるみ 2023-03-14 14:06:31 +08:00 committed by GitHub
commit cddf3570f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 85 additions and 20 deletions

View File

@ -39,3 +39,4 @@ exchangeStrategies:
maxAmount: 1_000 # max amount to buy or sell per order maxAmount: 1_000 # max amount to buy or sell per order
orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET orderType: LIMIT_MAKER # LIMIT, LIMIT_MAKER or MARKET
dryRun: false dryRun: false
onStart: false

View File

@ -55,7 +55,7 @@ func (m GeneralOrderExecutorMap) SubmitOrders(ctx context.Context, submitOrders
return nil, fmt.Errorf("order executor not found for symbol %s", submitOrder.Symbol) return nil, fmt.Errorf("order executor not found for symbol %s", submitOrder.Symbol)
} }
createdOrders, err := orderExecutor.SubmitOrders(ctx) createdOrders, err := orderExecutor.SubmitOrders(ctx, submitOrder)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -34,6 +34,7 @@ type Strategy struct {
MaxAmount fixedpoint.Value `json:"maxAmount"` // max amount to buy or sell per order MaxAmount fixedpoint.Value `json:"maxAmount"` // max amount to buy or sell per order
OrderType types.OrderType `json:"orderType"` OrderType types.OrderType `json:"orderType"`
DryRun bool `json:"dryRun"` DryRun bool `json:"dryRun"`
OnStart bool `json:"onStart"` // rebalance on start
PositionMap PositionMap `persistence:"positionMap"` PositionMap PositionMap `persistence:"positionMap"`
ProfitStatsMap ProfitStatsMap `persistence:"profitStatsMap"` ProfitStatsMap ProfitStatsMap `persistence:"profitStatsMap"`
@ -114,6 +115,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
s.activeOrderBook = bbgo.NewActiveOrderBook("") s.activeOrderBook = bbgo.NewActiveOrderBook("")
s.activeOrderBook.BindStream(s.session.UserDataStream) s.activeOrderBook.BindStream(s.session.UserDataStream)
session.UserDataStream.OnStart(func() {
if s.OnStart {
s.rebalance(ctx)
}
})
s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
s.rebalance(ctx) s.rebalance(ctx)
}) })
@ -133,12 +140,17 @@ func (s *Strategy) rebalance(ctx context.Context) {
log.WithError(err).Errorf("failed to cancel orders") log.WithError(err).Errorf("failed to cancel orders")
} }
submitOrders := s.generateSubmitOrders(ctx) submitOrders, err := s.generateSubmitOrders(ctx)
if err != nil {
log.WithError(err).Error("failed to generate submit orders")
return
}
for _, order := range submitOrders { for _, order := range submitOrders {
log.Infof("generated submit order: %s", order.String()) log.Infof("generated submit order: %s", order.String())
} }
if s.DryRun { if s.DryRun {
log.Infof("dry run, not submitting orders")
return return
} }
@ -150,7 +162,7 @@ func (s *Strategy) rebalance(ctx context.Context) {
s.activeOrderBook.Add(createdOrders...) s.activeOrderBook.Add(createdOrders...)
} }
func (s *Strategy) prices(ctx context.Context) types.ValueMap { func (s *Strategy) prices(ctx context.Context) (types.ValueMap, error) {
m := make(types.ValueMap) m := make(types.ValueMap)
for currency := range s.TargetWeights { for currency := range s.TargetWeights {
if currency == s.QuoteCurrency { if currency == s.QuoteCurrency {
@ -160,29 +172,37 @@ func (s *Strategy) prices(ctx context.Context) types.ValueMap {
ticker, err := s.session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency) ticker, err := s.session.Exchange.QueryTicker(ctx, currency+s.QuoteCurrency)
if err != nil { if err != nil {
log.WithError(err).Error("failed to query tickers") return nil, err
return nil
} }
m[currency] = ticker.Last m[currency] = ticker.Buy.Add(ticker.Sell).Div(fixedpoint.NewFromFloat(2.0))
} }
return m return m, nil
} }
func (s *Strategy) quantities() types.ValueMap { func (s *Strategy) balances() (types.BalanceMap, error) {
m := make(types.ValueMap) m := make(types.BalanceMap)
balances := s.session.GetAccount().Balances() balances := s.session.GetAccount().Balances()
for currency := range s.TargetWeights { for currency := range s.TargetWeights {
m[currency] = balances[currency].Total() balance, ok := balances[currency]
if !ok {
return nil, fmt.Errorf("no balance for %s", currency)
}
m[currency] = balance
} }
return m, nil
return m
} }
func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder) { func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []types.SubmitOrder, err error) {
prices := s.prices(ctx) prices, err := s.prices(ctx)
marketValues := prices.Mul(s.quantities()) if err != nil {
return nil, err
}
balances, err := s.balances()
if err != nil {
return nil, err
}
marketValues := prices.Mul(balanceToTotal(balances))
currentWeights := marketValues.Normalize() currentWeights := marketValues.Normalize()
for currency, targetWeight := range s.TargetWeights { for currency, targetWeight := range s.TargetWeights {
@ -221,8 +241,9 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ
quantity = quantity.Abs() quantity = quantity.Abs()
} }
if s.MaxAmount.Sign() > 0 { maxAmount := s.adjustMaxAmountByBalance(side, currency, currentPrice, balances)
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, s.MaxAmount) if maxAmount.Sign() > 0 {
quantity = bbgo.AdjustQuantityByMaxAmount(quantity, currentPrice, maxAmount)
log.Infof("adjust the quantity %v (%s %s @ %v) by max amount %v", log.Infof("adjust the quantity %v (%s %s @ %v) by max amount %v",
quantity, quantity,
symbol, symbol,
@ -241,10 +262,12 @@ func (s *Strategy) generateSubmitOrders(ctx context.Context) (submitOrders []typ
Price: currentPrice, Price: currentPrice,
} }
submitOrders = append(submitOrders, order) if ok := s.checkMinimalOrderQuantity(order); ok {
submitOrders = append(submitOrders, order)
}
} }
return submitOrders return submitOrders, err
} }
func (s *Strategy) symbols() (symbols []string) { func (s *Strategy) symbols() (symbols []string) {
@ -268,3 +291,44 @@ func (s *Strategy) markets() ([]types.Market, error) {
} }
return markets, nil return markets, nil
} }
func (s *Strategy) adjustMaxAmountByBalance(side types.SideType, currency string, currentPrice fixedpoint.Value, balances types.BalanceMap) fixedpoint.Value {
var maxAmount fixedpoint.Value
switch side {
case types.SideTypeBuy:
maxAmount = balances[s.QuoteCurrency].Available
case types.SideTypeSell:
maxAmount = balances[currency].Available.Mul(currentPrice)
default:
log.Errorf("unknown side type: %s", side)
return fixedpoint.Zero
}
if s.MaxAmount.Sign() > 0 {
maxAmount = fixedpoint.Min(s.MaxAmount, maxAmount)
}
return maxAmount
}
func (s *Strategy) checkMinimalOrderQuantity(order types.SubmitOrder) bool {
if order.Quantity.Compare(order.Market.MinQuantity) < 0 {
log.Infof("order quantity is too small: %f < %f", order.Quantity.Float64(), order.Market.MinQuantity.Float64())
return false
}
if order.Quantity.Mul(order.Price).Compare(order.Market.MinNotional) < 0 {
log.Infof("order min notional is too small: %f < %f", order.Quantity.Mul(order.Price).Float64(), order.Market.MinNotional.Float64())
return false
}
return true
}
func balanceToTotal(balances types.BalanceMap) types.ValueMap {
m := make(types.ValueMap)
for _, b := range balances {
m[b.Currency] = b.Total()
}
return m
}