From 304cc89f6848c7e4a500aa5fe53d45924addda2f Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 21 Apr 2022 15:29:59 +0800 Subject: [PATCH 01/13] binance: always sort trades back --- pkg/exchange/binance/exchange.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 4fba04a28..50d049705 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -1177,7 +1177,6 @@ func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval type } func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) { - if e.IsMargin { var remoteTrades []*binance.TradeV3 req := e.Client.NewListMarginTradesService(). @@ -1217,6 +1216,8 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type trades = append(trades, *localTrade) } + trades = types.SortTradesAscending(trades) + return trades, nil } else if e.IsFutures { var remoteTrades []*futures.AccountTrade @@ -1247,6 +1248,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type trades = append(trades, *localTrade) } + trades = types.SortTradesAscending(trades) return trades, nil } else { var remoteTrades []*binance.TradeV3 @@ -1285,10 +1287,12 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type trades = append(trades, *localTrade) } + trades = types.SortTradesAscending(trades) return trades, nil } } +// QueryDepth query the order book depth of a symbol func (e *Exchange) QueryDepth(ctx context.Context, symbol string) (snapshot types.SliceOrderBook, finalUpdateID int64, err error) { response, err := e.Client.NewDepthService().Symbol(symbol).Do(ctx) if err != nil { From fbe1906e7012e799851c8a3fdd345f6eb3ac846c Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 21 Apr 2022 15:53:52 +0800 Subject: [PATCH 02/13] binance: add more fields to the balance struct --- pkg/exchange/binance/exchange.go | 67 +++++++++++++++++++------------- pkg/types/account.go | 11 ++++++ 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 50d049705..ecf65519e 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -227,62 +227,77 @@ func (e *Exchange) NewStream() types.Stream { return stream } -func (e *Exchange) QueryMarginAccount(ctx context.Context) (*types.Account, error) { - account, err := e.Client.NewGetMarginAccountService().Do(ctx) +func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, error) { + marginAccount, err := e.Client.NewGetMarginAccountService().Do(ctx) if err != nil { return nil, err } a := &types.Account{ AccountType: types.AccountTypeMargin, - MarginInfo: toGlobalMarginAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition. + MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. + MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), } + // convert cross margin user assets into balances balances := types.BalanceMap{} - for _, userAsset := range account.UserAssets { + for _, userAsset := range marginAccount.UserAssets { balances[userAsset.Asset] = types.Balance{ Currency: userAsset.Asset, Available: fixedpoint.MustNewFromString(userAsset.Free), Locked: fixedpoint.MustNewFromString(userAsset.Locked), + Interest: fixedpoint.MustNewFromString(userAsset.Interest), + Borrowed: fixedpoint.MustNewFromString(userAsset.Borrowed), + NetAsset: fixedpoint.MustNewFromString(userAsset.NetAsset), } } a.UpdateBalances(balances) return a, nil } -func (e *Exchange) QueryIsolatedMarginAccount(ctx context.Context, symbols ...string) (*types.Account, error) { +func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Account, error) { req := e.Client.NewGetIsolatedMarginAccountService() - if len(symbols) > 0 { - req.Symbols(symbols...) - } + req.Symbols(e.IsolatedMarginSymbol) - account, err := req.Do(ctx) + marginAccount, err := req.Do(ctx) if err != nil { return nil, err } a := &types.Account{ AccountType: types.AccountTypeMargin, - IsolatedMarginInfo: toGlobalIsolatedMarginAccountInfo(account), // In binance GO api, Account define account info which mantain []*AccountAsset and []*AccountPosition. + IsolatedMarginInfo: toGlobalIsolatedMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. } + // for isolated margin account, we will only have one asset in the Assets array. + if len(marginAccount.Assets) > 1 { + return nil, fmt.Errorf("unexpected number of user assets returned, got %d user assets", len(marginAccount.Assets)) + } + + userAsset := marginAccount.Assets[0] + a.MarginLevel = fixedpoint.MustNewFromString(userAsset.MarginLevel) + + // Convert user assets into balances balances := types.BalanceMap{} - for _, userAsset := range account.Assets { - balances[userAsset.BaseAsset.Asset] = types.Balance{ - Currency: userAsset.BaseAsset.Asset, - Available: fixedpoint.MustNewFromString(userAsset.BaseAsset.Free), - Locked: fixedpoint.MustNewFromString(userAsset.BaseAsset.Locked), - } - - balances[userAsset.QuoteAsset.Asset] = types.Balance{ - Currency: userAsset.QuoteAsset.Asset, - Available: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Free), - Locked: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Locked), - } + balances[userAsset.BaseAsset.Asset] = types.Balance{ + Currency: userAsset.BaseAsset.Asset, + Available: fixedpoint.MustNewFromString(userAsset.BaseAsset.Free), + Locked: fixedpoint.MustNewFromString(userAsset.BaseAsset.Locked), + Interest: fixedpoint.MustNewFromString(userAsset.BaseAsset.Interest), + Borrowed: fixedpoint.MustNewFromString(userAsset.BaseAsset.Borrowed), + NetAsset: fixedpoint.MustNewFromString(userAsset.BaseAsset.NetAsset), } + + balances[userAsset.QuoteAsset.Asset] = types.Balance{ + Currency: userAsset.QuoteAsset.Asset, + Available: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Free), + Locked: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Locked), + Interest: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Interest), + Borrowed: fixedpoint.MustNewFromString(userAsset.QuoteAsset.Borrowed), + NetAsset: fixedpoint.MustNewFromString(userAsset.QuoteAsset.NetAsset), + } + a.UpdateBalances(balances) - - return a, nil } @@ -541,9 +556,9 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { if e.IsFutures { account, err = e.QueryFuturesAccount(ctx) } else if e.IsIsolatedMargin { - account, err = e.QueryIsolatedMarginAccount(ctx) + account, err = e.queryIsolatedMarginAccount(ctx) } else if e.IsMargin { - account, err = e.QueryMarginAccount(ctx) + account, err = e.queryCrossMarginAccount(ctx) } else { account, err = e.QuerySpotAccount(ctx) } diff --git a/pkg/types/account.go b/pkg/types/account.go index 42fffc4a9..0168b93ec 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -25,6 +25,14 @@ type Balance struct { Currency string `json:"currency"` Available fixedpoint.Value `json:"available"` Locked fixedpoint.Value `json:"locked,omitempty"` + + // margin related fields + Borrowed fixedpoint.Value `json:"borrowed,omitempty"` + Interest fixedpoint.Value `json:"interest,omitempty"` + + // NetAsset = (Available + Locked) - Borrowed - Interest + NetAsset fixedpoint.Value `json:"net,omitempty"` + } func (b Balance) Total() fixedpoint.Value { @@ -236,6 +244,9 @@ type Account struct { MarginInfo *MarginAccountInfo IsolatedMarginInfo *IsolatedMarginAccountInfo + // margin related common field + MarginLevel fixedpoint.Value `json:"marginLevel"` + MakerFeeRate fixedpoint.Value `json:"makerFeeRate,omitempty"` TakerFeeRate fixedpoint.Value `json:"takerFeeRate,omitempty"` From cf2e8c9f0a0a7800717f0e63bb3459b94a5ea21b Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 21 Apr 2022 16:23:06 +0800 Subject: [PATCH 03/13] all: extend balance field for margin --- pkg/exchange/binance/exchange.go | 9 ++++++--- pkg/types/account.go | 10 +++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index ecf65519e..00fa1c8a4 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -234,9 +234,10 @@ func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, } a := &types.Account{ - AccountType: types.AccountTypeMargin, - MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. - MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), + AccountType: types.AccountTypeMargin, + MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. + MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), + BorrowEnabled: marginAccount.BorrowEnabled, } // convert cross margin user assets into balances @@ -276,6 +277,8 @@ func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Accou userAsset := marginAccount.Assets[0] a.MarginLevel = fixedpoint.MustNewFromString(userAsset.MarginLevel) + a.BorrowEnabled = userAsset.BaseAsset.BorrowEnabled || userAsset.QuoteAsset.BorrowEnabled + // userAsset.LiquidatePrice // Convert user assets into balances balances := types.BalanceMap{} diff --git a/pkg/types/account.go b/pkg/types/account.go index 0168b93ec..31974792d 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -27,12 +27,11 @@ type Balance struct { Locked fixedpoint.Value `json:"locked,omitempty"` // margin related fields - Borrowed fixedpoint.Value `json:"borrowed,omitempty"` - Interest fixedpoint.Value `json:"interest,omitempty"` + Borrowed fixedpoint.Value `json:"borrowed,omitempty"` + Interest fixedpoint.Value `json:"interest,omitempty"` // NetAsset = (Available + Locked) - Borrowed - Interest - NetAsset fixedpoint.Value `json:"net,omitempty"` - + NetAsset fixedpoint.Value `json:"net,omitempty"` } func (b Balance) Total() fixedpoint.Value { @@ -245,7 +244,8 @@ type Account struct { IsolatedMarginInfo *IsolatedMarginAccountInfo // margin related common field - MarginLevel fixedpoint.Value `json:"marginLevel"` + MarginLevel fixedpoint.Value `json:"marginLevel"` + BorrowEnabled bool `json:"borrowEnabled"` MakerFeeRate fixedpoint.Value `json:"makerFeeRate,omitempty"` TakerFeeRate fixedpoint.Value `json:"takerFeeRate,omitempty"` From ecc19e1efde150ee516bc5a8c9314094af7ab994 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 13:41:44 +0800 Subject: [PATCH 04/13] binance: assign more margin fields to account --- pkg/exchange/binance/exchange.go | 5 ++++- pkg/types/account.go | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 00fa1c8a4..571bfaebf 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -238,6 +238,7 @@ func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), BorrowEnabled: marginAccount.BorrowEnabled, + TransferEnabled: marginAccount.TransferEnabled, } // convert cross margin user assets into balances @@ -277,8 +278,10 @@ func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Accou userAsset := marginAccount.Assets[0] a.MarginLevel = fixedpoint.MustNewFromString(userAsset.MarginLevel) + a.MarginRatio = fixedpoint.MustNewFromString(userAsset.MarginRatio) a.BorrowEnabled = userAsset.BaseAsset.BorrowEnabled || userAsset.QuoteAsset.BorrowEnabled - // userAsset.LiquidatePrice + a.LiquidationPrice = fixedpoint.MustNewFromString(userAsset.LiquidatePrice) + a.LiquidationRate = fixedpoint.MustNewFromString(userAsset.LiquidateRate) // Convert user assets into balances balances := types.BalanceMap{} diff --git a/pkg/types/account.go b/pkg/types/account.go index 31974792d..900a4ec27 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -244,8 +244,15 @@ type Account struct { IsolatedMarginInfo *IsolatedMarginAccountInfo // margin related common field - MarginLevel fixedpoint.Value `json:"marginLevel"` - BorrowEnabled bool `json:"borrowEnabled"` + MarginLevel fixedpoint.Value `json:"marginLevel,omitempty"` + BorrowEnabled bool `json:"borrowEnabled,omitempty"` + TransferEnabled bool `json:"transferEnabled,omitempty"` + + // isolated margin related fields + // LiquidationPrice is only used when account is in the isolated margin mode + MarginRatio fixedpoint.Value `json:"marginRatio,omitempty"` + LiquidationPrice fixedpoint.Value `json:"liquidationPrice,omitempty"` + LiquidationRate fixedpoint.Value `json:"liquidationRate,omitempty"` MakerFeeRate fixedpoint.Value `json:"makerFeeRate,omitempty"` TakerFeeRate fixedpoint.Value `json:"takerFeeRate,omitempty"` From a8fdd8006cbc7ae73e1066fdde7cd2b9455c9d2b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 13:57:29 +0800 Subject: [PATCH 05/13] binance: add transferCrossMarginAccount method --- pkg/exchange/binance/exchange.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 571bfaebf..6e98da8b2 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -227,6 +227,23 @@ func (e *Exchange) NewStream() types.Stream { return stream } +// transferCrossMarginAccount transfer asset to the cross margin account or to the main account +func (e *Exchange) transferCrossMarginAccount(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { + req := e.Client.NewMarginTransferService() + req.Asset(asset) + req.Amount(amount.String()) + + if io > 0 { // in + req.Type(binance.MarginTransferTypeToMargin) + } else if io < 0 { // out + req.Type(binance.MarginTransferTypeToMain) + } + resp, err := req.Do(ctx) + + log.Debugf("cross margin transfer, transaction id = %d", resp.TranID) + return err +} + func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, error) { marginAccount, err := e.Client.NewGetMarginAccountService().Do(ctx) if err != nil { @@ -234,10 +251,10 @@ func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, } a := &types.Account{ - AccountType: types.AccountTypeMargin, - MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. - MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), - BorrowEnabled: marginAccount.BorrowEnabled, + AccountType: types.AccountTypeMargin, + MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. + MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), + BorrowEnabled: marginAccount.BorrowEnabled, TransferEnabled: marginAccount.TransferEnabled, } From c2d1ef0fc88ddcab72bfa3ce1b6af838dcc60674 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 14:05:33 +0800 Subject: [PATCH 06/13] add margin borrow endpoint --- go.mod | 4 ++-- go.sum | 4 ++++ pkg/exchange/binance/exchange.go | 23 ++++++++++++++++++++--- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 63f509adc..e90c8e9f3 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ go 1.17 require ( github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/adshao/go-binance/v2 v2.3.3 + github.com/adshao/go-binance/v2 v2.3.5 github.com/c9s/requestgen v1.1.1-0.20211230171502-c042072e23cd github.com/c9s/rockhopper v1.2.1-0.20210217093258-2661955904a9 github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 @@ -16,7 +16,7 @@ require ( github.com/go-redis/redis/v8 v8.8.0 github.com/go-sql-driver/mysql v1.5.0 github.com/google/uuid v1.3.0 - github.com/gorilla/websocket v1.4.2 + github.com/gorilla/websocket v1.5.0 github.com/jmoiron/sqlx v1.3.4 github.com/joho/godotenv v1.3.0 github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 diff --git a/go.sum b/go.sum index b5f52f531..6d8932327 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/adshao/go-binance/v2 v2.3.3 h1:ts46Mq4n4Ji9pkbXhcuOZ2T2Zy1x3i5SboC6EKKqzyg= github.com/adshao/go-binance/v2 v2.3.3/go.mod h1:TfcBwfGtmRibSljDDR0XCaPkfBt1kc2N9lnNMYC3dCQ= +github.com/adshao/go-binance/v2 v2.3.5 h1:WVYZecm0w8l14YoWlnKZj6xxZT2AKMTHpMQSqIX1xxA= +github.com/adshao/go-binance/v2 v2.3.5/go.mod h1:8Pg/FGTLyAhq8QXA0IkoReKyRpoxJcK3LVujKDAZV/c= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -229,6 +231,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 6e98da8b2..dab1b8db1 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -227,8 +227,25 @@ func (e *Exchange) NewStream() types.Stream { return stream } -// transferCrossMarginAccount transfer asset to the cross margin account or to the main account -func (e *Exchange) transferCrossMarginAccount(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { +func (e *Exchange) repayCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { + return nil +} + +func (e *Exchange) borrowMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { + req := e.Client.NewMarginLoanService() + req.Asset(asset) + req.Amount(amount.String()) + if e.IsIsolatedMargin { + req.IsolatedSymbol(e.IsolatedMarginSymbol) + } + + resp, err := req.Do(ctx) + log.Debugf("margin borrowed %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID) + return err +} + +// transferCrossMarginAccountAsset transfer asset to the cross margin account or to the main account +func (e *Exchange) transferCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value, io int) error { req := e.Client.NewMarginTransferService() req.Asset(asset) req.Amount(amount.String()) @@ -240,7 +257,7 @@ func (e *Exchange) transferCrossMarginAccount(ctx context.Context, asset string, } resp, err := req.Do(ctx) - log.Debugf("cross margin transfer, transaction id = %d", resp.TranID) + log.Debugf("cross margin transfer %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID) return err } From 37b5d80f6fee27a6e431f1ddbad0e19db62732b4 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 14:07:09 +0800 Subject: [PATCH 07/13] add margin repay and borrow api --- pkg/exchange/binance/exchange.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index dab1b8db1..cfa85c39e 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -227,11 +227,20 @@ func (e *Exchange) NewStream() types.Stream { return stream } -func (e *Exchange) repayCrossMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { - return nil +func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { + req := e.Client.NewMarginRepayService() + req.Asset(asset) + req.Amount(amount.String()) + if e.IsIsolatedMargin { + req.IsolatedSymbol(e.IsolatedMarginSymbol) + } + + resp, err := req.Do(ctx) + log.Debugf("margin repayed %f %s, transaction id = %d", amount.Float64(), asset, resp.TranID) + return err } -func (e *Exchange) borrowMarginAccountAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { +func (e *Exchange) BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { req := e.Client.NewMarginLoanService() req.Asset(asset) req.Amount(amount.String()) From 9f9f13dfe286e7e41f1c0fedd1decdd821bfacff Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 14:08:29 +0800 Subject: [PATCH 08/13] add MarginBorrowRepay interface --- pkg/types/margin.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/types/margin.go b/pkg/types/margin.go index 6931d168f..fdaeb2ee0 100644 --- a/pkg/types/margin.go +++ b/pkg/types/margin.go @@ -1,6 +1,10 @@ package types -import "github.com/c9s/bbgo/pkg/fixedpoint" +import ( + "context" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) type FuturesExchange interface { UseFutures() @@ -48,6 +52,11 @@ type MarginExchange interface { // QueryMarginAccount(ctx context.Context) (*binance.MarginAccount, error) } +type MarginBorrowRepay interface { + RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error + BorrowMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error +} + type MarginSettings struct { IsMargin bool IsIsolatedMargin bool From 76733898db3879b56b45b329eb5ec6f50981f8f9 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 14:14:01 +0800 Subject: [PATCH 09/13] binance: add QueryMarginAssetMaxBorrowable api --- pkg/exchange/binance/exchange.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index cfa85c39e..fe0a8e509 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -227,6 +227,20 @@ func (e *Exchange) NewStream() types.Stream { return stream } +func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset string) (amount fixedpoint.Value, err error) { + req := e.Client.NewGetMaxBorrowableService() + req.Asset(asset) + if e.IsIsolatedMargin { + req.IsolatedSymbol(e.IsolatedMarginSymbol) + } + resp, err := req.Do(ctx) + if err != nil { + return 0, err + } + + return fixedpoint.NewFromString(resp.Amount) +} + func (e *Exchange) RepayMarginAsset(ctx context.Context, asset string, amount fixedpoint.Value) error { req := e.Client.NewMarginRepayService() req.Asset(asset) From 98a696a7d0c54e2f5cc6c8cf545fde8407bd2ba0 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 14:45:03 +0800 Subject: [PATCH 10/13] all: calculate MarginTolerance --- pkg/exchange/binance/exchange.go | 24 +++++++++++++++++++++--- pkg/types/account.go | 8 +++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index fe0a8e509..d8ad5829c 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -235,7 +235,7 @@ func (e *Exchange) QueryMarginAssetMaxBorrowable(ctx context.Context, asset stri } resp, err := req.Do(ctx) if err != nil { - return 0, err + return fixedpoint.Zero, err } return fixedpoint.NewFromString(resp.Amount) @@ -290,10 +290,12 @@ func (e *Exchange) queryCrossMarginAccount(ctx context.Context) (*types.Account, return nil, err } + marginLevel := fixedpoint.MustNewFromString(marginAccount.MarginLevel) a := &types.Account{ AccountType: types.AccountTypeMargin, MarginInfo: toGlobalMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. - MarginLevel: fixedpoint.MustNewFromString(marginAccount.MarginLevel), + MarginLevel: marginLevel, + MarginTolerance: calculateMarginTolerance(marginLevel), BorrowEnabled: marginAccount.BorrowEnabled, TransferEnabled: marginAccount.TransferEnabled, } @@ -334,7 +336,9 @@ func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Accou } userAsset := marginAccount.Assets[0] - a.MarginLevel = fixedpoint.MustNewFromString(userAsset.MarginLevel) + marginLevel := fixedpoint.MustNewFromString(userAsset.MarginLevel) + a.MarginLevel = marginLevel + a.MarginTolerance = calculateMarginTolerance(marginLevel) a.MarginRatio = fixedpoint.MustNewFromString(userAsset.MarginRatio) a.BorrowEnabled = userAsset.BaseAsset.BorrowEnabled || userAsset.QuoteAsset.BorrowEnabled a.LiquidationPrice = fixedpoint.MustNewFromString(userAsset.LiquidatePrice) @@ -1497,3 +1501,17 @@ func getLaunchDate() (time.Time, error) { return time.Date(2017, time.July, 14, 0, 0, 0, 0, loc), nil } + +// Margin tolerance ranges from 0.0 (liquidation) to 1.0 (safest level of margin). +func calculateMarginTolerance(marginLevel fixedpoint.Value) fixedpoint.Value { + if marginLevel.IsZero() { + // Although margin level shouldn't be zero, that would indicate a significant problem. + // In that case, margin tolerance should return 0.0 to also reflect that problem. + return fixedpoint.Zero + } + + // Formula created by operations team for our binance code. Liquidation occurs at 1.1, + // so when marginLevel equals 1.1, the formula becomes 1.0 - 1.0, or zero. + // = 1.0 - (1.1 / marginLevel) + return fixedpoint.One.Sub(fixedpoint.NewFromFloat(1.1).Div(marginLevel)) +} diff --git a/pkg/types/account.go b/pkg/types/account.go index 900a4ec27..5b9d893e6 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -243,8 +243,14 @@ type Account struct { MarginInfo *MarginAccountInfo IsolatedMarginInfo *IsolatedMarginAccountInfo - // margin related common field + // Margin related common field + // From binance: + // Margin Level = Total Asset Value / (Total Borrowed + Total Accrued Interest) + // If your margin level drops to 1.3, you will receive a Margin Call, which is a reminder that you should either increase your collateral (by depositing more funds) or reduce your loan (by repaying what you’ve borrowed). + // If your margin level drops to 1.1, your assets will be automatically liquidated, meaning that Binance will sell your funds at market price to repay the loan. MarginLevel fixedpoint.Value `json:"marginLevel,omitempty"` + MarginTolerance fixedpoint.Value `json:"marginTolerance,omitempty"` + BorrowEnabled bool `json:"borrowEnabled,omitempty"` TransferEnabled bool `json:"transferEnabled,omitempty"` From a1c9bd7ec8aa332e9bd0dda117cf303bdb1cdd42 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 14:45:45 +0800 Subject: [PATCH 11/13] all: add AccountTypeIsolatedMargin --- pkg/exchange/binance/exchange.go | 2 +- pkg/types/account.go | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index d8ad5829c..c4e0e237a 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -326,7 +326,7 @@ func (e *Exchange) queryIsolatedMarginAccount(ctx context.Context) (*types.Accou } a := &types.Account{ - AccountType: types.AccountTypeMargin, + AccountType: types.AccountTypeIsolatedMargin, IsolatedMarginInfo: toGlobalIsolatedMarginAccountInfo(marginAccount), // In binance GO api, Account define marginAccount info which mantain []*AccountAsset and []*AccountPosition. } diff --git a/pkg/types/account.go b/pkg/types/account.go index 5b9d893e6..f8680d64a 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -230,9 +230,10 @@ func (m BalanceMap) Print() { type AccountType string const ( - AccountTypeFutures = AccountType("futures") - AccountTypeMargin = AccountType("margin") - AccountTypeSpot = AccountType("spot") + AccountTypeFutures = AccountType("futures") + AccountTypeMargin = AccountType("margin") + AccountTypeIsolatedMargin = AccountType("isolated_margin") + AccountTypeSpot = AccountType("spot") ) type Account struct { @@ -251,8 +252,8 @@ type Account struct { MarginLevel fixedpoint.Value `json:"marginLevel,omitempty"` MarginTolerance fixedpoint.Value `json:"marginTolerance,omitempty"` - BorrowEnabled bool `json:"borrowEnabled,omitempty"` - TransferEnabled bool `json:"transferEnabled,omitempty"` + BorrowEnabled bool `json:"borrowEnabled,omitempty"` + TransferEnabled bool `json:"transferEnabled,omitempty"` // isolated margin related fields // LiquidationPrice is only used when account is in the isolated margin mode From 9e48a850bdcc36b8c2ce5475ee3a87237ed8dd89 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 15:14:44 +0800 Subject: [PATCH 12/13] bbgo: call queryAccount to update account --- pkg/bbgo/session.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index bac3ea259..6a82bafeb 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -311,15 +311,15 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) // query and initialize the balances if !session.PublicOnly { - log.Infof("querying balances from session %s...", session.Name) - balances, err := session.Exchange.QueryAccountBalances(ctx) + account, err := session.Exchange.QueryAccount(ctx) if err != nil { return err } + session.Account = account + log.Infof("%s account", session.Name) - balances.Print() - session.Account.UpdateBalances(balances) + account.Balances().Print() // forward trade updates and order updates to the order executor session.UserDataStream.OnTradeUpdate(session.OrderExecutor.EmitTradeUpdate) @@ -330,7 +330,7 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) // if metrics mode is enabled, we bind the callbacks to update metrics if viper.GetBool("metrics") { - session.metricsBalancesUpdater(balances) + session.metricsBalancesUpdater(account.Balances()) session.bindUserDataStreamMetrics(session.UserDataStream) } } From cf055c3f7d3c3f39431215ccbb164895c899f732 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 22 Apr 2022 18:53:06 +0800 Subject: [PATCH 13/13] bbgo: improve account updating --- pkg/bbgo/session.go | 31 +++++++++++++++++++++++++++++-- pkg/types/account.go | 12 ------------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 6a82bafeb..c06d78960 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "sync" "time" "github.com/c9s/bbgo/pkg/cache" @@ -190,7 +191,8 @@ type ExchangeSession struct { // --------------------------- // The exchange account states - Account *types.Account `json:"-" yaml:"-"` + Account *types.Account `json:"-" yaml:"-"` + accountMutex sync.Mutex IsInitialized bool `json:"-" yaml:"-"` @@ -280,6 +282,18 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { return session } +// UpdateAccount locks the account mutex and update the account object +func (session *ExchangeSession) UpdateAccount(ctx context.Context) error { + account, err := session.Exchange.QueryAccount(ctx) + if err != nil { + return err + } + session.accountMutex.Lock() + session.Account = account + session.accountMutex.Unlock() + return nil +} + // Init initializes the basic data structure and market information by its exchange. // Note that the subscribed symbols are not loaded in this stage. func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) error { @@ -316,7 +330,9 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) return err } + session.accountMutex.Lock() session.Account = account + session.accountMutex.Unlock() log.Infof("%s account", session.Name) account.Balances().Print() @@ -324,7 +340,18 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) // forward trade updates and order updates to the order executor session.UserDataStream.OnTradeUpdate(session.OrderExecutor.EmitTradeUpdate) session.UserDataStream.OnOrderUpdate(session.OrderExecutor.EmitOrderUpdate) - session.Account.BindStream(session.UserDataStream) + + session.UserDataStream.OnBalanceSnapshot(func(balances types.BalanceMap) { + session.accountMutex.Lock() + session.Account.UpdateBalances(balances) + session.accountMutex.Unlock() + }) + + session.UserDataStream.OnBalanceUpdate(func(balances types.BalanceMap) { + session.accountMutex.Lock() + session.Account.UpdateBalances(balances) + session.accountMutex.Unlock() + }) session.bindConnectionStatusNotification(session.UserDataStream, "user data") diff --git a/pkg/types/account.go b/pkg/types/account.go index f8680d64a..e18131d1e 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -418,18 +418,6 @@ func (a *Account) UpdateBalances(balances BalanceMap) { } } -func printBalanceUpdate(balances BalanceMap) { - logrus.Infof("balance update: %+v", balances) -} - -func (a *Account) BindStream(stream Stream) { - stream.OnBalanceUpdate(a.UpdateBalances) - stream.OnBalanceSnapshot(a.UpdateBalances) - if debugBalance { - stream.OnBalanceUpdate(printBalanceUpdate) - } -} - func (a *Account) Print() { a.Lock() defer a.Unlock()