mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user