diff --git a/pkg/bbgo/risk.go b/pkg/bbgo/risk.go index a28617f53..51b0ce7bb 100644 --- a/pkg/bbgo/risk.go +++ b/pkg/bbgo/risk.go @@ -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") diff --git a/pkg/bbgo/risk_test.go b/pkg/bbgo/risk_test.go index fcd9f532b..9c486af32 100644 --- a/pkg/bbgo/risk_test.go +++ b/pkg/bbgo/risk_test.go @@ -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) + }) + } +} diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 333e3f1e5..4b775532f 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -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") - } -} -*/