pkg/exchange: fix queryTrades and queryOrderTrade api

This commit is contained in:
Edwin 2024-01-15 17:10:14 +08:00
parent cee61c1cc7
commit 11506fb605
6 changed files with 289 additions and 106 deletions

View File

@ -149,6 +149,32 @@ func toGlobalTrades(orderDetails []okexapi.OrderDetails) ([]types.Trade, error)
return trades, nil
}
func tradeToGlobal(trade okexapi.Trade) types.Trade {
// ** We use the bill id as the trade id, because okx uses billId to perform pagination. **
billID := trade.BillId
side := toGlobalSide(trade.Side)
return types.Trade{
ID: uint64(billID),
OrderID: uint64(trade.OrderId),
Exchange: types.ExchangeOKEx,
Price: trade.FillPrice,
Quantity: trade.FillSize,
QuoteQuantity: trade.FillPrice.Mul(trade.FillSize),
Symbol: toGlobalSymbol(trade.InstrumentId),
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: trade.ExecutionType == okexapi.LiquidityTypeMaker,
Time: types.Time(trade.Timestamp),
// The fees obtained from the exchange are negative, hence they are forcibly converted to positive.
Fee: trade.Fee.Abs(),
FeeCurrency: trade.FeeCurrency,
IsMargin: false,
IsFutures: false,
IsIsolated: false,
}
}
func orderDetailToGlobal(order *okexapi.OrderDetail) (*types.Order, error) {
side := toGlobalSide(order.Side)

View File

@ -1,6 +1,7 @@
package okex
import (
"encoding/json"
"fmt"
"testing"
@ -82,3 +83,92 @@ func Test_orderDetailToGlobal(t *testing.T) {
})
}
func Test_tradeToGlobal(t *testing.T) {
var (
assert = assert.New(t)
raw = `{"side":"sell","fillSz":"1","fillPx":"46446.4","fillPxVol":"","fillFwdPx":"","fee":"-46","fillPnl":"0","ordId":"665951654130348158","feeRate":"-0.001","instType":"SPOT","fillPxUsd":"","instId":"BTC-USDT","clOrdId":"","posSide":"net","billId":"665951654138736652","fillMarkVol":"","tag":"","fillTime":"1705047247128","execType":"T","fillIdxPx":"","tradeId":"724072849","fillMarkPx":"","feeCcy":"USDT","ts":"1705047247130"}`
)
var res okexapi.Trade
err := json.Unmarshal([]byte(raw), &res)
assert.NoError(err)
t.Run("succeeds with sell/taker", func(t *testing.T) {
assert.Equal(tradeToGlobal(res), types.Trade{
ID: uint64(665951654138736652),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Price: fixedpoint.NewFromFloat(46446.4),
Quantity: fixedpoint.One,
QuoteQuantity: fixedpoint.NewFromFloat(46446.4),
Symbol: "BTCUSDT",
Side: types.SideTypeSell,
IsBuyer: false,
IsMaker: false,
Time: types.Time(types.NewMillisecondTimestampFromInt(1705047247130).Time()),
Fee: fixedpoint.NewFromFloat(46),
FeeCurrency: "USDT",
})
})
t.Run("succeeds with buy/taker", func(t *testing.T) {
newRes := res
newRes.Side = okexapi.SideTypeBuy
assert.Equal(tradeToGlobal(newRes), types.Trade{
ID: uint64(665951654138736652),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Price: fixedpoint.NewFromFloat(46446.4),
Quantity: fixedpoint.One,
QuoteQuantity: fixedpoint.NewFromFloat(46446.4),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
IsMaker: false,
Time: types.Time(types.NewMillisecondTimestampFromInt(1705047247130).Time()),
Fee: fixedpoint.NewFromFloat(46),
FeeCurrency: "USDT",
})
})
t.Run("succeeds with sell/maker", func(t *testing.T) {
newRes := res
newRes.ExecutionType = okexapi.LiquidityTypeMaker
assert.Equal(tradeToGlobal(newRes), types.Trade{
ID: uint64(665951654138736652),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Price: fixedpoint.NewFromFloat(46446.4),
Quantity: fixedpoint.One,
QuoteQuantity: fixedpoint.NewFromFloat(46446.4),
Symbol: "BTCUSDT",
Side: types.SideTypeSell,
IsBuyer: false,
IsMaker: true,
Time: types.Time(types.NewMillisecondTimestampFromInt(1705047247130).Time()),
Fee: fixedpoint.NewFromFloat(46),
FeeCurrency: "USDT",
})
})
t.Run("succeeds with buy/maker", func(t *testing.T) {
newRes := res
newRes.Side = okexapi.SideTypeBuy
newRes.ExecutionType = okexapi.LiquidityTypeMaker
assert.Equal(tradeToGlobal(newRes), types.Trade{
ID: uint64(665951654138736652),
OrderID: uint64(665951654130348158),
Exchange: types.ExchangeOKEx,
Price: fixedpoint.NewFromFloat(46446.4),
Quantity: fixedpoint.One,
QuoteQuantity: fixedpoint.NewFromFloat(46446.4),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
IsMaker: true,
Time: types.Time(types.NewMillisecondTimestampFromInt(1705047247130).Time()),
Fee: fixedpoint.NewFromFloat(46),
FeeCurrency: "USDT",
})
})
}

View File

@ -22,7 +22,6 @@ import (
// Market data limiter means public api, this includes QueryMarkets, QueryTicker, QueryTickers, QueryKLines
var (
marketDataLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 5)
orderRateLimiter = rate.NewLimiter(rate.Every(300*time.Millisecond), 5)
queryMarketLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10)
queryTickerLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10)
@ -32,6 +31,7 @@ var (
batchCancelOrderLimiter = rate.NewLimiter(rate.Every(5*time.Millisecond), 200)
queryOpenOrderLimiter = rate.NewLimiter(rate.Every(30*time.Millisecond), 30)
queryClosedOrderRateLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10)
queryTradeLimiter = rate.NewLimiter(rate.Every(100*time.Millisecond), 10)
)
const (
@ -448,8 +448,8 @@ func (e *Exchange) QueryOrder(ctx context.Context, q types.OrderQuery) (*types.O
return toGlobalOrder(order)
}
// Query order trades can query trades in last 3 months.
func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]types.Trade, error) {
// QueryOrderTrades quires order trades can query trades in last 3 months.
func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) (trades []types.Trade, err error) {
if len(q.ClientOrderID) != 0 {
log.Warn("!!!OKEX EXCHANGE API NOTICE!!! Okex does not support searching for trades using OrderClientId.")
}
@ -463,28 +463,18 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) ([]
req.OrderID(q.OrderID)
}
if err := orderRateLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("order rate limiter wait error: %w", err)
if err := queryTradeLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("order trade rate limiter wait error: %w", err)
}
response, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to query order trades, err: %w", err)
}
var trades []types.Trade
var errs error
for _, trade := range response {
res, err := toGlobalTrade(&trade)
if err != nil {
errs = multierr.Append(errs, err)
continue
}
trades = append(trades, *res)
trades = append(trades, tradeToGlobal(trade))
}
if errs != nil {
return nil, errs
}
return trades, nil
}
@ -549,69 +539,63 @@ 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.
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.
okx does not provide an API to query by trade ID, so we use the bill ID to do it. The trades result is ordered by timestamp.
REMARK: If your start time is 90 days earlier, we will update it to now - 90 days.
** StartTime and EndTime are inclusive. **
** StartTime and EndTime cannot exceed 90 days. **
** StartTime, EndTime, FromTradeId can be used together. **
If you want to query all trades within a large time range (e.g. total orders > 100), we recommend using batch.TradeBatchQuery.
*/
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) ([]types.Trade, error) {
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
if symbol == "" {
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.NewGetTransactionHistoryRequest().InstrumentID(toLocalSymbol(symbol))
limit := uint64(options.Limit)
limit := options.Limit
req.Limit(uint64(limit))
if limit > defaultQueryLimit || limit <= 0 {
limit = defaultQueryLimit
log.Infof("limit is exceeded default limit %d or zero, got: %d, use default limit", defaultQueryLimit, 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)
}
if err := orderRateLimiter.Wait(ctx); err != nil {
var newStartTime time.Time
if options.StartTime != nil {
newStartTime = *options.StartTime
if time.Since(newStartTime) > maxHistoricalDataQueryPeriod {
newStartTime = time.Now().Add(-maxHistoricalDataQueryPeriod)
log.Warnf("!!!OKX EXCHANGE API NOTICE!!! The trade API cannot query data beyond 90 days from the current date, update %s -> %s", *options.StartTime, newStartTime)
}
req.StartTime(newStartTime.UTC())
}
if options.EndTime != nil {
if options.EndTime.Before(newStartTime) {
return nil, fmt.Errorf("end time %s before start %s", *options.EndTime, newStartTime)
}
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.UTC())
}
req.Before(strconv.FormatUint(options.LastTradeID, 10))
if err := queryTradeLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("query trades rate limiter wait error: %w", err)
}
var err error
var response []okexapi.OrderDetails
if options.StartTime == nil && options.EndTime == nil {
return nil, fmt.Errorf("StartTime and EndTime are required parameter!")
} else { // query by time interval
if options.StartTime != nil {
req.StartTime(*options.StartTime)
}
if options.EndTime != nil {
req.EndTime(*options.EndTime)
}
var billID = "" // billId should be emtpy, can't be 0
for { // pagenation should use "after" (earlier than)
res, err := req.
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) {
break
}
billID = strconv.Itoa(int(res[limit-1].BillID))
}
}
trades, err := toGlobalTrades(response)
response, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to trans order detail to trades error: %w", err)
return nil, fmt.Errorf("failed to query trades, err: %w", err)
}
for _, trade := range response {
trades = append(trades, tradeToGlobal(trade))
}
return trades, nil
}

View File

@ -211,6 +211,59 @@ func TestClient_OrderHistoryByTimeRange(t *testing.T) {
t.Log(res)
}
func TestClient_TransactionHistoryByOrderId(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
c := client.NewGetTransactionHistoryRequest().OrderID("665951812901531754")
res, err := c.Do(ctx)
assert.NoError(t, err)
t.Log(res)
}
func TestClient_TransactionHistoryAll(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
beforeId := int64(0)
for {
c := client.NewGetTransactionHistoryRequest().Before(strconv.FormatInt(beforeId, 10)).Limit(1)
res, err := c.Do(ctx)
assert.NoError(t, err)
t.Log(res)
if len(res) != 1 {
break
}
//orders = append(orders, res...)
beforeId = int64(res[0].BillId)
t.Log(res[0])
}
}
func TestClient_TransactionHistoryWithTime(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()
beforeId := int64(0)
for {
// [{"side":"sell","fillSz":"1","fillPx":"46446.4","fillPxVol":"","fillFwdPx":"","fee":"-46.4464","fillPnl":"0","ordId":"665951654130348158","feeRate":"-0.001","instType":"SPOT","fillPxUsd":"","instId":"BTC-USDT","clOrdId":"","posSide":"net","billId":"665951654138736652","fillMarkVol":"","tag":"","fillTime":"1705047247128","execType":"T","fillIdxPx":"","tradeId":"724072849","fillMarkPx":"","feeCcy":"USDT","ts":"1705047247130"}]
// [{"side":"sell","fillSz":"11.053006","fillPx":"54.17","fillPxVol":"","fillFwdPx":"","fee":"-0.59874133502","fillPnl":"0","ordId":"665951812901531754","feeRate":"-0.001","instType":"SPOT","fillPxUsd":"","instId":"OKB-USDT","clOrdId":"","posSide":"net","billId":"665951812905726068","fillMarkVol":"","tag":"","fillTime":"1705047284982","execType":"T","fillIdxPx":"","tradeId":"589438381","fillMarkPx":"","feeCcy":"USDT","ts":"1705047284983"}]
// [{"side":"sell","fillSz":"88.946994","fillPx":"54.16","fillPxVol":"","fillFwdPx":"","fee":"-4.81736919504","fillPnl":"0","ordId":"665951812901531754","feeRate":"-0.001","instType":"SPOT","fillPxUsd":"","instId":"OKB-USDT","clOrdId":"","posSide":"net","billId":"665951812905726084","fillMarkVol":"","tag":"","fillTime":"1705047284982","execType":"T","fillIdxPx":"","tradeId":"589438382","fillMarkPx":"","feeCcy":"USDT","ts":"1705047284983"}]
c := client.NewGetTransactionHistoryRequest().Limit(1).Before(fmt.Sprintf("%d", beforeId)).
StartTime(types.NewMillisecondTimestampFromInt(1705047247130).Time()).
EndTime(types.NewMillisecondTimestampFromInt(1705047284983).Time())
res, err := c.Do(ctx)
assert.NoError(t, err)
t.Log(res)
if len(res) != 1 {
break
}
beforeId = int64(res[0].BillId)
}
}
func TestClient_BatchCancelOrderRequest(t *testing.T) {
client := getTestClientOrSkip(t)
ctx := context.Background()

View File

@ -4,17 +4,62 @@ import (
"time"
"github.com/c9s/requestgen"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
//go:generate GetRequest -url "/api/v5/trade/fills-history" -type GetTransactionHistoryRequest -responseDataType .APIResponse
//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Data
//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Data
type LiquidityType string
const (
LiquidityTypeMaker = "M"
LiquidityTypeTaker = "T"
)
type Trade struct {
InstrumentType InstrumentType `json:"instType"`
InstrumentId string `json:"instId"`
TradeId types.StrInt64 `json:"tradeId"`
OrderId types.StrInt64 `json:"ordId"`
ClientOrderId string `json:"clOrdId"`
// Data generation time, Unix timestamp format in milliseconds, e.g. 1597026383085.
Timestamp types.MillisecondTimestamp `json:"ts"`
FillTime types.MillisecondTimestamp `json:"fillTime"`
FeeCurrency string `json:"feeCcy"`
Fee fixedpoint.Value `json:"fee"`
BillId types.StrInt64 `json:"billId"`
Side SideType `json:"side"`
ExecutionType LiquidityType `json:"execType"`
Tag string `json:"tag"`
// Last filled price
FillPrice fixedpoint.Value `json:"fillPx"`
// Last filled quantity
FillSize fixedpoint.Value `json:"fillSz"`
// Index price at the moment of trade execution
//For cross currency spot pairs, it returns baseCcy-USDT index price. For example, for LTC-ETH, this field returns the index price of LTC-USDT.
FillIndexPrice fixedpoint.Value `json:"fillIdxPx"`
FillPnl string `json:"fillPnl"`
// Only applicable to options; return "" for other instrument types
FillPriceVolume fixedpoint.Value `json:"fillPxVol"`
FillPriceUsd fixedpoint.Value `json:"fillPxUsd"`
FillMarkVolume fixedpoint.Value `json:"fillMarkVol"`
FillForwardPrice fixedpoint.Value `json:"fillFwdPx"`
FillMarkPrice fixedpoint.Value `json:"fillMarkPx"`
PosSide string `json:"posSide"`
}
//go:generate GetRequest -url "/api/v5/trade/fills-history" -type GetTransactionHistoryRequest -responseDataType []Trade
type GetTransactionHistoryRequest struct {
client requestgen.AuthenticatedAPIClient
instrumentType InstrumentType `param:"instType,query"`
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"`
@ -33,7 +78,6 @@ type GetTransactionHistoryRequest struct {
type OrderList []OrderDetails
// NewGetOrderHistoriesRequest is descending order by createdTime
func (c *RestClient) NewGetTransactionHistoryRequest() *GetTransactionHistoryRequest {
return &GetTransactionHistoryRequest{
client: c,

View File

@ -1,4 +1,4 @@
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/trade/fills-history -type GetTransactionHistoryRequest -responseDataType .OrderList"; DO NOT EDIT.
// Code generated by "requestgen -method GET -responseType .APIResponse -responseDataField Data -url /api/v5/trade/fills-history -type GetTransactionHistoryRequest -responseDataType []Trade"; DO NOT EDIT.
package okexapi
@ -23,11 +23,6 @@ func (g *GetTransactionHistoryRequest) InstrumentID(instrumentID string) *GetTra
return g
}
func (g *GetTransactionHistoryRequest) OrderType(orderType OrderType) *GetTransactionHistoryRequest {
g.orderType = &orderType
return g
}
func (g *GetTransactionHistoryRequest) OrderID(orderID string) *GetTransactionHistoryRequest {
g.orderID = orderID
return g
@ -68,11 +63,6 @@ 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{}{}
@ -100,25 +90,6 @@ func (g *GetTransactionHistoryRequest) GetQueryParameters() (url.Values, error)
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 orderID field -> json key ordId
orderID := g.orderID
@ -194,11 +165,6 @@ 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
}
@ -282,22 +248,31 @@ func (g *GetTransactionHistoryRequest) GetSlugsMap() (map[string]string, error)
return slugs, nil
}
func (g *GetTransactionHistoryRequest) Do(ctx context.Context) (OrderList, error) {
// GetPath returns the request path of the API
func (g *GetTransactionHistoryRequest) GetPath() string {
return "/api/v5/trade/fills-history"
}
// empty params for GET operation
// Do generates the request object and send the request object to the API endpoint
func (g *GetTransactionHistoryRequest) Do(ctx context.Context) ([]Trade, error) {
// no body params
var params interface{}
query, err := g.GetQueryParameters()
if err != nil {
return nil, err
}
apiURL := "/api/v5/trade/fills-history"
var apiURL string
apiURL = g.GetPath()
req, err := g.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params)
if err != nil {
return nil, err
}
fmt.Println(">>", req.URL)
response, err := g.client.SendRequest(req)
if err != nil {
return nil, err
@ -307,7 +282,18 @@ func (g *GetTransactionHistoryRequest) Do(ctx context.Context) (OrderList, error
if err := response.DecodeJSON(&apiResponse); err != nil {
return nil, err
}
var data OrderList
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
}