diff --git a/pkg/exchange/bitget/bitgetapi/v2/client.go b/pkg/exchange/bitget/bitgetapi/v2/client.go new file mode 100644 index 000000000..3a2b2204d --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/v2/client.go @@ -0,0 +1,17 @@ +package bitgetapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" +) + +type APIResponse = bitgetapi.APIResponse + +type Client struct { + Client requestgen.AuthenticatedAPIClient +} + +func NewClient(client *bitgetapi.RestClient) *Client { + return &Client{Client: client} +} diff --git a/pkg/exchange/bitget/bitgetapi/v2/client_test.go b/pkg/exchange/bitget/bitgetapi/v2/client_test.go new file mode 100644 index 000000000..21c86dfc6 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/v2/client_test.go @@ -0,0 +1,42 @@ +package bitgetapi + +import ( + "context" + "os" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" + "github.com/c9s/bbgo/pkg/testutil" +) + +func getTestClientOrSkip(t *testing.T) *Client { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + key, secret, ok := testutil.IntegrationTestConfigured(t, "BITGET") + if !ok { + t.Skip("BITGET_* env vars are not configured") + return nil + } + + client := bitgetapi.NewClient() + client.Auth(key, secret, os.Getenv("BITGET_API_PASSPHRASE")) + + return NewClient(client) +} + +func TestClient(t *testing.T) { + client := getTestClientOrSkip(t) + ctx := context.Background() + + t.Run("GetUnfilledOrdersRequest", func(t *testing.T) { + req := client.NewGetUnfilledOrdersRequest().StartTime(1) + resp, err := req.Do(ctx) + assert.NoError(t, err) + t.Logf("resp: %+v", resp) + }) +} diff --git a/pkg/exchange/bitget/bitgetapi/v2/get_unfilled_orders_request.go b/pkg/exchange/bitget/bitgetapi/v2/get_unfilled_orders_request.go new file mode 100644 index 000000000..178b31bba --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/v2/get_unfilled_orders_request.go @@ -0,0 +1,50 @@ +package bitgetapi + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type UnfilledOrder struct { + UserId types.StrInt64 `json:"userId"` + Symbol string `json:"symbol"` + // OrderId are always numeric. It's confirmed with official customer service. https://t.me/bitgetOpenapi/24172 + OrderId types.StrInt64 `json:"orderId"` + ClientOrderId string `json:"clientOid"` + PriceAvg fixedpoint.Value `json:"priceAvg"` + // Size is base coin when orderType=limit; quote coin when orderType=market + Size fixedpoint.Value `json:"size"` + OrderType OrderType `json:"orderType"` + Side SideType `json:"side"` + Status OrderStatus `json:"status"` + BasePrice fixedpoint.Value `json:"basePrice"` + BaseVolume fixedpoint.Value `json:"baseVolume"` + QuoteVolume fixedpoint.Value `json:"quoteVolume"` + EnterPointSource string `json:"enterPointSource"` + OrderSource string `json:"orderSource"` + CTime types.MillisecondTimestamp `json:"cTime"` + UTime types.MillisecondTimestamp `json:"uTime"` +} + +//go:generate GetRequest -url "/api/v2/spot/trade/unfilled-orders" -type GetUnfilledOrdersRequest -responseDataType []UnfilledOrder +type GetUnfilledOrdersRequest struct { + client requestgen.AuthenticatedAPIClient + + symbol *string `param:"symbol,query"` + // Limit number default 100 max 100 + limit *string `param:"limit,query"` + // idLessThan requests the content on the page before this ID (older data), the value input should be the orderId of the corresponding interface. + idLessThan *string `param:"idLessThan,query"` + startTime *int64 `param:"startTime,query"` + endTime *int64 `param:"endTime,query"` + orderId *string `param:"orderId,query"` +} + +func (c *Client) NewGetUnfilledOrdersRequest() *GetUnfilledOrdersRequest { + return &GetUnfilledOrdersRequest{client: c.Client} +} diff --git a/pkg/exchange/bitget/bitgetapi/v2/get_unfilled_orders_request_requestgen.go b/pkg/exchange/bitget/bitgetapi/v2/get_unfilled_orders_request_requestgen.go new file mode 100644 index 000000000..a3bb59819 --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/v2/get_unfilled_orders_request_requestgen.go @@ -0,0 +1,221 @@ +// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v2/spot/trade/unfilled-orders -type GetUnfilledOrdersRequest -responseDataType []UnfilledOrder"; DO NOT EDIT. + +package bitgetapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" + "net/url" + "reflect" + "regexp" +) + +func (g *GetUnfilledOrdersRequest) Symbol(symbol string) *GetUnfilledOrdersRequest { + g.symbol = &symbol + return g +} + +func (g *GetUnfilledOrdersRequest) Limit(limit string) *GetUnfilledOrdersRequest { + g.limit = &limit + return g +} + +func (g *GetUnfilledOrdersRequest) IdLessThan(idLessThan string) *GetUnfilledOrdersRequest { + g.idLessThan = &idLessThan + return g +} + +func (g *GetUnfilledOrdersRequest) StartTime(startTime int64) *GetUnfilledOrdersRequest { + g.startTime = &startTime + return g +} + +func (g *GetUnfilledOrdersRequest) EndTime(endTime int64) *GetUnfilledOrdersRequest { + g.endTime = &endTime + return g +} + +func (g *GetUnfilledOrdersRequest) OrderId(orderId string) *GetUnfilledOrdersRequest { + g.orderId = &orderId + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetUnfilledOrdersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + // check symbol field -> json key symbol + if g.symbol != nil { + symbol := *g.symbol + + // assign parameter of symbol + params["symbol"] = symbol + } else { + } + // check limit field -> json key limit + if g.limit != nil { + limit := *g.limit + + // assign parameter of limit + params["limit"] = limit + } else { + } + // check idLessThan field -> json key idLessThan + if g.idLessThan != nil { + idLessThan := *g.idLessThan + + // assign parameter of idLessThan + params["idLessThan"] = idLessThan + } else { + } + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // assign parameter of startTime + params["startTime"] = startTime + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // assign parameter of endTime + params["endTime"] = endTime + } else { + } + // check orderId field -> json key orderId + if g.orderId != nil { + orderId := *g.orderId + + // assign parameter of orderId + params["orderId"] = orderId + } 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 *GetUnfilledOrdersRequest) 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 *GetUnfilledOrdersRequest) 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 *GetUnfilledOrdersRequest) 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 *GetUnfilledOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetUnfilledOrdersRequest) 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 *GetUnfilledOrdersRequest) 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 *GetUnfilledOrdersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetUnfilledOrdersRequest) 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 *GetUnfilledOrdersRequest) Do(ctx context.Context) ([]UnfilledOrder, error) { + + // no body params + var params interface{} + query, err := g.GetQueryParameters() + if err != nil { + return nil, err + } + + apiURL := "/api/v2/spot/trade/unfilled-orders" + + 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 bitgetapi.APIResponse + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + var data []UnfilledOrder + if err := json.Unmarshal(apiResponse.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/bitget/bitgetapi/v2/types.go b/pkg/exchange/bitget/bitgetapi/v2/types.go new file mode 100644 index 000000000..82a91859c --- /dev/null +++ b/pkg/exchange/bitget/bitgetapi/v2/types.go @@ -0,0 +1,42 @@ +package bitgetapi + +type SideType string + +const ( + SideTypeBuy SideType = "buy" + SideTypeSell SideType = "sell" +) + +type OrderType string + +const ( + OrderTypeLimit OrderType = "limit" + OrderTypeMarket OrderType = "market" +) + +type OrderForce string + +const ( + OrderForceGTC OrderForce = "gtc" + OrderForcePostOnly OrderForce = "post_only" + OrderForceFOK OrderForce = "fok" + OrderForceIOC OrderForce = "ioc" +) + +type OrderStatus string + +const ( + OrderStatusInit OrderStatus = "init" + OrderStatusNew OrderStatus = "new" + OrderStatusLive OrderStatus = "live" + OrderStatusPartialFilled OrderStatus = "partially_filled" + OrderStatusFilled OrderStatus = "filled" + OrderStatusCancelled OrderStatus = "cancelled" +) + +func (o OrderStatus) IsWorking() bool { + return o == OrderStatusInit || + o == OrderStatusNew || + o == OrderStatusLive || + o == OrderStatusPartialFilled +} diff --git a/pkg/exchange/bitget/convert.go b/pkg/exchange/bitget/convert.go index 1339089fd..a4a9756d0 100644 --- a/pkg/exchange/bitget/convert.go +++ b/pkg/exchange/bitget/convert.go @@ -1,10 +1,13 @@ package bitget import ( + "fmt" "math" + "strconv" "strings" "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" + v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -59,3 +62,99 @@ func toGlobalTicker(ticker bitgetapi.Ticker) types.Ticker { Sell: ticker.SellOne, } } + +func toGlobalSideType(side v2.SideType) (types.SideType, error) { + switch side { + case v2.SideTypeBuy: + return types.SideTypeBuy, nil + + case v2.SideTypeSell: + return types.SideTypeSell, nil + + default: + return types.SideType(side), fmt.Errorf("unexpected side: %s", side) + } +} + +func toGlobalOrderType(s v2.OrderType) (types.OrderType, error) { + switch s { + case v2.OrderTypeMarket: + return types.OrderTypeMarket, nil + + case v2.OrderTypeLimit: + return types.OrderTypeLimit, nil + + default: + return types.OrderType(s), fmt.Errorf("unexpected order type: %s", s) + } +} + +func toGlobalOrderStatus(status v2.OrderStatus) (types.OrderStatus, error) { + switch status { + case v2.OrderStatusInit, v2.OrderStatusNew, v2.OrderStatusLive: + return types.OrderStatusNew, nil + + case v2.OrderStatusPartialFilled: + return types.OrderStatusPartiallyFilled, nil + + case v2.OrderStatusFilled: + return types.OrderStatusFilled, nil + + case v2.OrderStatusCancelled: + return types.OrderStatusCanceled, nil + + default: + return types.OrderStatus(status), fmt.Errorf("unexpected order status: %s", status) + } +} + +// unfilledOrderToGlobalOrder convert the local order to global. +// +// Note that the quantity unit, according official document: Base coin when orderType=limit; Quote coin when orderType=market +// https://bitgetlimited.github.io/apidoc/zh/spot/#19671a1099 +func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) { + side, err := toGlobalSideType(order.Side) + if err != nil { + return nil, err + } + + orderType, err := toGlobalOrderType(order.OrderType) + if err != nil { + return nil, err + } + + status, err := toGlobalOrderStatus(order.Status) + if err != nil { + return nil, err + } + + qty := order.Size + price := order.PriceAvg + + // The market order will be executed immediately, so this check is used to handle corner cases. + if orderType == types.OrderTypeMarket { + qty = order.BaseVolume + log.Warnf("!!! The price(%f) and quantity(%f) are not verified for market orders, because we only receive limit orders in the test environment !!!", price.Float64(), qty.Float64()) + } + + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: order.ClientOrderId, + Symbol: order.Symbol, + Side: side, + Type: orderType, + Quantity: qty, + Price: price, + // Bitget does not include the "time-in-force" field in its API response for spot trading, so we set GTC. + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeBitget, + OrderID: uint64(order.OrderId), + UUID: strconv.FormatInt(int64(order.OrderId), 10), + Status: status, + ExecutedQuantity: order.BaseVolume, + IsWorking: order.Status.IsWorking(), + CreationTime: types.Time(order.CTime.Time()), + UpdateTime: types.Time(order.UTime.Time()), + }, nil +} diff --git a/pkg/exchange/bitget/convert_test.go b/pkg/exchange/bitget/convert_test.go index 770e4e5b8..8b8bc61c7 100644 --- a/pkg/exchange/bitget/convert_test.go +++ b/pkg/exchange/bitget/convert_test.go @@ -1,11 +1,13 @@ package bitget import ( + "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" + v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -143,3 +145,130 @@ func Test_toGlobalTicker(t *testing.T) { Sell: fixedpoint.NewFromFloat(24014.06), }, toGlobalTicker(ticker)) } + +func Test_toGlobalSideType(t *testing.T) { + side, err := toGlobalSideType(v2.SideTypeBuy) + assert.NoError(t, err) + assert.Equal(t, types.SideTypeBuy, side) + + side, err = toGlobalSideType(v2.SideTypeSell) + assert.NoError(t, err) + assert.Equal(t, types.SideTypeSell, side) + + _, err = toGlobalSideType("xxx") + assert.ErrorContains(t, err, "xxx") +} + +func Test_toGlobalOrderType(t *testing.T) { + orderType, err := toGlobalOrderType(v2.OrderTypeMarket) + assert.NoError(t, err) + assert.Equal(t, types.OrderTypeMarket, orderType) + + orderType, err = toGlobalOrderType(v2.OrderTypeLimit) + assert.NoError(t, err) + assert.Equal(t, types.OrderTypeLimit, orderType) + + _, err = toGlobalOrderType("xxx") + assert.ErrorContains(t, err, "xxx") +} + +func Test_toGlobalOrderStatus(t *testing.T) { + status, err := toGlobalOrderStatus(v2.OrderStatusInit) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusNew, status) + + status, err = toGlobalOrderStatus(v2.OrderStatusNew) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusNew, status) + + status, err = toGlobalOrderStatus(v2.OrderStatusLive) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusNew, status) + + status, err = toGlobalOrderStatus(v2.OrderStatusFilled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusFilled, status) + + status, err = toGlobalOrderStatus(v2.OrderStatusPartialFilled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusPartiallyFilled, status) + + status, err = toGlobalOrderStatus(v2.OrderStatusCancelled) + assert.NoError(t, err) + assert.Equal(t, types.OrderStatusCanceled, status) + + _, err = toGlobalOrderStatus("xxx") + assert.ErrorContains(t, err, "xxx") +} + +func Test_unfilledOrderToGlobalOrder(t *testing.T) { + var ( + assert = assert.New(t) + orderId = 1105087175647989764 + unfilledOrder = v2.UnfilledOrder{ + Symbol: "BTCUSDT", + OrderId: types.StrInt64(orderId), + ClientOrderId: "74b86af3-6098-479c-acac-bfb074c067f3", + PriceAvg: fixedpoint.NewFromFloat(1.2), + Size: fixedpoint.NewFromFloat(5), + OrderType: v2.OrderTypeLimit, + Side: v2.SideTypeBuy, + Status: v2.OrderStatusLive, + BasePrice: fixedpoint.NewFromFloat(0), + BaseVolume: fixedpoint.NewFromFloat(0), + QuoteVolume: fixedpoint.NewFromFloat(0), + EnterPointSource: "API", + OrderSource: "normal", + CTime: types.NewMillisecondTimestampFromInt(1660704288118), + UTime: types.NewMillisecondTimestampFromInt(1660704288118), + } + ) + + t.Run("succeeds", func(t *testing.T) { + order, err := unfilledOrderToGlobalOrder(unfilledOrder) + assert.NoError(err) + assert.Equal(&types.Order{ + SubmitOrder: types.SubmitOrder{ + ClientOrderID: "74b86af3-6098-479c-acac-bfb074c067f3", + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.NewFromFloat(5), + Price: fixedpoint.NewFromFloat(1.2), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeBitget, + OrderID: uint64(orderId), + UUID: strconv.FormatInt(int64(orderId), 10), + Status: types.OrderStatusNew, + ExecutedQuantity: fixedpoint.NewFromFloat(0), + IsWorking: true, + CreationTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()), + UpdateTime: types.Time(types.NewMillisecondTimestampFromInt(1660704288118).Time()), + }, order) + }) + + t.Run("failed to convert side", func(t *testing.T) { + newOrder := unfilledOrder + newOrder.Side = "xxx" + + _, err := unfilledOrderToGlobalOrder(newOrder) + assert.ErrorContains(err, "xxx") + }) + + t.Run("failed to convert oder type", func(t *testing.T) { + newOrder := unfilledOrder + newOrder.OrderType = "xxx" + + _, err := unfilledOrderToGlobalOrder(newOrder) + assert.ErrorContains(err, "xxx") + }) + + t.Run("failed to convert oder status", func(t *testing.T) { + newOrder := unfilledOrder + newOrder.Status = "xxx" + + _, err := unfilledOrderToGlobalOrder(newOrder) + assert.ErrorContains(err, "xxx") + }) +} diff --git a/pkg/exchange/bitget/exchange.go b/pkg/exchange/bitget/exchange.go index 700bd2ea7..542a3bfc2 100644 --- a/pkg/exchange/bitget/exchange.go +++ b/pkg/exchange/bitget/exchange.go @@ -3,18 +3,24 @@ package bitget import ( "context" "fmt" + "strconv" "time" "github.com/sirupsen/logrus" "golang.org/x/time/rate" "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi" + v2 "github.com/c9s/bbgo/pkg/exchange/bitget/bitgetapi/v2" "github.com/c9s/bbgo/pkg/types" ) -const ID = "bitget" +const ( + ID = "bitget" -const PlatformToken = "BGB" + PlatformToken = "BGB" + + queryOpenOrdersLimit = 100 +) var log = logrus.WithFields(logrus.Fields{ "exchange": ID, @@ -29,12 +35,15 @@ var ( queryTickerRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5) // queryTickersRateLimiter has its own rate limit. https://bitgetlimited.github.io/apidoc/en/spot/#get-all-tickers queryTickersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5) + // queryOpenOrdersRateLimiter has its own rate limit. https://www.bitget.com/zh-CN/api-doc/spot/trade/Get-Unfilled-Orders + queryOpenOrdersRateLimiter = rate.NewLimiter(rate.Every(time.Second/10), 5) ) type Exchange struct { key, secret, passphrase string - client *bitgetapi.RestClient + client *bitgetapi.RestClient + v2Client *v2.Client } func New(key, secret, passphrase string) *Exchange { @@ -49,6 +58,7 @@ func New(key, secret, passphrase string) *Exchange { secret: secret, passphrase: passphrase, client: client, + v2Client: v2.NewClient(client), } } @@ -174,8 +184,46 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr } func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { - // TODO implement me - panic("implement me") + var nextCursor types.StrInt64 + for { + if err := queryOpenOrdersRateLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("open order rate limiter wait error: %w", err) + } + + req := e.v2Client.NewGetUnfilledOrdersRequest(). + Symbol(symbol). + Limit(strconv.FormatInt(queryOpenOrdersLimit, 10)) + if nextCursor != 0 { + req.IdLessThan(strconv.FormatInt(int64(nextCursor), 10)) + } + + openOrders, err := req.Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query open orders: %w", err) + } + + for _, o := range openOrders { + order, err := unfilledOrderToGlobalOrder(o) + if err != nil { + return nil, fmt.Errorf("failed to convert order, err: %v", err) + } + + orders = append(orders, *order) + } + + orderLen := len(openOrders) + // a defensive programming to ensure the length of order response is expected. + if orderLen > queryOpenOrdersLimit { + return nil, fmt.Errorf("unexpected open orders length %d", orderLen) + } + + if orderLen < queryOpenOrdersLimit { + break + } + nextCursor = openOrders[orderLen-1].OrderId + } + + return orders, nil } func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error {