gateio/kucoin: assortment of fixes (#1404)

* gateio: fix unmarshal bug and update fields

* gateio: fix wrapper function function, add helper methods

* update order types and add kucoin wrapper fix

* currency pairs

* Add tests

* gateio; inspect error and continue for no funds in account, kucoin: fetch all settlement amounts

* futures: order fixit

* finish off gateio updates for market orders

* cute line

* Update exchanges/kucoin/kucoin_wrapper.go

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

* Update exchanges/kucoin/kucoin_wrapper.go

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

* Update exchanges/gateio/gateio.go

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

* Update exchanges/gateio/gateio_wrapper.go

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

* Update exchanges/gateio/gateio_wrapper.go

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

* glorious: nits

* glorious: nits - filter by pair match and fix bug where the endpoint returns details instead of message

* Add fix for leverage check (non-merge) my ip has been blocked from gateio still... scammmmmmmm

* glorious: nitters

* Update exchanges/gateio/gateio_test.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/gateio/gateio_test.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* Update exchanges/gateio/gateio_test.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2024-03-06 13:06:13 +11:00
committed by GitHub
parent 64dbfd6e4f
commit 6ccb0e0c2f
11 changed files with 558 additions and 245 deletions

View File

@@ -244,6 +244,11 @@ func CallExchangeMethod(t *testing.T, methodToCall reflect.Value, methodValues [
if isUnacceptableError(t, err) != nil {
literalInputs := make([]interface{}, len(methodValues))
for j := range methodValues {
if methodValues[j].Kind() == reflect.Ptr {
// dereference pointers just to add a bit more clarity
literalInputs[j] = methodValues[j].Elem().Interface()
continue
}
literalInputs[j] = methodValues[j].Interface()
}
t.Errorf("%v Func '%v' Error: '%v'. Inputs: %v.", exch.GetName(), methodName, err, literalInputs)
@@ -448,6 +453,7 @@ func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodAr
ClientID: "1337",
ClientOrderID: "13371337",
ImmediateOrCancel: true,
Leverage: 1,
})
case argGenerator.MethodInputType.AssignableTo(orderModifyParam):
input = reflect.ValueOf(&order.Modify{

View File

@@ -458,3 +458,37 @@ func (p Pairs) GetFormatting() (PairFormat, error) {
}
return pFmt, nil
}
// GetPairsByQuote returns all pairs that have a matching quote currency
func (p Pairs) GetPairsByQuote(quoteTerm Code) (Pairs, error) {
if len(p) == 0 {
return nil, ErrCurrencyPairsEmpty
}
if quoteTerm.IsEmpty() {
return nil, ErrCurrencyCodeEmpty
}
pairs := make(Pairs, 0, len(p))
for i := range p {
if p[i].Quote.Equal(quoteTerm) {
pairs = append(pairs, p[i])
}
}
return pairs, nil
}
// GetPairsByBase returns all pairs that have a matching base currency
func (p Pairs) GetPairsByBase(baseTerm Code) (Pairs, error) {
if len(p) == 0 {
return nil, ErrCurrencyPairsEmpty
}
if baseTerm.IsEmpty() {
return nil, ErrCurrencyCodeEmpty
}
pairs := make(Pairs, 0, len(p))
for i := range p {
if p[i].Base.Equal(baseTerm) {
pairs = append(pairs, p[i])
}
}
return pairs, nil
}

View File

@@ -816,3 +816,85 @@ func TestPairs_GetFormatting(t *testing.T) {
t.Error(err)
}
}
func TestGetPairsByQuote(t *testing.T) {
t.Parallel()
var available Pairs
if _, err := available.GetPairsByQuote(EMPTYCODE); !errors.Is(err, ErrCurrencyPairsEmpty) {
t.Fatalf("received: '%v' but expected '%v'", err, ErrCurrencyPairsEmpty)
}
available = Pairs{
NewPair(BTC, USD),
NewPair(LTC, USD),
NewPair(USD, NZD),
NewPair(LTC, USDT),
NewPair(LTC, DAI),
NewPair(USDT, XRP),
NewPair(DAI, XRP),
}
if _, err := available.GetPairsByQuote(EMPTYCODE); !errors.Is(err, ErrCurrencyCodeEmpty) {
t.Fatalf("received: '%v' but expected '%v'", err, ErrCurrencyCodeEmpty)
}
got, err := available.GetPairsByQuote(USD)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected '%v'", err, nil)
}
if len(got) != 2 {
t.Fatalf("received: '%v' but expected '%v'", len(got), 2)
}
got, err = available.GetPairsByQuote(BTC)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected '%v'", err, nil)
}
if len(got) != 0 {
t.Fatalf("received: '%v' but expected '%v'", len(got), 0)
}
}
func TestGetPairsByBase(t *testing.T) {
t.Parallel()
var available Pairs
if _, err := available.GetPairsByBase(EMPTYCODE); !errors.Is(err, ErrCurrencyPairsEmpty) {
t.Fatalf("received: '%v' but expected '%v'", err, ErrCurrencyPairsEmpty)
}
available = Pairs{
NewPair(BTC, USD),
NewPair(LTC, USD),
NewPair(USD, NZD),
NewPair(LTC, USDT),
NewPair(LTC, DAI),
NewPair(USDT, XRP),
NewPair(DAI, XRP),
}
if _, err := available.GetPairsByBase(EMPTYCODE); !errors.Is(err, ErrCurrencyCodeEmpty) {
t.Fatalf("received: '%v' but expected '%v'", err, ErrCurrencyCodeEmpty)
}
got, err := available.GetPairsByBase(USD)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected '%v'", err, nil)
}
if len(got) != 1 {
t.Fatalf("received: '%v' but expected '%v'", len(got), 1)
}
got, err = available.GetPairsByBase(LTC)
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected '%v'", err, nil)
}
if len(got) != 3 {
t.Fatalf("received: '%v' but expected '%v'", len(got), 3)
}
}

View File

@@ -164,6 +164,7 @@ var (
errInvalidSubAccountUserID = errors.New("sub-account user id is required")
errCannotParseSettlementCurrency = errors.New("cannot derive settlement currency")
errMissingAPIKey = errors.New("missing API key information")
errInvalidTextValue = errors.New("invalid text value, requires prefix `t-`")
)
// Gateio is the overarching type across this package
@@ -554,7 +555,6 @@ func (g *Gateio) CreateBatchOrders(ctx context.Context, args []CreateOrderReques
if len(args) > 10 {
return nil, fmt.Errorf("%w only 10 orders are canceled at once", errMultipleOrders)
}
var err error
for x := range args {
if (x != 0) && args[x-1].Account != args[x].Account {
return nil, errDifferentAccount
@@ -574,13 +574,6 @@ func (g *Gateio) CreateBatchOrders(ctx context.Context, args []CreateOrderReques
!strings.EqualFold(args[x].Account, asset.Margin.String()) {
return nil, errors.New("only spot, margin, and cross_margin area allowed")
}
if args[x].Text == "" {
args[x].Text, err = common.GenerateRandomString(10, common.NumberCharacters)
if err != nil {
return nil, err
}
args[x].Text = "t-" + args[x].Text
}
if args[x].Amount <= 0 {
return nil, errInvalidAmount
}
@@ -651,15 +644,6 @@ func (g *Gateio) PlaceSpotOrder(ctx context.Context, arg *CreateOrderRequestData
!strings.EqualFold(arg.Account, asset.Margin.String()) {
return nil, errors.New("only 'spot', 'cross_margin', and 'margin' area allowed")
}
if arg.Text != "" {
arg.Text = "t-" + arg.Text
} else {
randomString, err := common.GenerateRandomString(10, common.NumberCharacters)
if err != nil {
return nil, err
}
arg.Text = "t-" + randomString
}
if arg.Amount <= 0 {
return nil, errInvalidAmount
}
@@ -2155,12 +2139,16 @@ func (g *Gateio) GetFuturesAccountBooks(ctx context.Context, settle string, limi
}
// GetAllFuturesPositionsOfUsers list all positions of users.
func (g *Gateio) GetAllFuturesPositionsOfUsers(ctx context.Context, settle string) (*Position, error) {
func (g *Gateio) GetAllFuturesPositionsOfUsers(ctx context.Context, settle string, realPositionsOnly bool) ([]Position, error) {
if settle == "" {
return nil, errEmptySettlementCurrency
}
var response *Position
return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, futuresPath+settle+"/positions", nil, nil, &response)
params := url.Values{}
if realPositionsOnly {
params.Set("holding", "true")
}
var response []Position
return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPrivateEPL, http.MethodGet, futuresPath+settle+"/positions", params, nil, &response)
}
// GetSinglePosition returns a single position
@@ -2342,20 +2330,14 @@ func (g *Gateio) PlaceFuturesOrder(ctx context.Context, arg *OrderCreateParams)
if arg.Size == 0 {
return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", errInvalidOrderSide)
}
if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != focTIF {
if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != fokTIF {
return nil, errInvalidTimeInForce
}
if arg.Price < 0 {
if arg.Price == "" {
return nil, errInvalidPrice
}
if arg.Text != "" {
arg.Text = "t-" + arg.Text
} else {
randomString, err := common.GenerateRandomString(10, common.NumberCharacters)
if err != nil {
return nil, err
}
arg.Text = "t-" + randomString
if arg.Price == "0" && arg.TimeInForce != iocTIF && arg.TimeInForce != fokTIF {
return nil, errInvalidTimeInForce
}
if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") {
return nil, errInvalidAutoSizeValue
@@ -2366,10 +2348,13 @@ func (g *Gateio) PlaceFuturesOrder(ctx context.Context, arg *OrderCreateParams)
}
var response *Order
return response, g.SendAuthenticatedHTTPRequest(ctx,
exchange.RestSpot, perpetualSwapPlaceOrdersEPL,
exchange.RestSpot,
perpetualSwapPlaceOrdersEPL,
http.MethodPost,
futuresPath+arg.Settle+ordersPath,
nil, &arg, &response)
nil,
&arg,
&response)
}
// GetFuturesOrders retrieves list of futures orders
@@ -2378,11 +2363,10 @@ func (g *Gateio) GetFuturesOrders(ctx context.Context, contract currency.Pair, s
if settle == "" {
return nil, errEmptySettlementCurrency
}
if contract.IsInvalid() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
params := url.Values{}
params.Set("contract", contract.String())
if !contract.IsEmpty() {
params.Set("contract", contract.String())
}
if status != statusOpen && status != statusFinished {
return nil, fmt.Errorf("%w, only 'open' and 'finished' status are supported", errInvalidOrderStatus)
}
@@ -2448,17 +2432,17 @@ func (g *Gateio) PlaceBatchFuturesOrders(ctx context.Context, settle string, arg
if args[x].TimeInForce != gtcTIF &&
args[x].TimeInForce != iocTIF &&
args[x].TimeInForce != pocTIF &&
args[x].TimeInForce != focTIF {
args[x].TimeInForce != fokTIF {
return nil, errInvalidTimeInForce
}
if args[x].Price > 0 && args[x].TimeInForce == iocTIF {
args[x].Price = 0
}
if args[x].Price < 0 {
if args[x].Price == "" {
return nil, errInvalidPrice
}
if args[x].Price == "0" && args[x].TimeInForce != iocTIF && args[x].TimeInForce != fokTIF {
return nil, errInvalidTimeInForce
}
if args[x].Text != "" && !strings.HasPrefix(args[x].Text, "t-") {
args[x].Text = "t-" + args[x].Text
return nil, errInvalidTextValue
}
if args[x].AutoSize != "" && (args[x].AutoSize == "close_long" || args[x].AutoSize == "close_short") {
return nil, errInvalidAutoSizeValue
@@ -2980,21 +2964,12 @@ func (g *Gateio) PlaceDeliveryOrder(ctx context.Context, arg *OrderCreateParams)
if arg.Size == 0 {
return nil, fmt.Errorf("%w, specify positive number to make a bid, and negative number to ask", errInvalidOrderSide)
}
if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != focTIF {
if arg.TimeInForce != gtcTIF && arg.TimeInForce != iocTIF && arg.TimeInForce != pocTIF && arg.TimeInForce != fokTIF {
return nil, errInvalidTimeInForce
}
if arg.Price < 0 {
if arg.Price == "" {
return nil, errInvalidPrice
}
if arg.Text != "" {
arg.Text = "t-" + arg.Text
} else {
randomString, err := common.GenerateRandomString(10, common.NumberCharacters)
if err != nil {
return nil, err
}
arg.Text = "t-" + randomString
}
if arg.AutoSize != "" && (arg.AutoSize == "close_long" || arg.AutoSize == "close_short") {
return nil, errInvalidAutoSizeValue
}
@@ -3013,11 +2988,10 @@ func (g *Gateio) GetDeliveryOrders(ctx context.Context, contract currency.Pair,
if settle == "" {
return nil, errEmptySettlementCurrency
}
if contract.IsInvalid() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
params := url.Values{}
params.Set("contract", contract.String())
if !contract.IsEmpty() {
params.Set("contract", contract.String())
}
if status != statusOpen && status != statusFinished {
return nil, fmt.Errorf("%w, only 'open' and 'finished' status are supported", errInvalidOrderStatus)
}
@@ -3498,12 +3472,6 @@ func (g *Gateio) PlaceOptionOrder(ctx context.Context, arg *OptionOrderParam) (*
if arg.TimeInForce == iocTIF || arg.Price < 0 {
arg.Price = 0
}
var err error
arg.Text, err = common.GenerateRandomString(10, common.NumberCharacters)
if err != nil {
return nil, err
}
arg.Text = "t-" + arg.Text
var response *OptionOrderResponse
return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualSwapPlaceOrdersEPL, http.MethodPost,
gateioOptionsOrders, nil, &arg, &response)

View File

@@ -12,6 +12,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
@@ -1117,7 +1118,7 @@ func TestGetFuturesAccountBooks(t *testing.T) {
func TestGetAllPositionsOfUsers(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, g)
if _, err := g.GetAllFuturesPositionsOfUsers(context.Background(), settleUSDT); err != nil {
if _, err := g.GetAllFuturesPositionsOfUsers(context.Background(), settleUSDT, true); err != nil {
t.Errorf("%s GetAllPositionsOfUsers() error %v", g.Name, err)
}
}
@@ -1177,7 +1178,7 @@ func TestCreateDeliveryOrder(t *testing.T) {
Contract: getPair(t, asset.DeliveryFutures),
Size: 6024,
Iceberg: 0,
Price: 3765,
Price: "3765",
Text: "t-my-custom-id",
Settle: settle,
TimeInForce: gtcTIF,
@@ -1388,7 +1389,7 @@ func TestCreateFuturesOrder(t *testing.T) {
Contract: getPair(t, asset.Futures),
Size: 6024,
Iceberg: 0,
Price: 3765,
Price: "3765",
TimeInForce: "gtc",
Text: "t-my-custom-id",
Settle: settle,
@@ -1441,7 +1442,7 @@ func TestCreateBatchFuturesOrders(t *testing.T) {
Contract: getPair(t, asset.Futures),
Size: 6024,
Iceberg: 0,
Price: 3765,
Price: "3765",
TimeInForce: "gtc",
Text: "t-my-custom-id",
Settle: settle,
@@ -1450,7 +1451,7 @@ func TestCreateBatchFuturesOrders(t *testing.T) {
Contract: currency.NewPair(currency.BTC, currency.USDT),
Size: 232,
Iceberg: 0,
Price: 376225,
Price: "376225",
TimeInForce: "gtc",
Text: "t-my-custom-id",
Settle: settleBTC,
@@ -3483,3 +3484,78 @@ func getPair(tb testing.TB, a asset.Item) currency.Pair {
return pairs[a]
}
func TestGetClientOrderIDFromText(t *testing.T) {
t.Parallel()
assert.Zero(t, getClientOrderIDFromText("api"), "should not return anything")
assert.Equal(t, "t-123", getClientOrderIDFromText("t-123"), "should return t-123")
}
func TestGetTypeFromTimeInForce(t *testing.T) {
t.Parallel()
typeResp, postOnly := getTypeFromTimeInForce("gtc")
assert.Equal(t, order.Limit, typeResp, "should be a limit order")
assert.False(t, postOnly, "should return false")
typeResp, postOnly = getTypeFromTimeInForce("ioc")
assert.Equal(t, order.Market, typeResp, "should be market order")
assert.False(t, postOnly, "should return false")
typeResp, postOnly = getTypeFromTimeInForce("poc")
assert.Equal(t, order.Limit, typeResp, "should be limit order")
assert.True(t, postOnly, "should return true")
typeResp, postOnly = getTypeFromTimeInForce("fok")
assert.Equal(t, order.Market, typeResp, "should be market order")
assert.False(t, postOnly, "should return false")
}
func TestGetSideAndAmountFromSize(t *testing.T) {
t.Parallel()
side, amount, remaining := getSideAndAmountFromSize(1, 1)
assert.Equal(t, order.Long, side, "should be a buy order")
assert.Equal(t, 1.0, amount, "should be 1.0")
assert.Equal(t, 1.0, remaining, "should be 1.0")
side, amount, remaining = getSideAndAmountFromSize(-1, -1)
assert.Equal(t, order.Short, side, "should be a sell order")
assert.Equal(t, 1.0, amount, "should be 1.0")
assert.Equal(t, 1.0, remaining, "should be 1.0")
}
func TestGetFutureOrderSize(t *testing.T) {
t.Parallel()
_, err := getFutureOrderSize(&order.Submit{Side: order.CouldNotCloseShort, Amount: 1})
assert.ErrorIs(t, err, errInvalidOrderSide)
ret, err := getFutureOrderSize(&order.Submit{Side: order.Buy, Amount: 1})
require.NoError(t, err)
assert.Equal(t, 1.0, ret)
ret, err = getFutureOrderSize(&order.Submit{Side: order.Sell, Amount: 1})
require.NoError(t, err)
assert.Equal(t, -1.0, ret)
}
func TestGetTimeInForce(t *testing.T) {
t.Parallel()
_, err := getTimeInForce(&order.Submit{Type: order.Market, PostOnly: true})
assert.ErrorIs(t, err, errPostOnlyOrderTypeUnsupported)
ret, err := getTimeInForce(&order.Submit{Type: order.Market})
require.NoError(t, err)
assert.Equal(t, "ioc", ret)
ret, err = getTimeInForce(&order.Submit{Type: order.Limit, PostOnly: true})
require.NoError(t, err)
assert.Equal(t, "poc", ret)
ret, err = getTimeInForce(&order.Submit{Type: order.Limit})
require.NoError(t, err)
assert.Equal(t, "gtc", ret)
ret, err = getTimeInForce(&order.Submit{Type: order.Market, FillOrKill: true})
require.NoError(t, err)
assert.Equal(t, "fok", ret)
}

View File

@@ -21,7 +21,7 @@ const (
gtcTIF = "gtc" // good-'til-canceled
iocTIF = "ioc" // immediate-or-cancel
pocTIF = "poc"
focTIF = "foc" // fill-or-kill
fokTIF = "fok" // fill-or-kill
// frequently used order Status
@@ -1742,19 +1742,19 @@ type Position struct {
Size int64 `json:"size"`
Leverage types.Number `json:"leverage"`
RiskLimit types.Number `json:"risk_limit"`
LeverageMax string `json:"leverage_max"`
LeverageMax types.Number `json:"leverage_max"`
MaintenanceRate types.Number `json:"maintenance_rate"`
Value types.Number `json:"value"`
Margin types.Number `json:"margin"`
EntryPrice types.Number `json:"entry_price"`
LiqPrice types.Number `json:"liq_price"`
MarkPrice types.Number `json:"mark_price"`
UnrealisedPnl string `json:"unrealised_pnl"`
RealisedPnl string `json:"realised_pnl"`
HistoryPnl string `json:"history_pnl"`
LastClosePnl string `json:"last_close_pnl"`
RealisedPoint string `json:"realised_point"`
HistoryPoint string `json:"history_point"`
UnrealisedPnl types.Number `json:"unrealised_pnl"`
RealisedPnl types.Number `json:"realised_pnl"`
HistoryPnl types.Number `json:"history_pnl"`
LastClosePnl types.Number `json:"last_close_pnl"`
RealisedPoint types.Number `json:"realised_point"`
HistoryPoint types.Number `json:"history_point"`
AdlRanking int64 `json:"adl_ranking"`
PendingOrders int64 `json:"pending_orders"`
CloseOrder struct {
@@ -1794,18 +1794,16 @@ type DualModeResponse struct {
// OrderCreateParams represents future order creation parameters
type OrderCreateParams struct {
Contract currency.Pair `json:"contract"`
Size float64 `json:"size"`
Iceberg int64 `json:"iceberg"`
Price types.Number `json:"price"`
TimeInForce string `json:"tif"`
Text string `json:"text"`
// Optional Parameters
ClosePosition bool `json:"close,omitempty"`
ReduceOnly bool `json:"reduce_only,omitempty"`
AutoSize string `json:"auto_size,omitempty"`
Settle string `json:"-"`
Contract currency.Pair `json:"contract"`
Size float64 `json:"size"`
Iceberg int64 `json:"iceberg"`
Price string `json:"price"` // NOTE: Market orders require string "0"
TimeInForce string `json:"tif"`
Text string `json:"text"`
ClosePosition bool `json:"close,omitempty"`
ReduceOnly bool `json:"reduce_only,omitempty"`
AutoSize string `json:"auto_size,omitempty"`
Settle string `json:"-"` // Used in URL.
}
// Order represents future order response

View File

@@ -34,6 +34,11 @@ import (
"github.com/thrasher-corp/gocryptotrader/types"
)
// unfundedFuturesAccount defines an error string when an account associated
// with a settlement currency has not been funded. Use specific pairs to avoid
// this error.
const unfundedFuturesAccount = `please transfer funds first to create futures account`
// GetDefaultConfig returns a default exchange config
func (g *Gateio) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) {
g.SetDefaults()
@@ -780,7 +785,7 @@ func (g *Gateio) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.H
Currencies: currencies,
})
case asset.Futures, asset.DeliveryFutures:
currencies := make([]account.Balance, 3)
currencies := make([]account.Balance, 0, 3)
settles := []currency.Code{currency.BTC, currency.USDT, currency.USD}
for x := range settles {
var balance *FuturesAccount
@@ -793,14 +798,20 @@ func (g *Gateio) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.H
balance, err = g.GetDeliveryFuturesAccounts(ctx, settles[x].String())
}
if err != nil {
if strings.Contains(err.Error(), unfundedFuturesAccount) {
if g.Verbose {
log.Warnf(log.ExchangeSys, "%s %v for settlement: %v", g.Name, err, settles[x])
}
continue
}
return info, err
}
currencies[x] = account.Balance{
currencies = append(currencies, account.Balance{
Currency: currency.NewCode(balance.Currency),
Total: balance.Total.Float64(),
Hold: balance.Total.Float64() - balance.Available.Float64(),
Free: balance.Available.Float64(),
}
})
}
info.Accounts = append(info.Accounts, account.SubAccount{
AssetType: a,
@@ -999,15 +1010,7 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi
if err != nil {
return nil, err
}
var orderTypeFormat string
switch {
case s.Side.IsLong():
orderTypeFormat = order.Buy.Lower()
case s.Side.IsShort():
orderTypeFormat = order.Sell.Lower()
default:
return nil, errInvalidOrderSide
}
s.Pair, err = g.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
@@ -1018,8 +1021,16 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi
if s.Type != order.Limit {
return nil, errOnlyLimitOrderType
}
switch {
case s.Side.IsLong():
s.Side = order.Buy
case s.Side.IsShort():
s.Side = order.Sell
default:
return nil, errInvalidOrderSide
}
sOrder, err := g.PlaceSpotOrder(ctx, &CreateOrderRequestData{
Side: orderTypeFormat,
Side: s.Side.Lower(),
Type: s.Type.Lower(),
Account: g.assetTypeToString(s.AssetType),
Amount: types.Number(s.Amount),
@@ -1053,24 +1064,32 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi
response.LastUpdated = sOrder.UpdateTimeMs.Time()
return response, nil
case asset.Futures:
// TODO: See https://www.gate.io/docs/developers/apiv4/en/#create-a-futures-order
// * iceberg orders
// * auto_size (close_long, close_short)
// * stp_act (self trade prevention)
settle, err := g.getSettlementFromCurrency(s.Pair, true)
if err != nil {
return nil, err
}
if orderTypeFormat == "bid" && s.Price < 0 {
s.Price = -s.Price
} else if orderTypeFormat == "ask" && s.Price > 0 {
s.Price = -s.Price
var amountWithDirection float64
amountWithDirection, err = getFutureOrderSize(s)
if err != nil {
return nil, err
}
var timeInForce string
timeInForce, err = getTimeInForce(s)
if err != nil {
return nil, err
}
fOrder, err := g.PlaceFuturesOrder(ctx, &OrderCreateParams{
Contract: s.Pair,
Size: s.Amount,
Price: types.Number(s.Price),
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: "gtc",
Text: s.ClientOrderID,
})
TimeInForce: timeInForce,
Text: s.ClientOrderID})
if err != nil {
return nil, err
}
@@ -1078,34 +1097,44 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(fOrder.Status)
if err != nil {
return nil, err
var status = order.Open
if fOrder.Status != "open" {
status, err = order.StringToOrderStatus(fOrder.FinishAs)
if err != nil {
return nil, err
}
}
response.Status = status
response.Pair = s.Pair
response.Date = fOrder.CreateTime.Time()
response.ClientOrderID = fOrder.Text
response.ClientOrderID = getClientOrderIDFromText(fOrder.Text)
response.ReduceOnly = fOrder.IsReduceOnly
response.Amount = fOrder.RemainingAmount
response.Amount = math.Abs(fOrder.Size)
response.Price = fOrder.OrderPrice.Float64()
response.AverageExecutedPrice = fOrder.FillPrice.Float64()
return response, nil
case asset.DeliveryFutures:
settle, err := g.getSettlementFromCurrency(s.Pair, false)
if err != nil {
return nil, err
}
if orderTypeFormat == "bid" && s.Price < 0 {
s.Price = -s.Price
} else if orderTypeFormat == "ask" && s.Price > 0 {
s.Price = -s.Price
var amountWithDirection float64
amountWithDirection, err = getFutureOrderSize(s)
if err != nil {
return nil, err
}
var timeInForce string
timeInForce, err = getTimeInForce(s)
if err != nil {
return nil, err
}
newOrder, err := g.PlaceDeliveryOrder(ctx, &OrderCreateParams{
Contract: s.Pair,
Size: s.Amount,
Price: types.Number(s.Price),
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: "gtc",
TimeInForce: timeInForce,
Text: s.ClientOrderID,
})
if err != nil {
@@ -1115,16 +1144,20 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi
if err != nil {
return nil, err
}
status, err := order.StringToOrderStatus(newOrder.Status)
if err != nil {
return nil, err
var status = order.Open
if newOrder.Status != "open" {
status, err = order.StringToOrderStatus(newOrder.FinishAs)
if err != nil {
return nil, err
}
}
response.Status = status
response.Pair = s.Pair
response.Date = newOrder.CreateTime.Time()
response.ClientOrderID = newOrder.Text
response.Amount = newOrder.Size
response.ClientOrderID = getClientOrderIDFromText(newOrder.Text)
response.Amount = math.Abs(newOrder.Size)
response.Price = newOrder.OrderPrice.Float64()
response.AverageExecutedPrice = newOrder.FillPrice.Float64()
return response, nil
case asset.Options:
optionOrder, err := g.PlaceOptionOrder(ctx, &OptionOrderParam{
@@ -1415,11 +1448,7 @@ func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency
}, nil
case asset.Futures, asset.DeliveryFutures:
var settle string
if a == asset.Futures {
settle, err = g.getSettlementFromCurrency(pair, true)
} else {
settle, err = g.getSettlementFromCurrency(pair, false)
}
settle, err = g.getSettlementFromCurrency(pair, a == asset.Futures)
if err != nil {
return nil, err
}
@@ -1433,25 +1462,38 @@ func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency
if err != nil {
return nil, err
}
orderStatus, err := order.StringToOrderStatus(fOrder.Status)
if err != nil {
return nil, err
orderStatus := order.Open
if fOrder.Status != "open" {
orderStatus, err = order.StringToOrderStatus(fOrder.FinishAs)
if err != nil {
return nil, err
}
}
pair, err = currency.NewPairFromString(fOrder.Contract)
if err != nil {
return nil, err
}
side, amount, remaining := getSideAndAmountFromSize(fOrder.Size, fOrder.RemainingAmount)
ordertype, postonly := getTypeFromTimeInForce(fOrder.TimeInForce)
return &order.Detail{
Amount: fOrder.Size,
ExecutedAmount: fOrder.Size - fOrder.RemainingAmount,
Exchange: g.Name,
OrderID: orderID,
Status: orderStatus,
Price: fOrder.OrderPrice.Float64(),
Date: fOrder.CreateTime.Time(),
LastUpdated: fOrder.FinishTime.Time(),
Pair: pair,
AssetType: a,
Amount: amount,
ExecutedAmount: amount - remaining,
RemainingAmount: remaining,
Exchange: g.Name,
OrderID: orderID,
ClientOrderID: getClientOrderIDFromText(fOrder.Text),
Status: orderStatus,
Price: fOrder.OrderPrice.Float64(),
AverageExecutedPrice: fOrder.FillPrice.Float64(),
Date: fOrder.CreateTime.Time(),
LastUpdated: fOrder.FinishTime.Time(),
Pair: pair,
AssetType: a,
Type: ordertype,
PostOnly: postonly,
Side: side,
}, nil
case asset.Options:
optionOrder, err := g.GetSingleOptionOrder(ctx, orderID)
@@ -1619,50 +1661,68 @@ func (g *Gateio) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque
}
}
case asset.Futures, asset.DeliveryFutures:
settlements := map[string]bool{}
if len(req.Pairs) == 0 {
return nil, currency.ErrCurrencyPairsEmpty
settlements["btc"] = true
settlements["usdt"] = true
settlements["usd"] = true
} else {
for x := range req.Pairs {
var s string
s, err = g.getSettlementFromCurrency(req.Pairs[x], req.AssetType == asset.Futures)
if err != nil {
return nil, err
}
settlements[s] = true
}
}
for z := range req.Pairs {
var settle string
if req.AssetType == asset.Futures {
settle, err = g.getSettlementFromCurrency(req.Pairs[z], true)
} else {
settle, err = g.getSettlementFromCurrency(req.Pairs[z], false)
}
if err != nil {
return nil, err
}
for settlement := range settlements {
var futuresOrders []Order
if req.AssetType == asset.Futures {
futuresOrders, err = g.GetFuturesOrders(ctx, req.Pairs[z], "open", "", settle, 0, 0, 0)
futuresOrders, err = g.GetFuturesOrders(ctx, currency.EMPTYPAIR, "open", "", settlement, 0, 0, 0)
} else {
futuresOrders, err = g.GetDeliveryOrders(ctx, req.Pairs[z], "open", settle, "", 0, 0, 0)
futuresOrders, err = g.GetDeliveryOrders(ctx, currency.EMPTYPAIR, "open", settlement, "", 0, 0, 0)
}
if err != nil {
if strings.Contains(err.Error(), unfundedFuturesAccount) {
log.Warnf(log.ExchangeSys, "%s %v", g.Name, err)
continue
}
return nil, err
}
for x := range futuresOrders {
if futuresOrders[x].Status != "open" {
var pair currency.Pair
pair, err = currency.NewPairFromString(futuresOrders[x].Contract)
if err != nil {
return nil, err
}
if futuresOrders[x].Status != "open" || (len(req.Pairs) > 0 && !req.Pairs.Contains(pair, true)) {
continue
}
var status order.Status
status, err = order.StringToOrderStatus(futuresOrders[x].Status)
if err != nil {
log.Errorf(log.ExchangeSys, "%s %v", g.Name, err)
}
side, amount, remaining := getSideAndAmountFromSize(futuresOrders[x].Size, futuresOrders[x].RemainingAmount)
orders = append(orders, order.Detail{
Status: status,
Amount: futuresOrders[x].Size,
Pair: req.Pairs[x],
OrderID: strconv.FormatInt(futuresOrders[x].ID, 10),
Price: futuresOrders[x].OrderPrice.Float64(),
ExecutedAmount: futuresOrders[x].Size - futuresOrders[x].RemainingAmount,
RemainingAmount: futuresOrders[x].RemainingAmount,
LastUpdated: futuresOrders[x].FinishTime.Time(),
Date: futuresOrders[x].CreateTime.Time(),
ClientOrderID: futuresOrders[x].Text,
Exchange: g.Name,
AssetType: req.AssetType,
Status: order.Open,
Amount: amount,
ContractAmount: amount,
Pair: pair,
OrderID: strconv.FormatInt(futuresOrders[x].ID, 10),
ClientOrderID: getClientOrderIDFromText(futuresOrders[x].Text),
Price: futuresOrders[x].OrderPrice.Float64(),
ExecutedAmount: amount - remaining,
RemainingAmount: remaining,
LastUpdated: futuresOrders[x].FinishTime.Time(),
Date: futuresOrders[x].CreateTime.Time(),
Exchange: g.Name,
AssetType: req.AssetType,
Side: side,
Type: order.Limit,
SettlementCurrency: currency.NewCode(settlement),
ReduceOnly: futuresOrders[x].IsReduceOnly,
PostOnly: futuresOrders[x].TimeInForce == "poc",
AverageExecutedPrice: futuresOrders[x].FillPrice.Float64(),
})
}
}
@@ -2452,3 +2512,66 @@ func (g *Gateio) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]fut
}
return resp, nil
}
// getClientOrderIDFromText returns the client order ID from the text response
func getClientOrderIDFromText(text string) string {
if strings.HasPrefix(text, "t-") {
return text
}
return ""
}
// getTypeFromTimeInForce returns the order type and if the order is post only
func getTypeFromTimeInForce(tif string) (orderType order.Type, postOnly bool) {
switch tif {
case "ioc":
return order.Market, false
case "fok":
return order.Market, false
case "poc":
return order.Limit, true
default:
return order.Limit, false
}
}
// getSideAndAmountFromSize returns the order side, amount and remaining amounts
func getSideAndAmountFromSize(size, left float64) (side order.Side, amount, remaining float64) {
if size < 0 {
return order.Short, math.Abs(size), math.Abs(left)
}
return order.Long, size, left
}
// getFutureOrderSize sets the amount to a negative value if shorting.
func getFutureOrderSize(s *order.Submit) (float64, error) {
switch {
case s.Side.IsLong():
return s.Amount, nil
case s.Side.IsShort():
return -s.Amount, nil
default:
return 0, errInvalidOrderSide
}
}
var errPostOnlyOrderTypeUnsupported = errors.New("post only is only supported for limit orders")
// getTimeInForce returns the time in force for a given order. If Market order
// IOC
func getTimeInForce(s *order.Submit) (string, error) {
timeInForce := "gtc" // limit order taker/maker
if s.Type == order.Market || s.ImmediateOrCancel {
timeInForce = "ioc" // market taker only
}
if s.PostOnly {
if s.Type != order.Limit {
return "", fmt.Errorf("%w not for %v", errPostOnlyOrderTypeUnsupported, s.Type)
}
timeInForce = "poc" // limit order maker only
}
if s.FillOrKill {
timeInForce = "fok" // market order entire fill or kill
}
return timeInForce, nil
}

View File

@@ -1845,10 +1845,12 @@ func (ku *Kucoin) orderTypeToString(orderType order.Type) (string, error) {
}
func (ku *Kucoin) orderSideString(side order.Side) (string, error) {
switch side {
case order.Buy, order.Sell:
return side.Lower(), nil
case order.AnySide:
switch {
case side.IsLong():
return order.Buy.Lower(), nil
case side.IsShort():
return order.Sell.Lower(), nil
case side == order.AnySide:
return "", nil
default:
return "", fmt.Errorf("%w, side:%s", order.ErrSideIsInvalid, side.String())

View File

@@ -2209,13 +2209,8 @@ func TestSubmitOrder(t *testing.T) {
ClientOrderID: "myOrder",
AssetType: asset.Spot,
}
_, err := ku.SubmitOrder(context.Background(), orderSubmission)
if !errors.Is(err, order.ErrSideIsInvalid) {
t.Errorf("expected %v, but found %v", asset.ErrNotSupported, err)
}
orderSubmission.Side = order.Buy
orderSubmission.AssetType = asset.Options
_, err = ku.SubmitOrder(context.Background(), orderSubmission)
_, err := ku.SubmitOrder(context.Background(), orderSubmission)
if !errors.Is(err, asset.ErrNotSupported) {
t.Errorf("expected %v, but found %v", asset.ErrNotSupported, err)
}

View File

@@ -473,27 +473,30 @@ func (ku *Kucoin) UpdateOrderbook(ctx context.Context, pair currency.Pair, asset
// UpdateAccountInfo retrieves balances for all enabled currencies
func (ku *Kucoin) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
holding := account.Holdings{
Exchange: ku.Name,
}
holding := account.Holdings{Exchange: ku.Name}
err := ku.CurrencyPairs.IsAssetEnabled(assetType)
if err != nil {
return holding, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
switch assetType {
case asset.Futures:
accountH, err := ku.GetFuturesAccountOverview(ctx, "")
if err != nil {
return account.Holdings{}, err
}
holding.Accounts = append(holding.Accounts, account.SubAccount{
AssetType: assetType,
Currencies: []account.Balance{{
balances := make([]account.Balance, 2)
for i, settlement := range []string{"XBT", "USDT"} {
accountH, err := ku.GetFuturesAccountOverview(ctx, settlement)
if err != nil {
return account.Holdings{}, err
}
balances[i] = account.Balance{
Currency: currency.NewCode(accountH.Currency),
Total: accountH.AvailableBalance + accountH.FrozenFunds,
Hold: accountH.FrozenFunds,
Free: accountH.AvailableBalance,
}},
}
}
holding.Accounts = append(holding.Accounts, account.SubAccount{
AssetType: assetType,
Currencies: balances,
})
case asset.Spot, asset.Margin:
accountH, err := ku.GetAllAccounts(ctx, "", ku.accountTypeToString(assetType))
@@ -709,14 +712,18 @@ func (ku *Kucoin) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm
}
switch s.AssetType {
case asset.Futures:
if s.Leverage == 0 {
s.Leverage = 1
}
o, err := ku.PostFuturesOrder(ctx, &FuturesOrderParam{
ClientOrderID: s.ClientOrderID, Side: sideString, Symbol: s.Pair,
OrderType: s.Type.Lower(), Size: s.Amount, Price: s.Price, StopPrice: s.TriggerPrice,
Leverage: s.Leverage, VisibleSize: 0, ReduceOnly: s.ReduceOnly,
PostOnly: s.PostOnly, Hidden: s.Hidden})
ClientOrderID: s.ClientOrderID,
Side: sideString,
Symbol: s.Pair,
OrderType: s.Type.Lower(),
Size: s.Amount,
Price: s.Price,
StopPrice: s.TriggerPrice,
Leverage: s.Leverage,
ReduceOnly: s.ReduceOnly,
PostOnly: s.PostOnly,
Hidden: s.Hidden})
if err != nil {
return nil, err
}
@@ -907,6 +914,11 @@ func (ku *Kucoin) GetOrderInfo(ctx context.Context, orderID string, pair currenc
if err != nil {
return nil, err
}
if side == order.Sell {
side = order.Short
} else if side == order.Buy {
side = order.Long
}
if !pair.IsEmpty() && !nPair.Equal(pair) {
return nil, fmt.Errorf("order with id %s and currency Pair %v does not exist", orderID, pair)
}
@@ -1048,8 +1060,8 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M
if err != nil {
return nil, err
}
pair := ""
orders := []order.Detail{}
var pair string
var orders []order.Detail
switch getOrdersRequest.AssetType {
case asset.Futures:
if len(getOrdersRequest.Pairs) == 1 {
@@ -1072,40 +1084,55 @@ func (ku *Kucoin) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M
continue
}
var dPair currency.Pair
var isEnabled bool
dPair, isEnabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, true)
var enabled bool
dPair, enabled, err = ku.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, false)
if err != nil {
return nil, err
}
if !isEnabled {
if !enabled {
continue
}
for i := range getOrdersRequest.Pairs {
if !getOrdersRequest.Pairs[i].Equal(dPair) {
continue
}
side, err := order.StringToOrderSide(futuresOrders.Items[x].Side)
if err != nil {
return nil, err
}
oType, err := order.StringToOrderType(futuresOrders.Items[x].OrderType)
if err != nil {
return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err)
}
orders = append(orders, order.Detail{
OrderID: futuresOrders.Items[x].ID,
Amount: futuresOrders.Items[x].Size,
RemainingAmount: futuresOrders.Items[x].Size - futuresOrders.Items[x].FilledSize,
ExecutedAmount: futuresOrders.Items[x].FilledSize,
Exchange: ku.Name,
Date: futuresOrders.Items[x].CreatedAt.Time(),
LastUpdated: futuresOrders.Items[x].UpdatedAt.Time(),
Price: futuresOrders.Items[x].Price,
Side: side,
Type: oType,
Pair: dPair,
})
side, err := order.StringToOrderSide(futuresOrders.Items[x].Side)
if err != nil {
return nil, err
}
if side == order.Sell {
side = order.Short
} else if side == order.Buy {
side = order.Long
}
oType, err := order.StringToOrderType(futuresOrders.Items[x].OrderType)
if err != nil {
return nil, fmt.Errorf("asset type: %v order type: %v err: %w", getOrdersRequest.AssetType, getOrdersRequest.Type, err)
}
status, err := order.StringToOrderStatus(futuresOrders.Items[x].Status)
if err != nil {
return nil, err
}
orders = append(orders, order.Detail{
OrderID: futuresOrders.Items[x].ID,
ClientOrderID: futuresOrders.Items[x].ClientOid,
Amount: futuresOrders.Items[x].Size,
ContractAmount: futuresOrders.Items[x].Size,
RemainingAmount: futuresOrders.Items[x].Size - futuresOrders.Items[x].FilledSize,
ExecutedAmount: futuresOrders.Items[x].FilledSize,
Exchange: ku.Name,
Date: futuresOrders.Items[x].CreatedAt.Time(),
LastUpdated: futuresOrders.Items[x].UpdatedAt.Time(),
Price: futuresOrders.Items[x].Price,
Side: side,
Type: oType,
Pair: dPair,
PostOnly: futuresOrders.Items[x].PostOnly,
ReduceOnly: futuresOrders.Items[x].ReduceOnly,
Status: status,
SettlementCurrency: currency.NewCode(futuresOrders.Items[x].SettleCurrency),
Leverage: futuresOrders.Items[x].Leverage,
AssetType: getOrdersRequest.AssetType,
HiddenOrder: futuresOrders.Items[x].Hidden,
})
}
case asset.Spot, asset.Margin:
if len(getOrdersRequest.Pairs) == 1 {

View File

@@ -101,17 +101,18 @@ type SubmitResponse struct {
Pair currency.Pair
AssetType asset.Item
ImmediateOrCancel bool
FillOrKill bool
PostOnly bool
ReduceOnly bool
Leverage float64
Price float64
Amount float64
QuoteAmount float64
TriggerPrice float64
ClientID string
ClientOrderID string
ImmediateOrCancel bool
FillOrKill bool
PostOnly bool
ReduceOnly bool
Leverage float64
Price float64
AverageExecutedPrice float64
Amount float64
QuoteAmount float64
TriggerPrice float64
ClientID string
ClientOrderID string
LastUpdated time.Time
Date time.Time
@@ -222,6 +223,7 @@ type Detail struct {
Pair currency.Pair
MarginType margin.Type
Trades []TradeHistory
SettlementCurrency currency.Code
}
// Filter contains all properties an order can be filtered for