Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GateIO: Split Futures into USDT and CoinM #1786

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
16 changes: 16 additions & 0 deletions .github/workflows/config-versions-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: configs-versions-lint
on: [push, pull_request]
env:
GO_VERSION: 1.23.x
jobs:
lint:
name: config versions lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Check config versions are continuous
run: go test ./config/versions/ -tags config_versions -run Continuity
20 changes: 20 additions & 0 deletions config/versions/continuity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//go:build config_versions
// +build config_versions

// This test is run independently from CI for developer convenience when developing out-of-sequence versions
// Called from a separate github workflow to prevent a PR from being merged without failing the main unit tests

package versions

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestVersionContinuity(t *testing.T) {
t.Parallel()
for ver, v := range Manager.versions {
assert.NotNilf(t, v, "Version %d should not be empty", ver)
}
}
7 changes: 5 additions & 2 deletions config/versions/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ type PairsManager struct {
Pairs FullStore `json:"pairs"`
}

// FullStore contains a pair store by asset name
type FullStore map[string]struct {
// FullStore holds all supported asset types with the enabled and available pairs for an exchange.
type FullStore map[string]*PairStore

// PairStore contains a pair store
type PairStore struct {
Enabled string `json:"enabled"`
Available string `json:"available"`
RequestFormat *v0.PairFormat `json:"requestFormat,omitempty"`
Expand Down
27 changes: 27 additions & 0 deletions config/versions/v2/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package v2

import (
v0 "github.com/thrasher-corp/gocryptotrader/config/versions/v0"
)

// PairsManager contains exchange pair management config
type PairsManager struct {
BypassConfigFormatUpgrades bool `json:"bypassConfigFormatUpgrades"`
RequestFormat *v0.PairFormat `json:"requestFormat,omitempty"`
ConfigFormat *v0.PairFormat `json:"configFormat,omitempty"`
UseGlobalFormat bool `json:"useGlobalFormat,omitempty"`
LastUpdated int64 `json:"lastUpdated,omitempty"`
Pairs FullStore `json:"pairs"`
}

// FullStore holds all supported asset types with the enabled and available pairs for an exchange.
type FullStore map[string]*PairStore

// PairStore contains a pair store
type PairStore struct {
AssetEnabled bool `json:"assetEnabled"`
Enabled string `json:"enabled"`
Available string `json:"available"`
RequestFormat *v0.PairFormat `json:"requestFormat,omitempty"`
ConfigFormat *v0.PairFormat `json:"configFormat,omitempty"`
}
95 changes: 95 additions & 0 deletions config/versions/v5.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package versions

import (
"context"
"encoding/json"
"strings"

"github.com/buger/jsonparser"
v2 "github.com/thrasher-corp/gocryptotrader/config/versions/v2"
)

// Version5 is an ExchangeVersion to split GateIO futures into CoinM and USDT margined futures assets
type Version5 struct{}

func init() {
Manager.registerVersion(5, &Version5{})
}

// Exchanges returns just GateIO
func (v *Version5) Exchanges() []string { return []string{"GateIO"} }

// UpgradeExchange split GateIO futures into CoinM and USDT margined futures assets
func (v *Version5) UpgradeExchange(_ context.Context, e []byte) ([]byte, error) {
fs := v2.FullStore{"coinmarginedfutures": {}, "usdtmarginedfutures": {}}
fsJSON, _, _, err := jsonparser.Get(e, "currencyPairs", "pairs")
if err != nil {
return e, err
}
if err := json.Unmarshal(fsJSON, &fs); err != nil {
return e, err
}
futures, ok := fs["futures"]
if !ok {
// Not our job to add CoinM, USDT. Only to split them
return e, nil
}
for _, p := range strings.Split(futures.Available, ",") {
where := "usdtmarginedfutures"
if strings.HasSuffix(p, "USD") {
where = "coinmarginedfutures"
}
if fs[where].Available != "" {
fs[where].Available += ","
}
fs[where].Available += p
}
for _, p := range strings.Split(futures.Enabled, ",") {
where := "usdtmarginedfutures"
if strings.HasSuffix(p, "USD") {
where = "coinmarginedfutures"
}
if fs[where].Enabled != "" {
fs[where].Enabled += ","
}
fs[where].Enabled += p
}
fs["usdtmarginedfutures"].AssetEnabled = futures.AssetEnabled
fs["coinmarginedfutures"].AssetEnabled = futures.AssetEnabled
delete(fs, "futures")
val, err := json.Marshal(fs)
if err == nil {
e, err = jsonparser.Set(e, val, "currencyPairs", "pairs")
}
return e, err
}

// DowngradeExchange will merge GateIO CoinM and USDT margined futures assets into futures
func (v *Version5) DowngradeExchange(_ context.Context, e []byte) ([]byte, error) {
fs := v2.FullStore{"futures": {}, "coinmarginedfutures": {}, "usdtmarginedfutures": {}}
fsJSON, _, _, err := jsonparser.Get(e, "currencyPairs", "pairs")
if err != nil {
return e, err
}
if err := json.Unmarshal(fsJSON, &fs); err != nil {
return e, err
}
fs["futures"].Enabled = fs["coinmarginedfutures"].Enabled
if fs["futures"].Enabled != "" {
fs["futures"].Enabled += ","
}
fs["futures"].Enabled += fs["usdtmarginedfutures"].Enabled
fs["futures"].Available = fs["coinmarginedfutures"].Available
if fs["futures"].Available != "" {
fs["futures"].Available += ","
}
fs["futures"].Available += fs["usdtmarginedfutures"].Available
fs["futures"].AssetEnabled = fs["usdtmarginedfutures"].AssetEnabled || fs["coinmarginedfutures"].AssetEnabled
delete(fs, "coinmarginedfutures")
delete(fs, "usdtmarginedfutures")
val, err := json.Marshal(fs)
if err == nil {
e, err = jsonparser.Set(e, val, "currencyPairs", "pairs")
}
return e, err
}
66 changes: 66 additions & 0 deletions config/versions/v5_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package versions

import (
"context"
"encoding/json"
"testing"

"github.com/buger/jsonparser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestVersion5ExchangeType(t *testing.T) {
t.Parallel()
assert.Implements(t, (*ExchangeVersion)(nil), new(Version5))
}

func TestVersion5Exchanges(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{"GateIO"}, new(Version5).Exchanges())
}

func TestVersion5Upgrade(t *testing.T) {
t.Parallel()

in := []byte(`{"name":"GateIO","currencyPairs":{}}`)
_, err := new(Version5).UpgradeExchange(context.Background(), in)
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError)

in = []byte(`{"name":"GateIO","currencyPairs":{"pairs":14}}`)
_, err = new(Version5).UpgradeExchange(context.Background(), in)
require.Error(t, err)
var jsonErr *json.UnmarshalTypeError
assert.ErrorAs(t, err, &jsonErr, "UpgradeExchange should return a json.UnmarshalTypeError on bad type for pairs")

in = []byte(`{"name":"GateIO","currencyPairs":{"pairs":{"spot":{"assetEnabled":true,"enabled":"BTC-USDT","available":"BTC-USDT"},"futures":{"assetEnabled":true,"enabled":"BTC_USD,BTC_USDT,ETH_USDT","available":"BTC_USD,BTC_USDT,ETH_USDT,LTC_USDT"}}}}`)
out, err := new(Version5).UpgradeExchange(context.Background(), in)
require.NoError(t, err)
exp := `{"name":"GateIO","currencyPairs":{"pairs":{"coinmarginedfutures":{"assetEnabled":true,"enabled":"BTC_USD","available":"BTC_USD"},"spot":{"assetEnabled":true,"enabled":"BTC-USDT","available":"BTC-USDT"},"usdtmarginedfutures":{"assetEnabled":true,"enabled":"BTC_USDT,ETH_USDT","available":"BTC_USDT,ETH_USDT,LTC_USDT"}}}}`
assert.Equal(t, exp, string(out))

out, err = new(Version5).UpgradeExchange(context.Background(), out)
require.NoError(t, err)
assert.Equal(t, exp, string(out), "UpgradeExchange without futures should not alter the new entries")
}

func TestVersion5Downgrade(t *testing.T) {
t.Parallel()

in := []byte(`{"name":"GateIO","currencyPairs":{}}`)
_, err := new(Version5).DowngradeExchange(context.Background(), in)
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError)

in = []byte(`{"name":"GateIO","currencyPairs":{"pairs":14}}`)
_, err = new(Version5).DowngradeExchange(context.Background(), in)
require.Error(t, err)
var jsonErr *json.UnmarshalTypeError
assert.ErrorAs(t, err, &jsonErr)

in = []byte(`{"name":"GateIO","currencyPairs":{"pairs":{"spot":{"assetEnabled":true,"enabled":"BTC-USDT","available":"BTC-USDT"},"coinmarginedfutures":{"assetEnabled":true,"enabled":"BTC_USD","available":"BTC_USD"},"usdtmarginedfutures":{"assetEnabled":true,"enabled":"BTC_USDT,ETH_USDT","available":"BTC_USDT,ETH_USDT,LTC_USDT"}}}}`)
out, err := new(Version5).DowngradeExchange(context.Background(), in)
require.NoError(t, err)

exp := `{"name":"GateIO","currencyPairs":{"pairs":{"futures":{"assetEnabled":true,"enabled":"BTC_USD,BTC_USDT,ETH_USDT","available":"BTC_USD,BTC_USDT,ETH_USDT,LTC_USDT"},"spot":{"assetEnabled":true,"enabled":"BTC-USDT","available":"BTC-USDT"}}}}`
assert.Equal(t, exp, string(out))
}
6 changes: 1 addition & 5 deletions config/versions/versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
)

var (
errMissingVersion = errors.New("missing version")
errVersionIncompatible = errors.New("version does not implement ConfigVersion or ExchangeVersion")
errModifyingExchange = errors.New("error modifying exchange config")
errNoVersions = errors.New("error retrieving latest config version: No config versions are registered")
Expand Down Expand Up @@ -186,13 +185,10 @@ func (m *manager) checkVersions() error {
defer m.m.RUnlock()
for ver, v := range m.versions {
switch v.(type) {
case ExchangeVersion, ConfigVersion:
case ExchangeVersion, ConfigVersion, nil:
default:
return fmt.Errorf("%w: %v", errVersionIncompatible, ver)
}
if v == nil {
return fmt.Errorf("%w: v%v", errMissingVersion, ver)
}
}
return nil
}
5 changes: 1 addition & 4 deletions currency/pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@ func NewPairWithDelimiter(base, quote, delimiter string) Pair {
// with or without delimiter
func NewPairFromString(currencyPair string) (Pair, error) {
if len(currencyPair) < 3 {
return EMPTYPAIR,
fmt.Errorf("%w from %s string too short to be a currency pair",
errCannotCreatePair,
currencyPair)
return EMPTYPAIR, fmt.Errorf("%w from %s string too short to be a currency pair", errCannotCreatePair, currencyPair)
}

for x := range currencyPair {
Expand Down
28 changes: 8 additions & 20 deletions exchanges/gateio/gateio.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ const (
gateioFuturesLiveTradingAlternative = "https://fx-api.gateio.ws/" + gateioAPIVersion
gateioAPIVersion = "api/v4/"
tradeBaseURL = "https://www.gate.io/"
tradeSpot = "trade/"
tradeFutures = "futures/usdt/"
tradeDelivery = "futures-delivery/usdt/"

// SubAccount Endpoints
subAccounts = "sub_accounts"
Expand Down Expand Up @@ -140,7 +137,7 @@ var (
errInvalidOrderSize = errors.New("invalid order size")
errInvalidOrderID = errors.New("invalid order id")
errInvalidAmount = errors.New("invalid amount")
errInvalidOrEmptySubaccount = errors.New("invalid or empty subaccount")
errInvalidSubAccount = errors.New("invalid or empty subaccount")
errInvalidTransferDirection = errors.New("invalid transfer direction")
errInvalidOrderSide = errors.New("invalid order side")
errDifferentAccount = errors.New("account type must be identical for all orders")
Expand All @@ -166,7 +163,8 @@ var (
errMultipleOrders = errors.New("multiple orders passed")
errMissingWithdrawalID = errors.New("missing withdrawal ID")
errInvalidSubAccountUserID = errors.New("sub-account user id is required")
errCannotParseSettlementCurrency = errors.New("cannot derive settlement currency")
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-`")
)
Expand Down Expand Up @@ -1186,7 +1184,7 @@ func (g *Gateio) SubAccountTransfer(ctx context.Context, arg SubAccountTransferP
return currency.ErrCurrencyCodeEmpty
}
if arg.SubAccount == "" {
return errInvalidOrEmptySubaccount
return errInvalidSubAccount
}
arg.Direction = strings.ToLower(arg.Direction)
if arg.Direction != "to" && arg.Direction != "from" {
Expand All @@ -1195,8 +1193,10 @@ func (g *Gateio) SubAccountTransfer(ctx context.Context, arg SubAccountTransferP
if arg.Amount <= 0 {
return errInvalidAmount
}
if arg.SubAccountType != "" && arg.SubAccountType != asset.Spot.String() && arg.SubAccountType != asset.Futures.String() && arg.SubAccountType != asset.CrossMargin.String() {
return fmt.Errorf("%v; only %v,%v, and %v are allowed", asset.ErrNotSupported, asset.Spot, asset.Futures, asset.CrossMargin)
switch arg.SubAccountType {
case asset.Empty, asset.Spot, asset.Futures, asset.DeliveryFutures:
default:
return fmt.Errorf("%w: `%s`", asset.ErrNotSupported, arg.SubAccountType)
}
return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletSubAccountTransferEPL, http.MethodPost, walletSubAccountTransfer, nil, &arg, nil)
}
Expand Down Expand Up @@ -3674,15 +3674,3 @@ func (g *Gateio) GetUnderlyingFromCurrencyPair(p currency.Pair) (currency.Pair,
}
return currency.Pair{Base: currency.NewCode(ccies[0]), Delimiter: currency.UnderscoreDelimiter, Quote: currency.NewCode(ccies[1])}, nil
}
func getSettlementFromCurrency(currencyPair currency.Pair) (settlement currency.Code, err error) {
quote := currencyPair.Quote.Upper().String()

switch {
case strings.HasPrefix(quote, currency.USDT.String()):
return currency.USDT, nil
case strings.HasPrefix(quote, currency.USD.String()):
return currency.BTC, nil
default:
return currency.EMPTYCODE, fmt.Errorf("%w %v", errCannotParseSettlementCurrency, currencyPair)
}
}
Loading
Loading