Merge pull request #1419 from c9s/edwin/bitget/fix-xmaker-error

FIX: [bitget] use the now - 90 days instead of return err if since is 90 days earlier
This commit is contained in:
bailantaotao 2023-11-17 16:54:28 +08:00 committed by GitHub
commit afc864dfc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 167 additions and 92 deletions

View File

@ -130,8 +130,8 @@ the implementation.
- OKEx Spot Exchange
- Kucoin Spot Exchange
- MAX Spot Exchange (located in Taiwan)
- Bitget Exchange (In Progress)
- Bybit Exchange (In Progress)
- Bitget Exchange
- Bybit Exchange
## Documentation and General Topics

View File

@ -34,15 +34,16 @@ func TestClient(t *testing.T) {
ctx := context.Background()
t.Run("GetUnfilledOrdersRequest", func(t *testing.T) {
req := client.NewGetUnfilledOrdersRequest().StartTime(1)
startTime := time.Now().Add(-30 * 24 * time.Hour)
req := client.NewGetUnfilledOrdersRequest().StartTime(startTime)
resp, err := req.Do(ctx)
assert.NoError(t, err)
t.Logf("resp: %+v", resp)
})
t.Run("GetHistoryOrdersRequest", func(t *testing.T) {
// market buy
req, err := client.NewGetHistoryOrdersRequest().Symbol("APEUSDT").Do(ctx)
startTime := time.Now().Add(-30 * 24 * time.Hour)
req, err := client.NewGetHistoryOrdersRequest().Symbol("APEUSDT").StartTime(startTime).Do(ctx)
assert.NoError(t, err)
t.Logf("place order resp: %+v", req)
@ -61,7 +62,8 @@ func TestClient(t *testing.T) {
})
t.Run("GetTradeFillsRequest", func(t *testing.T) {
req, err := client.NewGetTradeFillsRequest().Symbol("APEUSDT").Do(ctx)
startTime := time.Now().Add(-30 * 24 * time.Hour)
req, err := client.NewGetTradeFillsRequest().Symbol("APEUSDT").StartTime(startTime).Do(ctx)
assert.NoError(t, err)
t.Logf("get trade fills resp: %+v", req)

View File

@ -6,6 +6,7 @@ package bitgetapi
import (
"encoding/json"
"fmt"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
@ -92,8 +93,8 @@ type GetHistoryOrdersRequest struct {
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"`
startTime *time.Time `param:"startTime,milliseconds,query"`
endTime *time.Time `param:"endTime,milliseconds,query"`
orderId *string `param:"orderId,query"`
}

View File

@ -10,6 +10,8 @@ import (
"net/url"
"reflect"
"regexp"
"strconv"
"time"
)
func (g *GetHistoryOrdersRequest) Symbol(symbol string) *GetHistoryOrdersRequest {
@ -27,12 +29,12 @@ func (g *GetHistoryOrdersRequest) IdLessThan(idLessThan string) *GetHistoryOrder
return g
}
func (g *GetHistoryOrdersRequest) StartTime(startTime int64) *GetHistoryOrdersRequest {
func (g *GetHistoryOrdersRequest) StartTime(startTime time.Time) *GetHistoryOrdersRequest {
g.startTime = &startTime
return g
}
func (g *GetHistoryOrdersRequest) EndTime(endTime int64) *GetHistoryOrdersRequest {
func (g *GetHistoryOrdersRequest) EndTime(endTime time.Time) *GetHistoryOrdersRequest {
g.endTime = &endTime
return g
}
@ -74,7 +76,8 @@ func (g *GetHistoryOrdersRequest) GetQueryParameters() (url.Values, error) {
startTime := *g.startTime
// assign parameter of startTime
params["startTime"] = 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
@ -82,7 +85,8 @@ func (g *GetHistoryOrdersRequest) GetQueryParameters() (url.Values, error) {
endTime := *g.endTime
// assign parameter of endTime
params["endTime"] = endTime
// convert time.Time to milliseconds time stamp
params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check orderId field -> json key orderId

View File

@ -1,6 +1,8 @@
package bitgetapi
import (
"time"
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
@ -60,8 +62,8 @@ type GetTradeFillsRequest struct {
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"`
startTime *time.Time `param:"startTime,milliseconds,query"`
endTime *time.Time `param:"endTime,milliseconds,query"`
orderId *string `param:"orderId,query"`
}

View File

@ -10,6 +10,8 @@ import (
"net/url"
"reflect"
"regexp"
"strconv"
"time"
)
func (s *GetTradeFillsRequest) Symbol(symbol string) *GetTradeFillsRequest {
@ -27,12 +29,12 @@ func (s *GetTradeFillsRequest) IdLessThan(idLessThan string) *GetTradeFillsReque
return s
}
func (s *GetTradeFillsRequest) StartTime(startTime int64) *GetTradeFillsRequest {
func (s *GetTradeFillsRequest) StartTime(startTime time.Time) *GetTradeFillsRequest {
s.startTime = &startTime
return s
}
func (s *GetTradeFillsRequest) EndTime(endTime int64) *GetTradeFillsRequest {
func (s *GetTradeFillsRequest) EndTime(endTime time.Time) *GetTradeFillsRequest {
s.endTime = &endTime
return s
}
@ -71,7 +73,8 @@ func (s *GetTradeFillsRequest) GetQueryParameters() (url.Values, error) {
startTime := *s.startTime
// assign parameter of startTime
params["startTime"] = 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
@ -79,7 +82,8 @@ func (s *GetTradeFillsRequest) GetQueryParameters() (url.Values, error) {
endTime := *s.endTime
// assign parameter of endTime
params["endTime"] = endTime
// convert time.Time to milliseconds time stamp
params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check orderId field -> json key orderId
@ -185,6 +189,12 @@ func (s *GetTradeFillsRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil
}
// GetPath returns the request path of the API
func (s *GetTradeFillsRequest) GetPath() string {
return "/api/v2/spot/trade/fills"
}
// Do generates the request object and send the request object to the API endpoint
func (s *GetTradeFillsRequest) Do(ctx context.Context) ([]Trade, error) {
// no body params
@ -194,7 +204,9 @@ func (s *GetTradeFillsRequest) Do(ctx context.Context) ([]Trade, error) {
return nil, err
}
apiURL := "/api/v2/spot/trade/fills"
var apiURL string
apiURL = s.GetPath()
req, err := s.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
@ -211,6 +223,15 @@ func (s *GetTradeFillsRequest) Do(ctx context.Context) ([]Trade, error) {
return nil, err
}
type responseValidator interface {
Validate() error
}
validator, ok := interface{}(apiResponse).(responseValidator)
if ok {
if err := validator.Validate(); err != nil {
return nil, err
}
}
var data []Trade
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
return nil, err

View File

@ -4,6 +4,8 @@ package bitgetapi
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
import (
"time"
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
@ -40,8 +42,8 @@ type GetUnfilledOrdersRequest struct {
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"`
startTime *time.Time `param:"startTime,milliseconds,query"`
endTime *time.Time `param:"endTime,milliseconds,query"`
orderId *string `param:"orderId,query"`
}

View File

@ -10,6 +10,8 @@ import (
"net/url"
"reflect"
"regexp"
"strconv"
"time"
)
func (g *GetUnfilledOrdersRequest) Symbol(symbol string) *GetUnfilledOrdersRequest {
@ -27,12 +29,12 @@ func (g *GetUnfilledOrdersRequest) IdLessThan(idLessThan string) *GetUnfilledOrd
return g
}
func (g *GetUnfilledOrdersRequest) StartTime(startTime int64) *GetUnfilledOrdersRequest {
func (g *GetUnfilledOrdersRequest) StartTime(startTime time.Time) *GetUnfilledOrdersRequest {
g.startTime = &startTime
return g
}
func (g *GetUnfilledOrdersRequest) EndTime(endTime int64) *GetUnfilledOrdersRequest {
func (g *GetUnfilledOrdersRequest) EndTime(endTime time.Time) *GetUnfilledOrdersRequest {
g.endTime = &endTime
return g
}
@ -74,7 +76,8 @@ func (g *GetUnfilledOrdersRequest) GetQueryParameters() (url.Values, error) {
startTime := *g.startTime
// assign parameter of startTime
params["startTime"] = 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
@ -82,7 +85,8 @@ func (g *GetUnfilledOrdersRequest) GetQueryParameters() (url.Values, error) {
endTime := *g.endTime
// assign parameter of endTime
params["endTime"] = endTime
// convert time.Time to milliseconds time stamp
params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10)
} else {
}
// check orderId field -> json key orderId
@ -188,6 +192,12 @@ func (g *GetUnfilledOrdersRequest) GetSlugsMap() (map[string]string, error) {
return slugs, nil
}
// GetPath returns the request path of the API
func (g *GetUnfilledOrdersRequest) GetPath() string {
return "/api/v2/spot/trade/unfilled-orders"
}
// Do generates the request object and send the request object to the API endpoint
func (g *GetUnfilledOrdersRequest) Do(ctx context.Context) ([]UnfilledOrder, error) {
// no body params
@ -197,7 +207,9 @@ func (g *GetUnfilledOrdersRequest) Do(ctx context.Context) ([]UnfilledOrder, err
return nil, err
}
apiURL := "/api/v2/spot/trade/unfilled-orders"
var apiURL string
apiURL = g.GetPath()
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
@ -213,6 +225,16 @@ func (g *GetUnfilledOrdersRequest) Do(ctx context.Context) ([]UnfilledOrder, err
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
type responseValidator interface {
Validate() error
}
validator, ok := interface{}(apiResponse).(responseValidator)
if ok {
if err := validator.Validate(); err != nil {
return nil, err
}
}
var data []UnfilledOrder
if err := json.Unmarshal(apiResponse.Data, &data); err != nil {
return nil, err

View File

@ -24,7 +24,7 @@ const (
queryLimit = 100
defaultKLineLimit = 100
maxOrderIdLen = 36
queryMaxDuration = 90 * 24 * time.Hour
maxHistoricalDataQueryPeriod = 90 * 24 * time.Hour
)
var log = logrus.WithFields(logrus.Fields{
@ -123,7 +123,7 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke
return nil, fmt.Errorf("unexpected length of query single symbol: %+v", resp)
}
ticker := toGlobalTicker(resp[1])
ticker := toGlobalTicker(resp[0])
return &ticker, nil
}
@ -286,10 +286,10 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr
// we support only GTC/PostOnly, this is because:
// 1. We support only SPOT trading.
// 2. The query oepn/closed order does not including the `force` in SPOT.
// 2. The query open/closed order does not include the `force` in SPOT.
// If we support FOK/IOC, but you can't query them, that would be unreasonable.
// The other case to consider is 'PostOnly', which is a trade-off because we want to support 'xmaker'.
if order.TimeInForce != types.TimeInForceGTC {
if len(order.TimeInForce) != 0 && order.TimeInForce != types.TimeInForceGTC {
return nil, fmt.Errorf("time-in-force %s not supported", order.TimeInForce)
}
req.Force(v2.OrderForceGTC)
@ -397,18 +397,24 @@ func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders [
// QueryClosedOrders queries closed order by time range(`CreatedTime`) and id. The order of the response is in descending order.
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.ClosedOrderBatchQuery.
//
// REMARK: If your start time is 90 days earlier, we will update it to now - 90 days.
// ** Since is inclusive, Until is exclusive. If you use a time range to query, you must provide both a start time and an end time. **
// ** Since and Until cannot exceed 90 days. **
// ** Since from the last 90 days can be queried. **
// ** Since from the last 90 days can be queried **
func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) (orders []types.Order, err error) {
if time.Since(since) > queryMaxDuration {
return nil, fmt.Errorf("start time from the last 90 days can be queried, got: %s", since)
newSince := since
now := time.Now()
if time.Since(newSince) > maxHistoricalDataQueryPeriod {
newSince = now.Add(-maxHistoricalDataQueryPeriod)
log.Warnf("!!!BITGET EXCHANGE API NOTICE!!! The closed order API cannot query data beyond 90 days from the current date, update %s -> %s", since, newSince)
}
if until.Before(since) {
return nil, fmt.Errorf("end time %s before start %s", until, since)
if until.Before(newSince) {
log.Warnf("!!!BITGET EXCHANGE API NOTICE!!! The 'until' comes before 'since', update until to now(%s -> %s).", until, now)
until = now
}
if until.Sub(since) > queryMaxDuration {
return nil, fmt.Errorf("the start time %s and end time %s cannot exceed 90 days", since, until)
if until.Sub(newSince) > maxHistoricalDataQueryPeriod {
return nil, fmt.Errorf("the start time %s and end time %s cannot exceed 90 days", newSince, until)
}
if lastOrderID != 0 {
log.Warn("!!!BITGET EXCHANGE API NOTICE!!! The order of response is in descending order, so the last order id not supported.")
@ -420,8 +426,8 @@ func (e *Exchange) QueryClosedOrders(ctx context.Context, symbol string, since,
res, err := e.v2client.NewGetHistoryOrdersRequest().
Symbol(symbol).
Limit(strconv.Itoa(queryLimit)).
StartTime(since.UnixMilli()).
EndTime(until.UnixMilli()).
StartTime(newSince).
EndTime(until).
Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to call get order histories error: %w", err)
@ -451,7 +457,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err
}
for _, order := range orders {
req := e.client.NewCancelOrderRequest()
req := e.v2client.NewCancelOrderRequest()
reqId := ""
switch {
@ -472,7 +478,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err
continue
}
req.Symbol(order.Market.Symbol)
req.Symbol(order.Symbol)
if err := cancelOrderRateLimiter.Wait(ctx); err != nil {
errs = multierr.Append(errs, fmt.Errorf("cancel order rate limiter wait, order id: %s, error: %w", order.ClientOrderID, err))
@ -485,8 +491,8 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err
}
// sanity check
if res.OrderId != reqId && res.ClientOrderId != reqId {
errs = multierr.Append(errs, fmt.Errorf("order id mismatch, exp: %s, respOrderId: %s, respClientOrderId: %s", reqId, res.OrderId, res.ClientOrderId))
if res.OrderId.String() != reqId && res.ClientOrderId != reqId {
errs = multierr.Append(errs, fmt.Errorf("order id mismatch, exp: %s, respOrderId: %d, respClientOrderId: %s", reqId, res.OrderId, res.ClientOrderId))
continue
}
}
@ -498,6 +504,7 @@ func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (err
// using (`CreatedTime`) as the search criteria.
// If you need to retrieve all data, please utilize the function pkg/exchange/batch.TradeBatchQuery.
//
// REMARK: If your start time is 90 days earlier, we will update it to now - 90 days.
// ** StartTime is inclusive, EndTime is exclusive. If you use the EndTime, the StartTime is required. **
// ** StartTime and EndTime cannot exceed 90 days. **
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
@ -508,24 +515,27 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
req := e.v2client.NewGetTradeFillsRequest()
req.Symbol(symbol)
var newStartTime time.Time
if options.StartTime != nil {
if time.Since(*options.StartTime) > queryMaxDuration {
return nil, fmt.Errorf("start time from the last 90 days can be queried, got: %s", options.StartTime)
newStartTime = *options.StartTime
if time.Since(newStartTime) > maxHistoricalDataQueryPeriod {
newStartTime = time.Now().Add(-maxHistoricalDataQueryPeriod)
log.Warnf("!!!BITGET EXCHANGE API NOTICE!!! The trade API cannot query data beyond 90 days from the current date, update %s -> %s", *options.StartTime, newStartTime)
}
req.StartTime(options.StartTime.UnixMilli())
req.StartTime(newStartTime)
}
if options.EndTime != nil {
if options.StartTime == nil {
if newStartTime.IsZero() {
return nil, errors.New("start time is required for query trades if you take end time")
}
if options.EndTime.Before(*options.StartTime) {
return nil, fmt.Errorf("end time %s before start %s", *options.EndTime, *options.StartTime)
if options.EndTime.Before(newStartTime) {
return nil, fmt.Errorf("end time %s before start %s", *options.EndTime, newStartTime)
}
if options.EndTime.Sub(*options.StartTime) > queryMaxDuration {
return nil, fmt.Errorf("start time %s and end time %s cannot greater than 90 days", options.StartTime, options.EndTime)
if options.EndTime.Sub(newStartTime) > maxHistoricalDataQueryPeriod {
return nil, fmt.Errorf("start time %s and end time %s cannot greater than 90 days", newStartTime, options.EndTime)
}
req.EndTime(options.EndTime.UnixMilli())
req.EndTime(*options.EndTime)
}
limit := options.Limit

View File

@ -15,30 +15,6 @@ const DateFormat = "2006-01-02"
type ExchangeName string
func (n *ExchangeName) Value() (driver.Value, error) {
return n.String(), nil
}
func (n *ExchangeName) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch s {
case "max", "binance", "okex", "kucoin":
*n = ExchangeName(s)
return nil
}
return fmt.Errorf("unknown or unsupported exchange name: %s, valid names are: max, binance, okex, kucoin", s)
}
func (n ExchangeName) String() string {
return string(n)
}
const (
ExchangeMax ExchangeName = "max"
ExchangeBinance ExchangeName = "binance"
@ -59,17 +35,45 @@ var SupportedExchanges = []ExchangeName{
// note: we are not using "backtest"
}
func ValidExchangeName(a string) (ExchangeName, error) {
aa := strings.ToLower(a)
for _, n := range SupportedExchanges {
if string(n) == aa {
return n, nil
}
func (n *ExchangeName) Value() (driver.Value, error) {
return n.String(), nil
}
func (n *ExchangeName) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
*n = ExchangeName(s)
if !n.IsValid() {
return fmt.Errorf("%s is an invalid exchange name", s)
}
return nil
}
func (n ExchangeName) IsValid() bool {
switch n {
case ExchangeBinance, ExchangeBitget, ExchangeBybit, ExchangeMax, ExchangeOKEx, ExchangeKucoin:
return true
}
return false
}
func (n ExchangeName) String() string {
return string(n)
}
func ValidExchangeName(a string) (ExchangeName, error) {
exName := ExchangeName(strings.ToLower(a))
if !exName.IsValid() {
return "", fmt.Errorf("invalid exchange name: %s", a)
}
return exName, nil
}
type ExchangeMinimal interface {
Name() ExchangeName
PlatformFeeCurrency() string

View File

@ -41,3 +41,10 @@ func (s *StrInt64) UnmarshalJSON(body []byte) error {
return nil
}
func (s *StrInt64) String() string {
if s == nil {
return ""
}
return strconv.FormatInt(int64(*s), 10)
}