2022-09-09 09:40:17 +00:00
|
|
|
package bbgo
|
2022-07-22 03:55:06 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"time"
|
|
|
|
|
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"
|
2022-07-22 03:55:06 +00:00
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
2024-10-04 11:45:07 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/pricesolver"
|
2022-09-09 09:40:17 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/risk"
|
2022-07-22 03:55:06 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
2022-09-09 05:57:39 +00:00
|
|
|
var defaultLeverage = fixedpoint.NewFromInt(3)
|
|
|
|
|
2022-09-11 15:26:48 +00:00
|
|
|
var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10)
|
|
|
|
|
|
|
|
var maxCrossMarginLeverage = fixedpoint.NewFromInt(3)
|
2022-07-22 03:55:06 +00:00
|
|
|
|
|
|
|
type AccountValueCalculator struct {
|
2024-10-04 11:45:07 +00:00
|
|
|
priceSolver *pricesolver.SimplePriceSolver
|
|
|
|
|
2022-09-09 09:40:17 +00:00
|
|
|
session *ExchangeSession
|
2022-07-22 03:55:06 +00:00
|
|
|
quoteCurrency string
|
|
|
|
prices map[string]fixedpoint.Value
|
|
|
|
tickers map[string]types.Ticker
|
|
|
|
updateTime time.Time
|
|
|
|
}
|
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
func NewAccountValueCalculator(
|
|
|
|
session *ExchangeSession,
|
|
|
|
priceSolver *pricesolver.SimplePriceSolver,
|
|
|
|
quoteCurrency string,
|
|
|
|
) *AccountValueCalculator {
|
2022-07-22 03:55:06 +00:00
|
|
|
return &AccountValueCalculator{
|
2024-10-04 11:45:07 +00:00
|
|
|
priceSolver: priceSolver,
|
2022-07-22 03:55:06 +00:00
|
|
|
session: session,
|
|
|
|
quoteCurrency: quoteCurrency,
|
|
|
|
prices: make(map[string]fixedpoint.Value),
|
|
|
|
tickers: make(map[string]types.Ticker),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
|
|
|
|
balances := c.session.Account.Balances()
|
|
|
|
currencies := balances.Currencies()
|
2024-10-04 11:45:07 +00:00
|
|
|
|
|
|
|
// TODO: improve this part
|
2022-07-22 03:55:06 +00:00
|
|
|
var symbols []string
|
|
|
|
for _, currency := range currencies {
|
2022-07-22 06:42:30 +00:00
|
|
|
if currency == c.quoteCurrency {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-07-22 03:55:06 +00:00
|
|
|
symbol := currency + c.quoteCurrency
|
|
|
|
symbols = append(symbols, symbol)
|
|
|
|
}
|
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
return c.priceSolver.UpdateFromTickers(ctx, c.session.Exchange, symbols...)
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *AccountValueCalculator) DebtValue(ctx context.Context) (fixedpoint.Value, error) {
|
|
|
|
debtValue := fixedpoint.Zero
|
|
|
|
|
|
|
|
if len(c.prices) == 0 {
|
|
|
|
if err := c.UpdatePrices(ctx); err != nil {
|
|
|
|
return debtValue, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
balances := c.session.Account.Balances()
|
|
|
|
for _, b := range balances {
|
|
|
|
symbol := b.Currency + c.quoteCurrency
|
|
|
|
price, ok := c.prices[symbol]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
debtValue = debtValue.Add(b.Debt().Mul(price))
|
|
|
|
}
|
|
|
|
|
|
|
|
return debtValue, nil
|
|
|
|
}
|
|
|
|
|
2022-07-22 06:42:30 +00:00
|
|
|
func (c *AccountValueCalculator) MarketValue(ctx context.Context) (fixedpoint.Value, error) {
|
|
|
|
marketValue := fixedpoint.Zero
|
2022-07-22 03:55:06 +00:00
|
|
|
|
|
|
|
if len(c.prices) == 0 {
|
|
|
|
if err := c.UpdatePrices(ctx); err != nil {
|
2022-07-22 06:42:30 +00:00
|
|
|
return marketValue, err
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
balances := c.session.Account.Balances()
|
|
|
|
for _, b := range balances {
|
2022-07-22 06:42:30 +00:00
|
|
|
if b.Currency == c.quoteCurrency {
|
|
|
|
marketValue = marketValue.Add(b.Total())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
if c.priceSolver != nil {
|
|
|
|
if price, ok := c.priceSolver.ResolvePrice(b.Currency, c.quoteCurrency); ok {
|
|
|
|
marketValue = marketValue.Add(b.Total().Mul(price))
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
symbol := b.Currency + c.quoteCurrency
|
|
|
|
if price, ok := c.prices[symbol]; ok {
|
|
|
|
marketValue = marketValue.Add(b.Total().Mul(price))
|
|
|
|
}
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-22 06:42:30 +00:00
|
|
|
return marketValue, nil
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
2022-07-22 06:42:30 +00:00
|
|
|
func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value, error) {
|
|
|
|
if len(c.prices) == 0 {
|
|
|
|
if err := c.UpdatePrices(ctx); err != nil {
|
2022-09-13 18:08:14 +00:00
|
|
|
return fixedpoint.Zero, err
|
2022-07-22 06:42:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
balances := c.session.Account.Balances()
|
2024-10-04 11:45:07 +00:00
|
|
|
accountValue := calculateNetValueInQuote(balances, c.priceSolver, c.quoteCurrency)
|
2022-09-13 18:08:14 +00:00
|
|
|
return accountValue, nil
|
|
|
|
}
|
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
func calculateNetValueInQuote(
|
|
|
|
balances types.BalanceMap, priceSolver *pricesolver.SimplePriceSolver, quoteCurrency string,
|
|
|
|
) (accountValue fixedpoint.Value) {
|
2022-09-13 18:08:14 +00:00
|
|
|
accountValue = fixedpoint.Zero
|
2022-07-22 06:42:30 +00:00
|
|
|
for _, b := range balances {
|
2022-09-13 18:08:14 +00:00
|
|
|
if b.Currency == quoteCurrency {
|
2022-07-22 06:42:30 +00:00
|
|
|
accountValue = accountValue.Add(b.Net())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
|
2022-09-11 16:05:22 +00:00
|
|
|
accountValue = accountValue.Add(b.Net().Mul(price))
|
2022-07-22 06:42:30 +00:00
|
|
|
}
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
2022-09-13 18:08:14 +00:00
|
|
|
return accountValue
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
2022-08-05 07:38:19 +00:00
|
|
|
func (c *AccountValueCalculator) AvailableQuote(ctx context.Context) (fixedpoint.Value, error) {
|
2022-08-05 07:11:15 +00:00
|
|
|
accountValue := fixedpoint.Zero
|
|
|
|
|
|
|
|
if len(c.prices) == 0 {
|
|
|
|
if err := c.UpdatePrices(ctx); err != nil {
|
|
|
|
return accountValue, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
balances := c.session.Account.Balances()
|
|
|
|
for _, b := range balances {
|
|
|
|
if b.Currency == c.quoteCurrency {
|
2022-08-13 05:28:45 +00:00
|
|
|
accountValue = accountValue.Add(b.Net())
|
2022-08-05 07:11:15 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
symbol := b.Currency + c.quoteCurrency
|
|
|
|
price, ok := c.prices[symbol]
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2022-08-13 05:28:45 +00:00
|
|
|
accountValue = accountValue.Add(b.Net().Mul(price))
|
2022-08-05 07:11:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return accountValue, nil
|
|
|
|
}
|
|
|
|
|
2022-07-22 06:54:25 +00:00
|
|
|
// MarginLevel calculates the margin level from the asset market value and the debt value
|
|
|
|
// See https://www.binance.com/en/support/faq/360030493931
|
2022-07-22 06:53:17 +00:00
|
|
|
func (c *AccountValueCalculator) MarginLevel(ctx context.Context) (fixedpoint.Value, error) {
|
|
|
|
marginLevel := fixedpoint.Zero
|
|
|
|
marketValue, err := c.MarketValue(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return marginLevel, err
|
|
|
|
}
|
|
|
|
|
|
|
|
debtValue, err := c.DebtValue(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return marginLevel, err
|
|
|
|
}
|
|
|
|
|
|
|
|
marginLevel = marketValue.Div(debtValue)
|
|
|
|
return marginLevel, nil
|
|
|
|
}
|
|
|
|
|
2022-09-13 18:08:14 +00:00
|
|
|
func aggregateUsdNetValue(balances types.BalanceMap) fixedpoint.Value {
|
2022-09-11 15:51:24 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-09-11 16:05:22 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
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
|
2022-07-22 03:55:06 +00:00
|
|
|
if leverage.IsZero() {
|
2022-09-09 05:57:39 +00:00
|
|
|
leverage = defaultLeverage
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
2022-09-11 16:13:49 +00:00
|
|
|
baseBalance, hasBaseBalance := session.Account.Balance(market.BaseCurrency)
|
|
|
|
balances := session.Account.Balances()
|
|
|
|
|
2022-07-22 03:55:06 +00:00
|
|
|
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-07-22 03:55:06 +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
|
|
|
}
|
2022-07-22 03:55:06 +00:00
|
|
|
|
2022-09-11 16:05:22 +00:00
|
|
|
usdBalances, restBalances := usdFiatBalances(balances)
|
2022-09-11 15:51:24 +00:00
|
|
|
|
|
|
|
// for isolated margin we can calculate from these two pair
|
|
|
|
totalUsdValue := fixedpoint.Zero
|
2022-09-11 16:05:22 +00:00
|
|
|
if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) {
|
2022-09-13 18:08:14 +00:00
|
|
|
totalUsdValue = aggregateUsdNetValue(balances)
|
2022-09-11 16:05:22 +00:00
|
|
|
} else if len(restBalances) > 1 {
|
2024-10-04 11:45:07 +00:00
|
|
|
accountValue := NewAccountValueCalculator(session, nil, "USDT")
|
2022-09-11 16:05:22 +00:00
|
|
|
netValue, err := accountValue.NetValue(context.Background())
|
|
|
|
if err != nil {
|
|
|
|
return quantity, err
|
|
|
|
}
|
|
|
|
|
|
|
|
totalUsdValue = netValue
|
2022-09-11 15:51:24 +00:00
|
|
|
} else {
|
|
|
|
// TODO: translate quote currency like BTC of ETH/BTC to usd value
|
2022-09-13 18:08:14 +00:00
|
|
|
totalUsdValue = aggregateUsdNetValue(usdBalances)
|
2022-09-11 15:51:24 +00:00
|
|
|
}
|
|
|
|
|
2022-07-22 04:04:43 +00:00
|
|
|
if !quantity.IsZero() {
|
|
|
|
return quantity, nil
|
|
|
|
}
|
2022-07-22 03:55:06 +00:00
|
|
|
|
2022-09-11 15:26:48 +00:00
|
|
|
if price.IsZero() {
|
|
|
|
return quantity, fmt.Errorf("%s price can not be zero", market.Symbol)
|
|
|
|
}
|
|
|
|
|
2022-07-26 17:28:18 +00:00
|
|
|
// using leverage -- starts from here
|
2022-09-13 18:08:14 +00:00
|
|
|
log.Infof("calculating available leveraged base quantity: base balance = %+v, total usd value %f", baseBalance, totalUsdValue.Float64())
|
2022-07-22 03:55:06 +00:00
|
|
|
|
2022-07-22 04:04:43 +00:00
|
|
|
// calculate the quantity automatically
|
|
|
|
if session.Margin || session.IsolatedMargin {
|
|
|
|
baseBalanceValue := baseBalance.Net().Mul(price)
|
2022-09-11 15:51:24 +00:00
|
|
|
accountUsdValue := baseBalanceValue.Add(totalUsdValue)
|
2022-07-22 03:55:06 +00:00
|
|
|
|
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
|
2022-09-11 15:51:24 +00:00
|
|
|
accountUsdValue = accountUsdValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
|
2022-07-22 03:55:06 +00:00
|
|
|
|
2022-09-11 15:51:24 +00:00
|
|
|
log.Infof("calculated account usd value %f %s", accountUsdValue.Float64(), market.QuoteCurrency)
|
2022-07-22 03:55:06 +00:00
|
|
|
|
2022-09-11 15:26:48 +00:00
|
|
|
originLeverage := leverage
|
2022-07-22 04:04:43 +00:00
|
|
|
if session.IsolatedMargin {
|
2022-09-11 15:26:48 +00:00
|
|
|
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(),
|
2022-07-22 03:55:06 +00:00
|
|
|
leverage.Float64())
|
|
|
|
}
|
|
|
|
|
2022-07-22 04:04:43 +00:00
|
|
|
// spot margin use the equity value, so we use the total quote balance here
|
2022-09-11 15:51:24 +00:00
|
|
|
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-07-22 03:55:06 +00:00
|
|
|
|
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(),
|
2022-09-11 15:51:24 +00:00
|
|
|
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 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
2022-07-22 04:04:43 +00:00
|
|
|
if session.Futures || session.IsolatedFutures {
|
2022-09-11 15:51:24 +00:00
|
|
|
maxPositionQuantity := risk.CalculateMaxPosition(price, totalUsdValue, leverage)
|
2022-07-22 04:04:43 +00:00
|
|
|
|
|
|
|
return maxPositionQuantity, nil
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
|
|
|
|
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"))
|
2022-07-22 03:55:06 +00:00
|
|
|
}
|
2022-08-05 07:59:20 +00:00
|
|
|
|
2024-10-04 11:45:07 +00:00
|
|
|
func CalculateQuoteQuantity(
|
|
|
|
ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value,
|
|
|
|
) (fixedpoint.Value, error) {
|
2022-08-05 07:59:20 +00:00
|
|
|
// default leverage guard
|
|
|
|
if leverage.IsZero() {
|
2022-09-09 05:57:39 +00:00
|
|
|
leverage = defaultLeverage
|
2022-08-05 07:59:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
quoteBalance, _ := session.Account.Balance(quoteCurrency)
|
|
|
|
|
|
|
|
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
|
|
|
|
if !usingLeverage {
|
|
|
|
// For spot, we simply return the quote balance
|
2022-08-05 08:28:42 +00:00
|
|
|
return quoteBalance.Available.Mul(fixedpoint.Min(leverage, fixedpoint.One)), nil
|
2022-08-05 07:59:20 +00:00
|
|
|
}
|
|
|
|
|
2022-09-11 15:51:24 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2022-08-05 07:59:20 +00:00
|
|
|
// using leverage -- starts from here
|
2024-10-04 11:45:07 +00:00
|
|
|
accountValue := NewAccountValueCalculator(session, nil, quoteCurrency)
|
2022-08-05 07:59:20 +00:00
|
|
|
availableQuote, err := accountValue.AvailableQuote(ctx)
|
|
|
|
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)
|
2022-08-05 07:59:20 +00:00
|
|
|
|
2022-08-05 08:28:42 +00:00
|
|
|
return availableQuote.Mul(leverage), nil
|
2022-08-05 07:59:20 +00:00
|
|
|
}
|