Skip to content

Commit 2214920

Browse files
authored
Merge pull request #935 from c9s/fix/open-position
bbgo: add price check and add max leverage for cross margin
2 parents 4ea723d + 20dd37c commit 2214920

File tree

3 files changed

+171
-57
lines changed

3 files changed

+171
-57
lines changed

pkg/bbgo/risk.go

+100-26
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import (
1414

1515
var defaultLeverage = fixedpoint.NewFromInt(3)
1616

17-
var maxLeverage = fixedpoint.NewFromInt(10)
17+
var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10)
18+
19+
var maxCrossMarginLeverage = fixedpoint.NewFromInt(3)
1820

1921
type AccountValueCalculator struct {
2022
session *ExchangeSession
@@ -128,13 +130,14 @@ func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value
128130
continue
129131
}
130132

131-
symbol := b.Currency + c.quoteCurrency
132-
price, ok := c.prices[symbol]
133-
if !ok {
134-
continue
133+
symbol := b.Currency + c.quoteCurrency // for BTC/USDT, ETH/USDT pairs
134+
symbolReverse := c.quoteCurrency + b.Currency // for USDT/USDC or USDT/TWD pairs
135+
if price, ok := c.prices[symbol]; ok {
136+
accountValue = accountValue.Add(b.Net().Mul(price))
137+
} else if priceReverse, ok2 := c.prices[symbolReverse]; ok2 {
138+
price2 := one.Div(priceReverse)
139+
accountValue = accountValue.Add(b.Net().Mul(price2))
135140
}
136-
137-
accountValue = accountValue.Add(b.Net().Mul(price))
138141
}
139142

140143
return accountValue, nil
@@ -186,60 +189,116 @@ func (c *AccountValueCalculator) MarginLevel(ctx context.Context) (fixedpoint.Va
186189
return marginLevel, nil
187190
}
188191

192+
func aggregateUsdValue(balances types.BalanceMap) fixedpoint.Value {
193+
totalUsdValue := fixedpoint.Zero
194+
// get all usd value if any
195+
for currency, balance := range balances {
196+
if types.IsUSDFiatCurrency(currency) {
197+
totalUsdValue = totalUsdValue.Add(balance.Net())
198+
}
199+
}
200+
201+
return totalUsdValue
202+
}
203+
204+
func usdFiatBalances(balances types.BalanceMap) (fiats types.BalanceMap, rest types.BalanceMap) {
205+
rest = make(types.BalanceMap)
206+
fiats = make(types.BalanceMap)
207+
for currency, balance := range balances {
208+
if types.IsUSDFiatCurrency(currency) {
209+
fiats[currency] = balance
210+
} else {
211+
rest[currency] = balance
212+
}
213+
}
214+
215+
return fiats, rest
216+
}
217+
189218
func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value) (fixedpoint.Value, error) {
190219
// default leverage guard
191220
if leverage.IsZero() {
192221
leverage = defaultLeverage
193222
}
194223

195-
baseBalance, _ := session.Account.Balance(market.BaseCurrency)
224+
baseBalance, hasBaseBalance := session.Account.Balance(market.BaseCurrency)
196225
quoteBalance, _ := session.Account.Balance(market.QuoteCurrency)
226+
balances := session.Account.Balances()
197227

198228
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
199229
if !usingLeverage {
200230
// For spot, we simply sell the base quoteCurrency
201-
balance, hasBalance := session.Account.Balance(market.BaseCurrency)
202-
if hasBalance {
231+
if hasBaseBalance {
203232
if quantity.IsZero() {
204-
log.Warnf("sell quantity is not set, using all available base balance: %v", balance)
205-
if !balance.Available.IsZero() {
206-
return balance.Available, nil
233+
log.Warnf("sell quantity is not set, using all available base balance: %v", baseBalance)
234+
if !baseBalance.Available.IsZero() {
235+
return baseBalance.Available, nil
207236
}
208237
} else {
209-
return fixedpoint.Min(quantity, balance.Available), nil
238+
return fixedpoint.Min(quantity, baseBalance.Available), nil
210239
}
211240
}
212241

213-
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings")
242+
return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances)
243+
}
244+
245+
usdBalances, restBalances := usdFiatBalances(balances)
246+
247+
// for isolated margin we can calculate from these two pair
248+
totalUsdValue := fixedpoint.Zero
249+
if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) {
250+
totalUsdValue = aggregateUsdValue(balances)
251+
} else if len(restBalances) > 1 {
252+
accountValue := NewAccountValueCalculator(session, "USDT")
253+
netValue, err := accountValue.NetValue(context.Background())
254+
if err != nil {
255+
return quantity, err
256+
}
257+
258+
totalUsdValue = netValue
259+
} else {
260+
// TODO: translate quote currency like BTC of ETH/BTC to usd value
261+
totalUsdValue = aggregateUsdValue(usdBalances)
214262
}
215263

216264
if !quantity.IsZero() {
217265
return quantity, nil
218266
}
219267

268+
if price.IsZero() {
269+
return quantity, fmt.Errorf("%s price can not be zero", market.Symbol)
270+
}
271+
220272
// using leverage -- starts from here
221273
log.Infof("calculating available leveraged base quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance)
222274

223275
// calculate the quantity automatically
224276
if session.Margin || session.IsolatedMargin {
225277
baseBalanceValue := baseBalance.Net().Mul(price)
226-
accountValue := baseBalanceValue.Add(quoteBalance.Net())
278+
accountUsdValue := baseBalanceValue.Add(totalUsdValue)
227279

228280
// avoid using all account value since there will be some trade loss for interests and the fee
229-
accountValue = accountValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
281+
accountUsdValue = accountUsdValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
230282

231-
log.Infof("calculated account value %f %s", accountValue.Float64(), market.QuoteCurrency)
283+
log.Infof("calculated account usd value %f %s", accountUsdValue.Float64(), market.QuoteCurrency)
232284

285+
originLeverage := leverage
233286
if session.IsolatedMargin {
234-
originLeverage := leverage
235-
leverage = fixedpoint.Min(leverage, maxLeverage)
236-
log.Infof("using isolated margin, maxLeverage=10 originalLeverage=%f currentLeverage=%f",
287+
leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage)
288+
log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
289+
maxIsolatedMarginLeverage.Float64(),
290+
originLeverage.Float64(),
291+
leverage.Float64())
292+
} else {
293+
leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage)
294+
log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
295+
maxCrossMarginLeverage.Float64(),
237296
originLeverage.Float64(),
238297
leverage.Float64())
239298
}
240299

241300
// spot margin use the equity value, so we use the total quote balance here
242-
maxPosition := risk.CalculateMaxPosition(price, accountValue, leverage)
301+
maxPosition := risk.CalculateMaxPosition(price, accountUsdValue, leverage)
243302
debt := baseBalance.Debt()
244303
maxQuantity := maxPosition.Sub(debt)
245304

@@ -248,7 +307,7 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
248307
maxPosition.Float64(),
249308
debt.Float64(),
250309
price.Float64(),
251-
accountValue.Float64(),
310+
accountUsdValue.Float64(),
252311
market.QuoteCurrency,
253312
leverage.Float64())
254313

@@ -257,10 +316,10 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
257316

258317
if session.Futures || session.IsolatedFutures {
259318
// TODO: get mark price here
260-
maxPositionQuantity := risk.CalculateMaxPosition(price, quoteBalance.Available, leverage)
319+
maxPositionQuantity := risk.CalculateMaxPosition(price, totalUsdValue, leverage)
261320
requiredPositionCost := risk.CalculatePositionCost(price, price, maxPositionQuantity, leverage, types.SideTypeSell)
262321
if quoteBalance.Available.Compare(requiredPositionCost) < 0 {
263-
return maxPositionQuantity, fmt.Errorf("available margin %f %s is not enough, can not submit order", quoteBalance.Available.Float64(), market.QuoteCurrency)
322+
return maxPositionQuantity, fmt.Errorf("margin total usd value %f is not enough, can not submit order", totalUsdValue.Float64())
264323
}
265324

266325
return maxPositionQuantity, nil
@@ -276,15 +335,30 @@ func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quote
276335
}
277336

278337
quoteBalance, _ := session.Account.Balance(quoteCurrency)
279-
accountValue := NewAccountValueCalculator(session, quoteCurrency)
280338

281339
usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures
282340
if !usingLeverage {
283341
// For spot, we simply return the quote balance
284342
return quoteBalance.Available.Mul(fixedpoint.Min(leverage, fixedpoint.One)), nil
285343
}
286344

345+
originLeverage := leverage
346+
if session.IsolatedMargin {
347+
leverage = fixedpoint.Min(leverage, maxIsolatedMarginLeverage)
348+
log.Infof("using isolated margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
349+
maxIsolatedMarginLeverage.Float64(),
350+
originLeverage.Float64(),
351+
leverage.Float64())
352+
} else {
353+
leverage = fixedpoint.Min(leverage, maxCrossMarginLeverage)
354+
log.Infof("using cross margin, maxLeverage=%f originalLeverage=%f currentLeverage=%f",
355+
maxCrossMarginLeverage.Float64(),
356+
originLeverage.Float64(),
357+
leverage.Float64())
358+
}
359+
287360
// using leverage -- starts from here
361+
accountValue := NewAccountValueCalculator(session, quoteCurrency)
288362
availableQuote, err := accountValue.AvailableQuote(ctx)
289363
if err != nil {
290364
log.WithError(err).Errorf("can not update available quote")

pkg/bbgo/risk_test.go

+71
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,74 @@ func TestNewAccountValueCalculator_MarginLevel(t *testing.T) {
155155
fixedpoint.NewFromFloat(21000.0).Div(fixedpoint.NewFromFloat(19000.0).Mul(fixedpoint.NewFromFloat(1.003))).FormatString(6),
156156
marginLevel.FormatString(6))
157157
}
158+
159+
func number(n float64) fixedpoint.Value {
160+
return fixedpoint.NewFromFloat(n)
161+
}
162+
163+
func Test_aggregateUsdValue(t *testing.T) {
164+
type args struct {
165+
balances types.BalanceMap
166+
}
167+
tests := []struct {
168+
name string
169+
args args
170+
want fixedpoint.Value
171+
}{
172+
{
173+
name: "mixed",
174+
args: args{
175+
balances: types.BalanceMap{
176+
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
177+
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
178+
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
179+
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
180+
},
181+
},
182+
want: number(250.0),
183+
},
184+
}
185+
for _, tt := range tests {
186+
t.Run(tt.name, func(t *testing.T) {
187+
assert.Equalf(t, tt.want, aggregateUsdValue(tt.args.balances), "aggregateUsdValue(%v)", tt.args.balances)
188+
})
189+
}
190+
}
191+
192+
func Test_usdFiatBalances(t *testing.T) {
193+
type args struct {
194+
balances types.BalanceMap
195+
}
196+
tests := []struct {
197+
name string
198+
args args
199+
wantFiats types.BalanceMap
200+
wantRest types.BalanceMap
201+
}{
202+
{
203+
args: args{
204+
balances: types.BalanceMap{
205+
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
206+
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
207+
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
208+
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
209+
},
210+
},
211+
wantFiats: types.BalanceMap{
212+
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
213+
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
214+
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
215+
},
216+
wantRest: types.BalanceMap{
217+
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
218+
},
219+
},
220+
}
221+
for _, tt := range tests {
222+
t.Run(tt.name, func(t *testing.T) {
223+
gotFiats, gotRest := usdFiatBalances(tt.args.balances)
224+
assert.Equalf(t, tt.wantFiats, gotFiats, "usdFiatBalances(%v)", tt.args.balances)
225+
assert.Equalf(t, tt.wantRest, gotRest, "usdFiatBalances(%v)", tt.args.balances)
226+
})
227+
}
228+
}

pkg/notifier/slacknotifier/slack.go

-31
Original file line numberDiff line numberDiff line change
@@ -158,34 +158,3 @@ func (n *Notifier) SendPhoto(buffer *bytes.Buffer) {
158158
func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) {
159159
// TODO
160160
}
161-
162-
/*
163-
func (n *Notifier) NotifyTrade(trade *types.Trade) {
164-
_, _, err := n.client.PostMessageContext(context.Background(), n.TradeChannel,
165-
slack.MsgOptionText(util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade), true),
166-
slack.MsgOptionAttachments(trade.SlackAttachment()))
167-
168-
if err != nil {
169-
logrus.WithError(err).Error("slack send error")
170-
}
171-
}
172-
*/
173-
174-
/*
175-
func (n *Notifier) NotifyPnL(report *pnl.AverageCostPnlReport) {
176-
attachment := report.SlackAttachment()
177-
178-
_, _, err := n.client.PostMessageContext(context.Background(), n.PnlChannel,
179-
slack.MsgOptionText(util.Render(
180-
`:heavy_dollar_sign: Here is your *{{ .symbol }}* PnL report collected since *{{ .startTime }}*`,
181-
map[string]interface{}{
182-
"symbol": report.Symbol,
183-
"startTime": report.StartTime.Format(time.RFC822),
184-
}), true),
185-
slack.MsgOptionAttachments(attachment))
186-
187-
if err != nil {
188-
logrus.WithError(err).Errorf("slack send error")
189-
}
190-
}
191-
*/

0 commit comments

Comments
 (0)