mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
exchanges/gateio: Add WebsocketSubmitOrders wrapper func for spot (#1924)
* exchanges/gateio: Add WebsocketSubmitOrders wrapper func for spot * linter: fix * Update exchanges/gateio/gateio_wrapper.go Co-authored-by: Scott <gloriousCode@users.noreply.github.com> * glorious 👶 nits * Update exchanges/gateio/gateio_wrapper.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/gateio/gateio_wrapper.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/gateio/gateio_wrapper.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * Update exchanges/gateio/gateio_wrapper.go Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com> * gk: nits * glorious: nits --------- Co-authored-by: shazbert <ryan.oharareid@thrasher.io> Co-authored-by: Scott <gloriousCode@users.noreply.github.com> Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
This commit is contained in:
@@ -1957,3 +1957,8 @@ func (b *Base) GetCachedAccountInfo(ctx context.Context, assetType asset.Item) (
|
||||
func (*Base) WebsocketSubmitOrder(context.Context, *order.Submit) (*order.SubmitResponse, error) {
|
||||
return nil, common.ErrFunctionNotSupported
|
||||
}
|
||||
|
||||
// WebsocketSubmitOrders submits multiple orders (batch) via the websocket connection
|
||||
func (*Base) WebsocketSubmitOrders(context.Context, []*order.Submit) (responses []*order.SubmitResponse, err error) {
|
||||
return nil, common.ErrFunctionNotSupported
|
||||
}
|
||||
|
||||
@@ -2897,3 +2897,8 @@ func TestWebsocketSubmitOrder(t *testing.T) {
|
||||
_, err := (&Base{}).WebsocketSubmitOrder(t.Context(), nil)
|
||||
require.ErrorIs(t, err, common.ErrFunctionNotSupported)
|
||||
}
|
||||
|
||||
func TestWebsocketSubmitOrders(t *testing.T) {
|
||||
_, err := (&Base{}).WebsocketSubmitOrders(t.Context(), nil)
|
||||
require.ErrorIs(t, err, common.ErrFunctionNotSupported)
|
||||
}
|
||||
|
||||
@@ -166,6 +166,7 @@ var (
|
||||
errInvalidSettlementBase = errors.New("symbol base currency does not match asset settlement currency")
|
||||
errMissingAPIKey = errors.New("missing API key information")
|
||||
errInvalidTextValue = errors.New("invalid text value, requires prefix `t-`")
|
||||
errSingleAssetRequired = errors.New("single asset type required")
|
||||
)
|
||||
|
||||
// validTimesInForce holds a list of supported time-in-force values and corresponding string representations.
|
||||
|
||||
@@ -3052,6 +3052,53 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "batch of spot orders with error at end",
|
||||
// This is specifically testing the return responses of WebsocketSpotSubmitOrders
|
||||
// AverageDealPrice is not returned when using this endpoint so purchased and cost fields cannot be set.
|
||||
orders: [][]byte{
|
||||
[]byte(`{"account":"spot","status":"closed","side":"buy","amount":"9.98","id":"775453816782","create_time":"1736980695","update_time":"1736980695","text":"t-740","left":"0.047239","currency_pair":"ETH_USDT","type":"market","finish_as":"filled","price":"0","time_in_force":"fok","iceberg":"0","filled_total":"9.932761","fill_price":"9.932761","create_time_ms":1736980695949,"update_time_ms":1736980695949,"succeeded":true}`),
|
||||
[]byte(`{"account":"spot","status":"closed","side":"buy","amount":"0.00289718","id":"775453816824","create_time":"1736980695","update_time":"1736980695","text":"t-741","left":"0.00000000962","currency_pair":"LIKE_ETH","type":"market","finish_as":"filled","price":"0","time_in_force":"fok","iceberg":"0","filled_total":"0.00289717038","fill_price":"0.00289717038","create_time_ms":1736980695956,"update_time_ms":1736980695956,"succeeded":true}`),
|
||||
[]byte(`{"text":"t-742","label":"BALANCE_NOT_ENOUGH","message":"Not enough balance"}`),
|
||||
},
|
||||
expected: []*order.SubmitResponse{
|
||||
{
|
||||
Exchange: e.Name,
|
||||
OrderID: "775453816782",
|
||||
AssetType: asset.Spot,
|
||||
Pair: currency.NewPair(currency.ETH, currency.USDT).Format(currency.PairFormat{Uppercase: true, Delimiter: "_"}),
|
||||
ClientOrderID: "t-740",
|
||||
Date: time.UnixMilli(1736980695949),
|
||||
LastUpdated: time.UnixMilli(1736980695949),
|
||||
Amount: 9.98,
|
||||
RemainingAmount: 0.047239,
|
||||
Type: order.Market,
|
||||
Side: order.Buy,
|
||||
Status: order.Filled,
|
||||
TimeInForce: order.FillOrKill,
|
||||
},
|
||||
{
|
||||
Exchange: e.Name,
|
||||
OrderID: "775453816824",
|
||||
AssetType: asset.Spot,
|
||||
Pair: currency.NewPair(currency.LIKE, currency.ETH).Format(currency.PairFormat{Uppercase: true, Delimiter: "_"}),
|
||||
ClientOrderID: "t-741",
|
||||
Date: time.UnixMilli(1736980695956),
|
||||
LastUpdated: time.UnixMilli(1736980695956),
|
||||
RemainingAmount: 0.00000000962,
|
||||
Amount: 0.00289718,
|
||||
Type: order.Market,
|
||||
Side: order.Buy,
|
||||
Status: order.Filled,
|
||||
TimeInForce: order.FillOrKill,
|
||||
},
|
||||
{
|
||||
Exchange: e.Name,
|
||||
ClientOrderID: "t-742",
|
||||
SubmissionError: order.ErrUnableToPlaceOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -3069,6 +3116,12 @@ func TestDeriveSpotWebsocketOrderResponses(t *testing.T) {
|
||||
|
||||
require.Len(t, got, len(tc.expected))
|
||||
for i := range got {
|
||||
if tc.expected[i].SubmissionError != nil {
|
||||
assert.ErrorIs(t, got[i].SubmissionError, tc.expected[i].SubmissionError)
|
||||
assert.Equal(t, tc.expected[i].Exchange, got[i].Exchange)
|
||||
assert.Equal(t, tc.expected[i].ClientOrderID, got[i].ClientOrderID)
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, tc.expected[i], got[i])
|
||||
}
|
||||
})
|
||||
@@ -3575,3 +3628,45 @@ func TestGetIntervalString(t *testing.T) {
|
||||
_, err = getIntervalString(kline.FiveDay)
|
||||
assert.ErrorIs(t, err, kline.ErrUnsupportedInterval, "Any other random interval should also be invalid")
|
||||
}
|
||||
|
||||
func TestWebsocketSubmitOrders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := e.WebsocketSubmitOrders(t.Context(), nil)
|
||||
require.ErrorIs(t, err, asset.ErrNotSupported)
|
||||
|
||||
sub := &order.Submit{
|
||||
Exchange: e.Name,
|
||||
AssetType: asset.Spot,
|
||||
Side: order.Buy,
|
||||
Type: order.Market,
|
||||
QuoteAmount: 10,
|
||||
}
|
||||
_, err = e.WebsocketSubmitOrders(t.Context(), []*order.Submit{sub})
|
||||
require.ErrorIs(t, err, order.ErrPairIsEmpty)
|
||||
|
||||
sub.Pair = currency.NewBTCUSD()
|
||||
cpy := *sub
|
||||
cpy.AssetType = asset.Futures
|
||||
_, err = e.WebsocketSubmitOrders(t.Context(), []*order.Submit{sub, &cpy})
|
||||
require.ErrorIs(t, err, errSingleAssetRequired)
|
||||
|
||||
cpy.AssetType = asset.Spread
|
||||
sub.AssetType = asset.Spread
|
||||
_, err = e.WebsocketSubmitOrders(t.Context(), []*order.Submit{sub, &cpy})
|
||||
require.ErrorIs(t, err, asset.ErrNotSupported)
|
||||
|
||||
sub.AssetType = asset.USDTMarginedFutures
|
||||
cpy.AssetType = asset.USDTMarginedFutures
|
||||
_, err = e.WebsocketSubmitOrders(t.Context(), []*order.Submit{sub, &cpy})
|
||||
require.ErrorIs(t, err, common.ErrNotYetImplemented)
|
||||
|
||||
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
|
||||
|
||||
e := newExchangeWithWebsocket(t, asset.Spot) //nolint:govet // Intentional shadow
|
||||
|
||||
sub.AssetType = asset.Spot
|
||||
cpy.AssetType = asset.Spot
|
||||
_, err = e.WebsocketSubmitOrders(request.WithVerbose(t.Context()), []*order.Submit{sub, &cpy})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ func (e *Exchange) WebsocketFuturesSubmitOrder(ctx context.Context, a asset.Item
|
||||
if len(resps) != 1 {
|
||||
return nil, common.ErrInvalidResponse
|
||||
}
|
||||
return &resps[0], err
|
||||
return resps[0], err
|
||||
}
|
||||
|
||||
// WebsocketFuturesSubmitOrders submits orders via the websocket connection. All orders must be for the same asset.
|
||||
func (e *Exchange) WebsocketFuturesSubmitOrders(ctx context.Context, a asset.Item, orders ...*ContractOrderCreateParams) ([]WebsocketFuturesOrderResponse, error) {
|
||||
func (e *Exchange) WebsocketFuturesSubmitOrders(ctx context.Context, a asset.Item, orders ...*ContractOrderCreateParams) ([]*WebsocketFuturesOrderResponse, error) {
|
||||
if len(orders) == 0 {
|
||||
return nil, errOrdersEmpty
|
||||
}
|
||||
@@ -64,12 +64,12 @@ func (e *Exchange) WebsocketFuturesSubmitOrders(ctx context.Context, a asset.Ite
|
||||
}
|
||||
|
||||
if len(orders) == 1 {
|
||||
var singleResponse WebsocketFuturesOrderResponse
|
||||
var singleResponse *WebsocketFuturesOrderResponse
|
||||
err := e.SendWebsocketRequest(ctx, perpetualSubmitOrderEPL, "futures.order_place", a, orders[0], &singleResponse, 2)
|
||||
return []WebsocketFuturesOrderResponse{singleResponse}, err
|
||||
return []*WebsocketFuturesOrderResponse{singleResponse}, err
|
||||
}
|
||||
|
||||
var resp []WebsocketFuturesOrderResponse
|
||||
var resp []*WebsocketFuturesOrderResponse
|
||||
return resp, e.SendWebsocketRequest(ctx, perpetualSubmitBatchOrdersEPL, "futures.order_batch_place", a, orders, &resp, 2)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,12 +33,12 @@ func (e *Exchange) WebsocketSpotSubmitOrder(ctx context.Context, order *CreateOr
|
||||
if len(resps) != 1 {
|
||||
return nil, common.ErrInvalidResponse
|
||||
}
|
||||
return &resps[0], nil
|
||||
return resps[0], nil
|
||||
}
|
||||
|
||||
// WebsocketSpotSubmitOrders submits orders via the websocket connection. You can
|
||||
// send multiple orders in a single request. But only for one asset route.
|
||||
func (e *Exchange) WebsocketSpotSubmitOrders(ctx context.Context, orders ...*CreateOrderRequest) ([]WebsocketOrderResponse, error) {
|
||||
func (e *Exchange) WebsocketSpotSubmitOrders(ctx context.Context, orders ...*CreateOrderRequest) ([]*WebsocketOrderResponse, error) {
|
||||
if len(orders) == 0 {
|
||||
return nil, errOrdersEmpty
|
||||
}
|
||||
@@ -63,10 +63,10 @@ func (e *Exchange) WebsocketSpotSubmitOrders(ctx context.Context, orders ...*Cre
|
||||
}
|
||||
|
||||
if len(orders) == 1 {
|
||||
var singleResponse WebsocketOrderResponse
|
||||
return []WebsocketOrderResponse{singleResponse}, e.SendWebsocketRequest(ctx, spotPlaceOrderEPL, "spot.order_place", asset.Spot, orders[0], &singleResponse, 2)
|
||||
var singleResponse *WebsocketOrderResponse
|
||||
return []*WebsocketOrderResponse{singleResponse}, e.SendWebsocketRequest(ctx, spotPlaceOrderEPL, "spot.order_place", asset.Spot, orders[0], &singleResponse, 2)
|
||||
}
|
||||
var resp []WebsocketOrderResponse
|
||||
var resp []*WebsocketOrderResponse
|
||||
return resp, e.SendWebsocketRequest(ctx, spotBatchOrdersEPL, "spot.order_place", asset.Spot, orders, &resp, 2)
|
||||
}
|
||||
|
||||
|
||||
@@ -231,6 +231,13 @@ func newExchangeWithWebsocket(t *testing.T, a asset.Item) *Exchange {
|
||||
require.NoError(t, e.CurrencyPairs.SetAssetEnabled(a, false))
|
||||
}
|
||||
|
||||
// Disable all other asset types to ensure only the specified asset type is used for websocket tests.
|
||||
for _, enabled := range e.GetAssetTypes(true) {
|
||||
if enabled != a {
|
||||
require.NoError(t, e.CurrencyPairs.SetAssetEnabled(enabled, false))
|
||||
}
|
||||
}
|
||||
|
||||
require.NoError(t, e.Websocket.Connect())
|
||||
return e
|
||||
}
|
||||
|
||||
@@ -86,6 +86,8 @@ type WebsocketOrderResponse struct {
|
||||
SelfTradePreventionID int `json:"stp_id"`
|
||||
SelfTradePreventionAction string `json:"stp_act"`
|
||||
AverageDealPrice types.Number `json:"avg_deal_price"`
|
||||
Label string `json:"label"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// WebsocketFuturesOrderResponse defines a websocket futures order response
|
||||
|
||||
@@ -2344,19 +2344,11 @@ func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*
|
||||
}
|
||||
return e.deriveSpotWebsocketOrderResponse(resp)
|
||||
case asset.CoinMarginedFutures, asset.USDTMarginedFutures:
|
||||
amountWithDirection, err := getFutureOrderSize(s)
|
||||
req, err := getFuturesOrderRequest(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := e.WebsocketFuturesSubmitOrder(ctx, s.AssetType, &ContractOrderCreateParams{
|
||||
Contract: s.Pair,
|
||||
Size: amountWithDirection,
|
||||
Price: strconv.FormatFloat(s.Price, 'f', -1, 64),
|
||||
ReduceOnly: s.ReduceOnly,
|
||||
TimeInForce: timeInForceString(s.TimeInForce),
|
||||
Text: s.ClientOrderID,
|
||||
})
|
||||
resp, err := e.WebsocketFuturesSubmitOrder(ctx, s.AssetType, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2366,6 +2358,22 @@ func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*
|
||||
}
|
||||
}
|
||||
|
||||
func getFuturesOrderRequest(s *order.Submit) (*ContractOrderCreateParams, error) {
|
||||
amountWithDirection, err := getFutureOrderSize(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ContractOrderCreateParams{
|
||||
Contract: s.Pair,
|
||||
Size: amountWithDirection,
|
||||
Price: strconv.FormatFloat(s.Price, 'f', -1, 64),
|
||||
ReduceOnly: s.ReduceOnly,
|
||||
TimeInForce: timeInForceString(s.TimeInForce),
|
||||
Text: s.ClientOrderID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// timeInForceString returns the most relevant time-in-force exchange string for a TimeInForce
|
||||
// Any TIF value that is combined with POC, IOC or FOK will just return that
|
||||
// Otherwise the lowercase representation is returned
|
||||
@@ -2398,8 +2406,17 @@ func (e *Exchange) deriveSpotWebsocketOrderResponses(responses []*WebsocketOrder
|
||||
return nil, common.ErrNoResponse
|
||||
}
|
||||
|
||||
out := make([]*order.SubmitResponse, 0, len(responses))
|
||||
for _, resp := range responses {
|
||||
out := make([]*order.SubmitResponse, len(responses))
|
||||
for i, resp := range responses {
|
||||
if resp.Label != "" { // batch only, denotes error type in string format
|
||||
out[i] = &order.SubmitResponse{
|
||||
Exchange: e.Name,
|
||||
ClientOrderID: resp.Text,
|
||||
SubmissionError: fmt.Errorf("%w reason label:%q message:%q", order.ErrUnableToPlaceOrder, resp.Label, resp.Message),
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
side, err := order.StringToOrderSide(resp.Side)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2431,7 +2448,7 @@ func (e *Exchange) deriveSpotWebsocketOrderResponses(responses []*WebsocketOrder
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, &order.SubmitResponse{
|
||||
out[i] = &order.SubmitResponse{
|
||||
Exchange: e.Name,
|
||||
OrderID: resp.ID,
|
||||
AssetType: resp.Account,
|
||||
@@ -2442,16 +2459,16 @@ func (e *Exchange) deriveSpotWebsocketOrderResponses(responses []*WebsocketOrder
|
||||
RemainingAmount: resp.Left.Float64(),
|
||||
Amount: resp.Amount.Float64(),
|
||||
Price: resp.Price.Float64(),
|
||||
AverageExecutedPrice: resp.AverageDealPrice.Float64(),
|
||||
Type: oType,
|
||||
Side: side,
|
||||
Status: status,
|
||||
Fee: resp.Fee.Float64(),
|
||||
FeeAsset: resp.FeeCurrency,
|
||||
TimeInForce: tif,
|
||||
Cost: cost,
|
||||
Purchased: purchased,
|
||||
Fee: resp.Fee.Float64(),
|
||||
FeeAsset: resp.FeeCurrency,
|
||||
})
|
||||
Status: status,
|
||||
AverageExecutedPrice: resp.AverageDealPrice.Float64(),
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -2573,3 +2590,44 @@ func getSettlementCurrency(p currency.Pair, a asset.Item) (currency.Code, error)
|
||||
}
|
||||
return currency.EMPTYCODE, fmt.Errorf("%w: %s", asset.ErrNotSupported, a)
|
||||
}
|
||||
|
||||
// WebsocketSubmitOrders submits orders to the exchange through the websocket
|
||||
func (e *Exchange) WebsocketSubmitOrders(ctx context.Context, orders []*order.Submit) ([]*order.SubmitResponse, error) {
|
||||
var a asset.Item
|
||||
for x := range orders {
|
||||
if err := orders[x].Validate(e.GetTradingRequirements()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if a == asset.Empty {
|
||||
a = orders[x].AssetType
|
||||
continue
|
||||
}
|
||||
|
||||
if a != orders[x].AssetType {
|
||||
return nil, fmt.Errorf("%w; Passed %q and %q", errSingleAssetRequired, a, orders[x].AssetType)
|
||||
}
|
||||
}
|
||||
|
||||
if !e.CurrencyPairs.IsAssetSupported(a) {
|
||||
return nil, fmt.Errorf("%w: %q", asset.ErrNotSupported, a)
|
||||
}
|
||||
|
||||
switch a {
|
||||
case asset.Spot:
|
||||
reqs := make([]*CreateOrderRequest, len(orders))
|
||||
for x := range orders {
|
||||
var err error
|
||||
if reqs[x], err = e.getSpotOrderRequest(orders[x]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resp, err := e.WebsocketSpotSubmitOrders(ctx, reqs...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.deriveSpotWebsocketOrderResponses(resp)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w for %s", common.ErrNotYetImplemented, a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ type OrderManagement interface {
|
||||
GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error)
|
||||
GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error)
|
||||
WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error)
|
||||
WebsocketSubmitOrders(ctx context.Context, orders []*order.Submit) (responses []*order.SubmitResponse, err error)
|
||||
}
|
||||
|
||||
// CurrencyStateManagement defines functionality for currency state management
|
||||
|
||||
@@ -101,12 +101,11 @@ type Submit struct {
|
||||
|
||||
// SubmitResponse is what is returned after submitting an order to an exchange
|
||||
type SubmitResponse struct {
|
||||
Exchange string
|
||||
Type Type
|
||||
Side Side
|
||||
Pair currency.Pair
|
||||
AssetType asset.Item
|
||||
|
||||
Exchange string
|
||||
Type Type
|
||||
Side Side
|
||||
Pair currency.Pair
|
||||
AssetType asset.Item
|
||||
TimeInForce TimeInForce
|
||||
ReduceOnly bool
|
||||
Leverage float64
|
||||
@@ -118,21 +117,19 @@ type SubmitResponse struct {
|
||||
ClientID string
|
||||
ClientOrderID string
|
||||
AverageExecutedPrice float64
|
||||
|
||||
LastUpdated time.Time
|
||||
Date time.Time
|
||||
Status Status
|
||||
OrderID string
|
||||
Trades []TradeHistory
|
||||
Fee float64
|
||||
FeeAsset currency.Code
|
||||
|
||||
Cost float64
|
||||
Purchased float64 // Buy in base currency, Sell in quote
|
||||
|
||||
BorrowSize float64
|
||||
LoanApplyID string
|
||||
MarginType margin.Type
|
||||
LastUpdated time.Time
|
||||
Date time.Time
|
||||
Status Status
|
||||
OrderID string
|
||||
Trades []TradeHistory
|
||||
Fee float64
|
||||
FeeAsset currency.Code
|
||||
Cost float64
|
||||
Purchased float64 // Buy in base currency, Sell in quote
|
||||
BorrowSize float64
|
||||
LoanApplyID string
|
||||
MarginType margin.Type
|
||||
SubmissionError error
|
||||
}
|
||||
|
||||
// TrackingMode defines how the stop price follows the market price.
|
||||
|
||||
Reference in New Issue
Block a user