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

@@ -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
}