pkg/exchange: add QueryClosedOrders

This commit is contained in:
Edwin 2023-07-27 17:44:47 +08:00
parent 1760a5b8d6
commit d2ad504579
6 changed files with 428 additions and 13 deletions

View File

@ -132,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)
})
}

View File

@ -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
}

View File

@ -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,
}
}

View File

@ -1,7 +1,10 @@
package bybit
import (
"context"
"fmt"
"github.com/pkg/errors"
"go.uber.org/multierr"
"math"
"strconv"
"testing"
@ -14,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:
//{

View File

@ -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
}

View File

@ -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"`