mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 00:35:15 +00:00
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:
commit
2214920b37
126
pkg/bbgo/risk.go
126
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")
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue
Block a user