pkg/exchange: remove the query after place order

This commit is contained in:
edwin 2024-03-14 11:34:40 +08:00
parent 747b75fa74
commit b1414b583e
3 changed files with 62 additions and 247 deletions

View File

@ -13,6 +13,7 @@ import (
"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"
)
@ -344,6 +345,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr
return nil, fmt.Errorf("place order rate limiter wait error: %w", err)
}
timeNow := time.Now()
res, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err)
@ -355,44 +357,22 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (cr
return nil, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order)
}
orderId := res.OrderId
debugf("fetching unfilled order info for order #%s", orderId)
ordersResp, err := e.v2client.NewGetUnfilledOrdersRequest().OrderId(orderId).Do(ctx)
intOrderId, err := strconv.ParseUint(res.OrderId, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to query open order by order id: %s, err: %w", orderId, err)
return nil, err
}
debugf("unfilled order response for order#%s: %+v", orderId, ordersResp)
if len(ordersResp) == 1 {
// 2023/11/05 The market order will be executed immediately, so we cannot retrieve it through the NewGetUnfilledOrdersRequest API.
// Try to get the order from the NewGetHistoryOrdersRequest API.
// 2024/03/06 After placing a Market Order, we can retrieve it through the unfilledOrder API, so we still need to
// handle the Market Order status.
return unfilledOrderToGlobalOrder(ordersResp[0])
} else if len(ordersResp) == 0 {
ordersResp, err := e.v2client.NewGetHistoryOrdersRequest().OrderId(orderId).Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to query history order by order id: %s, err: %w", orderId, err)
}
if len(ordersResp) != 1 {
// 2023/03/12 If it's a maker order and there is a corresponding order to be executed, then the order will be canceled,
// you can receive the status immediately from the websocket, but the RestAPI requires at least 200ms waiting time.
//
// Therefore, We don't want to waste time waiting for him, so we choose to manually enter the order
// information and send it back.
if order.Type == types.OrderTypeLimitMaker {
return fallbackPostOnlyOrder(order, orderId)
}
return nil, fmt.Errorf("unexpected length of history orders, expecting: 1, given: %d, ids: %s", len(ordersResp), orderId)
}
return toGlobalOrder(ordersResp[0])
}
return nil, fmt.Errorf("unexpected length of unfilled orders, expecting: 1, given: %d, ids: %s", len(ordersResp), orderId)
return &types.Order{
SubmitOrder: order,
Exchange: types.ExchangeBitget,
OrderID: intOrderId,
UUID: res.OrderId,
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(timeNow),
UpdateTime: types.Time(timeNow),
}, nil
}
func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) {

View File

@ -546,15 +546,21 @@ func TestExchange_QueryAccountBalances(t *testing.T) {
func TestExchange_SubmitOrder(t *testing.T) {
var (
assert = assert.New(t)
ex = New("key", "secret", "passphrase")
placeOrderUrl = "/api/v2/spot/trade/place-order"
openOrderUrl = "/api/v2/spot/trade/unfilled-orders"
tickerUrl = "/api/v2/spot/market/tickers"
historyOrderUrl = "/api/v2/spot/trade/history-orders"
clientOrderId = "684a79df-f931-474f-a9a5-f1deab1cd770"
expBtcSymbol = "BTCUSDT"
expOrder = &types.Order{
assert = assert.New(t)
ex = New("key", "secret", "passphrase")
placeOrderUrl = "/api/v2/spot/trade/place-order"
tickerUrl = "/api/v2/spot/market/tickers"
clientOrderId = "684a79df-f931-474f-a9a5-f1deab1cd770"
expBtcSymbol = "BTCUSDT"
mkt = types.Market{
Symbol: expBtcSymbol,
LocalSymbol: expBtcSymbol,
PricePrecision: fixedpoint.MustNewFromString("2").Int(),
VolumePrecision: fixedpoint.MustNewFromString("6").Int(),
StepSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(6)),
TickSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(2)),
}
expOrder = &types.Order{
SubmitOrder: types.SubmitOrder{
ClientOrderID: clientOrderId,
Symbol: expBtcSymbol,
@ -563,6 +569,7 @@ func TestExchange_SubmitOrder(t *testing.T) {
Quantity: fixedpoint.MustNewFromString("0.00009"),
Price: fixedpoint.MustNewFromString("66000"),
TimeInForce: types.TimeInForceGTC,
Market: mkt,
},
Exchange: types.ExchangeBitget,
OrderID: 1148903850645331968,
@ -580,15 +587,8 @@ func TestExchange_SubmitOrder(t *testing.T) {
Type: types.OrderTypeLimit,
Quantity: fixedpoint.MustNewFromString("0.00009"),
Price: fixedpoint.MustNewFromString("66000"),
Market: types.Market{
Symbol: expBtcSymbol,
LocalSymbol: expBtcSymbol,
PricePrecision: fixedpoint.MustNewFromString("2").Int(),
VolumePrecision: fixedpoint.MustNewFromString("6").Int(),
StepSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(6)),
TickSize: fixedpoint.NewFromFloat(1.0 / math.Pow10(2)),
},
TimeInForce: types.TimeInForceGTC,
Market: mkt,
TimeInForce: types.TimeInForceGTC,
}
)
@ -629,31 +629,14 @@ func TestExchange_SubmitOrder(t *testing.T) {
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
})
unfilledFile, err := os.ReadFile("bitgetapi/v2/testdata/get_unfilled_orders_request_limit_order.json")
assert.NoError(err)
transport.GET(openOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusOK, string(unfilledFile)), nil
})
acct, err := ex.SubmitOrder(context.Background(), reqLimitOrder)
assert.NoError(err)
expOrder.CreationTime = acct.CreationTime
expOrder.UpdateTime = acct.UpdateTime
assert.Equal(expOrder, acct)
})
t.Run("Limit Maker order", func(t *testing.T) {
emptyApiResp := v2.APIResponse{
Code: "00000",
Message: "",
Data: nil,
}
rawEmptyApiResp, err := json.Marshal(emptyApiResp)
assert.NoError(err)
transport := &httptesting.MockTransport{}
ex.client.HttpClient.Transport = transport
@ -680,29 +663,12 @@ func TestExchange_SubmitOrder(t *testing.T) {
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
})
transport.GET(openOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusOK, string(rawEmptyApiResp)), nil
})
transport.GET(historyOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusOK, string(rawEmptyApiResp)), nil
})
reqLimitOrder2 := reqLimitOrder
reqLimitOrder2.Type = types.OrderTypeLimitMaker
acct, err := ex.SubmitOrder(context.Background(), reqLimitOrder2)
assert.NoError(err)
expOrder2 := *expOrder
expOrder2.OriginalStatus = "FALLBACK_STATUS"
expOrder2.Status = types.OrderStatusNew
expOrder2.IsWorking = true
expOrder2.Type = types.OrderTypeLimitMaker
@ -751,18 +717,6 @@ func TestExchange_SubmitOrder(t *testing.T) {
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
})
// unfilled order
unfilledFile, err := os.ReadFile("bitgetapi/v2/testdata/get_unfilled_orders_request_market_buy_order.json")
assert.NoError(err)
transport.GET(openOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusOK, string(unfilledFile)), nil
})
reqMarketOrder := reqLimitOrder
reqMarketOrder.Side = types.SideTypeBuy
reqMarketOrder.Type = types.OrderTypeMarket
@ -771,8 +725,8 @@ func TestExchange_SubmitOrder(t *testing.T) {
expOrder2 := *expOrder
expOrder2.Side = types.SideTypeBuy
expOrder2.Type = types.OrderTypeMarket
expOrder2.Quantity = fixedpoint.Zero
expOrder2.Price = fixedpoint.Zero
expOrder2.CreationTime = acct.CreationTime
expOrder2.UpdateTime = acct.UpdateTime
assert.Equal(&expOrder2, acct)
})
@ -814,18 +768,6 @@ func TestExchange_SubmitOrder(t *testing.T) {
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
})
// unfilled order
unfilledFile, err := os.ReadFile("bitgetapi/v2/testdata/get_unfilled_orders_request_market_sell_order.json")
assert.NoError(err)
transport.GET(openOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusOK, string(unfilledFile)), nil
})
reqMarketOrder := reqLimitOrder
reqMarketOrder.Side = types.SideTypeSell
reqMarketOrder.Type = types.OrderTypeMarket
@ -834,7 +776,8 @@ func TestExchange_SubmitOrder(t *testing.T) {
expOrder2 := *expOrder
expOrder2.Side = types.SideTypeSell
expOrder2.Type = types.OrderTypeMarket
expOrder2.Price = fixedpoint.Zero
expOrder2.CreationTime = acct.CreationTime
expOrder2.UpdateTime = acct.UpdateTime
assert.Equal(&expOrder2, acct)
})
@ -858,127 +801,6 @@ func TestExchange_SubmitOrder(t *testing.T) {
_, err = ex.SubmitOrder(context.Background(), reqMarketOrder)
assert.ErrorContains(err, "Invalid IP")
})
t.Run("get order from history due to unfilled order not found", func(t *testing.T) {
transport := &httptesting.MockTransport{}
ex.client.HttpClient.Transport = transport
// get ticker to calculate btc amount
tickerFile, err := os.ReadFile("bitgetapi/v2/testdata/get_ticker_request.json")
assert.NoError(err)
transport.GET(tickerUrl, func(req *http.Request) (*http.Response, error) {
assert.Contains(req.URL.Query(), "symbol")
assert.Equal(req.URL.Query()["symbol"], []string{expBtcSymbol})
return httptesting.BuildResponseString(http.StatusOK, string(tickerFile)), nil
})
// place order
placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
assert.NoError(err)
transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
raw, err := io.ReadAll(req.Body)
assert.NoError(err)
reqq := &NewOrder{}
err = json.Unmarshal(raw, &reqq)
assert.NoError(err)
assert.Equal(&NewOrder{
ClientOid: expOrder.ClientOrderID,
Force: string(v2.OrderForceGTC),
OrderType: string(v2.OrderTypeMarket),
Price: "",
Side: string(v2.SideTypeBuy),
Size: reqLimitOrder.Market.FormatQuantity(fixedpoint.MustNewFromString("66554").Mul(fixedpoint.MustNewFromString("0.00009"))), // ticker: 66554, size: 0.00009
Symbol: expBtcSymbol,
}, reqq)
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
})
// unfilled order
transport.GET(openOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
apiResp := v2.APIResponse{Code: "00000"}
raw, err := json.Marshal(apiResp)
assert.NoError(err)
return httptesting.BuildResponseString(http.StatusOK, string(raw)), nil
})
// order history
historyOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/get_history_orders_request_market_buy.json")
assert.NoError(err)
transport.GET(historyOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusOK, string(historyOrderFile)), nil
})
reqMarketOrder := reqLimitOrder
reqMarketOrder.Side = types.SideTypeBuy
reqMarketOrder.Type = types.OrderTypeMarket
acct, err := ex.SubmitOrder(context.Background(), reqMarketOrder)
assert.NoError(err)
expOrder2 := *expOrder
expOrder2.Side = types.SideTypeBuy
expOrder2.Type = types.OrderTypeMarket
expOrder2.Status = types.OrderStatusFilled
expOrder2.ExecutedQuantity = fixedpoint.MustNewFromString("0.000089")
expOrder2.Quantity = fixedpoint.MustNewFromString("0.000089")
expOrder2.Price = fixedpoint.MustNewFromString("67360.87")
expOrder2.IsWorking = false
assert.Equal(&expOrder2, acct)
})
})
t.Run("error on query open orders", func(t *testing.T) {
transport := &httptesting.MockTransport{}
ex.client.HttpClient.Transport = transport
placeOrderFile, err := os.ReadFile("bitgetapi/v2/testdata/place_order_request.json")
assert.NoError(err)
transport.POST(placeOrderUrl, func(req *http.Request) (*http.Response, error) {
raw, err := io.ReadAll(req.Body)
assert.NoError(err)
reqq := &NewOrder{}
err = json.Unmarshal(raw, &reqq)
assert.NoError(err)
assert.Equal(&NewOrder{
ClientOid: expOrder.ClientOrderID,
Force: string(v2.OrderForceGTC),
OrderType: string(v2.OrderTypeLimit),
Price: "66000.00",
Side: string(v2.SideTypeBuy),
Size: "0.000090",
Symbol: expBtcSymbol,
}, reqq)
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
})
unfilledFile, err := os.ReadFile("bitgetapi/v2/testdata/request_error.json")
assert.NoError(err)
transport.GET(openOrderUrl, func(req *http.Request) (*http.Response, error) {
query := req.URL.Query()
assert.Len(query, 1)
assert.Contains(query, "orderId")
assert.Equal(query["orderId"], []string{strconv.FormatUint(expOrder.OrderID, 10)})
return httptesting.BuildResponseString(http.StatusBadRequest, string(unfilledFile)), nil
})
_, err = ex.SubmitOrder(context.Background(), reqLimitOrder)
assert.ErrorContains(err, "failed to query open order")
})
t.Run("unexpected client order id", func(t *testing.T) {
@ -1012,13 +834,19 @@ func TestExchange_SubmitOrder(t *testing.T) {
err = json.Unmarshal(apiResp.Data, &placeOrderResp)
assert.NoError(err)
// remove the client order id to test
placeOrderResp.ClientOrderId = ""
placeOrderResp.ClientOrderId = "unexpected client order id"
return httptesting.BuildResponseString(http.StatusOK, string(placeOrderFile)), nil
raw, err = json.Marshal(placeOrderResp)
assert.NoError(err)
apiResp.Data = raw
raw, err = json.Marshal(apiResp)
assert.NoError(err)
return httptesting.BuildResponseString(http.StatusOK, string(raw)), nil
})
_, err = ex.SubmitOrder(context.Background(), reqLimitOrder)
assert.ErrorContains(err, "failed to query open order")
assert.ErrorContains(err, "unexpected order id")
})
t.Run("failed to place order", func(t *testing.T) {

View File

@ -318,6 +318,7 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t
if err := orderRateLimiter.Wait(ctx); err != nil {
return nil, fmt.Errorf("place order rate limiter wait error: %w", err)
}
timeNow := time.Now()
res, err := req.Do(ctx)
if err != nil {
return nil, fmt.Errorf("failed to place order, order: %#v, err: %w", order, err)
@ -327,16 +328,22 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*t
return nil, fmt.Errorf("unexpected order id, resp: %#v, order: %#v", res, order)
}
ordersResp, err := e.client.NewGetOpenOrderRequest().OrderId(res.OrderId).Do(ctx)
intOrderId, err := strconv.ParseUint(res.OrderId, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to query order by client order id: %s, err: %w", res.OrderLinkId, err)
return nil, fmt.Errorf("failed to parse orderId: %s", res.OrderId)
}
if len(ordersResp.List) != 1 {
return nil, fmt.Errorf("unexpected order length, client order id: %s", res.OrderLinkId)
}
return toGlobalOrder(ordersResp.List[0])
return &types.Order{
SubmitOrder: order,
Exchange: types.ExchangeBybit,
OrderID: intOrderId,
UUID: res.OrderId,
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
CreationTime: types.Time(timeNow),
UpdateTime: types.Time(timeNow),
}, nil
}
func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) (errs error) {