From a463c02183c0ebfef8f10de0fa827e8e9c2b8ba4 Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 9 Jan 2024 14:20:56 +0800 Subject: [PATCH 1/2] pkg/exchange: generate account by requestgen --- pkg/exchange/okex/okexapi/client.go | 47 ------ pkg/exchange/okex/okexapi/client_test.go | 11 ++ .../okex/okexapi/get_account_info_request.go | 40 +++++ .../get_account_info_request_requestgen.go | 157 ++++++++++++++++++ 4 files changed, 208 insertions(+), 47 deletions(-) create mode 100644 pkg/exchange/okex/okexapi/get_account_info_request.go create mode 100644 pkg/exchange/okex/okexapi/get_account_info_request_requestgen.go diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index c7dd5021b..85b00b807 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -13,7 +13,6 @@ import ( "time" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" "github.com/c9s/requestgen" "github.com/pkg/errors" ) @@ -158,52 +157,6 @@ func (c *RestClient) NewAuthenticatedRequest(ctx context.Context, method, refURL return req, nil } -type BalanceDetail struct { - Currency string `json:"ccy"` - Available fixedpoint.Value `json:"availEq"` - CashBalance fixedpoint.Value `json:"cashBal"` - OrderFrozen fixedpoint.Value `json:"ordFrozen"` - Frozen fixedpoint.Value `json:"frozenBal"` - Equity fixedpoint.Value `json:"eq"` - EquityInUSD fixedpoint.Value `json:"eqUsd"` - UpdateTime types.MillisecondTimestamp `json:"uTime"` - UnrealizedProfitAndLoss fixedpoint.Value `json:"upl"` -} - -type Account struct { - TotalEquityInUSD fixedpoint.Value `json:"totalEq"` - UpdateTime string `json:"uTime"` - Details []BalanceDetail `json:"details"` -} - -func (c *RestClient) AccountBalances(ctx context.Context) (*Account, error) { - req, err := c.NewAuthenticatedRequest(ctx, "GET", "/api/v5/account/balance", nil, nil) - if err != nil { - return nil, err - } - - response, err := c.SendRequest(req) - if err != nil { - return nil, err - } - - var balanceResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []Account `json:"data"` - } - - if err := response.DecodeJSON(&balanceResponse); err != nil { - return nil, err - } - - if len(balanceResponse.Data) == 0 { - return nil, errors.New("empty account data") - } - - return &balanceResponse.Data[0], nil -} - type AssetBalance struct { Currency string `json:"ccy"` Balance fixedpoint.Value `json:"bal"` diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 1d8dc9c67..218dcfd67 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -60,6 +60,17 @@ func TestClient_GetMarketTicker(t *testing.T) { t.Logf("tickers: %+v", tickers) } +func TestClient_GetAcountInfo(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + req := client.NewGetAccountInfoRequest() + + acct, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, acct) + t.Logf("acct: %+v", acct) +} + func TestClient_GetFundingRateRequest(t *testing.T) { client := NewClient() ctx := context.Background() diff --git a/pkg/exchange/okex/okexapi/get_account_info_request.go b/pkg/exchange/okex/okexapi/get_account_info_request.go new file mode 100644 index 000000000..ae1d936a7 --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_account_info_request.go @@ -0,0 +1,40 @@ +package okexapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +type BalanceDetail struct { + Currency string `json:"ccy"` + Available fixedpoint.Value `json:"availEq"` + CashBalance fixedpoint.Value `json:"cashBal"` + OrderFrozen fixedpoint.Value `json:"ordFrozen"` + Frozen fixedpoint.Value `json:"frozenBal"` + Equity fixedpoint.Value `json:"eq"` + EquityInUSD fixedpoint.Value `json:"eqUsd"` + UpdateTime types.MillisecondTimestamp `json:"uTime"` + UnrealizedProfitAndLoss fixedpoint.Value `json:"upl"` +} + +type Account struct { + TotalEquityInUSD fixedpoint.Value `json:"totalEq"` + UpdateTime types.MillisecondTimestamp `json:"uTime"` + Details []BalanceDetail `json:"details"` +} + +//go:generate GetRequest -url "/api/v5/account/balance" -type GetAccountInfoRequest -responseDataType []Account +type GetAccountInfoRequest struct { + client requestgen.AuthenticatedAPIClient +} + +func (c *RestClient) NewGetAccountInfoRequest() *GetAccountInfoRequest { + return &GetAccountInfoRequest{ + client: c, + } +} diff --git a/pkg/exchange/okex/okexapi/get_account_info_request_requestgen.go b/pkg/exchange/okex/okexapi/get_account_info_request_requestgen.go new file mode 100644 index 000000000..b90836d19 --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_account_info_request_requestgen.go @@ -0,0 +1,157 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/account/balance -type GetAccountInfoRequest -responseDataType []Account"; DO NOT EDIT. + +package okexapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAccountInfoRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAccountInfoRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAccountInfoRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAccountInfoRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAccountInfoRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAccountInfoRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAccountInfoRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAccountInfoRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAccountInfoRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (g *GetAccountInfoRequest) GetPath() string { + return "/api/v5/account/balance" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetAccountInfoRequest) Do(ctx context.Context) ([]Account, error) { + + // no body params + var params interface{} + query := url.Values{} + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + + type responseValidator interface { + Validate() error + } + validator, ok := interface{}(apiResponse).(responseValidator) + if ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data []Account + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} From 9297293a4635d06c34bfa622be9393b64b35ee3a Mon Sep 17 00:00:00 2001 From: Edwin Date: Tue, 9 Jan 2024 14:23:39 +0800 Subject: [PATCH 2/2] pkg/exchange: refactor query account balance --- pkg/exchange/okex/exchange.go | 26 +++++++++++++++----------- pkg/exchange/okex/parse_test.go | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index fe3c5840f..9d3eac7fb 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -27,6 +27,7 @@ var ( queryMarketLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) queryTickerLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) queryTickersLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) + queryAccountLimiter = rate.NewLimiter(rate.Every(200*time.Millisecond), 5) ) const ID = "okex" @@ -167,28 +168,31 @@ func (e *Exchange) PlatformFeeCurrency() string { } func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { - accountBalance, err := e.client.AccountBalances(ctx) + bals, err := e.QueryAccountBalances(ctx) if err != nil { return nil, err } - var account = types.Account{ - AccountType: types.AccountTypeSpot, - } - - var balanceMap = toGlobalBalance(accountBalance) - account.UpdateBalances(balanceMap) - return &account, nil + account := types.NewAccount() + account.UpdateBalances(bals) + return account, nil } func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { - accountBalances, err := e.client.AccountBalances(ctx) + if err := queryAccountLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("account rate limiter wait error: %w", err) + } + + accountBalances, err := e.client.NewGetAccountInfoRequest().Do(ctx) if err != nil { return nil, err } - var balanceMap = toGlobalBalance(accountBalances) - return balanceMap, nil + if len(accountBalances) != 1 { + return nil, fmt.Errorf("unexpected length of balances: %v", accountBalances) + } + + return toGlobalBalance(&accountBalances[0]), nil } func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) { diff --git a/pkg/exchange/okex/parse_test.go b/pkg/exchange/okex/parse_test.go index 55c1d0e89..84395b58a 100644 --- a/pkg/exchange/okex/parse_test.go +++ b/pkg/exchange/okex/parse_test.go @@ -95,7 +95,7 @@ func Test_parseWebSocketEvent_accountEvent(t *testing.T) { exp := &okexapi.Account{ TotalEquityInUSD: fixedpoint.NewFromFloat(91884), - UpdateTime: "1614846244194", + UpdateTime: types.NewMillisecondTimestampFromInt(1614846244194), Details: []okexapi.BalanceDetail{ { Currency: "BTC",