From 99a69f4f2f5b0f7f092644d8e271db40e3f86be3 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 18 Sep 2023 15:55:58 +0800 Subject: [PATCH 1/7] add QueryClosedOrders() and QueryTrades() for okex, also fix conflict for QueryOrderTrades() and update typo error in QueryOrderTrades() --- pkg/exchange/okex/exchange.go | 138 +++++++- .../okex/okexapi/get_order_history_request.go | 40 +++ .../get_order_history_request_requestgen.go | 319 ++++++++++++++++++ ....go => get_transaction_history_request.go} | 10 +- ...transaction_history_request_requestgen.go} | 44 +-- pkg/exchange/okex/query_closed_orders_test.go | 62 ++++ pkg/exchange/okex/query_order_test.go | 2 +- pkg/exchange/okex/query_trades_test.go | 57 ++++ pkg/testutil/auth.go | 2 + 9 files changed, 641 insertions(+), 33 deletions(-) create mode 100644 pkg/exchange/okex/okexapi/get_order_history_request.go create mode 100644 pkg/exchange/okex/okexapi/get_order_history_request_requestgen.go rename pkg/exchange/okex/okexapi/{get_transaction_histories_request.go => get_transaction_history_request.go} (79%) rename pkg/exchange/okex/okexapi/{get_transaction_histories_request_requestgen.go => get_transaction_history_request_requestgen.go} (73%) create mode 100644 pkg/exchange/okex/query_closed_orders_test.go create mode 100644 pkg/exchange/okex/query_trades_test.go diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index b89d7740a6..d578554fe0 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -23,7 +23,7 @@ import ( // Market data limiter means public api, this includes QueryMarkets, QueryTicker, QueryTickers, QueryKLines var ( marketDataLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 5) - tradeRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 5) + tradeRateLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 5) orderRateLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 5) ) @@ -32,6 +32,11 @@ const ID = "okex" // PlatformToken is the platform currency of OKEx, pre-allocate static string here const PlatformToken = "OKB" +const ( + // Constant For query limit + defaultQueryLimit = 100 +) + var log = logrus.WithFields(logrus.Fields{ "exchange": ID, }) @@ -360,7 +365,7 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O return nil, errors.New("okex.QueryOrder: OrderId or ClientOrderId is required parameter") } req := e.client.NewGetOrderDetailsRequest() - req.InstrumentID(q.Symbol). + req.InstrumentID(toLocalSymbol(q.Symbol)). OrderID(q.OrderID). ClientOrderID(q.ClientOrderID) @@ -380,9 +385,9 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([] log.Warn("!!!OKEX EXCHANGE API NOTICE!!! Okex does not support searching for trades using OrderClientId.") } - req := e.client.NewGetTransactionHistoriesRequest() + req := e.client.NewGetTransactionHistoryRequest() if len(q.Symbol) != 0 { - req.InstrumentID(q.Symbol) + req.InstrumentID(toLocalSymbol(q.Symbol)) } if len(q.OrderID) != 0 { @@ -411,6 +416,131 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([] if errs != nil { return nil, errs } + return trades, nil +} + +/* +QueryClosedOrders can query closed orders in last 3 months, there are no time interval limitations, as long as until >= since. +Please Use lastOrderID as cursor, only return orders later than that order, that order is not included. +If you want to query orders by time range, please just pass since and until. +If you want to query by cursor, please pass lastOrderID. +Because it gets the correct response even when you pass all parameters with the right time interval and invalid lastOrderID, like 0. +Time interval boundary unit is second. +since is inclusive, ex. order created in 1694155903, get response if query since 1694155903, get empty if query since 1694155904 +until is not inclusive, ex. order created in 1694155903, get response if query until 1694155904, get empty if query until 1694155903 +*/ +func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error) { + if symbol == "" { + return nil, ErrSymbolRequired + } + + if err := tradeRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err) + } + + var lastOrder string + if lastOrderID <= 0 { + lastOrder = "" + } else { + lastOrder = strconv.FormatUint(lastOrderID, 10) + } + + res, err := e.client.NewGetOrderHistoryRequest(). + InstrumentID(toLocalSymbol(symbol)). + StartTime(since). + EndTime(until). + Limit(defaultQueryLimit). + Before(lastOrder). + Do(ctx) + + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + + var orders []types.Order + + for _, order := range res { + o, err2 := toGlobalOrder(&order) + if err2 != nil { + err = multierr.Append(err, err2) + continue + } + + orders = append(orders, *o) + } + if err != nil { + return nil, err + } + return types.SortOrdersAscending(orders), nil +} + +/* +QueryTrades can query trades in last 3 months, there are no time interval limitations, as long as end_time >= start_time. +OKEX do not provide api to query by tradeID, So use /api/v5/trade/orders-history-archive as its official site do. +Please Use LastTradeID as cursor, only return trades later than that trade, that trade is not included. +If you want to query trades by time range, please just pass start_time and end_time. +If you want to query by cursor, please pass LastTradeID. +Because it gets the correct response even when you pass all parameters with the right time interval and invalid LastTradeID, like 0. +*/ +func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { + if symbol == "" { + return nil, ErrSymbolRequired + } + + req := e.client.NewGetOrderHistoryRequest().InstrumentID(toLocalSymbol(symbol)) + + limit := uint64(options.Limit) + if limit > defaultQueryLimit || limit <= 0 { + log.Debugf("limit is exceeded default limit %d or zero, got: %d, Do not pass limit", defaultQueryLimit, options.Limit) + } else { + req.Limit(limit) + } + + if err := tradeRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query trades rate limiter wait error: %w", err) + } + + var err error + var response []okexapi.OrderDetails + // query by time interval + if options.StartTime != nil || options.EndTime != nil { + if options.StartTime != nil { + req.StartTime(*options.StartTime) + } + if options.EndTime != nil { + req.EndTime(*options.EndTime) + } + + response, err = req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + } else if options.StartTime == nil && options.EndTime == nil && options.LastTradeID == 0 { // query by no any parameters + response, err = req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + } else { // query by trade id + lastTradeID := strconv.FormatUint(options.LastTradeID, 10) + res, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + for _, trade := range res { + if trade.LastTradeID == lastTradeID { + response, err = req.Before(trade.OrderID).Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + break + } + } + } + + trades, err := toGlobalTrades(response) + if err != nil { + return nil, fmt.Errorf("failed to trans order detail to trades error: %w", err) + } return trades, nil } diff --git a/pkg/exchange/okex/okexapi/get_order_history_request.go b/pkg/exchange/okex/okexapi/get_order_history_request.go new file mode 100644 index 0000000000..00b4eca7a2 --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_order_history_request.go @@ -0,0 +1,40 @@ +package okexapi + +import ( + "time" + + "github.com/c9s/requestgen" +) + +//go:generate GetRequest -url "/api/v5/trade/orders-history-archive" -type GetOrderHistoryRequest -responseDataType .APIResponse +type GetOrderHistoryRequest struct { + client requestgen.AuthenticatedAPIClient + + instrumentType InstrumentType `param:"instType,query"` + instrumentID *string `param:"instId,query"` + orderType *OrderType `param:"ordType,query"` + // underlying and instrumentFamil Applicable to FUTURES/SWAP/OPTION + underlying *string `param:"uly,query"` + instrumentFamily *string `param:"instFamily,query"` + + state *OrderState `param:"state,query"` + after *string `param:"after,query"` + before *string `param:"before,query"` + startTime *time.Time `param:"begin,query,milliseconds"` + + // endTime for each request, startTime and endTime can be any interval, but should be in last 3 months + endTime *time.Time `param:"end,query,milliseconds"` + + // limit for data size per page. Default: 100 + limit *uint64 `param:"limit,query"` +} + +type OrderList []OrderDetails + +// NewGetOrderHistoriesRequest is descending order by createdTime +func (c *RestClient) NewGetOrderHistoryRequest() *GetOrderHistoryRequest { + return &GetOrderHistoryRequest{ + client: c, + instrumentType: InstrumentTypeSpot, + } +} diff --git a/pkg/exchange/okex/okexapi/get_order_history_request_requestgen.go b/pkg/exchange/okex/okexapi/get_order_history_request_requestgen.go new file mode 100644 index 0000000000..b9bc43596d --- /dev/null +++ b/pkg/exchange/okex/okexapi/get_order_history_request_requestgen.go @@ -0,0 +1,319 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/trade/orders-history-archive -type GetOrderHistoryRequest -responseDataType .OrderList"; DO NOT EDIT. + +package okexapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetOrderHistoryRequest) InstrumentType(instrumentType InstrumentType) *GetOrderHistoryRequest { + g.instrumentType = instrumentType + return g +} + +func (g *GetOrderHistoryRequest) InstrumentID(instrumentID string) *GetOrderHistoryRequest { + g.instrumentID = &instrumentID + return g +} + +func (g *GetOrderHistoryRequest) OrderType(orderType OrderType) *GetOrderHistoryRequest { + g.orderType = &orderType + return g +} + +func (g *GetOrderHistoryRequest) Underlying(underlying string) *GetOrderHistoryRequest { + g.underlying = &underlying + return g +} + +func (g *GetOrderHistoryRequest) InstrumentFamily(instrumentFamily string) *GetOrderHistoryRequest { + g.instrumentFamily = &instrumentFamily + return g +} + +func (g *GetOrderHistoryRequest) State(state OrderState) *GetOrderHistoryRequest { + g.state = &state + return g +} + +func (g *GetOrderHistoryRequest) After(after string) *GetOrderHistoryRequest { + g.after = &after + return g +} + +func (g *GetOrderHistoryRequest) Before(before string) *GetOrderHistoryRequest { + g.before = &before + return g +} + +func (g *GetOrderHistoryRequest) StartTime(startTime time.Time) *GetOrderHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetOrderHistoryRequest) EndTime(endTime time.Time) *GetOrderHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetOrderHistoryRequest) Limit(limit uint64) *GetOrderHistoryRequest { + g.limit = &limit + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check instrumentType field -> json key instType + instrumentType := g.instrumentType + + // TEMPLATE check-valid-values + switch instrumentType { + case InstrumentTypeSpot, InstrumentTypeSwap, InstrumentTypeFutures, InstrumentTypeOption, InstrumentTypeMARGIN: + params["instType"] = instrumentType + + default: + return nil, fmt.Errorf("instType value %v is invalid", instrumentType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of instrumentType + params["instType"] = instrumentType + // check instrumentID field -> json key instId + if g.instrumentID != nil { + instrumentID := *g.instrumentID + + // assign parameter of instrumentID + params["instId"] = instrumentID + } else { + } + // check orderType field -> json key ordType + if g.orderType != nil { + orderType := *g.orderType + + // TEMPLATE check-valid-values + switch orderType { + case OrderTypeMarket, OrderTypeLimit, OrderTypePostOnly, OrderTypeFOK, OrderTypeIOC: + params["ordType"] = orderType + + default: + return nil, fmt.Errorf("ordType value %v is invalid", orderType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderType + params["ordType"] = orderType + } else { + } + // check underlying field -> json key uly + if g.underlying != nil { + underlying := *g.underlying + + // assign parameter of underlying + params["uly"] = underlying + } else { + } + // check instrumentFamily field -> json key instFamily + if g.instrumentFamily != nil { + instrumentFamily := *g.instrumentFamily + + // assign parameter of instrumentFamily + params["instFamily"] = instrumentFamily + } else { + } + // check state field -> json key state + if g.state != nil { + state := *g.state + + // TEMPLATE check-valid-values + switch state { + case OrderStateCanceled, OrderStateLive, OrderStatePartiallyFilled, OrderStateFilled: + params["state"] = state + + default: + return nil, fmt.Errorf("state value %v is invalid", state) + + } + // END TEMPLATE check-valid-values + + // assign parameter of state + params["state"] = state + } else { + } + // check after field -> json key after + if g.after != nil { + after := *g.after + + // assign parameter of after + params["after"] = after + } else { + } + // check before field -> json key before + if g.before != nil { + before := *g.before + + // assign parameter of before + params["before"] = before + } else { + } + // check startTime field -> json key begin + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["begin"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key end + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["end"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + + 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 *GetOrderHistoryRequest) 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 *GetOrderHistoryRequest) 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 *GetOrderHistoryRequest) 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 *GetOrderHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderHistoryRequest) 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 *GetOrderHistoryRequest) 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 *GetOrderHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderHistoryRequest) 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 +} + +func (g *GetOrderHistoryRequest) Do(ctx context.Context) (OrderList, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/api/v5/trade/orders-history-archive" + + 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 + } + var data OrderList + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/okex/okexapi/get_transaction_histories_request.go b/pkg/exchange/okex/okexapi/get_transaction_history_request.go similarity index 79% rename from pkg/exchange/okex/okexapi/get_transaction_histories_request.go rename to pkg/exchange/okex/okexapi/get_transaction_history_request.go index 7e16d3b547..23e02fd4aa 100644 --- a/pkg/exchange/okex/okexapi/get_transaction_histories_request.go +++ b/pkg/exchange/okex/okexapi/get_transaction_history_request.go @@ -6,8 +6,8 @@ import ( "github.com/c9s/requestgen" ) -//go:generate GetRequest -url "/api/v5/trade/fills-history" -type GetTransactionHistoriesRequest -responseDataType .APIResponse -type GetTransactionHistoriesRequest struct { +//go:generate GetRequest -url "/api/v5/trade/fills-history" -type GetTransactionHistoryRequest -responseDataType .APIResponse +type GetTransactionHistoryRequest struct { client requestgen.AuthenticatedAPIClient instrumentType InstrumentType `param:"instType,query"` @@ -29,11 +29,9 @@ type GetTransactionHistoriesRequest struct { limit *uint64 `param:"limit,query"` } -type OrderList []OrderDetails - // NewGetOrderHistoriesRequest is descending order by createdTime -func (c *RestClient) NewGetTransactionHistoriesRequest() *GetTransactionHistoriesRequest { - return &GetTransactionHistoriesRequest{ +func (c *RestClient) NewGetTransactionHistoryRequest() *GetTransactionHistoryRequest { + return &GetTransactionHistoryRequest{ client: c, instrumentType: InstrumentTypeSpot, } diff --git a/pkg/exchange/okex/okexapi/get_transaction_histories_request_requestgen.go b/pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go similarity index 73% rename from pkg/exchange/okex/okexapi/get_transaction_histories_request_requestgen.go rename to pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go index f88a68513a..00c3d71da5 100644 --- a/pkg/exchange/okex/okexapi/get_transaction_histories_request_requestgen.go +++ b/pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/trade/fills-history -type GetTransactionHistoriesRequest -responseDataType .OrderList"; DO NOT EDIT. +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/trade/fills-history -type GetTransactionHistoryRequest -responseDataType .OrderList"; DO NOT EDIT. package okexapi @@ -13,63 +13,63 @@ import ( "time" ) -func (g *GetTransactionHistoriesRequest) InstrumentType(instrumentType InstrumentType) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) InstrumentType(instrumentType InstrumentType) *GetTransactionHistoryRequest { g.instrumentType = instrumentType return g } -func (g *GetTransactionHistoriesRequest) InstrumentID(instrumentID string) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) InstrumentID(instrumentID string) *GetTransactionHistoryRequest { g.instrumentID = &instrumentID return g } -func (g *GetTransactionHistoriesRequest) OrderType(orderType OrderType) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) OrderType(orderType OrderType) *GetTransactionHistoryRequest { g.orderType = &orderType return g } -func (g *GetTransactionHistoriesRequest) OrderID(orderID string) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) OrderID(orderID string) *GetTransactionHistoryRequest { g.orderID = orderID return g } -func (g *GetTransactionHistoriesRequest) Underlying(underlying string) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) Underlying(underlying string) *GetTransactionHistoryRequest { g.underlying = &underlying return g } -func (g *GetTransactionHistoriesRequest) InstrumentFamily(instrumentFamily string) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) InstrumentFamily(instrumentFamily string) *GetTransactionHistoryRequest { g.instrumentFamily = &instrumentFamily return g } -func (g *GetTransactionHistoriesRequest) After(after string) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) After(after string) *GetTransactionHistoryRequest { g.after = &after return g } -func (g *GetTransactionHistoriesRequest) Before(before string) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) Before(before string) *GetTransactionHistoryRequest { g.before = &before return g } -func (g *GetTransactionHistoriesRequest) StartTime(startTime time.Time) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) StartTime(startTime time.Time) *GetTransactionHistoryRequest { g.startTime = &startTime return g } -func (g *GetTransactionHistoriesRequest) EndTime(endTime time.Time) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) EndTime(endTime time.Time) *GetTransactionHistoryRequest { g.endTime = &endTime return g } -func (g *GetTransactionHistoriesRequest) Limit(limit uint64) *GetTransactionHistoriesRequest { +func (g *GetTransactionHistoryRequest) Limit(limit uint64) *GetTransactionHistoryRequest { g.limit = &limit return g } // GetQueryParameters builds and checks the query parameters and returns url.Values -func (g *GetTransactionHistoriesRequest) GetQueryParameters() (url.Values, error) { +func (g *GetTransactionHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} // check instrumentType field -> json key instType instrumentType := g.instrumentType @@ -187,14 +187,14 @@ func (g *GetTransactionHistoriesRequest) GetQueryParameters() (url.Values, error } // GetParameters builds and checks the parameters and return the result in a map object -func (g *GetTransactionHistoriesRequest) GetParameters() (map[string]interface{}, error) { +func (g *GetTransactionHistoryRequest) 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 *GetTransactionHistoriesRequest) GetParametersQuery() (url.Values, error) { +func (g *GetTransactionHistoryRequest) GetParametersQuery() (url.Values, error) { query := url.Values{} params, err := g.GetParameters() @@ -216,7 +216,7 @@ func (g *GetTransactionHistoriesRequest) GetParametersQuery() (url.Values, error } // GetParametersJSON converts the parameters from GetParameters into the JSON format -func (g *GetTransactionHistoriesRequest) GetParametersJSON() ([]byte, error) { +func (g *GetTransactionHistoryRequest) GetParametersJSON() ([]byte, error) { params, err := g.GetParameters() if err != nil { return nil, err @@ -226,13 +226,13 @@ func (g *GetTransactionHistoriesRequest) GetParametersJSON() ([]byte, error) { } // GetSlugParameters builds and checks the slug parameters and return the result in a map object -func (g *GetTransactionHistoriesRequest) GetSlugParameters() (map[string]interface{}, error) { +func (g *GetTransactionHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} return params, nil } -func (g *GetTransactionHistoriesRequest) applySlugsToUrl(url string, slugs map[string]string) string { +func (g *GetTransactionHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { for _k, _v := range slugs { needleRE := regexp.MustCompile(":" + _k + "\\b") url = needleRE.ReplaceAllString(url, _v) @@ -241,7 +241,7 @@ func (g *GetTransactionHistoriesRequest) applySlugsToUrl(url string, slugs map[s return url } -func (g *GetTransactionHistoriesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { +func (g *GetTransactionHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { sliceValue := reflect.ValueOf(slice) for _i := 0; _i < sliceValue.Len(); _i++ { it := sliceValue.Index(_i).Interface() @@ -249,7 +249,7 @@ func (g *GetTransactionHistoriesRequest) iterateSlice(slice interface{}, _f func } } -func (g *GetTransactionHistoriesRequest) isVarSlice(_v interface{}) bool { +func (g *GetTransactionHistoryRequest) isVarSlice(_v interface{}) bool { rt := reflect.TypeOf(_v) switch rt.Kind() { case reflect.Slice: @@ -258,7 +258,7 @@ func (g *GetTransactionHistoriesRequest) isVarSlice(_v interface{}) bool { return false } -func (g *GetTransactionHistoriesRequest) GetSlugsMap() (map[string]string, error) { +func (g *GetTransactionHistoryRequest) GetSlugsMap() (map[string]string, error) { slugs := map[string]string{} params, err := g.GetSlugParameters() if err != nil { @@ -272,7 +272,7 @@ func (g *GetTransactionHistoriesRequest) GetSlugsMap() (map[string]string, error return slugs, nil } -func (g *GetTransactionHistoriesRequest) Do(ctx context.Context) (OrderList, error) { +func (g *GetTransactionHistoryRequest) Do(ctx context.Context) (OrderList, error) { // no body params var params interface{} diff --git a/pkg/exchange/okex/query_closed_orders_test.go b/pkg/exchange/okex/query_closed_orders_test.go new file mode 100644 index 0000000000..9b77e76658 --- /dev/null +++ b/pkg/exchange/okex/query_closed_orders_test.go @@ -0,0 +1,62 @@ +package okex + +import ( + "context" + "testing" + "time" + + "github.com/c9s/bbgo/pkg/testutil" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_QueryClosedOrders(t *testing.T) { + + key, secret, passphrase, ok := testutil.IntegrationTestWithPassphraseConfigured(t, types.ExchangeOKEx.String()) + if !ok { + t.Skip("Please configure all credentials about OKEX") + } + + e := New(key, secret, passphrase) + + queryOrder := types.OrderQuery{ + Symbol: "BTCUSDT", + } + + // test by order id as a cursor + closedOrder, err := e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Time{}, time.Time{}, 609869603774656544) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + // test by time interval + closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Now().Add(-90*24*time.Hour), time.Now(), 0) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + // test by no parameter + closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Time{}, time.Time{}, 0) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + // test by time interval (boundary test) + closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Unix(1694155903, 999), time.Now(), 0) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + // test by time interval (boundary test) + closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Unix(1694154903, 999), time.Unix(1694155904, 0), 0) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + // test by time interval and order id together + closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Unix(1694154903, 999), time.Now(), 609869603774656544) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) +} diff --git a/pkg/exchange/okex/query_order_test.go b/pkg/exchange/okex/query_order_test.go index 3c32da40cf..03c3af1a44 100644 --- a/pkg/exchange/okex/query_order_test.go +++ b/pkg/exchange/okex/query_order_test.go @@ -25,7 +25,7 @@ func Test_QueryOrder(t *testing.T) { e := New(key, secret, passphrase) queryOrder := types.OrderQuery{ - Symbol: "BTC-USDT", + Symbol: "BTCUSDT", OrderID: "609869603774656544", } orderDetail, err := e.QueryOrder(context.Background(), queryOrder) diff --git a/pkg/exchange/okex/query_trades_test.go b/pkg/exchange/okex/query_trades_test.go new file mode 100644 index 0000000000..0058b6bcc6 --- /dev/null +++ b/pkg/exchange/okex/query_trades_test.go @@ -0,0 +1,57 @@ +package okex + +import ( + "context" + "testing" + "time" + + "github.com/c9s/bbgo/pkg/testutil" + "github.com/c9s/bbgo/pkg/types" + "github.com/stretchr/testify/assert" +) + +func Test_QueryTrades(t *testing.T) { + key, secret, passphrase, ok := testutil.IntegrationTestWithPassphraseConfigured(t, "OKEX") + if !ok { + t.Skip("Please configure all credentials about OKEX") + } + + e := New(key, secret, passphrase) + + queryOrder := types.OrderQuery{ + Symbol: "BTCUSDT", + } + + since := time.Now().AddDate(0, -3, 0) + until := time.Now() + + queryOption := types.TradeQueryOptions{ + StartTime: &since, + EndTime: &until, + Limit: 100, + } + // query by time interval + transactionDetail, err := e.QueryTrades(context.Background(), queryOrder.Symbol, &queryOption) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } + t.Logf("transaction detail: %+v", transactionDetail) + // query by trade id + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{LastTradeID: 432044402}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } + t.Logf("transaction detail: %+v", transactionDetail) + // query by no time interval and no trade id + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } + t.Logf("transaction detail: %+v", transactionDetail) + // query by limit exceed default value + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{Limit: 150}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } + t.Logf("transaction detail: %+v", transactionDetail) +} diff --git a/pkg/testutil/auth.go b/pkg/testutil/auth.go index 8e5bd43c7c..a4fae74b03 100644 --- a/pkg/testutil/auth.go +++ b/pkg/testutil/auth.go @@ -3,6 +3,7 @@ package testutil import ( "os" "regexp" + "strings" "testing" ) @@ -26,6 +27,7 @@ func IntegrationTestConfigured(t *testing.T, prefix string) (key, secret string, func IntegrationTestWithPassphraseConfigured(t *testing.T, prefix string) (key, secret, passphrase string, ok bool) { var hasKey, hasSecret, hasPassphrase bool + prefix = strings.ToUpper(prefix) key, hasKey = os.LookupEnv(prefix + "_API_KEY") secret, hasSecret = os.LookupEnv(prefix + "_API_SECRET") passphrase, hasPassphrase = os.LookupEnv(prefix + "_API_PASSPHRASE") From ad7206271f65218dd2b92de7a0e9e62613c53cc9 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Fri, 22 Sep 2023 13:42:42 +0800 Subject: [PATCH 2/7] QueryTrades only allow query by time interval, required --- pkg/exchange/okex/exchange.go | 39 ++++++++------------------ pkg/exchange/okex/query_trades_test.go | 12 ++++---- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index d578554fe0..337fff97d6 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -23,7 +23,6 @@ import ( // Market data limiter means public api, this includes QueryMarkets, QueryTicker, QueryTickers, QueryKLines var ( marketDataLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 5) - tradeRateLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 5) orderRateLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 5) ) @@ -434,7 +433,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, return nil, ErrSymbolRequired } - if err := tradeRateLimiter.Wait(ctx); err != nil { + if err := orderRateLimiter.Wait(ctx); err != nil { return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err) } @@ -478,9 +477,7 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, /* QueryTrades can query trades in last 3 months, there are no time interval limitations, as long as end_time >= start_time. OKEX do not provide api to query by tradeID, So use /api/v5/trade/orders-history-archive as its official site do. -Please Use LastTradeID as cursor, only return trades later than that trade, that trade is not included. If you want to query trades by time range, please just pass start_time and end_time. -If you want to query by cursor, please pass LastTradeID. Because it gets the correct response even when you pass all parameters with the right time interval and invalid LastTradeID, like 0. */ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { @@ -488,6 +485,10 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return nil, ErrSymbolRequired } + if options.LastTradeID > 0 { + log.Warn("!!!OKEX EXCHANGE API NOTICE!!! Okex does not support searching for trades using TradeId.") + } + req := e.client.NewGetOrderHistoryRequest().InstrumentID(toLocalSymbol(symbol)) limit := uint64(options.Limit) @@ -497,14 +498,15 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type req.Limit(limit) } - if err := tradeRateLimiter.Wait(ctx); err != nil { + if err := orderRateLimiter.Wait(ctx); err != nil { return nil, fmt.Errorf("query trades rate limiter wait error: %w", err) } var err error var response []okexapi.OrderDetails - // query by time interval - if options.StartTime != nil || options.EndTime != nil { + if options.StartTime == nil && options.EndTime == nil { + return nil, fmt.Errorf("StartTime and EndTime are require parameter!") + } else { // query by time interval if options.StartTime != nil { req.StartTime(*options.StartTime) } @@ -512,30 +514,11 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type req.EndTime(*options.EndTime) } - response, err = req.Do(ctx) - if err != nil { - return nil, fmt.Errorf("failed to call get order histories error: %w", err) - } - } else if options.StartTime == nil && options.EndTime == nil && options.LastTradeID == 0 { // query by no any parameters - response, err = req.Do(ctx) - if err != nil { - return nil, fmt.Errorf("failed to call get order histories error: %w", err) - } - } else { // query by trade id - lastTradeID := strconv.FormatUint(options.LastTradeID, 10) - res, err := req.Do(ctx) + response, err = req. + Do(ctx) if err != nil { return nil, fmt.Errorf("failed to call get order histories error: %w", err) } - for _, trade := range res { - if trade.LastTradeID == lastTradeID { - response, err = req.Before(trade.OrderID).Do(ctx) - if err != nil { - return nil, fmt.Errorf("failed to call get order histories error: %w", err) - } - break - } - } } trades, err := toGlobalTrades(response) diff --git a/pkg/exchange/okex/query_trades_test.go b/pkg/exchange/okex/query_trades_test.go index 0058b6bcc6..5188be8d5b 100644 --- a/pkg/exchange/okex/query_trades_test.go +++ b/pkg/exchange/okex/query_trades_test.go @@ -38,20 +38,20 @@ func Test_QueryTrades(t *testing.T) { t.Logf("transaction detail: %+v", transactionDetail) // query by trade id transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{LastTradeID: 432044402}) - if assert.NoError(t, err) { - assert.NotEmpty(t, transactionDetail) + if assert.Error(t, err) { + assert.Empty(t, transactionDetail) } t.Logf("transaction detail: %+v", transactionDetail) // query by no time interval and no trade id transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{}) - if assert.NoError(t, err) { - assert.NotEmpty(t, transactionDetail) + if assert.Error(t, err) { + assert.Empty(t, transactionDetail) } t.Logf("transaction detail: %+v", transactionDetail) // query by limit exceed default value transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{Limit: 150}) - if assert.NoError(t, err) { - assert.NotEmpty(t, transactionDetail) + if assert.Error(t, err) { + assert.Empty(t, transactionDetail) } t.Logf("transaction detail: %+v", transactionDetail) } From 3b63858d23a9b5ac766c0d70d663905f1a8eed08 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Wed, 27 Sep 2023 11:06:41 +0800 Subject: [PATCH 3/7] handle pagenation for QueryTrade --- pkg/exchange/okex/exchange.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 337fff97d6..e7454fe768 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -514,10 +514,21 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type req.EndTime(*options.EndTime) } - response, err = req. - Do(ctx) - if err != nil { - return nil, fmt.Errorf("failed to call get order histories error: %w", err) + var res []okexapi.OrderDetails + var lastOrderID = "0" + for { // pagenation should use "after" (earlier than) + res, err = req. + After(lastOrderID). + Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + response = append(response, res...) + if len(res) == defaultQueryLimit { + lastOrderID = res[defaultQueryLimit-1].OrderID + } else { + break + } } } From 6fd86fefda5398a22deac42ab7e3cd04397dbc0a Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 2 Oct 2023 10:49:33 +0800 Subject: [PATCH 4/7] add unit test for QueryTrade() --- pkg/exchange/okex/exchange.go | 8 ++++---- pkg/exchange/okex/query_trades_test.go | 7 +++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index e7454fe768..633e4b367d 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -493,6 +493,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type limit := uint64(options.Limit) if limit > defaultQueryLimit || limit <= 0 { + limit = defaultQueryLimit log.Debugf("limit is exceeded default limit %d or zero, got: %d, Do not pass limit", defaultQueryLimit, options.Limit) } else { req.Limit(limit) @@ -514,18 +515,17 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type req.EndTime(*options.EndTime) } - var res []okexapi.OrderDetails var lastOrderID = "0" for { // pagenation should use "after" (earlier than) - res, err = req. + res, err := req. After(lastOrderID). Do(ctx) if err != nil { return nil, fmt.Errorf("failed to call get order histories error: %w", err) } response = append(response, res...) - if len(res) == defaultQueryLimit { - lastOrderID = res[defaultQueryLimit-1].OrderID + if len(res) == int(limit) { + lastOrderID = res[limit-1].OrderID } else { break } diff --git a/pkg/exchange/okex/query_trades_test.go b/pkg/exchange/okex/query_trades_test.go index 5188be8d5b..10afdecd1c 100644 --- a/pkg/exchange/okex/query_trades_test.go +++ b/pkg/exchange/okex/query_trades_test.go @@ -54,4 +54,11 @@ func Test_QueryTrades(t *testing.T) { assert.Empty(t, transactionDetail) } t.Logf("transaction detail: %+v", transactionDetail) + // pagenation test + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{EndTime: &until, Limit: 1}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + assert.Less(t, 1, len(transactionDetail)) + } + t.Logf("transaction detail: %+v", transactionDetail) } From 648b82ead35381aa82686929c1f86c4fcf4d9dc9 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Mon, 2 Oct 2023 18:47:05 +0800 Subject: [PATCH 5/7] use NewGetTransactionHistoryRequest for QueryTrades and use billID for pagination --- pkg/exchange/okex/convert.go | 7 ++++--- pkg/exchange/okex/exchange.go | 10 +++++----- .../okex/okexapi/get_transaction_history_request.go | 2 ++ .../get_transaction_history_request_requestgen.go | 12 +++++++++++- pkg/exchange/okex/okexapi/trade.go | 1 + pkg/exchange/okex/query_trades_test.go | 5 ----- 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index 193ba233e1..a83ce014ac 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -286,9 +286,10 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails) (*types.Order, error) { } func toGlobalTrade(orderDetail *okexapi.OrderDetails) (*types.Trade, error) { - tradeID, err := strconv.ParseInt(orderDetail.LastTradeID, 10, 64) + // Should use tradeId, but okex use billId to perform pagination, so use billID as tradeID instead. + billID, err := strconv.ParseInt(orderDetail.BillID, 10, 64) if err != nil { - return nil, errors.Wrapf(err, "error parsing tradeId value: %s", orderDetail.LastTradeID) + return nil, errors.Wrapf(err, "error parsing billId value: %s", orderDetail.BillID) } orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64) @@ -309,7 +310,7 @@ func toGlobalTrade(orderDetail *okexapi.OrderDetails) (*types.Trade, error) { } return &types.Trade{ - ID: uint64(tradeID), + ID: uint64(billID), OrderID: uint64(orderID), Exchange: types.ExchangeOKEx, Price: orderDetail.LastFilledPrice, diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 633e4b367d..1e7773760d 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -489,7 +489,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type log.Warn("!!!OKEX EXCHANGE API NOTICE!!! Okex does not support searching for trades using TradeId.") } - req := e.client.NewGetOrderHistoryRequest().InstrumentID(toLocalSymbol(symbol)) + req := e.client.NewGetTransactionHistoryRequest().InstrumentID(toLocalSymbol(symbol)) limit := uint64(options.Limit) if limit > defaultQueryLimit || limit <= 0 { @@ -515,17 +515,17 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type req.EndTime(*options.EndTime) } - var lastOrderID = "0" - for { // pagenation should use "after" (earlier than) + var billID = "" // billId should be emtpy, can't be 0 + for { // pagenation should use "after" (earlier than) res, err := req. - After(lastOrderID). + After(billID). Do(ctx) if err != nil { return nil, fmt.Errorf("failed to call get order histories error: %w", err) } response = append(response, res...) if len(res) == int(limit) { - lastOrderID = res[limit-1].OrderID + billID = res[limit-1].BillID } else { break } diff --git a/pkg/exchange/okex/okexapi/get_transaction_history_request.go b/pkg/exchange/okex/okexapi/get_transaction_history_request.go index 23e02fd4aa..2b7a0ddf77 100644 --- a/pkg/exchange/okex/okexapi/get_transaction_history_request.go +++ b/pkg/exchange/okex/okexapi/get_transaction_history_request.go @@ -14,6 +14,8 @@ type GetTransactionHistoryRequest struct { instrumentID *string `param:"instId,query"` orderType *OrderType `param:"ordType,query"` orderID string `param:"ordId,query"` + billID string `param:"billId"` + // Underlying and InstrumentFamily Applicable to FUTURES/SWAP/OPTION underlying *string `param:"uly,query"` instrumentFamily *string `param:"instFamily,query"` diff --git a/pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go b/pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go index 00c3d71da5..f2eaf336dc 100644 --- a/pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go +++ b/pkg/exchange/okex/okexapi/get_transaction_history_request_requestgen.go @@ -68,6 +68,11 @@ func (g *GetTransactionHistoryRequest) Limit(limit uint64) *GetTransactionHistor return g } +func (g *GetTransactionHistoryRequest) BillID(billID string) *GetTransactionHistoryRequest { + g.billID = billID + return g +} + // GetQueryParameters builds and checks the query parameters and returns url.Values func (g *GetTransactionHistoryRequest) GetQueryParameters() (url.Values, error) { var params = map[string]interface{}{} @@ -189,6 +194,11 @@ func (g *GetTransactionHistoryRequest) GetQueryParameters() (url.Values, error) // GetParameters builds and checks the parameters and return the result in a map object func (g *GetTransactionHistoryRequest) GetParameters() (map[string]interface{}, error) { var params = map[string]interface{}{} + // check billID field -> json key billId + billID := g.billID + + // assign parameter of billID + params["billId"] = billID return params, nil } @@ -274,7 +284,7 @@ func (g *GetTransactionHistoryRequest) GetSlugsMap() (map[string]string, error) func (g *GetTransactionHistoryRequest) Do(ctx context.Context) (OrderList, error) { - // no body params + // empty params for GET operation var params interface{} query, err := g.GetQueryParameters() if err != nil { diff --git a/pkg/exchange/okex/okexapi/trade.go b/pkg/exchange/okex/okexapi/trade.go index 164ae46dae..8d673ee454 100644 --- a/pkg/exchange/okex/okexapi/trade.go +++ b/pkg/exchange/okex/okexapi/trade.go @@ -276,6 +276,7 @@ type OrderDetails struct { LastFilledFee fixedpoint.Value `json:"fillFee"` LastFilledFeeCurrency string `json:"fillFeeCcy"` LastFilledPnl fixedpoint.Value `json:"fillPnl"` + BillID string `json:"billId"` // ExecutionType = liquidity (M = maker or T = taker) ExecutionType string `json:"execType"` diff --git a/pkg/exchange/okex/query_trades_test.go b/pkg/exchange/okex/query_trades_test.go index 10afdecd1c..6f082e3638 100644 --- a/pkg/exchange/okex/query_trades_test.go +++ b/pkg/exchange/okex/query_trades_test.go @@ -35,30 +35,25 @@ func Test_QueryTrades(t *testing.T) { if assert.NoError(t, err) { assert.NotEmpty(t, transactionDetail) } - t.Logf("transaction detail: %+v", transactionDetail) // query by trade id transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{LastTradeID: 432044402}) if assert.Error(t, err) { assert.Empty(t, transactionDetail) } - t.Logf("transaction detail: %+v", transactionDetail) // query by no time interval and no trade id transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{}) if assert.Error(t, err) { assert.Empty(t, transactionDetail) } - t.Logf("transaction detail: %+v", transactionDetail) // query by limit exceed default value transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{Limit: 150}) if assert.Error(t, err) { assert.Empty(t, transactionDetail) } - t.Logf("transaction detail: %+v", transactionDetail) // pagenation test transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{EndTime: &until, Limit: 1}) if assert.NoError(t, err) { assert.NotEmpty(t, transactionDetail) assert.Less(t, 1, len(transactionDetail)) } - t.Logf("transaction detail: %+v", transactionDetail) } From cc55d67eebed0fdd2b10f8d053d1cfc1eb6c21be Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 3 Oct 2023 12:29:30 +0800 Subject: [PATCH 6/7] use default limit if not pass AND add more unit test --- pkg/exchange/okex/exchange.go | 10 +++++----- pkg/exchange/okex/query_trades_test.go | 25 ++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 1e7773760d..371809c8f1 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -494,7 +494,8 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type limit := uint64(options.Limit) if limit > defaultQueryLimit || limit <= 0 { limit = defaultQueryLimit - log.Debugf("limit is exceeded default limit %d or zero, got: %d, Do not pass limit", defaultQueryLimit, options.Limit) + req.Limit(defaultQueryLimit) + log.Debugf("limit is exceeded default limit %d or zero, got: %d, use default limit", defaultQueryLimit, options.Limit) } else { req.Limit(limit) } @@ -506,7 +507,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type var err error var response []okexapi.OrderDetails if options.StartTime == nil && options.EndTime == nil { - return nil, fmt.Errorf("StartTime and EndTime are require parameter!") + return nil, fmt.Errorf("StartTime and EndTime are required parameter!") } else { // query by time interval if options.StartTime != nil { req.StartTime(*options.StartTime) @@ -524,11 +525,10 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return nil, fmt.Errorf("failed to call get order histories error: %w", err) } response = append(response, res...) - if len(res) == int(limit) { - billID = res[limit-1].BillID - } else { + if len(res) != int(limit) { break } + billID = res[limit-1].BillID } } diff --git a/pkg/exchange/okex/query_trades_test.go b/pkg/exchange/okex/query_trades_test.go index 6f082e3638..361074934a 100644 --- a/pkg/exchange/okex/query_trades_test.go +++ b/pkg/exchange/okex/query_trades_test.go @@ -50,10 +50,33 @@ func Test_QueryTrades(t *testing.T) { if assert.Error(t, err) { assert.Empty(t, transactionDetail) } - // pagenation test + // pagenation test and test time interval : only end time transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{EndTime: &until, Limit: 1}) if assert.NoError(t, err) { assert.NotEmpty(t, transactionDetail) assert.Less(t, 1, len(transactionDetail)) } + // query by time interval: only start time + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{StartTime: &since, Limit: 100}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } + // query by combination: start time, end time and after + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{StartTime: &since, EndTime: &until, Limit: 1}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } + // query by time interval: 3 months earlier with start time and end time + since = time.Now().AddDate(0, -6, 0) + until = time.Now().AddDate(0, -3, 0) + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{StartTime: &since, EndTime: &until, Limit: 100}) + if assert.NoError(t, err) { + assert.Empty(t, transactionDetail) + } + // query by time interval: 3 months earlier with start time + since = time.Now().AddDate(0, -6, 0) + transactionDetail, err = e.QueryTrades(context.Background(), queryOrder.Symbol, &types.TradeQueryOptions{StartTime: &since, Limit: 100}) + if assert.NoError(t, err) { + assert.NotEmpty(t, transactionDetail) + } } From b1c6e01e45dd0259bbeb718b805a6d925540bac0 Mon Sep 17 00:00:00 2001 From: "Alan.sung" Date: Tue, 3 Oct 2023 15:14:49 +0800 Subject: [PATCH 7/7] use types.StrInt64 for billID and add more comment for QueryTrades() and comment out personal unit test --- pkg/exchange/okex/convert.go | 5 +--- pkg/exchange/okex/exchange.go | 5 +++- pkg/exchange/okex/okexapi/trade.go | 2 +- pkg/exchange/okex/query_closed_orders_test.go | 26 +++++++++++-------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/pkg/exchange/okex/convert.go b/pkg/exchange/okex/convert.go index a83ce014ac..bc05ce744a 100644 --- a/pkg/exchange/okex/convert.go +++ b/pkg/exchange/okex/convert.go @@ -287,10 +287,7 @@ func toGlobalOrder(okexOrder *okexapi.OrderDetails) (*types.Order, error) { func toGlobalTrade(orderDetail *okexapi.OrderDetails) (*types.Trade, error) { // Should use tradeId, but okex use billId to perform pagination, so use billID as tradeID instead. - billID, err := strconv.ParseInt(orderDetail.BillID, 10, 64) - if err != nil { - return nil, errors.Wrapf(err, "error parsing billId value: %s", orderDetail.BillID) - } + billID := orderDetail.BillID orderID, err := strconv.ParseInt(orderDetail.OrderID, 10, 64) if err != nil { diff --git a/pkg/exchange/okex/exchange.go b/pkg/exchange/okex/exchange.go index 371809c8f1..6122545d53 100644 --- a/pkg/exchange/okex/exchange.go +++ b/pkg/exchange/okex/exchange.go @@ -479,6 +479,9 @@ QueryTrades can query trades in last 3 months, there are no time interval limita OKEX do not provide api to query by tradeID, So use /api/v5/trade/orders-history-archive as its official site do. If you want to query trades by time range, please just pass start_time and end_time. Because it gets the correct response even when you pass all parameters with the right time interval and invalid LastTradeID, like 0. +No matter how you pass parameter, QueryTrades return descending order. +If you query time period 3 months earlier with start time and end time, will return [] empty slice +But If you query time period 3 months earlier JUST with start time, will return like start with 3 months ago. */ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) { if symbol == "" { @@ -528,7 +531,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type if len(res) != int(limit) { break } - billID = res[limit-1].BillID + billID = strconv.Itoa(int(res[limit-1].BillID)) } } diff --git a/pkg/exchange/okex/okexapi/trade.go b/pkg/exchange/okex/okexapi/trade.go index 8d673ee454..f42553c517 100644 --- a/pkg/exchange/okex/okexapi/trade.go +++ b/pkg/exchange/okex/okexapi/trade.go @@ -276,7 +276,7 @@ type OrderDetails struct { LastFilledFee fixedpoint.Value `json:"fillFee"` LastFilledFeeCurrency string `json:"fillFeeCcy"` LastFilledPnl fixedpoint.Value `json:"fillPnl"` - BillID string `json:"billId"` + BillID types.StrInt64 `json:"billId"` // ExecutionType = liquidity (M = maker or T = taker) ExecutionType string `json:"execType"` diff --git a/pkg/exchange/okex/query_closed_orders_test.go b/pkg/exchange/okex/query_closed_orders_test.go index 9b77e76658..67bdae2910 100644 --- a/pkg/exchange/okex/query_closed_orders_test.go +++ b/pkg/exchange/okex/query_closed_orders_test.go @@ -24,13 +24,15 @@ func Test_QueryClosedOrders(t *testing.T) { } // test by order id as a cursor - closedOrder, err := e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Time{}, time.Time{}, 609869603774656544) - if assert.NoError(t, err) { - assert.NotEmpty(t, closedOrder) - } - t.Logf("closed order detail: %+v", closedOrder) + /* + closedOrder, err := e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Time{}, time.Time{}, 609869603774656544) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + */ // test by time interval - closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Now().Add(-90*24*time.Hour), time.Now(), 0) + closedOrder, err := e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Now().Add(-90*24*time.Hour), time.Now(), 0) if assert.NoError(t, err) { assert.NotEmpty(t, closedOrder) } @@ -54,9 +56,11 @@ func Test_QueryClosedOrders(t *testing.T) { } t.Logf("closed order detail: %+v", closedOrder) // test by time interval and order id together - closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Unix(1694154903, 999), time.Now(), 609869603774656544) - if assert.NoError(t, err) { - assert.NotEmpty(t, closedOrder) - } - t.Logf("closed order detail: %+v", closedOrder) + /* + closedOrder, err = e.QueryClosedOrders(context.Background(), string(queryOrder.Symbol), time.Unix(1694154903, 999), time.Now(), 609869603774656544) + if assert.NoError(t, err) { + assert.NotEmpty(t, closedOrder) + } + t.Logf("closed order detail: %+v", closedOrder) + */ }