GateIO: Split futures into USDTM and CoinM futures (#1786)

* Config: v5 Split GateIO futures into CoinM and USDT

* GateIO: Split asset.Futures into CoinM and USDT

* Fix CancelBatchOrders using wrong endpoint for CoinMarginedFutures
* Fix TestGetActiveOrders expecting currency.ErrCurrencyPairsEmpty

* Config: Add config version continuity step to CI

* GateIO: Pin CoinM futures to just BTC/USD

Right now we only have a /btc endpoint available, and only BTCUSD is
available.
If GateIO offers more, we'll need to add a settlement currencies list
again
This commit is contained in:
Gareth Kirwan
2025-04-30 07:39:39 +02:00
committed by GitHub
parent 977fecab19
commit 88ac5274c9
16 changed files with 1201 additions and 1849 deletions

View File

@@ -0,0 +1,16 @@
name: configs-versions-lint
on: [push, pull_request]
env:
GO_VERSION: 1.24.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

View File

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

View File

@@ -8,6 +8,7 @@ import (
v4 "github.com/thrasher-corp/gocryptotrader/config/versions/v4"
v5 "github.com/thrasher-corp/gocryptotrader/config/versions/v5"
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
v7 "github.com/thrasher-corp/gocryptotrader/config/versions/v7"
)
func init() {
@@ -18,4 +19,5 @@ func init() {
Manager.registerVersion(4, &v4.Version{})
Manager.registerVersion(5, &v5.Version{})
Manager.registerVersion(6, &v6.Version{})
Manager.registerVersion(7, &v7.Version{})
}

View File

@@ -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"`

View File

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

92
config/versions/v7/v7.go Normal file
View File

@@ -0,0 +1,92 @@
package v7
import (
"context"
"encoding/json" //nolint:depguard // Used instead of gct encoding/json so that we can ensure consistent library functionality between versions
"strings"
"github.com/buger/jsonparser"
v2 "github.com/thrasher-corp/gocryptotrader/config/versions/v2"
)
// Version is an ExchangeVersion to split GateIO futures into CoinM and USDT margined futures assets
type Version struct{}
// Exchanges returns just GateIO
func (v *Version) Exchanges() []string { return []string{"GateIO"} }
// UpgradeExchange split GateIO futures into CoinM and USDT margined futures assets
func (v *Version) 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
}
f, ok := fs["futures"]
if !ok {
// Version.UpgradeExchange should only split futures into CoinM and USDT
// If the exchange config doesn't have futures, we have nothing to do
return e, nil
}
for p := range strings.SplitSeq(f.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.SplitSeq(f.Enabled, ",") {
where := "usdtmarginedfutures"
if strings.HasSuffix(p, "USD") {
where = "coinmarginedfutures"
}
if fs[where].Enabled != "" {
fs[where].Enabled += ","
}
fs[where].Enabled += p
}
fs["usdtmarginedfutures"].AssetEnabled = f.AssetEnabled
fs["coinmarginedfutures"].AssetEnabled = f.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 *Version) 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
}

View File

@@ -0,0 +1,62 @@
package v7_test
import (
"context"
"encoding/json" //nolint:depguard // Used instead of gct encoding/json so that we can ensure consistent library functionality between versions
"testing"
"github.com/buger/jsonparser"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v7 "github.com/thrasher-corp/gocryptotrader/config/versions/v7"
)
func TestExchanges(t *testing.T) {
t.Parallel()
assert.Equal(t, []string{"GateIO"}, new(v7.Version).Exchanges())
}
func TestUpgrade(t *testing.T) {
t.Parallel()
in := []byte(`{"name":"GateIO","currencyPairs":{}}`)
_, err := new(v7.Version).UpgradeExchange(context.Background(), in)
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError)
in = []byte(`{"name":"GateIO","currencyPairs":{"pairs":14}}`)
_, err = new(v7.Version).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(v7.Version).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(v7.Version).UpgradeExchange(context.Background(), out)
require.NoError(t, err)
assert.Equal(t, exp, string(out), "UpgradeExchange without futures should not alter the new entries")
}
func TestDowngrade(t *testing.T) {
t.Parallel()
in := []byte(`{"name":"GateIO","currencyPairs":{}}`)
_, err := new(v7.Version).DowngradeExchange(context.Background(), in)
require.ErrorIs(t, err, jsonparser.KeyPathNotFoundError)
in = []byte(`{"name":"GateIO","currencyPairs":{"pairs":14}}`)
_, err = new(v7.Version).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,WIF-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(v7.Version).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,WIF-USDT"}}}}`
assert.Equal(t, exp, string(out))
}

View File

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

View File

@@ -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"
@@ -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")
errDifferentAccount = errors.New("account type must be identical for all orders")
errInvalidPrice = errors.New("invalid price")
@@ -165,7 +162,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-`")
)
@@ -1185,7 +1183,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" {
@@ -1194,8 +1192,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 "", "spot", "futures", "delivery":
default:
return fmt.Errorf("%w `%s` for SubAccountTransfer; Supported: [spot, futures, delivery]", asset.ErrNotSupported, arg.SubAccountType)
}
return g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, walletSubAccountTransferEPL, http.MethodPost, walletSubAccountTransfer, nil, &arg, nil)
}
@@ -3696,19 +3696,6 @@ 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)
}
}
// GetAccountDetails retrieves account details
func (g *Gateio) GetAccountDetails(ctx context.Context) (*AccountDetails, error) {
var resp *AccountDetails

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,6 @@ const (
sideBorrow = "borrow"
)
var settlementCurrencies = []currency.Code{currency.BTC, currency.USDT}
// WithdrawalFees the large list of predefined withdrawal fees
// Prone to change
var WithdrawalFees = map[currency.Code]float64{

View File

@@ -3,9 +3,7 @@ package gateio
import (
"context"
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
@@ -26,8 +24,8 @@ import (
)
const (
futuresWebsocketBtcURL = "wss://fx-ws.gateio.ws/v4/ws/btc"
futuresWebsocketUsdtURL = "wss://fx-ws.gateio.ws/v4/ws/usdt"
btcFuturesWebsocketURL = "wss://fx-ws.gateio.ws/v4/ws/btc"
usdtFuturesWebsocketURL = "wss://fx-ws.gateio.ws/v4/ws/usdt"
futuresPingChannel = "futures.ping"
futuresTickersChannel = "futures.tickers"
@@ -59,12 +57,14 @@ var defaultFuturesSubscriptions = []string{
// WsFuturesConnect initiates a websocket connection for futures account
func (g *Gateio) WsFuturesConnect(ctx context.Context, conn websocket.Connection) error {
err := g.CurrencyPairs.IsAssetEnabled(asset.Futures)
if err != nil {
a := asset.USDTMarginedFutures
if conn.GetURL() == btcFuturesWebsocketURL {
a = asset.CoinMarginedFutures
}
if err := g.CurrencyPairs.IsAssetEnabled(a); err != nil {
return err
}
err = conn.DialContext(ctx, &gws.Dialer{}, http.Header{})
if err != nil {
if err := conn.DialContext(ctx, &gws.Dialer{}, http.Header{}); err != nil {
return err
}
pingMessage, err := json.Marshal(WsInput{
@@ -85,13 +85,13 @@ func (g *Gateio) WsFuturesConnect(ctx context.Context, conn websocket.Connection
}
// GenerateFuturesDefaultSubscriptions returns default subscriptions information.
func (g *Gateio) GenerateFuturesDefaultSubscriptions(settlement currency.Code) (subscription.List, error) {
func (g *Gateio) GenerateFuturesDefaultSubscriptions(a asset.Item) (subscription.List, error) {
channelsToSubscribe := defaultFuturesSubscriptions
if g.Websocket.CanUseAuthenticatedEndpoints() {
channelsToSubscribe = append(channelsToSubscribe, futuresOrdersChannel, futuresUserTradesChannel, futuresBalancesChannel)
}
pairs, err := g.GetEnabledPairs(asset.Futures)
pairs, err := g.GetEnabledPairs(a)
if err != nil {
if errors.Is(err, asset.ErrNotEnabled) {
return nil, nil // no enabled pairs, subscriptions require an associated pair.
@@ -99,15 +99,6 @@ func (g *Gateio) GenerateFuturesDefaultSubscriptions(settlement currency.Code) (
return nil, err
}
switch {
case settlement.Equal(currency.USDT):
pairs = slices.DeleteFunc(pairs, func(p currency.Pair) bool { return !p.Quote.Equal(currency.USDT) })
case settlement.Equal(currency.BTC):
pairs = slices.DeleteFunc(pairs, func(p currency.Pair) bool { return p.Quote.Equal(currency.USDT) })
default:
return nil, fmt.Errorf("settlement currency %s not supported", settlement)
}
var subscriptions subscription.List
for i := range channelsToSubscribe {
for j := range pairs {
@@ -122,7 +113,7 @@ func (g *Gateio) GenerateFuturesDefaultSubscriptions(settlement currency.Code) (
params["frequency"] = kline.ThousandMilliseconds
params["level"] = "100"
}
fPair, err := g.FormatExchangeCurrency(pairs[j], asset.Futures)
fPair, err := g.FormatExchangeCurrency(pairs[j], a)
if err != nil {
return nil, err
}
@@ -130,7 +121,7 @@ func (g *Gateio) GenerateFuturesDefaultSubscriptions(settlement currency.Code) (
Channel: channelsToSubscribe[i],
Pairs: currency.Pairs{fPair.Upper()},
Params: params,
Asset: asset.Futures,
Asset: a,
})
}
}

View File

@@ -168,37 +168,11 @@ func (g *Gateio) WebsocketFuturesGetOrderStatus(ctx context.Context, contract cu
return &resp, g.SendWebsocketRequest(ctx, perpetualFetchOrderEPL, "futures.order_status", a, params, &resp, 1)
}
func getAssetFromFuturesPair(pair currency.Pair) (asset.Item, error) {
if pair.IsEmpty() {
return asset.Empty, currency.ErrCurrencyPairEmpty
}
switch pair.Quote.Item {
case currency.USDT.Item:
return asset.USDTMarginedFutures, nil
case currency.USD.Item:
return asset.CoinMarginedFutures, nil
default:
return asset.Empty, fmt.Errorf("%w futures pair: `%v`", asset.ErrNotSupported, pair)
}
}
// validateFuturesPairAsset enforces the asset.Item to be either USDT or Coin margined futures in relation to the pair
// for correct routing.
// validateFuturesPairAsset enforces that a futures pair's quote currency matches the given asset
func validateFuturesPairAsset(pair currency.Pair, a asset.Item) error {
if pair.IsEmpty() {
return currency.ErrCurrencyPairEmpty
}
switch a {
case asset.USDTMarginedFutures:
if pair.Quote.Item != currency.USDT.Item {
return fmt.Errorf("%w: '%v' for pair '%v'", asset.ErrNotSupported, a, pair)
}
case asset.CoinMarginedFutures:
if pair.Quote.Item != currency.USD.Item {
return fmt.Errorf("%w: '%v' for pair '%v'", asset.ErrNotSupported, a, pair)
}
default:
return fmt.Errorf("%w: '%v' for pair '%v'", asset.ErrNotSupported, a, pair)
}
return nil
_, err := getSettlementCurrency(pair, a)
return err
}

View File

@@ -93,7 +93,7 @@ func TestWebsocketFuturesCancelOrder(t *testing.T) {
_, err = g.WebsocketFuturesCancelOrder(t.Context(), "42069", currency.EMPTYPAIR, asset.Empty)
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
_, err = g.WebsocketFuturesCancelOrder(t.Context(), "42069", BTCUSDT, asset.CoinMarginedFutures)
_, err = g.WebsocketFuturesCancelOrder(t.Context(), "42069", BTCUSDT, asset.Empty)
require.ErrorIs(t, err, asset.ErrNotSupported)
sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders)
@@ -204,32 +204,3 @@ func TestWebsocketFuturesGetOrderStatus(t *testing.T) {
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestGetAssetFromFuturesPair(t *testing.T) {
t.Parallel()
_, err := getAssetFromFuturesPair(currency.Pair{})
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
_, err = getAssetFromFuturesPair(currency.NewPair(currency.BTC, currency.USDC))
require.ErrorIs(t, err, asset.ErrNotSupported)
a, err := getAssetFromFuturesPair(BTCUSDT)
require.NoError(t, err)
require.Equal(t, asset.USDTMarginedFutures, a)
a, err = getAssetFromFuturesPair(BTCUSD)
require.NoError(t, err)
require.Equal(t, asset.CoinMarginedFutures, a)
}
func TestValidateFuturesPairAsset(t *testing.T) {
t.Parallel()
err := validateFuturesPairAsset(currency.Pair{}, asset.USDTMarginedFutures)
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
err = validateFuturesPairAsset(BTCUSDT, asset.USDTMarginedFutures)
require.NoError(t, err)
err = validateFuturesPairAsset(BTCUSD, asset.USDTMarginedFutures)
require.ErrorIs(t, err, asset.ErrNotSupported)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{"time":1541659086,"channel":"futures.tickers","event":"update","error":null,"result":[{"contract":"BTC_USD","last":"118.4","change_percentage":"0.77","funding_rate":"-0.000114","funding_rate_indicative":"0.01875","mark_price":"118.35","index_price":"118.36","total_size":"73648","volume_24h":"745487577","volume_24h_btc":"117","volume_24h_usd":"419950","quanto_base_rate":"","volume_24h_quote":"1665006","volume_24h_settle":"178","volume_24h_base":"5526","low_24h":"99.2","high_24h":"132.5"}]}
{"channel":"futures.trades","event":"update","time":1541503698,"result":[{"size":-108,"id":27753479,"create_time":1545136464,"create_time_ms":1545136464123,"price":"96.4","contract":"BTC_USD"}]}
{"time":1615366379,"channel":"futures.book_ticker","event":"update","error":null,"result":{"t":1615366379123,"u":2517661076,"s":"BTC_USD","b":"54696.6","B":37000,"a":"54696.7","A":47061}}
{"channel":"futures.orders","event":"update","time":1541505434,"result":[{"contract":"BTC_USD","create_time":1628736847,"create_time_ms":1628736847325,"fill_price":40000.4,"finish_as":"filled","finish_time":1628736848,"finish_time_ms":1628736848321,"iceberg":0,"id":4872460,"is_close":false,"is_liq":false,"is_reduce_only":false,"left":0,"mkfr":-0.00025,"price":40000.4,"refr":0,"refu":0,"size":1,"status":"finished","text":"-","tif":"gtc","tkfr":0.0005,"user":"110xxxxx"}]}
{"time":1543205083,"channel":"futures.usertrades","event":"update","error":null,"result":[{"id":"3335259","create_time":1628736848,"create_time_ms":1628736848321,"contract":"BTC_USD","order_id":"4872460","size":1,"price":"40000.4","role":"maker","text":"api","fee":0.0009290592,"point_fee":0}]}
{"channel":"futures.liquidates","event":"update","time":1541505434,"result":[{"entry_price":209,"fill_price":215.1,"left":0,"leverage":0,"liq_price":213,"margin":0.007816722941,"mark_price":213,"order_id":4093362,"order_price":215.1,"size":-124,"time":1541486601,"time_ms":1541486601123,"contract":"BTC_USD","user":"1040xxxx"}]}
{"channel": "futures.auto_deleverages", "event": "update", "time": 1541505434, "result": [{"entry_price": 209,"fill_price": 215.1,"position_size": 10,"trade_size": 10,"time": 1541486601,"time_ms": 1541486601123,"contract": "BTC_USD","user": "1040"} ]}
{"channel":"futures.position_closes","event":"update","time":1541505434,"result":[{"contract":"BTC_USD","pnl":-0.000624354791,"side":"long","text":"web","time":1547198562,"time_ms":1547198562123,"user":"211xxxx"}]}
{"channel":"futures.balances","event":"update","time":1541505434,"result":[{"balance":9.998739899488,"change":-2.074115e-06,"text":"BTC_USD:3914424","time":1547199246,"time_ms":1547199246123,"type":"fee","user":"211xxx"}]}
{"time":1551858330,"channel":"futures.reduce_risk_limits","event":"update","error":null,"result":[{"cancel_orders":0,"contract":"BTC_USD","leverage_max":10,"liq_price":136.53,"maintenance_rate":0.09,"risk_limit":450,"time":1551858330,"time_ms":1551858330123,"user":"20011"}]}
{"time": 1588212926,"channel": "futures.positions", "event": "update", "error": null, "result": [ { "contract": "BTC_USD", "cross_leverage_limit": 0, "entry_price": 40000.36666661111, "history_pnl": -0.000108569505, "history_point": 0, "last_close_pnl": -0.000050123368,"leverage": 0,"leverage_max": 100,"liq_price": 0.1,"maintenance_rate": 0.005,"margin": 49.999890611186,"mode": "single","realised_pnl": -1.25e-8,"realised_point": 0,"risk_limit": 100,"size": 3,"time": 1628736848,"time_ms": 1628736848321,"user": "110xxxxx"}]}
{"time":1596798126,"channel":"futures.autoorders","event":"update","error":null,"result":[{"user":123456,"trigger":{"strategy_type":0,"price_type":0,"price":"10000","rule":2,"expiration":86400},"initial":{"contract":"BTC_USDT","size":10,"price":"10000","tif":"gtc","text":"web","iceberg":0,"is_close":false,"is_reduce_only":false},"id":9256,"trade_id":0,"status":"open","reason":"","create_time":1596798126,"name":"price_autoorders","is_stop_order":false,"stop_trigger":{"rule":0,"trigger_price":"","order_price":""}}]}
{"time":1678468497,"time_ms":1678468497232,"channel":"futures.order_book","event":"all","result":{"t":1678468497168,"id":4010394406,"contract":"BTC_USD","asks":[{"p":"19909","s":3100},{"p":"19909.1","s":5000},{"p":"19910","s":3100},{"p":"19914.4","s":4400},{"p":"19916.6","s":5000},{"p":"19917.2","s":8255},{"p":"19919.2","s":5000},{"p":"19920.3","s":11967},{"p":"19922.2","s":5000},{"p":"19924.2","s":5000},{"p":"19927.1","s":17129},{"p":"19927.2","s":5000},{"p":"19929","s":20864},{"p":"19929.3","s":5000},{"p":"19929.7","s":24683},{"p":"19930.3","s":750},{"p":"19931.4","s":5000},{"p":"19931.5","s":1},{"p":"19934.2","s":5000},{"p":"19935.4","s":1}],"bids":[{"p":"19901.2","s":5000},{"p":"19900.3","s":3100},{"p":"19900.2","s":5000},{"p":"19899.3","s":2983},{"p":"19899.2","s":6035},{"p":"19897.2","s":5000},{"p":"19895.7","s":5984},{"p":"19895","s":5000},{"p":"19892.9","s":195},{"p":"19892.8","s":5000},{"p":"19889.4","s":5000},{"p":"19889","s":8800},{"p":"19888.5","s":11968},{"p":"19887.1","s":5000},{"p":"19886.4","s":24683},{"p":"19885.7","s":1},{"p":"19883.8","s":5000},{"p":"19880.2","s":5000},{"p":"19878.2","s":5000},{"p":"19876.8","s":1}]}}
{"time":1678469222,"time_ms":1678469222982,"channel":"futures.order_book_update","event":"update","result":{"t":1678469222617,"s":"BTC_USD","U":4010424331,"u":4010424361,"b":[{"p":"19860.7","s":5984},{"p":"19858.6","s":5000},{"p":"19845.4","s":20864},{"p":"19859.1","s":0},{"p":"19862.5","s":0},{"p":"19358","s":0},{"p":"19864.5","s":5000},{"p":"19840.7","s":0},{"p":"19863.6","s":3100},{"p":"19839.3","s":0},{"p":"19851.5","s":8800},{"p":"19720","s":0},{"p":"19333","s":0},{"p":"19852.7","s":5000},{"p":"19861.5","s":0},{"p":"19860.6","s":3100},{"p":"19833.6","s":0},{"p":"19360","s":0},{"p":"19863.5","s":5000},{"p":"19736.9","s":0},{"p":"19838.5","s":0},{"p":"19841.3","s":0},{"p":"19858.1","s":3100},{"p":"19710.9","s":0},{"p":"19342","s":0},{"p":"19852.1","s":11967},{"p":"19343","s":0},{"p":"19705","s":0},{"p":"19836.5","s":0},{"p":"19862.6","s":3100},{"p":"19729.6","s":0},{"p":"19849.9","s":5000}],"a":[{"p":"19900.5","s":0},{"p":"19883.1","s":11967},{"p":"19910.9","s":0},{"p":"19897.7","s":5000},{"p":"19875.9","s":5984},{"p":"19899.6","s":0},{"p":"19878","s":4400},{"p":"19877.6","s":0},{"p":"19889.5","s":5000},{"p":"19875.5","s":3100},{"p":"19875.3","s":0},{"p":"19878.5","s":0},{"p":"19895.2","s":0},{"p":"20284.6","s":0},{"p":"19880.7","s":5000},{"p":"19875.4","s":0},{"p":"19985.8","s":0},{"p":"19887.1","s":5000},{"p":"19896","s":1},{"p":"19869.3","s":0},{"p":"19900","s":0},{"p":"19875.6","s":5000},{"p":"19980.6","s":0},{"p":"19885.1","s":5000},{"p":"19877.7","s":5000},{"p":"20000","s":0},{"p":"19892.2","s":8255},{"p":"19886.8","s":0},{"p":"20257.4","s":0},{"p":"20280","s":0},{"p":"20002.5","s":0},{"p":"20263.1","s":0},{"p":"19900.2","s":0}]}}
{"time":1678469467,"time_ms":1678469467981,"channel":"futures.candlesticks","event":"update","result":[{"t":1678469460,"v":0,"c":"19896","h":"19896","l":"19896","o":"19896","n":"1m_BTC_USD"}]}