gateio: Fix contract market order validation and refactor related code (#1950)

* :gateio: fix market order requirement for contracts + clean things

* cranktakular: nits

* fix misc check

* Update exchanges/gateio/gateio_types.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* mv func to gateio.go

* cranktakular: nit

* glorious: nits

* gk: nits

* revert change errInvalidClientOrderIDTextPrefix

* gk: nits

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
Ryan O'Hara-Reid
2025-09-05 15:58:46 +10:00
committed by GitHub
parent a3c042cb93
commit 454de17bf4
6 changed files with 244 additions and 158 deletions

View File

@@ -150,10 +150,9 @@ var (
errInvalidRepayMode = errors.New("invalid repay mode specified, must be 'all' or 'partial'")
errMissingPreviewID = errors.New("missing required parameter: preview_id")
errChangeHasToBePositive = errors.New("change has to be positive")
errInvalidLeverageValue = errors.New("invalid leverage value")
errInvalidLeverage = errors.New("invalid leverage value")
errInvalidRiskLimit = errors.New("new position risk limit")
errInvalidCountTotalValue = errors.New("invalid \"count_total\" value, supported \"count_total\" values are 0 and 1")
errInvalidAutoSizeValue = errors.New("invalid \"auto_size\" value, only \"close_long\" and \"close_short\" are supported")
errInvalidAutoSize = errors.New("invalid autoSize")
errTooManyOrderRequest = errors.New("too many order creation request")
errInvalidTimeout = errors.New("invalid timeout, should be in seconds At least 5 seconds, 0 means cancel the countdown")
errNoTickerData = errors.New("no ticker data available")
@@ -165,7 +164,7 @@ var (
errInvalidSettlementQuote = errors.New("symbol quote currency does not match asset settlement currency")
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-`")
errInvalidTextPrefix = errors.New("invalid text value, requires prefix `t-`")
errSingleAssetRequired = errors.New("single asset type required")
)
@@ -2128,7 +2127,7 @@ func (e *Exchange) UpdateFuturesPositionLeverage(ctx context.Context, settle cur
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
if leverage < 0 {
return nil, errInvalidLeverageValue
return nil, fmt.Errorf("%w: %f", errInvalidLeverage, leverage)
}
params := url.Values{}
params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64))
@@ -2204,7 +2203,7 @@ func (e *Exchange) UpdatePositionLeverageInDualMode(ctx context.Context, settle
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
if leverage < 0 {
return nil, errInvalidLeverageValue
return nil, fmt.Errorf("%w: %f", errInvalidLeverage, leverage)
}
params := url.Values{}
params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64))
@@ -2240,38 +2239,16 @@ func (e *Exchange) UpdatePositionRiskLimitInDualMode(ctx context.Context, settle
// In single position mode, to close a position, you need to set size to 0 and close to true
// In dual position mode, to close one side position, you need to set auto_size side, reduce_only to true and size to 0
func (e *Exchange) PlaceFuturesOrder(ctx context.Context, arg *ContractOrderCreateParams) (*Order, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Contract.IsEmpty() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
if arg.Size == 0 {
return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", order.ErrSideIsInvalid)
}
if _, err := timeInForceFromString(arg.TimeInForce); err != nil {
if err := arg.validate(true); err != nil {
return nil, err
}
if arg.Price == "" {
return nil, errInvalidPrice
}
if arg.Price == "0" && arg.TimeInForce != iocTIF && arg.TimeInForce != fokTIF {
return nil, fmt.Errorf("%w: %q; only 'IOC' and 'FOK' allowed for market order", order.ErrUnsupportedTimeInForce, arg.TimeInForce)
}
if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") {
return nil, errInvalidAutoSizeValue
}
if arg.Settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
var response *Order
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSubmitOrderEPL, http.MethodPost, futuresPath+arg.Settle.Item.Lower+ordersPath, nil, &arg, &response)
}
// GetFuturesOrders retrieves list of futures orders
// Zero-filled order cannot be retrieved 10 minutes after order cancellation
func (e *Exchange) GetFuturesOrders(ctx context.Context, contract currency.Pair, status, lastID string, settle currency.Code, limit, offset uint64, countTotal int64) ([]Order, error) {
func (e *Exchange) GetFuturesOrders(ctx context.Context, contract currency.Pair, status, lastID string, settle currency.Code, limit, offset uint64, countTotal bool) ([]Order, error) {
if settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
@@ -2292,10 +2269,8 @@ func (e *Exchange) GetFuturesOrders(ctx context.Context, contract currency.Pair,
if lastID != "" {
params.Set("last_id", lastID)
}
if countTotal == 1 && status != statusOpen {
params.Set("count_total", strconv.FormatInt(countTotal, 10))
} else if countTotal != 0 && countTotal != 1 {
return nil, errInvalidCountTotalValue
if countTotal && status != statusOpen {
params.Set("count_total", "1")
}
var response []Order
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualGetOrdersEPL, http.MethodGet, futuresPath+settle.Item.Lower+ordersPath, params, nil, &response)
@@ -2335,26 +2310,11 @@ func (e *Exchange) PlaceBatchFuturesOrders(ctx context.Context, settle currency.
return nil, errTooManyOrderRequest
}
for x := range args {
if args[x].Size == 0 {
return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", order.ErrSideIsInvalid)
}
if _, err := timeInForceFromString(args[x].TimeInForce); err != nil {
if err := args[x].validate(true); err != nil {
return nil, err
}
if args[x].Price == "" {
return nil, errInvalidPrice
}
if args[x].Price == "0" && args[x].TimeInForce != iocTIF && args[x].TimeInForce != fokTIF {
return nil, fmt.Errorf("%w: %q; only 'ioc' and 'fok' allowed for market order", order.ErrUnsupportedTimeInForce, args[x].TimeInForce)
}
if args[x].Text != "" && !strings.HasPrefix(args[x].Text, "t-") {
return nil, errInvalidTextValue
}
if args[x].AutoSize != "" && (args[x].AutoSize == "close_long" || args[x].AutoSize == "close_short") {
return nil, errInvalidAutoSizeValue
}
if !args[x].Settle.Equal(currency.BTC) && !args[x].Settle.Equal(currency.USDT) {
return nil, errEmptyOrInvalidSettlementCurrency
if !args[x].Settle.Equal(settle) {
return nil, fmt.Errorf("%w: %q expected %q", errEmptyOrInvalidSettlementCurrency, args[x].Settle, settle)
}
}
var response []Order
@@ -2787,7 +2747,7 @@ func (e *Exchange) UpdateDeliveryPositionLeverage(ctx context.Context, settle cu
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
if leverage < 0 {
return nil, errInvalidLeverageValue
return nil, errInvalidLeverage
}
params := url.Values{}
params.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64))
@@ -2813,34 +2773,16 @@ func (e *Exchange) UpdateDeliveryPositionRiskLimit(ctx context.Context, settle c
// PlaceDeliveryOrder create a futures order
// Zero-filled order cannot be retrieved 10 minutes after order cancellation
func (e *Exchange) PlaceDeliveryOrder(ctx context.Context, arg *ContractOrderCreateParams) (*Order, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Contract.IsEmpty() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
if arg.Size == 0 {
return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", order.ErrSideIsInvalid)
}
if _, err := timeInForceFromString(arg.TimeInForce); err != nil {
if err := arg.validate(true); err != nil {
return nil, err
}
if arg.Price == "" {
return nil, errInvalidPrice
}
if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") {
return nil, errInvalidAutoSizeValue
}
if arg.Settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
var response *Order
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, deliverySubmitOrderEPL, http.MethodPost, deliveryPath+arg.Settle.Item.Lower+ordersPath, nil, &arg, &response)
}
// GetDeliveryOrders list futures orders
// Zero-filled order cannot be retrieved 10 minutes after order cancellation
func (e *Exchange) GetDeliveryOrders(ctx context.Context, contract currency.Pair, status string, settle currency.Code, lastID string, limit, offset uint64, countTotal int64) ([]Order, error) {
func (e *Exchange) GetDeliveryOrders(ctx context.Context, contract currency.Pair, status string, settle currency.Code, lastID string, limit, offset uint64, countTotal bool) ([]Order, error) {
if settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
@@ -2861,10 +2803,8 @@ func (e *Exchange) GetDeliveryOrders(ctx context.Context, contract currency.Pair
if lastID != "" {
params.Set("last_id", lastID)
}
if countTotal == 1 && status != statusOpen {
params.Set("count_total", strconv.FormatInt(countTotal, 10))
} else if countTotal != 0 && countTotal != 1 {
return nil, errInvalidCountTotalValue
if countTotal && status != statusOpen {
params.Set("count_total", "1")
}
var response []Order
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, deliveryGetOrdersEPL, http.MethodGet, deliveryPath+settle.Item.Lower+ordersPath, params, nil, &response)
@@ -3646,3 +3586,42 @@ func (e *Exchange) GetUserTransactionRateLimitInfo(ctx context.Context) ([]UserT
var resp []UserTransactionRateLimitInfo
return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotAccountsEPL, http.MethodGet, "account/rate_limit", nil, nil, &resp)
}
// validate validates the ContractOrderCreateParams
func (c *ContractOrderCreateParams) validate(isRest bool) error {
if err := common.NilGuard(c); err != nil {
return err
}
if c.Contract.IsEmpty() {
return currency.ErrCurrencyPairEmpty
}
if c.Size == 0 && c.AutoSize == "" {
return errInvalidOrderSize
}
if c.TimeInForce != "" {
if _, err := timeInForceFromString(c.TimeInForce); err != nil {
return err
}
}
if c.Price == 0 && c.TimeInForce != iocTIF && c.TimeInForce != fokTIF {
return fmt.Errorf("%w: %q; only 'ioc' and 'fok' allowed for market order", order.ErrUnsupportedTimeInForce, c.TimeInForce)
}
if c.Text != "" && !strings.HasPrefix(c.Text, "t-") {
return errInvalidTextPrefix
}
if c.AutoSize != "" {
if c.AutoSize != "close_long" && c.AutoSize != "close_short" {
return fmt.Errorf("%w: %q", errInvalidAutoSize, c.AutoSize)
}
if c.Size != 0 {
return fmt.Errorf("%w: size needs to be zero when auto size is set", errInvalidOrderSize)
}
}
// REST requests require a settlement currency, but it can be anything
// Websocket requests may have an empty settlement currency, or it must be BTC or USDT
if (isRest && c.Settle.IsEmpty()) ||
(!isRest && !c.Settle.IsEmpty() && !c.Settle.Equal(currency.BTC) && !c.Settle.Equal(currency.USDT)) {
return errEmptyOrInvalidSettlementCurrency
}
return nil
}

View File

@@ -1042,7 +1042,7 @@ func TestPlaceDeliveryOrder(t *testing.T) {
Contract: getPair(t, asset.DeliveryFutures),
Size: 6024,
Iceberg: 0,
Price: "3765",
Price: 3765,
Text: "t-my-custom-id",
Settle: currency.USDT,
TimeInForce: gtcTIF,
@@ -1053,7 +1053,7 @@ func TestPlaceDeliveryOrder(t *testing.T) {
func TestGetDeliveryOrders(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
_, err := e.GetDeliveryOrders(t.Context(), getPair(t, asset.DeliveryFutures), statusOpen, currency.USDT, "", 0, 0, 1)
_, err := e.GetDeliveryOrders(t.Context(), getPair(t, asset.DeliveryFutures), statusOpen, currency.USDT, "", 0, 0, true)
assert.NoError(t, err, "GetDeliveryOrders should not error")
}
@@ -1204,7 +1204,7 @@ func TestPlaceFuturesOrder(t *testing.T) {
Contract: getPair(t, asset.CoinMarginedFutures),
Size: 6024,
Iceberg: 0,
Price: "3765",
Price: 3765,
TimeInForce: "gtc",
Text: "t-my-custom-id",
Settle: currency.BTC,
@@ -1215,7 +1215,7 @@ func TestPlaceFuturesOrder(t *testing.T) {
func TestGetFuturesOrders(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
_, err := e.GetFuturesOrders(t.Context(), currency.NewBTCUSD(), statusOpen, "", currency.BTC, 0, 0, 1)
_, err := e.GetFuturesOrders(t.Context(), currency.NewBTCUSD(), statusOpen, "", currency.BTC, 0, 0, true)
assert.NoError(t, err, "GetFuturesOrders should not error")
}
@@ -1248,7 +1248,7 @@ func TestPlaceBatchFuturesOrders(t *testing.T) {
Contract: getPair(t, asset.CoinMarginedFutures),
Size: 6024,
Iceberg: 0,
Price: "3765",
Price: 3765,
TimeInForce: "gtc",
Text: "t-my-custom-id",
Settle: currency.BTC,
@@ -1257,7 +1257,7 @@ func TestPlaceBatchFuturesOrders(t *testing.T) {
Contract: getPair(t, asset.CoinMarginedFutures),
Size: 232,
Iceberg: 0,
Price: "376225",
Price: 376225,
TimeInForce: "gtc",
Text: "t-my-custom-id",
Settle: currency.BTC,
@@ -2641,6 +2641,13 @@ func TestGetClientOrderIDFromText(t *testing.T) {
assert.Equal(t, "t-123", getClientOrderIDFromText("t-123"), "should return t-123")
}
func TestFormatClientOrderID(t *testing.T) {
t.Parallel()
assert.Empty(t, formatClientOrderID(""), "should not return anything")
assert.Equal(t, "t-123", formatClientOrderID("t-123"), "should return t-123")
assert.Equal(t, "t-456", formatClientOrderID("456"), "should return t-456")
}
func TestGetSideAndAmountFromSize(t *testing.T) {
t.Parallel()
side, amount, remaining := getSideAndAmountFromSize(1, 1)
@@ -3288,11 +3295,34 @@ func TestGetTypeFromTimeInForce(t *testing.T) {
assert.Equal(t, order.Market, typeResp, "should be market order")
}
func TestTimeInForceString(t *testing.T) {
func TestToExchangeTIF(t *testing.T) {
t.Parallel()
assert.Empty(t, timeInForceString(order.UnknownTIF))
for _, valid := range validTimesInForce {
assert.Equal(t, valid.String, timeInForceString(valid.TimeInForce))
for _, tc := range []struct {
tif order.TimeInForce
price float64
expected string
err error
}{
{price: 0, expected: iocTIF}, // market orders default to IOC
{price: 0, tif: order.FillOrKill, expected: fokTIF},
{price: 420, expected: gtcTIF}, // limit orders default to GTC
{price: 420, tif: order.GoodTillCancel, expected: gtcTIF},
{price: 420, tif: order.ImmediateOrCancel, expected: iocTIF},
{price: 420, tif: order.PostOnly, expected: pocTIF},
{price: 420, tif: order.FillOrKill, expected: fokTIF},
{tif: order.GoodTillTime, err: order.ErrUnsupportedTimeInForce},
} {
t.Run(fmt.Sprintf("TIF:%q Price:'%v'", tc.tif, tc.price), func(t *testing.T) {
t.Parallel()
got, err := toExchangeTIF(tc.tif, tc.price)
if tc.err != nil {
require.ErrorIs(t, err, tc.err)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.expected, got)
})
}
}
@@ -3558,3 +3588,85 @@ func TestWebsocketSubmitOrders(t *testing.T) {
_, err = e.WebsocketSubmitOrders(request.WithVerbose(t.Context()), []*order.Submit{sub, &cpy})
require.NoError(t, err)
}
func TestValidateContractOrderCreateParams(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
params *ContractOrderCreateParams
isRest bool
err error
}{
{
err: common.ErrNilPointer,
},
{
params: &ContractOrderCreateParams{}, err: currency.ErrCurrencyPairEmpty,
},
{
params: &ContractOrderCreateParams{Contract: BTCUSDT},
err: errInvalidOrderSize,
},
{
params: &ContractOrderCreateParams{Contract: BTCUSDT, Size: 1, TimeInForce: "bad"},
err: order.ErrUnsupportedTimeInForce,
},
{
params: &ContractOrderCreateParams{Contract: BTCUSDT, Size: 1, TimeInForce: pocTIF},
err: order.ErrUnsupportedTimeInForce,
},
{
params: &ContractOrderCreateParams{Contract: BTCUSDT, Size: 1, TimeInForce: iocTIF, Text: "test"},
err: errInvalidTextPrefix,
},
{
params: &ContractOrderCreateParams{
Contract: BTCUSDT, Size: 1, TimeInForce: iocTIF, Text: "t-test", AutoSize: "silly_billy",
},
err: errInvalidAutoSize,
},
{
params: &ContractOrderCreateParams{
Contract: BTCUSDT, Size: 1, TimeInForce: iocTIF, Text: "t-test", AutoSize: "close_long",
},
err: errInvalidOrderSize,
},
{
params: &ContractOrderCreateParams{
Contract: BTCUSDT, TimeInForce: iocTIF, Text: "t-test", AutoSize: "close_long",
},
isRest: true,
err: errEmptyOrInvalidSettlementCurrency,
},
{
params: &ContractOrderCreateParams{
Contract: BTCUSDT, TimeInForce: iocTIF, Text: "t-test", AutoSize: "close_long", Settle: currency.NewCode("Silly"),
},
err: errEmptyOrInvalidSettlementCurrency,
},
{
params: &ContractOrderCreateParams{
Contract: BTCUSDT, TimeInForce: iocTIF, Text: "t-test", AutoSize: "close_long", Settle: currency.USDT,
},
},
} {
assert.ErrorIs(t, tc.params.validate(tc.isRest), tc.err)
}
}
func TestMarshalJSONNumber(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
number number
expected string
}{
{number: 0, expected: `"0"`},
{number: 1, expected: `"1"`},
{number: 1.5, expected: `"1.5"`},
} {
payload, err := tc.number.MarshalJSON()
require.NoError(t, err, "MarshalJSON must not error")
assert.Equal(t, tc.expected, string(payload), "MarshalJSON should return expected value")
}
}

View File

@@ -1793,12 +1793,19 @@ type DualModeResponse struct {
} `json:"history"`
}
// number represents a number type for JSON marshaling with zero value as "0"
type number float64
func (n number) MarshalJSON() ([]byte, error) {
return []byte(`"` + strconv.FormatFloat(float64(n), 'f', -1, 64) + `"`), nil
}
// ContractOrderCreateParams represents future order creation parameters
type ContractOrderCreateParams struct {
Contract currency.Pair `json:"contract"`
Size float64 `json:"size"` // positive long, negative short
Iceberg int64 `json:"iceberg"` // required; can be zero
Price string `json:"price"` // NOTE: Market orders require string "0"
Price number `json:"price"` // NOTE: Market orders require string "0"
TimeInForce string `json:"tif"`
Text string `json:"text,omitempty"` // errors when empty; Either populated or omitted
ClosePosition bool `json:"close,omitempty"` // Size needs to be zero if true

View File

@@ -12,10 +12,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
)
var (
errInvalidAutoSize = errors.New("invalid auto size")
errStatusNotSet = errors.New("status not set")
)
var errStatusNotSet = errors.New("status not set")
// authenticateFutures sends an authentication message to the websocket connection
func (e *Exchange) authenticateFutures(ctx context.Context, conn websocket.Connection) error {
@@ -41,25 +38,11 @@ func (e *Exchange) WebsocketFuturesSubmitOrders(ctx context.Context, a asset.Ite
}
for _, o := range orders {
if err := validateFuturesPairAsset(o.Contract, a); err != nil {
if err := o.validate(false); err != nil {
return nil, err
}
if o.Price == "" && o.TimeInForce != "ioc" {
return nil, fmt.Errorf("%w: cannot be zero when time in force is not IOC", errInvalidPrice)
}
if o.Size == 0 && o.AutoSize == "" {
return nil, fmt.Errorf("%w: size cannot be zero", errInvalidAmount)
}
if o.AutoSize != "" {
if o.AutoSize != "close_long" && o.AutoSize != "close_short" {
return nil, fmt.Errorf("%w: %s", errInvalidAutoSize, o.AutoSize)
}
if o.Size != 0 {
return nil, fmt.Errorf("%w: size needs to be zero when auto size is set", errInvalidAmount)
}
if _, err := getSettlementCurrency(o.Contract, a); err != nil {
return nil, err
}
}

View File

@@ -22,11 +22,9 @@ func TestWebsocketFuturesSubmitOrder(t *testing.T) {
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
out := &ContractOrderCreateParams{Contract: BTCUSDT}
_, err = e.WebsocketFuturesSubmitOrder(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidPrice)
out.Price = "40000"
_, err = e.WebsocketFuturesSubmitOrder(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidAmount)
require.ErrorIs(t, err, errInvalidOrderSize)
out.Size = 1 // 1 lovely long contract
out.Price = 40000
out.AutoSize = "silly_billies"
_, err = e.WebsocketFuturesSubmitOrder(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidAutoSize)
@@ -53,23 +51,26 @@ func TestWebsocketFuturesSubmitOrders(t *testing.T) {
out.Contract = BTCUSDT
_, err = e.WebsocketFuturesSubmitOrders(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidPrice)
out.Price = "40000"
_, err = e.WebsocketFuturesSubmitOrders(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidAmount)
require.ErrorIs(t, err, errInvalidOrderSize)
out.Size = 1 // 1 lovely long contract
_, err = e.WebsocketFuturesSubmitOrders(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, order.ErrUnsupportedTimeInForce)
out.Price = 40000
out.AutoSize = "silly_billies"
_, err = e.WebsocketFuturesSubmitOrders(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidAutoSize)
out.AutoSize = "close_long"
_, err = e.WebsocketFuturesSubmitOrders(t.Context(), asset.USDTMarginedFutures, out)
require.ErrorIs(t, err, errInvalidAmount)
require.ErrorIs(t, err, errInvalidOrderSize)
out.AutoSize = ""
_, err = e.WebsocketFuturesSubmitOrders(t.Context(), asset.Binary, out)
require.ErrorIs(t, err, asset.ErrNotSupported)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
g := newExchangeWithWebsocket(t, asset.Futures)

View File

@@ -918,6 +918,8 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub
return nil, err
}
s.ClientOrderID = formatClientOrderID(s.ClientOrderID)
s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
@@ -961,29 +963,19 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub
// * iceberg orders
// * auto_size (close_long, close_short)
// * stp_act (self trade prevention)
amountWithDirection, err := getFutureOrderSize(s)
fOrder, err := getFuturesOrderRequest(s)
if err != nil {
return nil, err
}
settle, err := getSettlementCurrency(s.Pair, s.AssetType)
fOrder.Settle, err = getSettlementCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
orderParams := &ContractOrderCreateParams{
Contract: s.Pair,
Size: amountWithDirection,
Price: strconv.FormatFloat(s.Price, 'f', -1, 64), // Cannot be an empty string, requires "0" for market orders.
Settle: settle,
ReduceOnly: s.ReduceOnly,
TimeInForce: timeInForceString(s.TimeInForce),
Text: s.ClientOrderID,
}
var o *Order
op := e.PlaceFuturesOrder
if s.AssetType == asset.DeliveryFutures {
o, err = e.PlaceDeliveryOrder(ctx, orderParams)
} else {
o, err = e.PlaceFuturesOrder(ctx, orderParams)
op = e.PlaceDeliveryOrder
}
o, err := op(ctx, fOrder)
if err != nil {
return nil, err
}
@@ -991,13 +983,11 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub
if err != nil {
return nil, err
}
resp.Status = order.Open
if o.Status != statusOpen {
resp.Status, err = order.StringToOrderStatus(o.FinishAs)
if err != nil {
if resp.Status, err = order.StringToOrderStatus(o.FinishAs); err != nil {
return nil, err
}
} else {
resp.Status = order.Open
}
resp.Date = o.CreateTime.Time()
resp.ClientOrderID = getClientOrderIDFromText(o.Text)
@@ -1482,9 +1472,9 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, req *order.MultiOrderReq
}
var futuresOrders []Order
if req.AssetType == asset.DeliveryFutures {
futuresOrders, err = e.GetDeliveryOrders(ctx, currency.EMPTYPAIR, statusOpen, settle, "", 0, 0, 0)
futuresOrders, err = e.GetDeliveryOrders(ctx, currency.EMPTYPAIR, statusOpen, settle, "", 0, 0, false)
} else {
futuresOrders, err = e.GetFuturesOrders(ctx, currency.EMPTYPAIR, statusOpen, "", settle, 0, 0, 0)
futuresOrders, err = e.GetFuturesOrders(ctx, currency.EMPTYPAIR, statusOpen, "", settle, 0, 0, false)
}
if err != nil {
return nil, err
@@ -2329,6 +2319,13 @@ func getClientOrderIDFromText(text string) string {
return ""
}
func formatClientOrderID(clientOrderID string) string {
if clientOrderID == "" || strings.HasPrefix(clientOrderID, "t-") {
return clientOrderID
}
return "t-" + clientOrderID
}
// getTypeFromTimeInForce returns the order type and if the order is post only
func getTypeFromTimeInForce(tif string, price float64) (orderType order.Type) {
switch tif {
@@ -2396,6 +2393,8 @@ func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (*
return nil, err
}
s.ClientOrderID = formatClientOrderID(s.ClientOrderID)
s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
@@ -2435,31 +2434,39 @@ func getFuturesOrderRequest(s *order.Submit) (*ContractOrderCreateParams, error)
return nil, err
}
tif, err := toExchangeTIF(s.TimeInForce, s.Price)
if err != nil {
return nil, err
}
return &ContractOrderCreateParams{
Contract: s.Pair,
Size: amountWithDirection,
Price: strconv.FormatFloat(s.Price, 'f', -1, 64),
Price: number(s.Price),
ReduceOnly: s.ReduceOnly,
TimeInForce: timeInForceString(s.TimeInForce),
TimeInForce: tif,
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
func timeInForceString(tif order.TimeInForce) string {
// toExchangeTIF converts a TimeInForce to its corresponding exchange-compatible string.
func toExchangeTIF(tif order.TimeInForce, price float64) (string, error) {
switch {
case tif == order.UnknownTIF:
if price == 0 {
return iocTIF, nil // Market orders default to IOC
}
return gtcTIF, nil // Default to GTC for limit orders
case tif.Is(order.PostOnly):
return "poc"
return pocTIF, nil
case tif.Is(order.ImmediateOrCancel):
return iocTIF
return iocTIF, nil
case tif.Is(order.FillOrKill):
return fokTIF
return fokTIF, nil
case tif.Is(order.GoodTillCancel):
return gtcTIF
return gtcTIF, nil
default:
return tif.Lower()
return "", fmt.Errorf("%w: %q", order.ErrUnsupportedTimeInForce, tif)
}
}
@@ -2619,12 +2626,9 @@ func (e *Exchange) getSpotOrderRequest(s *order.Submit) (*CreateOrderRequest, er
return nil, order.ErrSideIsInvalid
}
var timeInForce string
switch s.TimeInForce {
case order.ImmediateOrCancel, order.FillOrKill, order.GoodTillCancel:
timeInForce = s.TimeInForce.Lower()
case order.PostOnly:
timeInForce = "poc"
tif, err := toExchangeTIF(s.TimeInForce, s.Price)
if err != nil {
return nil, err
}
return &CreateOrderRequest{
@@ -2635,7 +2639,7 @@ func (e *Exchange) getSpotOrderRequest(s *order.Submit) (*CreateOrderRequest, er
Price: types.Number(s.Price),
CurrencyPair: s.Pair,
Text: s.ClientOrderID,
TimeInForce: timeInForce,
TimeInForce: tif,
}, nil
}