diff --git a/pkg/exchange/bitget/convert.go b/pkg/exchange/bitget/convert.go index 460eb49428..2dda16d7a3 100644 --- a/pkg/exchange/bitget/convert.go +++ b/pkg/exchange/bitget/convert.go @@ -225,27 +225,6 @@ func unfilledOrderToGlobalOrder(order v2.UnfilledOrder) (*types.Order, error) { }, nil } -func fallbackPostOnlyOrder(order types.SubmitOrder, orderId string) (*types.Order, error) { - intOrderId, err := strconv.ParseUint(orderId, 10, 64) - if err != nil { - return nil, err - } - now := time.Now() - - return &types.Order{ - SubmitOrder: order, - Exchange: types.ExchangeBitget, - OrderID: intOrderId, - UUID: orderId, - Status: types.OrderStatusNew, - OriginalStatus: "FALLBACK_STATUS", - ExecutedQuantity: fixedpoint.Zero, - IsWorking: true, - CreationTime: types.Time(now), - UpdateTime: types.Time(now), - }, nil -} - func toGlobalOrder(order v2.OrderDetail) (*types.Order, error) { side, err := toGlobalSideType(order.Side) if err != nil { diff --git a/pkg/exchange/bitget/exchange.go b/pkg/exchange/bitget/exchange.go index e70b8a27f0..2b22733323 100644 --- a/pkg/exchange/bitget/exchange.go +++ b/pkg/exchange/bitget/exchange.go @@ -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) - } - - 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, err } - 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) { diff --git a/pkg/exchange/bitget/exchange_test.go b/pkg/exchange/bitget/exchange_test.go index 389df54f88..efacc667e6 100644 --- a/pkg/exchange/bitget/exchange_test.go +++ b/pkg/exchange/bitget/exchange_test.go @@ -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) { diff --git a/pkg/exchange/bybit/exchange.go b/pkg/exchange/bybit/exchange.go index ba220216fe..e45ab2d207 100644 --- a/pkg/exchange/bybit/exchange.go +++ b/pkg/exchange/bybit/exchange.go @@ -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) - } - - if len(ordersResp.List) != 1 { - return nil, fmt.Errorf("unexpected order length, client order id: %s", res.OrderLinkId) - } - - return toGlobalOrder(ordersResp.List[0]) + return nil, fmt.Errorf("failed to parse orderId: %s", res.OrderId) + } + + 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) {