bbgo_origin/pkg/bbgo/account_value_calc.go

353 lines
11 KiB
Go
Raw Normal View History

2022-09-09 09:40:17 +00:00
package bbgo
import (
"context"
"fmt"
2022-09-22 11:26:18 +00:00
"github.com/pkg/errors"
2022-09-09 09:40:17 +00:00
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/pricesolver"
2022-09-09 09:40:17 +00:00
"github.com/c9s/bbgo/pkg/risk"
"github.com/c9s/bbgo/pkg/types"
)
2022-09-09 05:57:39 +00:00
var defaultLeverage = fixedpoint.NewFromInt(3)
var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10)
var maxCrossMarginLeverage = fixedpoint.NewFromInt(3)
type AccountValueCalculator struct {
2024-10-05 06:22:13 +00:00
priceSolver *pricesolver.SimplePriceSolver
2022-09-09 09:40:17 +00:00
session *ExchangeSession
quoteCurrency string
}
func NewAccountValueCalculator(
session *ExchangeSession,
priceSolver *pricesolver.SimplePriceSolver,
quoteCurrency string,
) *AccountValueCalculator {
return &AccountValueCalculator{
priceSolver: priceSolver,
session: session,
quoteCurrency: quoteCurrency,
}
}
2024-10-05 06:22:13 +00:00
// UpdatePrices updates the price index from the existing balances
func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
balances := c.session.Account.Balances()
currencies := balances.Currencies()
2024-10-05 06:22:13 +00:00
markets := c.session.Markets()
var symbols []string
for _, currency := range currencies {
if currency == c.quoteCurrency {
continue
}
symbol := currency + c.quoteCurrency
2024-10-05 06:22:13 +00:00
reversedSymbol := c.quoteCurrency + currency
if _, ok := markets[symbol]; ok {
symbols = append(symbols, symbol)
} else if _, ok2 := markets[reversedSymbol]; ok2 {
symbols = append(symbols, reversedSymbol)
}
}
return c.priceSolver.UpdateFromTickers(ctx, c.session.Exchange, symbols...)
}
func (c *AccountValueCalculator) DebtValue() fixedpoint.Value {
balances := c.session.Account.Balances()
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Debt().Mul(price))
})
}
func (c *AccountValueCalculator) TotalValue() fixedpoint.Value {
balances := c.session.Account.Balances()
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Total().Mul(price))
})
}
func (c *AccountValueCalculator) NetValue() fixedpoint.Value {
balances := c.session.Account.Balances()
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Net().Mul(price))
})
}
func totalValueInQuote(
balances types.BalanceMap,
priceSolver *pricesolver.SimplePriceSolver,
quoteCurrency string,
algo func(prev fixedpoint.Value, b types.Balance, price fixedpoint.Value) fixedpoint.Value,
) (totalValue fixedpoint.Value) {
totalValue = fixedpoint.Zero
for _, b := range balances {
if b.Currency == quoteCurrency {
totalValue = algo(totalValue, b, fixedpoint.One)
continue
} else if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
totalValue = algo(totalValue, b, price)
}
}
return totalValue
}
func calculateNetValueInQuote(
balances types.BalanceMap,
priceSolver *pricesolver.SimplePriceSolver,
quoteCurrency string,
) fixedpoint.Value {
return totalValueInQuote(balances, priceSolver, quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Net().Mul(price))
})
}
func (c *AccountValueCalculator) AvailableQuote() (fixedpoint.Value, error) {
accountValue := fixedpoint.Zero
balances := c.session.Account.Balances()
for _, b := range balances {
if b.Currency == c.quoteCurrency {
accountValue = accountValue.Add(b.Net())
continue
}
if price, ok := c.priceSolver.ResolvePrice(b.Currency, c.quoteCurrency); ok {
accountValue = accountValue.Add(b.Net().Mul(price))
}
}
return accountValue, nil
}
// MarginLevel calculates the margin level from the asset market value and the debt value
// See https://www.binance.com/en/support/faq/360030493931
func (c *AccountValueCalculator) MarginLevel() fixedpoint.Value {
marketValue := c.TotalValue()
debtValue := c.DebtValue()
2022-07-22 06:53:17 +00:00
if marketValue.IsZero() || debtValue.IsZero() {
return fixedpoint.NewFromFloat(999.0)
2022-07-22 06:53:17 +00:00
}
return marketValue.Div(debtValue)
}
func (c *AccountValueCalculator) Leverage() fixedpoint.Value {
netValue := c.NetValue()
debtValue := c.DebtValue()
if debtValue.IsZero() || netValue.IsZero() {
return fixedpoint.One
}
return debtValue.Div(netValue)
2022-07-22 06:53:17 +00:00
}
func aggregateUsdNetValue(balances types.BalanceMap) fixedpoint.Value {
totalUsdValue := fixedpoint.Zero
// get all usd value if any
for currency, balance := range balances {
if types.IsUSDFiatCurrency(currency) {
totalUsdValue = totalUsdValue.Add(balance.Net())
}
}
return totalUsdValue
}
func usdFiatBalances(balances types.BalanceMap) (fiats types.BalanceMap, rest types.BalanceMap) {
rest = make(types.BalanceMap)
fiats = make(types.BalanceMap)
for currency, balance := range balances {
if types.IsUSDFiatCurrency(currency) {
fiats[currency] = balance
} else {
rest[currency] = balance
}
}
return fiats, rest
}
func CalculateBaseQuantity(
session *ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value,
) (fixedpoint.Value, error) {
2022-07-22 04:04:43 +00:00
// default leverage guard
if leverage.IsZero() {
2022-09-09 05:57:39 +00:00
leverage = defaultLeverage
}
2022-09-11 16:13:49 +00:00
baseBalance, hasBaseBalance := session.Account.Balance(market.BaseCurrency)
balances := session.Account.Balances()
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
2022-07-22 04:04:43 +00:00
if !usingLeverage {
// For spot, we simply sell the base quoteCurrency
2022-09-11 16:13:49 +00:00
if hasBaseBalance {
2022-07-22 04:04:43 +00:00
if quantity.IsZero() {
2022-09-11 16:13:49 +00:00
log.Warnf("sell quantity is not set, using all available base balance: %v", baseBalance)
if !baseBalance.Available.IsZero() {
return baseBalance.Available, nil
2022-07-22 04:04:43 +00:00
}
} else {
2022-09-11 16:13:49 +00:00
return fixedpoint.Min(quantity, baseBalance.Available), nil
2022-07-22 04:04:43 +00:00
}
}
2022-09-22 11:26:18 +00:00
return quantity, types.NewZeroAssetError(
fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances))
2022-07-22 04:04:43 +00:00
}
usdBalances, restBalances := usdFiatBalances(balances)
2024-10-05 06:22:13 +00:00
// for isolated margin, we can calculate from these two pair
totalUsdValue := fixedpoint.Zero
if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) {
totalUsdValue = aggregateUsdNetValue(balances)
} else if len(restBalances) > 1 {
2024-10-05 06:22:13 +00:00
priceSolver := pricesolver.NewSimplePriceResolver(session.Markets())
accountValue := NewAccountValueCalculator(session, priceSolver, "USDT")
if err := accountValue.UpdatePrices(context.Background()); err != nil {
return fixedpoint.Zero, err
}
netValue := accountValue.NetValue()
totalUsdValue = netValue
} else {
// TODO: translate quote currency like BTC of ETH/BTC to usd value
totalUsdValue = aggregateUsdNetValue(usdBalances)
}
2022-07-22 04:04:43 +00:00
if !quantity.IsZero() {
return quantity, nil
}
if price.IsZero() {
return quantity, fmt.Errorf("%s price can not be zero", market.Symbol)
}
// using leverage -- starts from here
log.Infof("calculating available leveraged base quantity: base balance = %+v, total usd value %f", baseBalance, totalUsdValue.Float64())
2022-07-22 04:04:43 +00:00
// calculate the quantity automatically
if session.Margin || session.IsolatedMargin {
baseBalanceValue := baseBalance.Net().Mul(price)
accountUsdValue := baseBalanceValue.Add(totalUsdValue)
2022-07-22 04:04:43 +00:00
// avoid using all account value since there will be some trade loss for interests and the fee
accountUsdValue = accountUsdValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
log.Infof("calculated account usd value %f %s", accountUsdValue.Float64(), market.QuoteCurrency)
originLeverage := leverage
2022-07-22 04:04:43 +00:00
if session.IsolatedMargin {
leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage)
log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
maxIsolatedMarginLeverage.Float64(),
originLeverage.Float64(),
leverage.Float64())
} else {
leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage)
log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
maxCrossMarginLeverage.Float64(),
2022-07-22 04:04:43 +00:00
originLeverage.Float64(),
leverage.Float64())
}
2022-07-22 04:04:43 +00:00
// spot margin use the equity value, so we use the total quote balance here
maxPosition := risk.CalculateMaxPosition(price, accountUsdValue, leverage)
2022-07-22 04:04:43 +00:00
debt := baseBalance.Debt()
2022-07-26 03:47:07 +00:00
maxQuantity := maxPosition.Sub(debt)
2022-09-09 09:40:17 +00:00
log.Infof("margin leverage: calculated maxQuantity=%f maxPosition=%f debt=%f price=%f accountValue=%f %s leverage=%f",
2022-07-26 03:47:07 +00:00
maxQuantity.Float64(),
2022-07-22 04:04:43 +00:00
maxPosition.Float64(),
debt.Float64(),
price.Float64(),
accountUsdValue.Float64(),
2022-07-22 04:04:43 +00:00
market.QuoteCurrency,
leverage.Float64())
2022-07-26 03:47:07 +00:00
return maxQuantity, nil
}
2022-07-22 04:04:43 +00:00
if session.Futures || session.IsolatedFutures {
maxPositionQuantity := risk.CalculateMaxPosition(price, totalUsdValue, leverage)
2022-07-22 04:04:43 +00:00
return maxPositionQuantity, nil
}
2022-09-22 11:26:18 +00:00
return quantity, types.NewZeroAssetError(
errors.New("quantity is zero, can not submit sell order, please check your settings"))
}
func CalculateQuoteQuantity(
ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value,
) (fixedpoint.Value, error) {
// default leverage guard
if leverage.IsZero() {
2022-09-09 05:57:39 +00:00
leverage = defaultLeverage
}
quoteBalance, _ := session.Account.Balance(quoteCurrency)
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
if !usingLeverage {
// For spot, we simply return the quote balance
return quoteBalance.Available.Mul(fixedpoint.Min(leverage, fixedpoint.One)), nil
}
originLeverage := leverage
if session.IsolatedMargin {
leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage)
log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
maxIsolatedMarginLeverage.Float64(),
originLeverage.Float64(),
leverage.Float64())
} else {
leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage)
log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
maxCrossMarginLeverage.Float64(),
originLeverage.Float64(),
leverage.Float64())
}
// using leverage -- starts from here
2024-10-05 06:22:13 +00:00
priceSolver := pricesolver.NewSimplePriceResolver(session.Markets())
accountValue := NewAccountValueCalculator(session, priceSolver, quoteCurrency)
if err := accountValue.UpdatePrices(ctx); err != nil {
return fixedpoint.Zero, err
}
availableQuote, err := accountValue.AvailableQuote()
if err != nil {
log.WithError(err).Errorf("can not update available quote")
return fixedpoint.Zero, err
}
2022-09-09 09:40:17 +00:00
log.Infof("calculating available leveraged quote quantity: account available quote = %+v", availableQuote)
return availableQuote.Mul(leverage), nil
}