Merge pull request #935 from c9s/fix/open-position

bbgo: add price check and add max leverage for cross margin
This commit is contained in:
Yo-An Lin 2022-09-12 00:42:12 +08:00 committed by GitHub
commit 2214920b37
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 57 deletions

View File

@ -14,7 +14,9 @@ import (
var defaultLeverage = fixedpoint.NewFromInt(3)
var maxLeverage = fixedpoint.NewFromInt(10)
var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10)
var maxCrossMarginLeverage = fixedpoint.NewFromInt(3)
type AccountValueCalculator struct {
session *ExchangeSession
@ -128,13 +130,14 @@ func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value
continue
}
symbol := b.Currency + c.quoteCurrency
price, ok := c.prices[symbol]
if !ok {
continue
symbol := b.Currency + c.quoteCurrency // for BTC/USDT, ETH/USDT pairs
symbolReverse := c.quoteCurrency + b.Currency // for USDT/USDC or USDT/TWD pairs
if price, ok := c.prices[symbol]; ok {
accountValue = accountValue.Add(b.Net().Mul(price))
} else if priceReverse, ok2 := c.prices[symbolReverse]; ok2 {
price2 := one.Div(priceReverse)
accountValue = accountValue.Add(b.Net().Mul(price2))
}
accountValue = accountValue.Add(b.Net().Mul(price))
}
return accountValue, nil
@ -186,60 +189,116 @@ func (c *AccountValueCalculator) MarginLevel(ctx context.Context) (fixedpoint.Va
return marginLevel, nil
}
func aggregateUsdValue(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) {
// default leverage guard
if leverage.IsZero() {
leverage = defaultLeverage
}
baseBalance, _ := session.Account.Balance(market.BaseCurrency)
baseBalance, hasBaseBalance := session.Account.Balance(market.BaseCurrency)
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency)
balances := session.Account.Balances()
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
if !usingLeverage {
// For spot, we simply sell the base quoteCurrency
balance, hasBalance := session.Account.Balance(market.BaseCurrency)
if hasBalance {
if hasBaseBalance {
if quantity.IsZero() {
log.Warnf("sell quantity is not set, using all available base balance: %v", balance)
if !balance.Available.IsZero() {
return balance.Available, nil
log.Warnf("sell quantity is not set, using all available base balance: %v", baseBalance)
if !baseBalance.Available.IsZero() {
return baseBalance.Available, nil
}
} else {
return fixedpoint.Min(quantity, balance.Available), nil
return fixedpoint.Min(quantity, baseBalance.Available), nil
}
}
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings")
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances)
}
usdBalances, restBalances := usdFiatBalances(balances)
// for isolated margin we can calculate from these two pair
totalUsdValue := fixedpoint.Zero
if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) {
totalUsdValue = aggregateUsdValue(balances)
} else if len(restBalances) > 1 {
accountValue := NewAccountValueCalculator(session, "USDT")
netValue, err := accountValue.NetValue(context.Background())
if err != nil {
return quantity, err
}
totalUsdValue = netValue
} else {
// TODO: translate quote currency like BTC of ETH/BTC to usd value
totalUsdValue = aggregateUsdValue(usdBalances)
}
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, quote balance = %+v", baseBalance, quoteBalance)
// calculate the quantity automatically
if session.Margin || session.IsolatedMargin {
baseBalanceValue := baseBalance.Net().Mul(price)
accountValue := baseBalanceValue.Add(quoteBalance.Net())
accountUsdValue := baseBalanceValue.Add(totalUsdValue)
// avoid using all account value since there will be some trade loss for interests and the fee
accountValue = accountValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
accountUsdValue = accountUsdValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
log.Infof("calculated account value %f %s", accountValue.Float64(), market.QuoteCurrency)
log.Infof("calculated account usd value %f %s", accountUsdValue.Float64(), market.QuoteCurrency)
originLeverage := leverage
if session.IsolatedMargin {
originLeverage := leverage
leverage = fixedpoint.Min(leverage, maxLeverage)
log.Infof("using isolated margin, maxLeverage=10 originalLeverage=%f currentLeverage=%f",
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())
}
// spot margin use the equity value, so we use the total quote balance here
maxPosition := risk.CalculateMaxPosition(price, accountValue, leverage)
maxPosition := risk.CalculateMaxPosition(price, accountUsdValue, leverage)
debt := baseBalance.Debt()
maxQuantity := maxPosition.Sub(debt)
@ -248,7 +307,7 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
maxPosition.Float64(),
debt.Float64(),
price.Float64(),
accountValue.Float64(),
accountUsdValue.Float64(),
market.QuoteCurrency,
leverage.Float64())
@ -257,10 +316,10 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
if session.Futures || session.IsolatedFutures {
// TODO: get mark price here
maxPositionQuantity := risk.CalculateMaxPosition(price, quoteBalance.Available, leverage)
maxPositionQuantity := risk.CalculateMaxPosition(price, totalUsdValue, leverage)
requiredPositionCost := risk.CalculatePositionCost(price, price, maxPositionQuantity, leverage, types.SideTypeSell)
if quoteBalance.Available.Compare(requiredPositionCost) < 0 {
return maxPositionQuantity, fmt.Errorf("available margin %f %s is not enough, can not submit order", quoteBalance.Available.Float64(), market.QuoteCurrency)
return maxPositionQuantity, fmt.Errorf("margin total usd value %f is not enough, can not submit order", totalUsdValue.Float64())
}
return maxPositionQuantity, nil
@ -276,7 +335,6 @@ func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quote
}
quoteBalance, _ := session.Account.Balance(quoteCurrency)
accountValue := NewAccountValueCalculator(session, quoteCurrency)
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
if !usingLeverage {
@ -284,7 +342,23 @@ func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quote
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
accountValue := NewAccountValueCalculator(session, quoteCurrency)
availableQuote, err := accountValue.AvailableQuote(ctx)
if err != nil {
log.WithError(err).Errorf("can not update available quote")

View File

@ -155,3 +155,74 @@ func TestNewAccountValueCalculator_MarginLevel(t *testing.T) {
fixedpoint.NewFromFloat(21000.0).Div(fixedpoint.NewFromFloat(19000.0).Mul(fixedpoint.NewFromFloat(1.003))).FormatString(6),
marginLevel.FormatString(6))
}
func number(n float64) fixedpoint.Value {
return fixedpoint.NewFromFloat(n)
}
func Test_aggregateUsdValue(t *testing.T) {
type args struct {
balances types.BalanceMap
}
tests := []struct {
name string
args args
want fixedpoint.Value
}{
{
name: "mixed",
args: args{
balances: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
},
},
want: number(250.0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, aggregateUsdValue(tt.args.balances), "aggregateUsdValue(%v)", tt.args.balances)
})
}
}
func Test_usdFiatBalances(t *testing.T) {
type args struct {
balances types.BalanceMap
}
tests := []struct {
name string
args args
wantFiats types.BalanceMap
wantRest types.BalanceMap
}{
{
args: args{
balances: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
},
},
wantFiats: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
},
wantRest: types.BalanceMap{
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotFiats, gotRest := usdFiatBalances(tt.args.balances)
assert.Equalf(t, tt.wantFiats, gotFiats, "usdFiatBalances(%v)", tt.args.balances)
assert.Equalf(t, tt.wantRest, gotRest, "usdFiatBalances(%v)", tt.args.balances)
})
}
}

View File

@ -158,34 +158,3 @@ func (n *Notifier) SendPhoto(buffer *bytes.Buffer) {
func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) {
// TODO
}
/*
func (n *Notifier) NotifyTrade(trade *types.Trade) {
_, _, err := n.client.PostMessageContext(context.Background(), n.TradeChannel,
slack.MsgOptionText(util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade), true),
slack.MsgOptionAttachments(trade.SlackAttachment()))
if err != nil {
logrus.WithError(err).Error("slack send error")
}
}
*/
/*
func (n *Notifier) NotifyPnL(report *pnl.AverageCostPnlReport) {
attachment := report.SlackAttachment()
_, _, err := n.client.PostMessageContext(context.Background(), n.PnlChannel,
slack.MsgOptionText(util.Render(
`:heavy_dollar_sign: Here is your *{{ .symbol }}* PnL report collected since *{{ .startTime }}*`,
map[string]interface{}{
"symbol": report.Symbol,
"startTime": report.StartTime.Format(time.RFC822),
}), true),
slack.MsgOptionAttachments(attachment))
if err != nil {
logrus.WithError(err).Errorf("slack send error")
}
}
*/