diff --git a/pkg/exchange/bybit/bybitapi/client_test.go b/pkg/exchange/bybit/bybitapi/client_test.go index f74eb2295..0daf9eb2b 100644 --- a/pkg/exchange/bybit/bybitapi/client_test.go +++ b/pkg/exchange/bybit/bybitapi/client_test.go @@ -92,6 +92,9 @@ func TestClient(t *testing.T) { assert.NoError(t, err) t.Logf("apiResp: %+v", apiResp) + _, err = strconv.ParseUint(apiResp.OrderId, 10, 64) + assert.NoError(t, err) + ordersResp, err := client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) assert.NoError(t, err) assert.Equal(t, len(ordersResp.List), 1) @@ -129,4 +132,27 @@ func TestClient(t *testing.T) { assert.Equal(t, ordersResp.List[0].OrderStatus, OrderStatusCancelled) t.Logf("apiResp: %+v", ordersResp.List[0]) }) + + t.Run("GetOrderHistoriesRequest", func(t *testing.T) { + req := client.NewPlaceOrderRequest(). + Symbol("DOTUSDT"). + Side(SideBuy). + OrderType(OrderTypeLimit). + Qty("1"). + Price("4.6"). + OrderLinkId(uuid.NewString()). + TimeInForce(TimeInForceGTC) + apiResp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %+v", apiResp) + + ordersResp, err := client.NewGetOpenOrderRequest().OrderLinkId(apiResp.OrderLinkId).Do(ctx) + assert.NoError(t, err) + assert.Equal(t, len(ordersResp.List), 1) + t.Logf("apiResp: %+v", ordersResp.List[0]) + + orderResp, err := client.NewGetOrderHistoriesRequest().Symbol("DOTUSDT").Cursor("0").Do(ctx) + assert.NoError(t, err) + t.Logf("apiResp: %#v", orderResp) + }) } diff --git a/pkg/exchange/bybit/bybitapi/get_open_order_request.go b/pkg/exchange/bybit/bybitapi/get_open_order_request.go index e56ea01c1..3634d005f 100644 --- a/pkg/exchange/bybit/bybitapi/get_open_order_request.go +++ b/pkg/exchange/bybit/bybitapi/get_open_order_request.go @@ -9,13 +9,13 @@ import ( //go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result //go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result -type OpenOrdersResponse struct { - List []OpenOrder `json:"list"` - NextPageCursor string `json:"nextPageCursor"` - Category string `json:"category"` +type OrdersResponse struct { + List []Order `json:"list"` + NextPageCursor string `json:"nextPageCursor"` + Category string `json:"category"` } -type OpenOrder struct { +type Order struct { OrderId string `json:"orderId"` OrderLinkId string `json:"orderLinkId"` BlockTradeId string `json:"blockTradeId"` @@ -59,7 +59,7 @@ type OpenOrder struct { UpdatedTime types.MillisecondTimestamp `json:"updatedTime"` } -//go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse +//go:generate GetRequest -url "/v5/order/realtime" -type GetOpenOrdersRequest -responseDataType .OrdersResponse type GetOpenOrdersRequest struct { client requestgen.AuthenticatedAPIClient @@ -75,6 +75,8 @@ type GetOpenOrdersRequest struct { cursor *string `param:"cursor,query"` } +// NewGetOpenOrderRequest queries unfilled or partially filled orders in real-time. To query older order records, +// please use the order history interface. func (c *RestClient) NewGetOpenOrderRequest() *GetOpenOrdersRequest { return &GetOpenOrdersRequest{ client: c, diff --git a/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go index d68c31f4d..aa9d13cc6 100644 --- a/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go +++ b/pkg/exchange/bybit/bybitapi/get_open_orders_request_requestgen.go @@ -1,4 +1,4 @@ -// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/realtime -type GetOpenOrdersRequest -responseDataType .OpenOrdersResponse"; DO NOT EDIT. +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/realtime -type GetOpenOrdersRequest -responseDataType .OrdersResponse"; DO NOT EDIT. package bybitapi @@ -258,7 +258,7 @@ func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { return slugs, nil } -func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OpenOrdersResponse, error) { +func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OrdersResponse, error) { // no body params var params interface{} @@ -283,7 +283,7 @@ func (g *GetOpenOrdersRequest) Do(ctx context.Context) (*OpenOrdersResponse, err if err := response.DecodeJSON(&apiResponse); err != nil { return nil, err } - var data OpenOrdersResponse + var data OrdersResponse if err := json.Unmarshal(apiResponse.Result, &data); err != nil { return nil, err } diff --git a/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go b/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go new file mode 100644 index 000000000..4fafe108b --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_order_histories_request_requestgen.go @@ -0,0 +1,282 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Result -url /v5/order/history -type GetOrderHistoriesRequest -responseDataType .OrdersResponse"; DO NOT EDIT. + +package bybitapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetOrderHistoriesRequest) Category(category Category) *GetOrderHistoriesRequest { + g.category = category + return g +} + +func (g *GetOrderHistoriesRequest) Symbol(symbol string) *GetOrderHistoriesRequest { + g.symbol = &symbol + return g +} + +func (g *GetOrderHistoriesRequest) OrderId(orderId string) *GetOrderHistoriesRequest { + g.orderId = &orderId + return g +} + +func (g *GetOrderHistoriesRequest) OrderFilter(orderFilter string) *GetOrderHistoriesRequest { + g.orderFilter = &orderFilter + return g +} + +func (g *GetOrderHistoriesRequest) OrderStatus(orderStatus OrderStatus) *GetOrderHistoriesRequest { + g.orderStatus = &orderStatus + return g +} + +func (g *GetOrderHistoriesRequest) StartTime(startTime time.Time) *GetOrderHistoriesRequest { + g.startTime = &startTime + return g +} + +func (g *GetOrderHistoriesRequest) EndTime(endTime time.Time) *GetOrderHistoriesRequest { + g.endTime = &endTime + return g +} + +func (g *GetOrderHistoriesRequest) Limit(limit uint64) *GetOrderHistoriesRequest { + g.limit = &limit + return g +} + +func (g *GetOrderHistoriesRequest) Cursor(cursor string) *GetOrderHistoriesRequest { + g.cursor = &cursor + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOrderHistoriesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check category field -> json key category + category := g.category + + // TEMPLATE check-valid-values + switch category { + case "spot": + params["category"] = category + + default: + return nil, fmt.Errorf("category value %v is invalid", category) + + } + // END TEMPLATE check-valid-values + + // assign parameter of category + params["category"] = category + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check orderId field -> json key orderId + if g.orderId != nil { + orderId := *g.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } else { + } + // check orderFilter field -> json key orderFilter + if g.orderFilter != nil { + orderFilter := *g.orderFilter + + // assign parameter of orderFilter + params["orderFilter"] = orderFilter + } else { + } + // check orderStatus field -> json key orderStatus + if g.orderStatus != nil { + orderStatus := *g.orderStatus + + // TEMPLATE check-valid-values + switch orderStatus { + case OrderStatusCreated, OrderStatusNew, OrderStatusRejected, OrderStatusPartiallyFilled, OrderStatusPartiallyFilledCanceled, OrderStatusFilled, OrderStatusCancelled, OrderStatusDeactivated, OrderStatusActive: + params["orderStatus"] = orderStatus + + default: + return nil, fmt.Errorf("orderStatus value %v is invalid", orderStatus) + + } + // END TEMPLATE check-valid-values + + // assign parameter of orderStatus + params["orderStatus"] = orderStatus + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = 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 { + } + // check cursor field -> json key cursor + if g.cursor != nil { + cursor := *g.cursor + + // assign parameter of cursor + params["cursor"] = cursor + } 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 *GetOrderHistoriesRequest) 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 *GetOrderHistoriesRequest) 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 *GetOrderHistoriesRequest) 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 *GetOrderHistoriesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOrderHistoriesRequest) 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 *GetOrderHistoriesRequest) 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 *GetOrderHistoriesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOrderHistoriesRequest) 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 *GetOrderHistoriesRequest) Do(ctx context.Context) (*OrdersResponse, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/v5/order/history" + + 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 OrdersResponse + if err := json.Unmarshal(apiResponse.Result, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/bybit/bybitapi/get_order_history_request.go b/pkg/exchange/bybit/bybitapi/get_order_history_request.go new file mode 100644 index 000000000..7d46c8eff --- /dev/null +++ b/pkg/exchange/bybit/bybitapi/get_order_history_request.go @@ -0,0 +1,51 @@ +package bybitapi + +import ( + "time" + + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Result +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Result + +//go:generate GetRequest -url "/v5/order/history" -type GetOrderHistoriesRequest -responseDataType .OpenOrdersResponse +type GetOrderHistoriesRequest struct { + client requestgen.AuthenticatedAPIClient + + category Category `param:"category,query" validValues:"spot"` + + symbol *string `param:"symbol,query"` + orderId *string `param:"orderId,query"` + // orderFilter supports 3 types of Order: + // 1. active order, 2. StopOrder: conditional order, 3. tpslOrder: spot TP/SL order + // Normal spot: return Order active order by default + // Others: all kinds of orders by default + orderFilter *string `param:"orderFilter,query"` + // orderStatus if the account belongs to Normal spot, orderStatus is not supported. + //// For other accounts, return all status orders if not explicitly passed. + orderStatus *OrderStatus `param:"orderStatus,query"` + + // startTime must + // Normal spot is not supported temporarily + // startTime and endTime must be passed together + // If not passed, query the past 7 days data by default + // For each request, startTime and endTime interval should be less then 7 days + startTime *time.Time `param:"startTime,query,milliseconds"` + + // endTime for each request, startTime and endTime interval should be less then 7 days + endTime *time.Time `param:"endTime,query,milliseconds"` + + // limit for data size per page. [1, 50]. Default: 20 + limit *uint64 `param:"limit,query"` + // cursor uses the nextPageCursor token from the response to retrieve the next page of the result set + cursor *string `param:"cursor,query"` +} + +// NewGetOrderHistoriesRequest is descending order by createdTime +func (c *RestClient) NewGetOrderHistoriesRequest() *GetOrderHistoriesRequest { + return &GetOrderHistoriesRequest{ + client: c, + category: CategorySpot, + } +} diff --git a/pkg/exchange/bybit/convert.go b/pkg/exchange/bybit/convert.go index 85ce0c013..b70a05201 100644 --- a/pkg/exchange/bybit/convert.go +++ b/pkg/exchange/bybit/convert.go @@ -4,6 +4,7 @@ import ( "fmt" "hash/fnv" "math" + "strconv" "time" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" @@ -46,7 +47,7 @@ func toGlobalTicker(stats bybitapi.Ticker, time time.Time) types.Ticker { } } -func toGlobalOrder(order bybitapi.OpenOrder) (*types.Order, error) { +func toGlobalOrder(order bybitapi.Order) (*types.Order, error) { side, err := toGlobalSideType(order.Side) if err != nil { return nil, err @@ -67,6 +68,13 @@ func toGlobalOrder(order bybitapi.OpenOrder) (*types.Order, error) { if err != nil { return nil, err } + // linear and inverse : 42f4f364-82e1-49d3-ad1d-cd8cf9aa308d (UUID format) + // spot : 1468264727470772736 (only numbers) + // Now we only use spot trading. + orderIdNum, err := strconv.ParseUint(order.OrderId, 10, 64) + if err != nil { + return nil, fmt.Errorf("unexpected order id: %s, err: %v", order.OrderId, err) + } return &types.Order{ SubmitOrder: types.SubmitOrder{ @@ -79,7 +87,7 @@ func toGlobalOrder(order bybitapi.OpenOrder) (*types.Order, error) { TimeInForce: timeInForce, }, Exchange: types.ExchangeBybit, - OrderID: hashStringID(order.OrderId), + OrderID: orderIdNum, UUID: order.OrderId, Status: status, ExecutedQuantity: order.CumExecQty, diff --git a/pkg/exchange/bybit/convert_test.go b/pkg/exchange/bybit/convert_test.go index c5c8b497c..e4bffcf30 100644 --- a/pkg/exchange/bybit/convert_test.go +++ b/pkg/exchange/bybit/convert_test.go @@ -1,8 +1,12 @@ package bybit import ( + "context" "fmt" + "github.com/pkg/errors" + "go.uber.org/multierr" "math" + "strconv" "testing" "time" @@ -13,6 +17,18 @@ import ( "github.com/c9s/bbgo/pkg/types" ) +func TestU(t *testing.T) { + e := returnErr() + + t.Log(errors.Is(e, context.DeadlineExceeded)) + +} + +func returnErr() error { + var err error + return multierr.Append(multierr.Append(err, fmt.Errorf("got err: %w", context.DeadlineExceeded)), fmt.Errorf("GG")) +} + func TestToGlobalMarket(t *testing.T) { // sample: //{ @@ -178,7 +194,7 @@ func TestToGlobalOrder(t *testing.T) { // "UpdatedTime": "2023-07-25 17:12:57.868 +0800 CST" //} timeNow := time.Now() - openOrder := bybitapi.OpenOrder{ + openOrder := bybitapi.Order{ OrderId: "1472539279335923200", OrderLinkId: "1690276361150", BlockTradeId: "", @@ -231,6 +247,8 @@ func TestToGlobalOrder(t *testing.T) { assert.NoError(t, err) working, err := isWorking(openOrder.OrderStatus) assert.NoError(t, err) + orderIdNum, err := strconv.ParseUint(openOrder.OrderId, 10, 64) + assert.NoError(t, err) exp := types.Order{ SubmitOrder: types.SubmitOrder{ @@ -243,7 +261,7 @@ func TestToGlobalOrder(t *testing.T) { TimeInForce: tif, }, Exchange: types.ExchangeBybit, - OrderID: hashStringID(openOrder.OrderId), + OrderID: orderIdNum, UUID: openOrder.OrderId, Status: status, ExecutedQuantity: openOrder.CumExecQty, diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index a8a75b85b..33a68f2dc 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -3,6 +3,7 @@ package bybit import ( "context" "fmt" + "strconv" "time" "github.com/sirupsen/logrus" @@ -14,7 +15,8 @@ import ( ) const ( - maxOrderIdLen = 36 + maxOrderIdLen = 36 + defaultQueryClosedLen = 50 ) // https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit @@ -26,6 +28,7 @@ var ( sharedRateLimiter = rate.NewLimiter(rate.Every(time.Second/2), 2) tradeRateLimiter = rate.NewLimiter(rate.Every(time.Second/5), 5) orderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10) + closedRateLimiter = rate.NewLimiter(rate.Every(time.Second), 1) log = logrus.WithFields(logrus.Fields{ "exchange": "bybit", @@ -67,7 +70,7 @@ func (e *Exchange) PlatformFeeCurrency() string { func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("markets rate limiter wait error: %v", err) + return nil, fmt.Errorf("markets rate limiter wait error: %w", err) } instruments, err := e.client.NewGetInstrumentsInfoRequest().Do(ctx) @@ -85,12 +88,12 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { if err := sharedRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("ticker order rate limiter wait error: %v", err) + return nil, fmt.Errorf("ticker order rate limiter wait error: %w", err) } s, err := e.client.NewGetTickersRequest().Symbol(symbol).DoWithResponseTime(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to call ticker, symbol: %s, err: %w", symbol, err) } if len(s.List) != 1 { @@ -117,11 +120,11 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbols ...string) (map[str } if err := sharedRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("tickers rate limiter wait error: %v", err) + return nil, fmt.Errorf("tickers rate limiter wait error: %w", err) } allTickers, err := e.client.NewGetTickersRequest().DoWithResponseTime(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to call ticker, err: %w", err) } for _, s := range allTickers.List { @@ -141,11 +144,11 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [ } if err = tradeRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("place order rate limiter wait error: %v", err) + return nil, fmt.Errorf("place order rate limiter wait error: %w", err) } res, err := req.Do(ctx) if err != nil { - return nil, fmt.Errorf("failed to query open orders, err: %v", err) + return nil, fmt.Errorf("failed to query open orders, err: %w", err) } for _, order := range res.List { @@ -214,11 +217,11 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t req.OrderLinkId(order.ClientOrderID) if err := orderRateLimiter.Wait(ctx); err != nil { - return nil, fmt.Errorf("place order rate limiter wait error: %v", err) + return nil, fmt.Errorf("place order rate limiter wait error: %w", err) } res, err := req.Do(ctx) if err != nil { - return nil, fmt.Errorf("failed to place order, order: %#v, err: %v", order, err) + return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err) } if len(res.OrderId) == 0 || res.OrderLinkId != order.ClientOrderID { @@ -227,7 +230,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t ordersResp, err := e.client.NewGetOpenOrderRequest().OrderLinkId(res.OrderLinkId).Do(ctx) if err != nil { - return nil, fmt.Errorf("failed to query order by client order id: %s", res.OrderLinkId) + return nil, fmt.Errorf("failed to query order by client order id: %s, err: %w", res.OrderLinkId, err) } if len(ordersResp.List) != 1 { @@ -261,12 +264,12 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err req.Symbol(order.Market.Symbol) if err := orderRateLimiter.Wait(ctx); err != nil { - errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %v", order.ClientOrderID, err)) + errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %w", order.ClientOrderID, err)) continue } res, err := req.Do(ctx) if err != nil { - errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %v", order.ClientOrderID, err)) + errs = multierr.Append(errs, fmt.Errorf("failed to cancel order id: %s, err: %w", order.ClientOrderID, err)) continue } if res.OrderId != order.UUID || res.OrderLinkId != order.ClientOrderID { @@ -277,3 +280,38 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err return errs } + +func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, util time.Time, lastOrderID uint64) (orders []types.Order, err error) { + if !since.IsZero() || !util.IsZero() { + log.Warn("!!!BYBIT EXCHANGE API NOTICE!!! the since/until conditions will not be effected on SPOT account, bybit exchange does not support time-range-based query currently") + } + + if err := closedRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query closed order rate limiter wait error: %w", err) + } + res, err := e.client.NewGetOrderHistoriesRequest(). + Symbol(symbol). + Cursor(strconv.FormatUint(lastOrderID, 10)). + Limit(defaultQueryClosedLen). + Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to call get order histories error: %w", err) + } + + for _, order := range res.List { + o, err2 := toGlobalOrder(order) + if err2 != nil { + err = multierr.Append(err, err2) + continue + } + + if o.Status.Closed() { + orders = append(orders, *o) + } + } + if err != nil { + return nil, err + } + + return types.SortOrdersAscending(orders), nil +} diff --git a/pkg/types/order.go b/pkg/types/order.go index 369b8889b..5e29fdd36 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -116,6 +116,12 @@ const ( OrderStatusRejected OrderStatus = "REJECTED" ) +func (o OrderStatus) Closed() bool { + return o == OrderStatusFilled || + o == OrderStatusCanceled || + o == OrderStatusRejected +} + type SubmitOrder struct { ClientOrderID string `json:"clientOrderID,omitempty" db:"client_order_id"`