diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index ba18389b30..fe3c5840f3 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -24,7 +24,9 @@ var ( marketDataLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 5) orderRateLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 5) - queryMarketLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) + 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) ) const ID = "okex" @@ -112,18 +114,29 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { } func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { - symbol = toLocalSymbol(symbol) + if err := queryTickerLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("ticker rate limiter wait error: %w", err) + } - marketTicker, err := e.client.MarketTicker(ctx, symbol) + symbol = toLocalSymbol(symbol) + marketTicker, err := e.client.NewGetTickerRequest().InstId(symbol).Do(ctx) if err != nil { return nil, err } - return toGlobalTicker(*marketTicker), nil + if len(marketTicker) != 1 { + return nil, fmt.Errorf("unexpected length of %s market ticker, got: %v", symbol, marketTicker) + } + + return toGlobalTicker(marketTicker[0]), nil } func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[string]types.Ticker, error) { - marketTickers, err := e.client.MarketTickers(ctx, okexapi.InstrumentTypeSpot) + if err := queryTickersLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("tickers rate limiter wait error: %w", err) + } + + marketTickers, err := e.client.NewGetTickersRequest().Do(ctx) if err != nil { return nil, err } diff --git a/pkg/exchange/okex/okexapi/client.go b/pkg/exchange/okex/okexapi/client.go index 5b0cb7c91b..c7dd5021b9 100644 --- a/pkg/exchange/okex/okexapi/client.go +++ b/pkg/exchange/okex/okexapi/client.go @@ -7,7 +7,6 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" - "fmt" "net/http" "net/url" "strings" @@ -273,90 +272,6 @@ func (c *RestClient) AssetCurrencies(ctx context.Context) ([]AssetCurrency, erro return currencyResponse.Data, nil } -type MarketTicker struct { - InstrumentType string `json:"instType"` - InstrumentID string `json:"instId"` - - // last traded price - Last fixedpoint.Value `json:"last"` - - // last traded size - LastSize fixedpoint.Value `json:"lastSz"` - - AskPrice fixedpoint.Value `json:"askPx"` - AskSize fixedpoint.Value `json:"askSz"` - - BidPrice fixedpoint.Value `json:"bidPx"` - BidSize fixedpoint.Value `json:"bidSz"` - - Open24H fixedpoint.Value `json:"open24h"` - High24H fixedpoint.Value `json:"high24H"` - Low24H fixedpoint.Value `json:"low24H"` - Volume24H fixedpoint.Value `json:"vol24h"` - VolumeCurrency24H fixedpoint.Value `json:"volCcy24h"` - - // Millisecond timestamp - Timestamp types.MillisecondTimestamp `json:"ts"` -} - -func (c *RestClient) MarketTicker(ctx context.Context, instId string) (*MarketTicker, error) { - // SPOT, SWAP, FUTURES, OPTION - var params = url.Values{} - params.Add("instId", instId) - - req, err := c.NewRequest(ctx, "GET", "/api/v5/market/ticker", params, nil) - if err != nil { - return nil, err - } - - response, err := c.SendRequest(req) - if err != nil { - return nil, err - } - - var tickerResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []MarketTicker `json:"data"` - } - if err := response.DecodeJSON(&tickerResponse); err != nil { - return nil, err - } - - if len(tickerResponse.Data) == 0 { - return nil, fmt.Errorf("ticker of %s not found", instId) - } - - return &tickerResponse.Data[0], nil -} - -func (c *RestClient) MarketTickers(ctx context.Context, instType InstrumentType) ([]MarketTicker, error) { - // SPOT, SWAP, FUTURES, OPTION - var params = url.Values{} - params.Add("instType", string(instType)) - - req, err := c.NewRequest(ctx, "GET", "/api/v5/market/tickers", params, nil) - if err != nil { - return nil, err - } - - response, err := c.SendRequest(req) - if err != nil { - return nil, err - } - - var tickerResponse struct { - Code string `json:"code"` - Message string `json:"msg"` - Data []MarketTicker `json:"data"` - } - if err := response.DecodeJSON(&tickerResponse); err != nil { - return nil, err - } - - return tickerResponse.Data, nil -} - func Sign(payload string, secret string) string { var sig = hmac.New(sha256.New, []byte(secret)) _, err := sig.Write([]byte(payload)) diff --git a/pkg/exchange/okex/okexapi/client_test.go b/pkg/exchange/okex/okexapi/client_test.go index 51ea9e74c2..1d8dc9c67c 100644 --- a/pkg/exchange/okex/okexapi/client_test.go +++ b/pkg/exchange/okex/okexapi/client_test.go @@ -38,6 +38,28 @@ func TestClient_GetInstrumentsRequest(t *testing.T) { t.Logf("instruments: %+v", instruments) } +func TestClient_GetMarketTickers(t *testing.T) { + client := NewClient() + ctx := context.Background() + req := client.NewGetTickersRequest() + + tickers, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, tickers) + t.Logf("tickers: %+v", tickers) +} + +func TestClient_GetMarketTicker(t *testing.T) { + client := NewClient() + ctx := context.Background() + req := client.NewGetTickerRequest().InstId("BTC-USDT") + + tickers, err := req.Do(ctx) + assert.NoError(t, err) + assert.NotEmpty(t, tickers) + t.Logf("tickers: %+v", tickers) +} + func TestClient_GetFundingRateRequest(t *testing.T) { client := NewClient() ctx := context.Background() diff --git a/pkg/exchange/okex/okexapi/get_ticker_request.go b/pkg/exchange/okex/okexapi/get_ticker_request.go new file mode 100644 index 0000000000..a1c8c61a05 --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_ticker_request.go @@ -0,0 +1,21 @@ +package okexapi + +import ( + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +//go:generate GetRequest -url "/api/v5/market/ticker" -type GetTickerRequest -responseDataType []MarketTicker +type GetTickerRequest struct { + client requestgen.APIClient + + instId string `param:"instId,query"` +} + +func (c *RestClient) NewGetTickerRequest() *GetTickerRequest { + return &GetTickerRequest{ + client: c, + } +} diff --git a/pkg/exchange/okex/okexapi/get_ticker_request_requestgen.go b/pkg/exchange/okex/okexapi/get_ticker_request_requestgen.go new file mode 100644 index 0000000000..e5ab52129c --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_ticker_request_requestgen.go @@ -0,0 +1,170 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/market/ticker -type GetTickerRequest -responseDataType []MarketTicker"; DO NOT EDIT. + +package okexapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetTickerRequest) InstId(instId string) *GetTickerRequest { + g.instId = instId + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTickerRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check instId field -> json key instId + instId := g.instId + + // assign parameter of instId + params["instId"] = instId + + 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 *GetTickerRequest) 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 *GetTickerRequest) 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 *GetTickerRequest) 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 *GetTickerRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTickerRequest) 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 *GetTickerRequest) 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 *GetTickerRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTickerRequest) 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 *GetTickerRequest) GetPath() string { + return "/api/v5/market/ticker" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetTickerRequest) Do(ctx context.Context) ([]MarketTicker, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(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 []MarketTicker + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/okex/okexapi/get_tickers_request.go b/pkg/exchange/okex/okexapi/get_tickers_request.go new file mode 100644 index 0000000000..fbd1107291 --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_tickers_request.go @@ -0,0 +1,51 @@ +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 MarketTicker struct { + InstrumentType string `json:"instType"` + InstrumentID string `json:"instId"` + + // last traded price + Last fixedpoint.Value `json:"last"` + + // last traded size + LastSize fixedpoint.Value `json:"lastSz"` + + AskPrice fixedpoint.Value `json:"askPx"` + AskSize fixedpoint.Value `json:"askSz"` + + BidPrice fixedpoint.Value `json:"bidPx"` + BidSize fixedpoint.Value `json:"bidSz"` + + Open24H fixedpoint.Value `json:"open24h"` + High24H fixedpoint.Value `json:"high24H"` + Low24H fixedpoint.Value `json:"low24H"` + Volume24H fixedpoint.Value `json:"vol24h"` + VolumeCurrency24H fixedpoint.Value `json:"volCcy24h"` + + // Millisecond timestamp + Timestamp types.MillisecondTimestamp `json:"ts"` +} + +//go:generate GetRequest -url "/api/v5/market/tickers" -type GetTickersRequest -responseDataType []MarketTicker +type GetTickersRequest struct { + client requestgen.APIClient + + instType InstrumentType `param:"instType,query" validValues:"SPOT"` +} + +func (c *RestClient) NewGetTickersRequest() *GetTickersRequest { + return &GetTickersRequest{ + client: c, + instType: InstrumentTypeSpot, + } +} diff --git a/pkg/exchange/okex/okexapi/get_tickers_request_requestgen.go b/pkg/exchange/okex/okexapi/get_tickers_request_requestgen.go new file mode 100644 index 0000000000..a847106f56 --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_tickers_request_requestgen.go @@ -0,0 +1,181 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/market/tickers -type GetTickersRequest -responseDataType []MarketTicker"; DO NOT EDIT. + +package okexapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetTickersRequest) InstType(instType InstrumentType) *GetTickersRequest { + g.instType = instType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTickersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check instType field -> json key instType + instType := g.instType + + // TEMPLATE check-valid-values + switch instType { + case "SPOT": + params["instType"] = instType + + default: + return nil, fmt.Errorf("instType value %v is invalid", instType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of instType + params["instType"] = instType + + 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 *GetTickersRequest) 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 *GetTickersRequest) 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 *GetTickersRequest) 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 *GetTickersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTickersRequest) 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 *GetTickersRequest) 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 *GetTickersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTickersRequest) 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 *GetTickersRequest) GetPath() string { + return "/api/v5/market/tickers" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetTickersRequest) Do(ctx context.Context) ([]MarketTicker, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(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 []MarketTicker + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +}