Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IMPROVE: [xmaker] implement allowMarginHedge and its tests #1852

Merged
merged 2 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions pkg/bbgo/account_value_calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
}

func (c *AccountValueCalculator) DebtValue() fixedpoint.Value {
balances := c.session.Account.Balances()
balances := c.session.Account.Balances().Debts()
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
Expand Down Expand Up @@ -100,9 +100,12 @@ func totalValueInQuote(
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)
} else {
if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
totalValue = algo(totalValue, b, price)
} else {
log.Warnf("unable to solve price for %s/%s", b.Currency, quoteCurrency)
}
}
}

Expand Down
179 changes: 131 additions & 48 deletions pkg/strategy/xmaker/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,110 @@ func (s *Strategy) getLayerPrice(
return price
}

// margin level = totalValue / totalDebtValue
func calculateDebtQuota(totalValue, debtValue, minMarginLevel fixedpoint.Value) fixedpoint.Value {
if minMarginLevel.IsZero() || totalValue.IsZero() {
return fixedpoint.Zero
}

debtCap := totalValue.Div(minMarginLevel)
debtQuota := debtCap.Sub(debtValue)
if debtQuota.Sign() < 0 {
return fixedpoint.Zero
}

return debtQuota
}

func (s *Strategy) allowMarginHedge(side types.SideType) (bool, fixedpoint.Value) {
zero := fixedpoint.Zero

if !s.sourceSession.Margin {
return false, zero
}

// GetAccount() is a lightweight operation, it doesn't make any API request
hedgeAccount := s.sourceSession.GetAccount()
lastPrice := s.lastPrice.Get()

if hedgeAccount.MarginLevel.IsZero() || s.MinMarginLevel.IsZero() {
return false, zero
}

marketValue := s.accountValueCalculator.MarketValue()
debtValue := s.accountValueCalculator.DebtValue()

// if the margin level is higher than the minimal margin level,
// we can hedge the position, but we need to check the debt quota
if hedgeAccount.MarginLevel.Compare(s.MinMarginLevel) > 0 {
debtQuota := calculateDebtQuota(marketValue, debtValue, s.MinMarginLevel)

if debtQuota.Sign() <= 0 {
return false, zero
}

switch side {
case types.SideTypeBuy:
return true, debtQuota

case types.SideTypeSell:
if lastPrice.IsZero() {
return false, zero
}

return true, debtQuota.Div(lastPrice)

}
return true, zero
}

// side here is the side of maker
// if the margin level is too low, check if we can hedge the position with repayments to reduce the position
quoteBal, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency)
if !ok {
quoteBal = types.NewZeroBalance(s.sourceMarket.QuoteCurrency)
}

baseBal, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency)
if !ok {
baseBal = types.NewZeroBalance(s.sourceMarket.BaseCurrency)
}

switch side {
case types.SideTypeBuy:
if baseBal.Available.IsZero() {
return false, zero
}

quota := baseBal.Available.Mul(lastPrice)

// for buy orders, we need to check if we can repay the quoteBal asset via selling the base balance
quoteDebt := quoteBal.Debt()
if quoteDebt.Sign() > 0 {
return true, fixedpoint.Min(quota, quoteDebt)
}

return false, zero

case types.SideTypeSell:
if quoteBal.Available.IsZero() {
return false, zero
}

quota := quoteBal.Available.Div(lastPrice)

baseDebt := baseBal.Debt()
if baseDebt.Sign() > 0 {
// return how much quote bal amount we can use to place the buy order
return true, fixedpoint.Min(quota, baseDebt)
}

return false, zero
}

return false, zero
}

func (s *Strategy) updateQuote(ctx context.Context) error {
cancelMakerOrdersProfile := timeprofile.Start("cancelMakerOrders")

Expand Down Expand Up @@ -844,58 +948,31 @@ func (s *Strategy) updateQuote(ctx context.Context) error {
s.logger.Infof("hedge account margin level %s is less then the min margin level %s, calculating the borrowed positions",
hedgeAccount.MarginLevel.String(),
s.MinMarginLevel.String())

// TODO: should consider base asset debt as well.
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
quoteDebt := quote.Debt()
if quoteDebt.Sign() > 0 {
hedgeQuota.BaseAsset.Add(quoteDebt.Div(bestBid.Price))
}
}

if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
baseDebt := base.Debt()
if baseDebt.Sign() > 0 {
hedgeQuota.QuoteAsset.Add(baseDebt.Mul(bestAsk.Price))
}
}
} else {
s.logger.Infof("hedge account margin level %s is greater than the min margin level %s, calculating the net value",
hedgeAccount.MarginLevel.String(),
s.MinMarginLevel.String())
}

netValueInUsd := s.accountValueCalculator.NetValue()

// calculate credit buffer
s.logger.Infof("hedge account net value in usd: %f", netValueInUsd.Float64())

maximumValueInUsd := netValueInUsd.Mul(s.MaxHedgeAccountLeverage)

s.logger.Infof("hedge account maximum leveraged value in usd: %f (%f x)", maximumValueInUsd.Float64(), s.MaxHedgeAccountLeverage.Float64())

if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
debt := quote.Debt()
quota := maximumValueInUsd.Sub(debt)

s.logger.Infof("hedge account quote balance: %s, debt: %s, quota: %s",
quote.String(),
debt.String(),
quota.String())

hedgeQuota.QuoteAsset.Add(quota)
}
// calculate credit buffer
netValueInUsd := s.accountValueCalculator.NetValue()
s.logger.Infof("hedge account net value in usd: %f", netValueInUsd.Float64())

if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
debt := base.Debt()
quota := maximumValueInUsd.Div(bestAsk.Price).Sub(debt)
maximumValueInUsd := netValueInUsd.Mul(s.MaxHedgeAccountLeverage)
s.logger.Infof("hedge account maximum leveraged value in usd: %f (%f x)", maximumValueInUsd.Float64(), s.MaxHedgeAccountLeverage.Float64())

s.logger.Infof("hedge account base balance: %s, debt: %s, quota: %s",
base.String(),
debt.String(),
quota.String())
allowMarginBuy, bidQuota := s.allowMarginHedge(types.SideTypeBuy)
if allowMarginBuy {
hedgeQuota.BaseAsset.Add(bidQuota.Div(bestBid.Price))
} else {
disableMakerBid = true
}

hedgeQuota.BaseAsset.Add(quota)
}
allowMarginSell, sellQuota := s.allowMarginHedge(types.SideTypeSell)
if allowMarginSell {
hedgeQuota.QuoteAsset.Add(sellQuota.Mul(bestAsk.Price))
} else {
disableMakerAsk = true
}
} else {
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
Expand Down Expand Up @@ -989,7 +1066,7 @@ func (s *Strategy) updateQuote(ctx context.Context) error {
askMarginMetrics.With(s.metricsLabels).Set(quote.AskMargin.Float64())

if s.EnableArbitrage {
done, err := s.tryArbitrage(ctx, quote, makerBalances, hedgeBalances)
done, err := s.tryArbitrage(ctx, quote, makerBalances, hedgeBalances, disableMakerBid, disableMakerAsk)
if err != nil {
s.logger.WithError(err).Errorf("unable to arbitrage")
} else if done {
Expand Down Expand Up @@ -1184,7 +1261,9 @@ func aggregatePriceVolumeSliceWithPriceFilter(
}

// tryArbitrage tries to arbitrage between the source and maker exchange
func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, makerBalances, hedgeBalances types.BalanceMap) (bool, error) {
func (s *Strategy) tryArbitrage(
ctx context.Context, quote *Quote, makerBalances, hedgeBalances types.BalanceMap, disableBid, disableAsk bool,
) (bool, error) {
if s.makerBook == nil {
return false, nil
}
Expand All @@ -1200,7 +1279,7 @@ func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, makerBalances
var iocOrders []types.SubmitOrder
if makerAsk.Price.Compare(marginBidPrice) <= 0 {
quoteBalance, hasQuote := makerBalances[s.makerMarket.QuoteCurrency]
if !hasQuote {
if !hasQuote || disableBid {
return false, nil
}

Expand Down Expand Up @@ -1233,7 +1312,7 @@ func (s *Strategy) tryArbitrage(ctx context.Context, quote *Quote, makerBalances

} else if makerBid.Price.Compare(marginAskPrice) >= 0 {
baseBalance, hasBase := makerBalances[s.makerMarket.BaseCurrency]
if !hasBase {
if !hasBase || disableAsk {
return false, nil
}

Expand Down Expand Up @@ -1612,6 +1691,10 @@ func (s *Strategy) Defaults() error {
}
}

if s.MinMarginLevel.IsZero() {
s.MinMarginLevel = fixedpoint.NewFromFloat(2.0)
}

// circuitBreakerAlertLimiter is for CircuitBreaker alerts
s.circuitBreakerAlertLimiter = rate.NewLimiter(rate.Every(3*time.Minute), 2)
s.reportProfitStatsRateLimiter = rate.NewLimiter(rate.Every(3*time.Minute), 1)
Expand Down
111 changes: 111 additions & 0 deletions pkg/strategy/xmaker/strategy_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build !dnum
// +build !dnum

package xmaker

import (
Expand All @@ -6,12 +9,120 @@ import (

"github.com/stretchr/testify/assert"

"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/pricesolver"
"github.com/c9s/bbgo/pkg/types"

. "github.com/c9s/bbgo/pkg/testing/testhelper"
)

func TestStrategy_allowMarginHedge(t *testing.T) {
symbol := "BTCUSDT"
market := Market(symbol)
priceSolver := pricesolver.NewSimplePriceResolver(AllMarkets())
priceSolver.Update("BTCUSDT", Number(98000.0))
priceSolver.Update("ETHUSDT", Number(3800.0))

// total equity value = 2 BTC at 98,000 + 200,000.0 USDT = 196,000 USDT + 200,000.0 USDT = 396,000 USDT
// net equity value = 1 BTC at 98,000 + 200,000.0 USDT = 98,000 USDT + 200,000.0 USDT = 298,000 USDT
// debt value = 1 BTC at 98,000 = 98,000 USDT
// current margin level = total equity value / debt value = 396,000 / 98,000 = 4.04081632653
// borrowing quota = (total equity value / min margin level 1.7) - debt value = (396,000 / 1.7) - 98,000 = 232,941.176470588
t.Run("safe margin level, calculate borrowing quota", func(t *testing.T) {
account := types.NewAccount()
account.MarginLevel = Number(3.04081632)
account.SetBalance("BTC", types.Balance{
Currency: "BTC",
Available: Number(2.0),
Borrowed: Number(1.0),
})

account.SetBalance("USDT", types.Balance{
Currency: "USDT",
Available: Number(200_000.0),
})

session := &bbgo.ExchangeSession{
Margin: true,
Account: account,
}

accountValueCalc := bbgo.NewAccountValueCalculator(session, priceSolver, types.USDT)
assert.Equal(t, "98000", accountValueCalc.DebtValue().String())
assert.Equal(t, "298000", accountValueCalc.NetValue().String())

s := &Strategy{
MinMarginLevel: Number(1.7),
makerMarket: market,
sourceMarket: market,
sourceSession: session,
accountValueCalculator: accountValueCalc,
}
s.lastPrice.Set(Number(98000.0))

allowed, quota := s.allowMarginHedge(types.SideTypeBuy)
if assert.True(t, allowed) {
assert.InDelta(t, 134941.176470588, quota.Float64(), 1.0, "should be able to borrow %f USDT", quota.Float64())
}

allowed, quota = s.allowMarginHedge(types.SideTypeSell)
if assert.True(t, allowed) {
assert.InDelta(t, 1.376951, quota.Float64(), 0.0001, "should be able to borrow %f BTC", quota.Float64())
}
})

// total equity value = 2 BTC at 98,000 + 200,000.0 USDT = 196,000 USDT + 200,000.0 USDT = 396,000 USDT
// net equity value = -2 BTC at 98,000 + 200,000.0 USDT = -196,000 USDT + 200,000.0 USDT = 4,000 USDT
// debt value = 4 BTC at 98,000 = 392,000 USDT
// current margin level = total equity value / debt value = 396,000 / 392,000 = 1.01020408163
t.Run("low margin level, calculate quota", func(t *testing.T) {
account := types.NewAccount()
account.SetBalance("BTC", types.Balance{
Currency: "BTC",
Available: Number(2.0),
Borrowed: Number(4.0),
})

account.SetBalance("USDT", types.Balance{
Currency: "USDT",
Available: Number(200_000.0),
})

session := &bbgo.ExchangeSession{
Margin: true,
Account: account,
}

accountValueCalc := bbgo.NewAccountValueCalculator(session, priceSolver, types.USDT)
assert.Equal(t, "392000", accountValueCalc.DebtValue().String())
assert.Equal(t, "4000", accountValueCalc.NetValue().String())

var err error
account.MarginLevel, err = accountValueCalc.MarginLevel()
if assert.NoError(t, err) {
assert.InDelta(t, 1.01, account.MarginLevel.Float64(), 0.001)
}

s := &Strategy{
MinMarginLevel: Number(1.7),
makerMarket: market,
sourceMarket: market,
sourceSession: session,
accountValueCalculator: accountValueCalc,
}
s.lastPrice.Set(Number(98000.0))

allowed, quota := s.allowMarginHedge(types.SideTypeBuy)
assert.False(t, allowed)

allowed, quota = s.allowMarginHedge(types.SideTypeSell)
if assert.True(t, allowed) {
assert.InDelta(t, 2.04, quota.Float64(), 0.001, "should be able to borrow %f BTC", quota.Float64())
}
})
}

func TestStrategy_getLayerPrice(t *testing.T) {
symbol := "BTCUSDT"
market := Market(symbol)
Expand Down
Loading
Loading