1975 lines
88 KiB
Python
1975 lines
88 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
import ccxt.async_support
|
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Bool, Int, Liquidation, Market, MarketType, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import NotSupported
|
|
from ccxt.base.errors import ChecksumError
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class gate(ccxt.async_support.gate):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(gate, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'cancelAllOrdersWs': True,
|
|
'cancelOrderWs': True,
|
|
'createMarketBuyOrderWithCostWs': True,
|
|
'createMarketOrderWs': True,
|
|
'createMarketOrderWithCostWs': False,
|
|
'createMarketSellOrderWithCostWs': False,
|
|
'createOrderWs': True,
|
|
'createOrdersWs': True,
|
|
'createPostOnlyOrderWs': True,
|
|
'createReduceOnlyOrderWs': True,
|
|
'createStopLimitOrderWs': True,
|
|
'createStopLossOrderWs': True,
|
|
'createStopMarketOrderWs': False,
|
|
'createStopOrderWs': True,
|
|
'createTakeProfitOrderWs': True,
|
|
'createTriggerOrderWs': True,
|
|
'editOrderWs': True,
|
|
'fetchOrderWs': True,
|
|
'fetchOrdersWs': False,
|
|
'fetchOpenOrdersWs': True,
|
|
'fetchClosedOrdersWs': True,
|
|
'watchOrderBook': True,
|
|
'watchBidsAsks': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
'watchMyTrades': True,
|
|
'watchOHLCV': True,
|
|
'watchBalance': True,
|
|
'watchOrders': True,
|
|
'watchLiquidations': False,
|
|
'watchLiquidationsForSymbols': False,
|
|
'watchMyLiquidations': True,
|
|
'watchMyLiquidationsForSymbols': True,
|
|
'watchPositions': True,
|
|
},
|
|
'urls': {
|
|
'api': {
|
|
'ws': 'wss://ws.gate.io/v4',
|
|
'spot': 'wss://api.gateio.ws/ws/v4/',
|
|
'swap': {
|
|
'usdt': 'wss://fx-ws.gateio.ws/v4/ws/usdt',
|
|
'btc': 'wss://fx-ws.gateio.ws/v4/ws/btc',
|
|
},
|
|
'future': {
|
|
'usdt': 'wss://fx-ws.gateio.ws/v4/ws/delivery/usdt',
|
|
'btc': 'wss://fx-ws.gateio.ws/v4/ws/delivery/btc',
|
|
},
|
|
'option': {
|
|
'usdt': 'wss://op-ws.gateio.live/v4/ws/usdt',
|
|
'btc': 'wss://op-ws.gateio.live/v4/ws/btc',
|
|
},
|
|
},
|
|
'test': {
|
|
'swap': {
|
|
'usdt': 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt',
|
|
'btc': 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc',
|
|
},
|
|
'future': {
|
|
'usdt': 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt',
|
|
'btc': 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc',
|
|
},
|
|
'option': {
|
|
'usdt': 'wss://op-ws-testnet.gateio.live/v4/ws/usdt',
|
|
'btc': 'wss://op-ws-testnet.gateio.live/v4/ws/btc',
|
|
},
|
|
},
|
|
},
|
|
'options': {
|
|
'tradesLimit': 1000,
|
|
'OHLCVLimit': 1000,
|
|
'watchTradesSubscriptions': {},
|
|
'watchTickerSubscriptions': {},
|
|
'watchOrderBookSubscriptions': {},
|
|
'watchTicker': {
|
|
'name': 'tickers', # or book_ticker
|
|
},
|
|
'watchOrderBook': {
|
|
'interval': '100ms',
|
|
'snapshotDelay': 10, # how many deltas to cache before fetching a snapshot
|
|
'snapshotMaxRetries': 3,
|
|
'checksum': True,
|
|
},
|
|
'watchBalance': {
|
|
'settle': 'usdt', # or btc
|
|
'spot': 'spot.balances', # spot.margin_balances, spot.funding_balances or spot.cross_balances
|
|
},
|
|
'watchPositions': {
|
|
'fetchPositionsSnapshot': True, # or False
|
|
'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates
|
|
},
|
|
},
|
|
'exceptions': {
|
|
'ws': {
|
|
'exact': {
|
|
'1': BadRequest,
|
|
'2': BadRequest,
|
|
'4': AuthenticationError,
|
|
'6': AuthenticationError,
|
|
'11': AuthenticationError,
|
|
},
|
|
'broad': {},
|
|
},
|
|
},
|
|
})
|
|
|
|
async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#order-place
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-place
|
|
|
|
Create an order on the exchange
|
|
:param str symbol: Unified CCXT market symbol
|
|
:param str type: 'limit' or 'market' *"market" is contract only*
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: the amount of currency to trade
|
|
:param float [price]: *ignored in "market" orders* the price at which the order is to be fulfilled at in units of the quote currency
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param float [params.stopPrice]: The price at which a trigger order is triggered at
|
|
:param str [params.timeInForce]: "GTC", "IOC", or "PO"
|
|
:param float [params.stopLossPrice]: The price at which a stop loss order is triggered at
|
|
:param float [params.takeProfitPrice]: The price at which a take profit order is triggered at
|
|
:param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used
|
|
:param int [params.iceberg]: Amount to display for the iceberg order, Null or 0 for normal orders, Set to -1 to hide the order completely
|
|
:param str [params.text]: User defined information
|
|
:param str [params.account]: *spot and margin only* "spot", "margin" or "cross_margin"
|
|
:param bool [params.auto_borrow]: *margin only* Used in margin or cross margin trading to allow automatic loan of insufficient amount if balance is not enough
|
|
:param str [params.settle]: *contract only* Unified Currency Code for settle currency
|
|
:param bool [params.reduceOnly]: *contract only* Indicates if self order is to reduce the size of a position
|
|
:param bool [params.close]: *contract only* Set to close the position, with size set to 0
|
|
:param bool [params.auto_size]: *contract only* Set side to close dual-mode position, close_long closes the long side, while close_short the short one, size also needs to be set to 0
|
|
:param int [params.price_type]: *contract only* 0 latest deal price, 1 mark price, 2 index price
|
|
:param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount
|
|
:returns dict|None: `An order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_place'
|
|
url = self.get_url_by_market(market)
|
|
params['textIsRequired'] = True
|
|
request = self.create_order_request(symbol, type, side, amount, price, params)
|
|
await self.authenticate(url, messageType)
|
|
rawOrder = await self.request_private(url, request, channel)
|
|
order = self.parse_order(rawOrder, market)
|
|
return order
|
|
|
|
async def create_orders_ws(self, orders: List[OrderRequest], params={}):
|
|
"""
|
|
create a list of trade orders
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-batch-place
|
|
|
|
:param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request = self.createOrdersRequest(orders, params)
|
|
firstOrder = orders[0]
|
|
market = self.market(firstOrder['symbol'])
|
|
if market['swap'] is not True:
|
|
raise NotSupported(self.id + ' createOrdersWs is not supported for swap markets')
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_batch_place'
|
|
url = self.get_url_by_market(market)
|
|
await self.authenticate(url, messageType)
|
|
rawOrders = await self.request_private(url, request, channel)
|
|
return self.parse_orders(rawOrders, market)
|
|
|
|
async def cancel_all_orders_ws(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#cancel-all-open-orders-matched
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#order-cancel-all-with-specified-currency-pair
|
|
|
|
:param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.channel]: the channel to use, defaults to spot.order_cancel_cp or futures.order_cancel_cp
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None if (symbol is None) else self.market(symbol)
|
|
trigger = self.safe_bool_2(params, 'stop', 'trigger')
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_cancel_cp'
|
|
channel, params = self.handle_option_and_params(params, 'cancelAllOrdersWs', 'channel', channel)
|
|
url = self.get_url_by_market(market)
|
|
params = self.omit(params, ['stop', 'trigger'])
|
|
type, query = self.handle_market_type_and_params('cancelAllOrders', market, params)
|
|
request, requestParams = self.multiOrderSpotPrepareRequest(market, trigger, query) if (type == 'spot') else self.prepareRequest(market, type, query)
|
|
await self.authenticate(url, messageType)
|
|
rawOrders = await self.request_private(url, self.extend(request, requestParams), channel)
|
|
return self.parse_orders(rawOrders, market)
|
|
|
|
async def cancel_order_ws(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
Cancels an open order
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#order-cancel
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-cancel
|
|
|
|
:param str id: Order id
|
|
:param str symbol: Unified market symbol
|
|
:param dict [params]: Parameters specified by the exchange api
|
|
:param bool [params.trigger]: True if the order to be cancelled is a trigger order
|
|
:returns: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None if (symbol is None) else self.market(symbol)
|
|
trigger = self.safe_value_n(params, ['is_stop_order', 'stop', 'trigger'], False)
|
|
params = self.omit(params, ['is_stop_order', 'stop', 'trigger'])
|
|
type, query = self.handle_market_type_and_params('cancelOrder', market, params)
|
|
request, requestParams = self.spotOrderPrepareRequest(market, trigger, query) if (type == 'spot' or type == 'margin') else self.prepareRequest(market, type, query)
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_cancel'
|
|
url = self.get_url_by_market(market)
|
|
await self.authenticate(url, messageType)
|
|
request['order_id'] = str(id)
|
|
res = await self.request_private(url, self.extend(request, requestParams), channel)
|
|
return self.parse_order(res, market)
|
|
|
|
async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
|
|
"""
|
|
edit a trade order, gate currently only supports the modification of the price or amount fields
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#order-amend
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-amend
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of the currency you want to trade in units of the base currency
|
|
:param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
extendedRequest = self.edit_order_request(id, symbol, type, side, amount, price, params)
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_amend'
|
|
url = self.get_url_by_market(market)
|
|
await self.authenticate(url, messageType)
|
|
rawOrder = await self.request_private(url, extendedRequest, channel)
|
|
return self.parse_order(rawOrder, market)
|
|
|
|
async def fetch_order_ws(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
Retrieves information on an order
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#order-status
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-status
|
|
|
|
:param str id: Order id
|
|
:param str symbol: Unified market symbol, *required for spot and margin*
|
|
:param dict [params]: Parameters specified by the exchange api
|
|
:param bool [params.trigger]: True if the order being fetched is a trigger order
|
|
:param str [params.marginMode]: 'cross' or 'isolated' - marginMode for margin trading if not provided self.options['defaultMarginMode'] is used
|
|
:param str [params.type]: 'spot', 'swap', or 'future', if not provided self.options['defaultMarginMode'] is used
|
|
:param str [params.settle]: 'btc' or 'usdt' - settle currency for perpetual swap and future - market settle currency is used if symbol is not None, default="usdt" for swap and "btc" for future
|
|
:returns: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None if (symbol is None) else self.market(symbol)
|
|
request, requestParams = self.fetchOrderRequest(id, symbol, params)
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_status'
|
|
url = self.get_url_by_market(market)
|
|
await self.authenticate(url, messageType)
|
|
rawOrder = await self.request_private(url, self.extend(request, requestParams), channel)
|
|
return self.parse_order(rawOrder, market)
|
|
|
|
async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-list
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
return await self.fetch_orders_by_status_ws('open', symbol, since, limit, params)
|
|
|
|
async def fetch_closed_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple closed orders made by the user
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-list
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
return await self.fetch_orders_by_status_ws('finished', symbol, since, limit, params)
|
|
|
|
async def fetch_orders_by_status_ws(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#order-list
|
|
|
|
fetches information on multiple orders made by the user by status
|
|
:param str status: requested order status
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int|None [since]: the earliest time in ms to fetch orders for
|
|
:param int|None [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.orderId]: order id to begin at
|
|
:param int [params.limit]: the maximum number of order structures to retrieve
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
if market['swap'] is not True:
|
|
raise NotSupported(self.id + ' fetchOrdersByStatusWs is only supported by swap markets. Use rest API for other markets')
|
|
request, requestParams = self.prepareOrdersByStatusRequest(status, symbol, since, limit, params)
|
|
newRequest = self.omit(request, ['settle'])
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_list'
|
|
url = self.get_url_by_market(market)
|
|
await self.authenticate(url, messageType)
|
|
rawOrders = await self.request_private(url, self.extend(newRequest, requestParams), channel)
|
|
orders = self.parse_orders(rawOrders, market)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit)
|
|
|
|
async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://www.gate.com/docs/developers/apiv4/ws/en/#order-book-channel
|
|
https://www.gate.com/docs/developers/apiv4/ws/en/#order-book-v2-api
|
|
https://www.gate.com/docs/developers/futures/ws/en/#order-book-api
|
|
https://www.gate.com/docs/developers/futures/ws/en/#order-book-v2-api
|
|
https://www.gate.com/docs/developers/delivery/ws/en/#order-book-api
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
marketId = market['id']
|
|
interval, query = self.handle_option_and_params(params, 'watchOrderBook', 'interval', '100ms')
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_book_update'
|
|
messageHash = 'orderbook' + ':' + symbol
|
|
url = self.get_url_by_market(market)
|
|
payload = [marketId, interval]
|
|
if limit is None:
|
|
limit = 100 # max 100 atm
|
|
if market['contract']:
|
|
stringLimit = str(limit)
|
|
payload.append(stringLimit)
|
|
subscription: dict = {
|
|
'symbol': symbol,
|
|
'limit': limit,
|
|
}
|
|
orderbook = await self.subscribe_public(url, messageHash, payload, channel, query, subscription)
|
|
return orderbook.limit()
|
|
|
|
async def un_watch_order_book(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
marketId = market['id']
|
|
interval = '100ms'
|
|
interval, params = self.handle_option_and_params(params, 'watchOrderBook', 'interval', interval)
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.order_book_update'
|
|
subMessageHash = 'orderbook' + ':' + symbol
|
|
messageHash = 'unsubscribe:orderbook' + ':' + symbol
|
|
url = self.get_url_by_market(market)
|
|
payload = [marketId, interval]
|
|
limit = self.safe_integer(params, 'limit', 100)
|
|
if market['contract']:
|
|
stringLimit = str(limit)
|
|
payload.append(stringLimit)
|
|
return await self.un_subscribe_public_multiple(url, 'orderbook', [symbol], [messageHash], [subMessageHash], payload, channel, params)
|
|
|
|
def handle_order_book_subscription(self, client: Client, message, subscription):
|
|
symbol = self.safe_string(subscription, 'symbol')
|
|
limit = self.safe_integer(subscription, 'limit')
|
|
self.orderbooks[symbol] = self.order_book({}, limit)
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# "time": 1650189272,
|
|
# "channel": "spot.order_book_update",
|
|
# "event": "update",
|
|
# "result": {
|
|
# "t": 1650189272515,
|
|
# "e": "depthUpdate",
|
|
# "E": 1650189272,
|
|
# "s": "GMT_USDT",
|
|
# "U": 140595902,
|
|
# "u": 140595902,
|
|
# "b": [
|
|
# ['2.51518', "228.119"],
|
|
# ['2.50587', "1510.11"],
|
|
# ['2.49944', "67.6"],
|
|
# ],
|
|
# "a": [
|
|
# ['2.5182', "4.199"],
|
|
# ["2.51926", "1874"],
|
|
# ['2.53528', "96.529"],
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
# swap
|
|
#
|
|
# {
|
|
# "id": null,
|
|
# "time": 1650188898,
|
|
# "channel": "futures.order_book_update",
|
|
# "event": "update",
|
|
# "error": null,
|
|
# "result": {
|
|
# "t": 1650188898938,
|
|
# "s": "GMT_USDT",
|
|
# "U": 1577718307,
|
|
# "u": 1577719254,
|
|
# "b": [
|
|
# {p: "2.5178", s: 0},
|
|
# {p: "2.5179", s: 0},
|
|
# {p: "2.518", s: 0},
|
|
# ],
|
|
# "a": [
|
|
# {p: "2.52", s: 0},
|
|
# {p: "2.5201", s: 0},
|
|
# {p: "2.5203", s: 0},
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
channel = self.safe_string(message, 'channel')
|
|
channelParts = channel.split('.')
|
|
rawMarketType = self.safe_string(channelParts, 0)
|
|
isSpot = rawMarketType == 'spot'
|
|
marketType = 'spot' if isSpot else 'contract'
|
|
delta = self.safe_value(message, 'result')
|
|
deltaStart = self.safe_integer(delta, 'U')
|
|
deltaEnd = self.safe_integer(delta, 'u')
|
|
marketId = self.safe_string(delta, 's')
|
|
symbol = self.safe_symbol(marketId, None, '_', marketType)
|
|
messageHash = 'orderbook:' + symbol
|
|
storedOrderBook = self.safe_value(self.orderbooks, symbol, self.order_book({}))
|
|
nonce = self.safe_integer(storedOrderBook, 'nonce')
|
|
if nonce is None:
|
|
cacheLength = 0
|
|
if storedOrderBook is not None:
|
|
cacheLength = len(storedOrderBook.cache)
|
|
snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 10)
|
|
waitAmount = snapshotDelay if isSpot else 0
|
|
if cacheLength == waitAmount:
|
|
# max limit is 100
|
|
subscription = client.subscriptions[messageHash]
|
|
limit = self.safe_integer(subscription, 'limit')
|
|
self.spawn(self.load_order_book, client, messageHash, symbol, limit, {}) # needed for c#, number of args needs to match
|
|
storedOrderBook.cache.append(delta)
|
|
return
|
|
elif nonce >= deltaEnd:
|
|
return
|
|
elif nonce >= deltaStart - 1:
|
|
self.handle_delta(storedOrderBook, delta)
|
|
else:
|
|
del client.subscriptions[messageHash]
|
|
del self.orderbooks[symbol]
|
|
checksum = self.handle_option('watchOrderBook', 'checksum', True)
|
|
if checksum:
|
|
error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol))
|
|
client.reject(error, messageHash)
|
|
client.resolve(storedOrderBook, messageHash)
|
|
|
|
def get_cache_index(self, orderBook, cache):
|
|
nonce = self.safe_integer(orderBook, 'nonce')
|
|
firstDelta = cache[0]
|
|
firstDeltaStart = self.safe_integer(firstDelta, 'U')
|
|
if nonce < firstDeltaStart:
|
|
return -1
|
|
for i in range(0, len(cache)):
|
|
delta = cache[i]
|
|
deltaStart = self.safe_integer(delta, 'U')
|
|
deltaEnd = self.safe_integer(delta, 'u')
|
|
if (nonce >= deltaStart - 1) and (nonce < deltaEnd):
|
|
return i
|
|
return len(cache)
|
|
|
|
def handle_bid_asks(self, bookSide, bidAsks):
|
|
for i in range(0, len(bidAsks)):
|
|
bidAsk = bidAsks[i]
|
|
if isinstance(bidAsk, list):
|
|
bookSide.storeArray(self.parse_bid_ask(bidAsk))
|
|
else:
|
|
price = self.safe_float(bidAsk, 'p')
|
|
amount = self.safe_float(bidAsk, 's')
|
|
bookSide.store(price, amount)
|
|
|
|
def handle_delta(self, orderbook, delta):
|
|
timestamp = self.safe_integer(delta, 't')
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
orderbook['nonce'] = self.safe_integer(delta, 'u')
|
|
bids = self.safe_value(delta, 'b', [])
|
|
asks = self.safe_value(delta, 'a', [])
|
|
storedBids = orderbook['bids']
|
|
storedAsks = orderbook['asks']
|
|
self.handle_bid_asks(storedBids, bids)
|
|
self.handle_bid_asks(storedAsks, asks)
|
|
|
|
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel
|
|
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
params['callerMethodName'] = 'watchTicker'
|
|
result = await self.watch_tickers([symbol], params)
|
|
return self.safe_value(result, symbol)
|
|
|
|
async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel
|
|
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
|
|
:param str[] symbols: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
return await self.subscribe_watch_tickers_and_bids_asks(symbols, 'watchTickers', self.extend({'method': 'tickers'}, params))
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "time": 1649326221,
|
|
# "channel": "spot.tickers",
|
|
# "event": "update",
|
|
# "result": {
|
|
# "currency_pair": "BTC_USDT",
|
|
# "last": "43444.82",
|
|
# "lowest_ask": "43444.82",
|
|
# "highest_bid": "43444.81",
|
|
# "change_percentage": "-4.0036",
|
|
# "base_volume": "5182.5412425462",
|
|
# "quote_volume": "227267634.93123952",
|
|
# "high_24h": "47698",
|
|
# "low_24h": "42721.03"
|
|
# }
|
|
# }
|
|
#
|
|
self.handle_ticker_and_bid_ask('ticker', client, message)
|
|
|
|
async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#best-bid-or-ask-price
|
|
https://www.gate.io/docs/developers/apiv4/ws/en/#order-book-channel
|
|
|
|
watches best bid & ask for symbols
|
|
:param str[] symbols: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
return await self.subscribe_watch_tickers_and_bids_asks(symbols, 'watchBidsAsks', self.extend({'method': 'book_ticker'}, params))
|
|
|
|
def handle_bid_ask(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "time": 1671363004,
|
|
# "time_ms": 1671363004235,
|
|
# "channel": "spot.book_ticker",
|
|
# "event": "update",
|
|
# "result": {
|
|
# "t": 1671363004228,
|
|
# "u": 9793320464,
|
|
# "s": "BTC_USDT",
|
|
# "b": "16716.8",
|
|
# "B": "0.0134",
|
|
# "a": "16716.9",
|
|
# "A": "0.0353"
|
|
# }
|
|
# }
|
|
#
|
|
self.handle_ticker_and_bid_ask('bidask', client, message)
|
|
|
|
async def subscribe_watch_tickers_and_bids_asks(self, symbols: Strings = None, callerMethodName: Str = None, params={}) -> Tickers:
|
|
await self.load_markets()
|
|
callerMethodName, params = self.handle_param_string(params, 'callerMethodName', callerMethodName)
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
market = self.market(symbols[0])
|
|
messageType = self.get_type_by_market(market)
|
|
marketIds = self.market_ids(symbols)
|
|
channelName = None
|
|
channelName, params = self.handle_option_and_params(params, callerMethodName, 'method')
|
|
url = self.get_url_by_market(market)
|
|
channel = messageType + '.' + channelName
|
|
isWatchTickers = callerMethodName.find('watchTicker') >= 0
|
|
prefix = 'ticker' if isWatchTickers else 'bidask'
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
messageHashes.append(prefix + ':' + symbol)
|
|
tickerOrBidAsk = await self.subscribe_public_multiple(url, messageHashes, marketIds, channel, params)
|
|
if self.newUpdates:
|
|
items: dict = {}
|
|
items[tickerOrBidAsk['symbol']] = tickerOrBidAsk
|
|
return items
|
|
result = self.tickers if isWatchTickers else self.bidsasks
|
|
return self.filter_by_array(result, 'symbol', symbols, True)
|
|
|
|
def handle_ticker_and_bid_ask(self, objectName: str, client: Client, message):
|
|
channel = self.safe_string(message, 'channel')
|
|
parts = channel.split('.')
|
|
rawMarketType = self.safe_string(parts, 0)
|
|
marketType = 'contract' if (rawMarketType == 'futures') else 'spot'
|
|
result = self.safe_value(message, 'result')
|
|
results = []
|
|
if isinstance(result, list):
|
|
results = self.safe_list(message, 'result', [])
|
|
else:
|
|
rawTicker = self.safe_dict(message, 'result', {})
|
|
results = [rawTicker]
|
|
isTicker = (objectName == 'ticker') # whether ticker or bid-ask
|
|
for i in range(0, len(results)):
|
|
rawTicker = results[i]
|
|
marketId = self.safe_string(rawTicker, 's')
|
|
market = self.safe_market(marketId, None, '_', marketType)
|
|
parsedItem = self.parse_ticker(rawTicker, market)
|
|
symbol = parsedItem['symbol']
|
|
if isTicker:
|
|
self.tickers[symbol] = parsedItem
|
|
else:
|
|
self.bidsasks[symbol] = parsedItem
|
|
messageHash = objectName + ':' + symbol
|
|
client.resolve(parsedItem, messageHash)
|
|
|
|
async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
return await self.watch_trades_for_symbols([symbol], since, limit, params)
|
|
|
|
async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
marketIds = self.market_ids(symbols)
|
|
market = self.market(symbols[0])
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.trades'
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
messageHashes.append('trades:' + symbol)
|
|
url = self.get_url_by_market(market)
|
|
trades = await self.subscribe_public_multiple(url, messageHashes, marketIds, channel, params)
|
|
if self.newUpdates:
|
|
first = self.safe_value(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
marketIds = self.market_ids(symbols)
|
|
market = self.market(symbols[0])
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.trades'
|
|
subMessageHashes = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
subMessageHashes.append('trades:' + symbol)
|
|
messageHashes.append('unsubscribe:trades:' + symbol)
|
|
url = self.get_url_by_market(market)
|
|
return await self.un_subscribe_public_multiple(url, 'trades', symbols, messageHashes, subMessageHashes, marketIds, channel, params)
|
|
|
|
async def un_watch_trades(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
return await self.un_watch_trades_for_symbols([symbol], params)
|
|
|
|
def handle_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "time": 1648725035,
|
|
# "channel": "spot.trades",
|
|
# "event": "update",
|
|
# "result": [{
|
|
# "id": 3130257995,
|
|
# "create_time": 1648725035,
|
|
# "create_time_ms": "1648725035923.0",
|
|
# "side": "sell",
|
|
# "currency_pair": "LTC_USDT",
|
|
# "amount": "0.0116",
|
|
# "price": "130.11"
|
|
# }]
|
|
# }
|
|
#
|
|
result = self.safe_value(message, 'result')
|
|
if not isinstance(result, list):
|
|
result = [result]
|
|
parsedTrades = self.parse_trades(result)
|
|
for i in range(0, len(parsedTrades)):
|
|
trade = parsedTrades[i]
|
|
symbol = trade['symbol']
|
|
cachedTrades = self.safe_value(self.trades, symbol)
|
|
if cachedTrades is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
cachedTrades = ArrayCache(limit)
|
|
self.trades[symbol] = cachedTrades
|
|
cachedTrades.append(trade)
|
|
hash = 'trades:' + symbol
|
|
client.resolve(cachedTrades, hash)
|
|
|
|
async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
marketId = market['id']
|
|
interval = self.safe_string(self.timeframes, timeframe, timeframe)
|
|
messageType = self.get_type_by_market(market)
|
|
channel = messageType + '.candlesticks'
|
|
messageHash = 'candles:' + interval + ':' + market['symbol']
|
|
url = self.get_url_by_market(market)
|
|
payload = [interval, marketId]
|
|
ohlcv = await self.subscribe_public(url, messageHash, payload, channel, params)
|
|
if self.newUpdates:
|
|
limit = ohlcv.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "time": 1606292600,
|
|
# "channel": "spot.candlesticks",
|
|
# "event": "update",
|
|
# "result": {
|
|
# "t": "1606292580", # total volume
|
|
# "v": "2362.32035", # volume
|
|
# "c": "19128.1", # close
|
|
# "h": "19128.1", # high
|
|
# "l": "19128.1", # low
|
|
# "o": "19128.1", # open
|
|
# "n": "1m_BTC_USDT" # sub
|
|
# }
|
|
# }
|
|
#
|
|
channel = self.safe_string(message, 'channel')
|
|
channelParts = channel.split('.')
|
|
rawMarketType = self.safe_string(channelParts, 0)
|
|
marketType = 'spot' if (rawMarketType == 'spot') else 'contract'
|
|
result = self.safe_value(message, 'result')
|
|
if not isinstance(result, list):
|
|
result = [result]
|
|
marketIds: dict = {}
|
|
for i in range(0, len(result)):
|
|
ohlcv = result[i]
|
|
subscription = self.safe_string(ohlcv, 'n', '')
|
|
parts = subscription.split('_')
|
|
timeframe = self.safe_string(parts, 0)
|
|
timeframeId = self.find_timeframe(timeframe)
|
|
prefix = timeframe + '_'
|
|
marketId = subscription.replace(prefix, '')
|
|
symbol = self.safe_symbol(marketId, None, '_', marketType)
|
|
parsed = self.parse_ohlcv(ohlcv)
|
|
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
|
stored = self.safe_value(self.ohlcvs[symbol], timeframe)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
|
stored = ArrayCacheByTimestamp(limit)
|
|
self.ohlcvs[symbol][timeframeId] = stored
|
|
stored.append(parsed)
|
|
marketIds[symbol] = timeframe
|
|
keys = list(marketIds.keys())
|
|
for i in range(0, len(keys)):
|
|
symbol = keys[i]
|
|
timeframe = marketIds[symbol]
|
|
interval = self.find_timeframe(timeframe)
|
|
hash = 'candles' + ':' + interval + ':' + symbol
|
|
stored = self.safe_value(self.ohlcvs[symbol], interval)
|
|
client.resolve(stored, hash)
|
|
|
|
async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
watches information on multiple trades made by the user
|
|
:param str symbol: unified market symbol of the market trades were made in
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
subType = None
|
|
type = None
|
|
marketId = '!' + 'all'
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
marketId = market['id']
|
|
type, params = self.handle_market_type_and_params('watchMyTrades', market, params)
|
|
subType, params = self.handle_sub_type_and_params('watchMyTrades', market, params)
|
|
messageType = self.get_supported_mapping(type, {
|
|
'spot': 'spot',
|
|
'margin': 'spot',
|
|
'future': 'futures',
|
|
'swap': 'futures',
|
|
'option': 'options',
|
|
})
|
|
channel = messageType + '.usertrades'
|
|
messageHash = 'myTrades'
|
|
if symbol is not None:
|
|
messageHash += ':' + symbol
|
|
isInverse = (subType == 'inverse')
|
|
url = self.get_url_by_market_type(type, isInverse)
|
|
payload = [marketId]
|
|
# uid required for non spot markets
|
|
requiresUid = (type != 'spot')
|
|
trades = await self.subscribe_private(url, messageHash, payload, channel, params, requiresUid)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_my_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "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"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_value(message, 'result', [])
|
|
tradesLength = len(result)
|
|
if tradesLength == 0:
|
|
return
|
|
cachedTrades = self.myTrades
|
|
if cachedTrades is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
cachedTrades = ArrayCacheBySymbolById(limit)
|
|
self.myTrades = cachedTrades
|
|
parsed = self.parse_trades(result)
|
|
marketIds: dict = {}
|
|
for i in range(0, len(parsed)):
|
|
trade = parsed[i]
|
|
cachedTrades.append(trade)
|
|
symbol = trade['symbol']
|
|
marketIds[symbol] = True
|
|
keys = list(marketIds.keys())
|
|
for i in range(0, len(keys)):
|
|
market = keys[i]
|
|
hash = 'myTrades:' + market
|
|
client.resolve(cachedTrades, hash)
|
|
client.resolve(cachedTrades, 'myTrades')
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
watch balance and get the amount of funds available for trading or funds locked in orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
type = None
|
|
subType = None
|
|
type, params = self.handle_market_type_and_params('watchBalance', None, params)
|
|
subType, params = self.handle_sub_type_and_params('watchBalance', None, params)
|
|
isInverse = (subType == 'inverse')
|
|
url = self.get_url_by_market_type(type, isInverse)
|
|
requiresUid = (type != 'spot')
|
|
channelType = self.get_supported_mapping(type, {
|
|
'spot': 'spot',
|
|
'margin': 'spot',
|
|
'future': 'futures',
|
|
'swap': 'futures',
|
|
'option': 'options',
|
|
})
|
|
channel = channelType + '.balances'
|
|
messageHash = type + '.balance'
|
|
return await self.subscribe_private(url, messageHash, None, channel, params, requiresUid)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# spot order fill
|
|
# {
|
|
# "time": 1653664351,
|
|
# "channel": "spot.balances",
|
|
# "event": "update",
|
|
# "result": [
|
|
# {
|
|
# "timestamp": "1653664351",
|
|
# "timestamp_ms": "1653664351017",
|
|
# "user": "10406147",
|
|
# "currency": "LTC",
|
|
# "change": "-0.0002000000000000",
|
|
# "total": "0.09986000000000000000",
|
|
# "available": "0.09986000000000000000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# account transfer
|
|
#
|
|
# {
|
|
# "id": null,
|
|
# "time": 1653665088,
|
|
# "channel": "futures.balances",
|
|
# "event": "update",
|
|
# "error": null,
|
|
# "result": [
|
|
# {
|
|
# "balance": 25.035008537,
|
|
# "change": 25,
|
|
# "text": "-",
|
|
# "time": 1653665088,
|
|
# "time_ms": 1653665088286,
|
|
# "type": "dnw",
|
|
# "user": "10406147"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# swap order fill
|
|
# {
|
|
# "id": null,
|
|
# "time": 1653665311,
|
|
# "channel": "futures.balances",
|
|
# "event": "update",
|
|
# "error": null,
|
|
# "result": [
|
|
# {
|
|
# "balance": 20.031873037,
|
|
# "change": -0.0031355,
|
|
# "text": "LTC_USDT:165551103273",
|
|
# "time": 1653665311,
|
|
# "time_ms": 1653665311437,
|
|
# "type": "fee",
|
|
# "user": "10406147"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_value(message, 'result', [])
|
|
timestamp = self.safe_integer(message, 'time_ms')
|
|
self.balance['info'] = result
|
|
self.balance['timestamp'] = timestamp
|
|
self.balance['datetime'] = self.iso8601(timestamp)
|
|
for i in range(0, len(result)):
|
|
rawBalance = result[i]
|
|
account = self.account()
|
|
currencyId = self.safe_string(rawBalance, 'currency', 'USDT') # when not present it is USDT
|
|
code = self.safe_currency_code(currencyId)
|
|
account['free'] = self.safe_string(rawBalance, 'available')
|
|
account['total'] = self.safe_string_2(rawBalance, 'total', 'balance')
|
|
self.balance[code] = account
|
|
channel = self.safe_string(message, 'channel')
|
|
parts = channel.split('.')
|
|
rawType = self.safe_string(parts, 0)
|
|
channelType = self.get_supported_mapping(rawType, {
|
|
'spot': 'spot',
|
|
'futures': 'swap',
|
|
'options': 'option',
|
|
})
|
|
messageHash = channelType + '.balance'
|
|
self.balance = self.safe_balance(self.balance)
|
|
client.resolve(self.balance, messageHash)
|
|
|
|
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
|
|
"""
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#positions-subscription
|
|
https://www.gate.io/docs/developers/delivery/ws/en/#positions-subscription
|
|
https://www.gate.io/docs/developers/options/ws/en/#positions-channel
|
|
|
|
watch all open positions
|
|
:param str[] [symbols]: list of unified market symbols to watch positions for
|
|
:param int [since]: the earliest time in ms to fetch positions for
|
|
:param int [limit]: the maximum number of positions to retrieve
|
|
:param dict params: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
symbols = self.market_symbols(symbols)
|
|
payload = ['!' + 'all']
|
|
if not self.is_empty(symbols):
|
|
market = self.get_market_from_symbols(symbols)
|
|
type = None
|
|
query = None
|
|
type, query = self.handle_market_type_and_params('watchPositions', market, params)
|
|
if type == 'spot':
|
|
type = 'swap'
|
|
typeId = self.get_supported_mapping(type, {
|
|
'future': 'futures',
|
|
'swap': 'futures',
|
|
'option': 'options',
|
|
})
|
|
messageHash = type + ':positions'
|
|
if not self.is_empty(symbols):
|
|
messageHash += '::' + ','.join(symbols)
|
|
channel = typeId + '.positions'
|
|
subType = None
|
|
subType, query = self.handle_sub_type_and_params('watchPositions', market, query)
|
|
isInverse = (subType == 'inverse')
|
|
url = self.get_url_by_market_type(type, isInverse)
|
|
client = self.client(url)
|
|
self.set_positions_cache(client, type, symbols)
|
|
fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True)
|
|
awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True)
|
|
cache = self.safe_value(self.positions, type)
|
|
if fetchPositionsSnapshot and awaitPositionsSnapshot and cache is None:
|
|
return await client.future(type + ':fetchPositionsSnapshot')
|
|
positions = await self.subscribe_private(url, messageHash, payload, channel, query, True)
|
|
if self.newUpdates:
|
|
return positions
|
|
return self.filter_by_symbols_since_limit(self.positions[type], symbols, since, limit, True)
|
|
|
|
def set_positions_cache(self, client: Client, type, symbols: Strings = None):
|
|
if self.positions is None:
|
|
self.positions = {}
|
|
if type in self.positions:
|
|
return
|
|
fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False)
|
|
if fetchPositionsSnapshot:
|
|
messageHash = type + ':fetchPositionsSnapshot'
|
|
if not (messageHash in client.futures):
|
|
client.future(messageHash)
|
|
self.spawn(self.load_positions_snapshot, client, messageHash, type)
|
|
else:
|
|
self.positions[type] = ArrayCacheBySymbolBySide()
|
|
|
|
async def load_positions_snapshot(self, client, messageHash, type):
|
|
positions = await self.fetch_positions(None, {'type': type})
|
|
self.positions[type] = ArrayCacheBySymbolBySide()
|
|
cache = self.positions[type]
|
|
for i in range(0, len(positions)):
|
|
position = positions[i]
|
|
contracts = self.safe_number(position, 'contracts', 0)
|
|
if contracts > 0:
|
|
cache.append(position)
|
|
# don't remove the future from the .futures cache
|
|
future = client.futures[messageHash]
|
|
future.resolve(cache)
|
|
client.resolve(cache, type + ':position')
|
|
|
|
def handle_positions(self, client, message):
|
|
#
|
|
# {
|
|
# time: 1693158497,
|
|
# time_ms: 1693158497204,
|
|
# channel: 'futures.positions',
|
|
# event: 'update',
|
|
# result: [{
|
|
# contract: 'XRP_USDT',
|
|
# cross_leverage_limit: 0,
|
|
# entry_price: 0.5253,
|
|
# history_pnl: 0,
|
|
# history_point: 0,
|
|
# last_close_pnl: 0,
|
|
# leverage: 0,
|
|
# leverage_max: 50,
|
|
# liq_price: 0.0361,
|
|
# maintenance_rate: 0.01,
|
|
# margin: 4.89609962852,
|
|
# mode: 'single',
|
|
# realised_pnl: -0.0026265,
|
|
# realised_point: 0,
|
|
# risk_limit: 500000,
|
|
# size: 1,
|
|
# time: 1693158497,
|
|
# time_ms: 1693158497195,
|
|
# update_id: 1,
|
|
# user: '10444586'
|
|
# }]
|
|
# }
|
|
#
|
|
type = self.get_market_type_by_url(client.url)
|
|
data = self.safe_value(message, 'result', [])
|
|
cache = self.positions[type]
|
|
newPositions = []
|
|
for i in range(0, len(data)):
|
|
rawPosition = data[i]
|
|
position = self.parse_position(rawPosition)
|
|
symbol = self.safe_string(position, 'symbol')
|
|
side = self.safe_string(position, 'side')
|
|
# Control when position is closed no side is returned
|
|
if side is None:
|
|
prevLongPosition = self.safe_dict(cache, symbol + 'long')
|
|
if prevLongPosition is not None:
|
|
position['side'] = prevLongPosition['side']
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
prevShortPosition = self.safe_dict(cache, symbol + 'short')
|
|
if prevShortPosition is not None:
|
|
position['side'] = prevShortPosition['side']
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
# if no prev position is found, default to long
|
|
if prevLongPosition is None and prevShortPosition is None:
|
|
position['side'] = 'long'
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
else:
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
messageHashes = self.find_message_hashes(client, type + ':positions::')
|
|
for i in range(0, len(messageHashes)):
|
|
messageHash = messageHashes[i]
|
|
parts = messageHash.split('::')
|
|
symbolsString = parts[1]
|
|
symbols = symbolsString.split(',')
|
|
positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
|
|
if not self.is_empty(positions):
|
|
client.resolve(positions, messageHash)
|
|
client.resolve(newPositions, type + ':positions')
|
|
|
|
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
watches information on multiple orders made by the user
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.type]: spot, margin, swap, future, or option. Required if listening to all symbols.
|
|
:param boolean [params.isInverse]: if future, listen to inverse or linear contracts
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
type = None
|
|
query = None
|
|
type, query = self.handle_market_type_and_params('watchOrders', market, params)
|
|
typeId = self.get_supported_mapping(type, {
|
|
'spot': 'spot',
|
|
'margin': 'spot',
|
|
'future': 'futures',
|
|
'swap': 'futures',
|
|
'option': 'options',
|
|
})
|
|
channel = typeId + '.orders'
|
|
messageHash = 'orders'
|
|
payload = ['!' + 'all']
|
|
if symbol is not None:
|
|
messageHash += ':' + market['id']
|
|
payload = [market['id']]
|
|
subType = None
|
|
subType, query = self.handle_sub_type_and_params('watchOrders', market, query)
|
|
isInverse = (subType == 'inverse')
|
|
url = self.get_url_by_market_type(type, isInverse)
|
|
# uid required for non spot markets
|
|
requiresUid = (type != 'spot')
|
|
orders = await self.subscribe_private(url, messageHash, payload, channel, query, requiresUid)
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)
|
|
|
|
def handle_order(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "time": 1605175506,
|
|
# "channel": "spot.orders",
|
|
# "event": "update",
|
|
# "result": [
|
|
# {
|
|
# "id": "30784435",
|
|
# "user": 123456,
|
|
# "text": "t-abc",
|
|
# "create_time": "1605175506",
|
|
# "create_time_ms": "1605175506123",
|
|
# "update_time": "1605175506",
|
|
# "update_time_ms": "1605175506123",
|
|
# "event": "put",
|
|
# "currency_pair": "BTC_USDT",
|
|
# "type": "limit",
|
|
# "account": "spot",
|
|
# "side": "sell",
|
|
# "amount": "1",
|
|
# "price": "10001",
|
|
# "time_in_force": "gtc",
|
|
# "left": "1",
|
|
# "filled_total": "0",
|
|
# "fee": "0",
|
|
# "fee_currency": "USDT",
|
|
# "point_fee": "0",
|
|
# "gt_fee": "0",
|
|
# "gt_discount": True,
|
|
# "rebated_fee": "0",
|
|
# "rebated_fee_currency": "USDT"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
orders = self.safe_value(message, 'result', [])
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
if self.orders is None:
|
|
self.orders = ArrayCacheBySymbolById(limit)
|
|
stored = self.orders
|
|
marketIds: dict = {}
|
|
parsedOrders = self.parse_orders(orders)
|
|
for i in range(0, len(parsedOrders)):
|
|
parsed = parsedOrders[i]
|
|
# inject order status
|
|
info = self.safe_value(parsed, 'info')
|
|
event = self.safe_string(info, 'event')
|
|
if event == 'put' or event == 'update':
|
|
parsed['status'] = 'open'
|
|
elif event == 'finish':
|
|
status = self.safe_string(parsed, 'status')
|
|
if status is None:
|
|
left = self.safe_integer(info, 'left')
|
|
parsed['status'] = 'closed' if (left == 0) else 'canceled'
|
|
stored.append(parsed)
|
|
symbol = parsed['symbol']
|
|
market = self.market(symbol)
|
|
marketIds[market['id']] = True
|
|
keys = list(marketIds.keys())
|
|
for i in range(0, len(keys)):
|
|
messageHash = 'orders:' + keys[i]
|
|
client.resolve(self.orders, messageHash)
|
|
client.resolve(self.orders, 'orders')
|
|
|
|
async def watch_my_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
|
|
"""
|
|
watch the public liquidations of a trading pair
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#liquidates-api
|
|
https://www.gate.io/docs/developers/delivery/ws/en/#liquidates-api
|
|
https://www.gate.io/docs/developers/options/ws/en/#liquidates-channel
|
|
|
|
:param str symbol: unified CCXT market symbol
|
|
:param int [since]: the earliest time in ms to fetch liquidations for
|
|
:param int [limit]: the maximum number of liquidation structures to retrieve
|
|
:param dict [params]: exchange specific parameters for the bitmex api endpoint
|
|
:returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
|
|
"""
|
|
return self.watch_my_liquidations_for_symbols([symbol], since, limit, params)
|
|
|
|
async def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
|
|
"""
|
|
watch the private liquidations of a trading pair
|
|
|
|
https://www.gate.io/docs/developers/futures/ws/en/#liquidates-api
|
|
https://www.gate.io/docs/developers/delivery/ws/en/#liquidates-api
|
|
https://www.gate.io/docs/developers/options/ws/en/#liquidates-channel
|
|
|
|
:param str[] symbols: unified CCXT market symbols
|
|
:param int [since]: the earliest time in ms to fetch liquidations for
|
|
:param int [limit]: the maximum number of liquidation structures to retrieve
|
|
:param dict [params]: exchange specific parameters for the gate api endpoint
|
|
:returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, True, True)
|
|
market = self.get_market_from_symbols(symbols)
|
|
type = None
|
|
query = None
|
|
type, query = self.handle_market_type_and_params('watchMyLiquidationsForSymbols', market, params)
|
|
typeId = self.get_supported_mapping(type, {
|
|
'future': 'futures',
|
|
'swap': 'futures',
|
|
'option': 'options',
|
|
})
|
|
subType = None
|
|
subType, query = self.handle_sub_type_and_params('watchMyLiquidationsForSymbols', market, query)
|
|
isInverse = (subType == 'inverse')
|
|
url = self.get_url_by_market_type(type, isInverse)
|
|
payload = []
|
|
messageHash = ''
|
|
if self.is_empty(symbols):
|
|
if typeId != 'futures' and not isInverse:
|
|
raise BadRequest(self.id + ' watchMyLiquidationsForSymbols() does not support listening to all symbols, you must call watchMyLiquidations() instead for each symbol you wish to watch.')
|
|
messageHash = 'myLiquidations'
|
|
payload.append('not all')
|
|
else:
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength != 1:
|
|
raise BadRequest(self.id + ' watchMyLiquidationsForSymbols() only allows one symbol at a time. To listen to several symbols call watchMyLiquidationsForSymbols() several times.')
|
|
messageHash = 'myLiquidations::' + symbols[0]
|
|
payload.append(market['id'])
|
|
channel = typeId + '.liquidates'
|
|
newLiquidations = await self.subscribe_private(url, messageHash, payload, channel, query, True)
|
|
if self.newUpdates:
|
|
return newLiquidations
|
|
return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True)
|
|
|
|
def handle_liquidation(self, client: Client, message):
|
|
#
|
|
# future / delivery
|
|
# {
|
|
# "channel":"futures.liquidates",
|
|
# "event":"update",
|
|
# "time":1541505434,
|
|
# "time_ms":1541505434123,
|
|
# "result":[
|
|
# {
|
|
# "entry_price":209,
|
|
# "fill_price":215.1,
|
|
# "left":0,
|
|
# "leverage":0.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"
|
|
# }
|
|
# ]
|
|
# }
|
|
# option
|
|
# {
|
|
# "channel":"options.liquidates",
|
|
# "event":"update",
|
|
# "time":1630654851,
|
|
# "result":[
|
|
# {
|
|
# "user":"1xxxx",
|
|
# "init_margin":1190,
|
|
# "maint_margin":1042.5,
|
|
# "order_margin":0,
|
|
# "time":1639051907,
|
|
# "time_ms":1639051907000
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rawLiquidations = self.safe_list(message, 'result', [])
|
|
newLiquidations = []
|
|
for i in range(0, len(rawLiquidations)):
|
|
rawLiquidation = rawLiquidations[i]
|
|
liquidation = self.parse_ws_liquidation(rawLiquidation)
|
|
symbol = self.safe_string(liquidation, 'symbol')
|
|
liquidations = self.safe_value(self.liquidations, symbol)
|
|
if liquidations is None:
|
|
limit = self.safe_integer(self.options, 'liquidationsLimit', 1000)
|
|
liquidations = ArrayCache(limit)
|
|
liquidations.append(liquidation)
|
|
self.liquidations[symbol] = liquidations
|
|
client.resolve(liquidations, 'myLiquidations::' + symbol)
|
|
client.resolve(newLiquidations, 'myLiquidations')
|
|
|
|
def parse_ws_liquidation(self, liquidation, market=None):
|
|
#
|
|
# future / delivery
|
|
# {
|
|
# "entry_price": 209,
|
|
# "fill_price": 215.1,
|
|
# "left": 0,
|
|
# "leverage": 0.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"
|
|
# }
|
|
# option
|
|
# {
|
|
# "user": "1xxxx",
|
|
# "init_margin": 1190,
|
|
# "maint_margin": 1042.5,
|
|
# "order_margin": 0,
|
|
# "time": 1639051907,
|
|
# "time_ms": 1639051907000
|
|
# }
|
|
#
|
|
marketId = self.safe_string(liquidation, 'contract')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_integer(liquidation, 'time_ms')
|
|
originalSize = self.safe_string(liquidation, 'size')
|
|
left = self.safe_string(liquidation, 'left')
|
|
amount = Precise.string_abs(Precise.string_sub(originalSize, left))
|
|
return self.safe_liquidation({
|
|
'info': liquidation,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'contracts': self.parse_number(amount),
|
|
'contractSize': self.safe_number(market, 'contractSize'),
|
|
'price': self.safe_number(liquidation, 'fill_price'),
|
|
'baseValue': None,
|
|
'quoteValue': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
})
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
#
|
|
# {
|
|
# "time": 1647274664,
|
|
# "channel": "futures.orders",
|
|
# "event": "subscribe",
|
|
# "error": {code: 2, message: "unknown contract BTC_USDT_20220318"},
|
|
# }
|
|
# {
|
|
# "time": 1647276473,
|
|
# "channel": "futures.orders",
|
|
# "event": "subscribe",
|
|
# "error": {
|
|
# "code": 4,
|
|
# "message": "{"label":"INVALID_KEY","message":"Invalid key provided"}\n"
|
|
# },
|
|
# "result": null
|
|
# }
|
|
# {
|
|
# header: {
|
|
# response_time: '1718551891329',
|
|
# status: '400',
|
|
# channel: 'spot.order_place',
|
|
# event: 'api',
|
|
# client_id: '81.34.68.6-0xc16375e2c0',
|
|
# conn_id: '9539116e0e09678f'
|
|
# },
|
|
# data: {errs: {label: 'AUTHENTICATION_FAILED', message: 'Not login'}},
|
|
# request_id: '10406147'
|
|
# }
|
|
# {
|
|
# "time": 1739853211,
|
|
# "time_ms": 1739853211201,
|
|
# "id": 1,
|
|
# "conn_id": "62f2c1dabbe186d7",
|
|
# "trace_id": "cdb02a8c0b61086b2fe6f8fad2f98c54",
|
|
# "channel": "spot.trades",
|
|
# "event": "subscribe",
|
|
# "payload": [
|
|
# "LUNARLENS_USDT",
|
|
# "ETH_USDT"
|
|
# ],
|
|
# "error": {
|
|
# "code": 2,
|
|
# "message": "unknown currency pair: LUNARLENS_USDT"
|
|
# },
|
|
# "result": {
|
|
# "status": "fail"
|
|
# },
|
|
# "requestId": "cdb02a8c0b61086b2fe6f8fad2f98c54"
|
|
# }
|
|
#
|
|
data = self.safe_dict(message, 'data')
|
|
errs = self.safe_dict(data, 'errs')
|
|
error = self.safe_dict(message, 'error', errs)
|
|
code = self.safe_string_2(error, 'code', 'label')
|
|
id = self.safe_string_n(message, ['id', 'requestId', 'request_id'])
|
|
if error is not None:
|
|
messageHash = self.safe_string(client.subscriptions, id)
|
|
try:
|
|
self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, self.json(message))
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, self.json(errs))
|
|
errorMessage = self.safe_string(error, 'message', self.safe_string(errs, 'message'))
|
|
self.throw_broadly_matched_exception(self.exceptions['ws']['broad'], errorMessage, self.json(message))
|
|
raise ExchangeError(self.json(message))
|
|
except Exception as e:
|
|
client.reject(e, messageHash)
|
|
if (messageHash is not None) and (messageHash in client.subscriptions):
|
|
del client.subscriptions[messageHash]
|
|
# remove subscriptions for watchSymbols
|
|
channel = self.safe_string(message, 'channel')
|
|
if (channel is not None) and (channel.find('.') > 0):
|
|
parsedChannel = channel.split('.')
|
|
payload = self.safe_list(message, 'payload', [])
|
|
for i in range(0, len(payload)):
|
|
marketType = parsedChannel[0] == 'swap' if 'futures' else parsedChannel[0]
|
|
symbol = self.safe_symbol(payload[i], None, '_', marketType)
|
|
messageHashSymbol = parsedChannel[1] + ':' + symbol
|
|
if (messageHashSymbol is not None) and (messageHashSymbol in client.subscriptions):
|
|
del client.subscriptions[messageHashSymbol]
|
|
if (id is not None) and (id in client.subscriptions):
|
|
del client.subscriptions[id]
|
|
return True
|
|
return False
|
|
|
|
def handle_balance_subscription(self, client: Client, message, subscription=None):
|
|
self.balance = {}
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
channel = self.safe_string(message, 'channel')
|
|
methods: dict = {
|
|
'balance': self.handle_balance_subscription,
|
|
'spot.order_book_update': self.handle_order_book_subscription,
|
|
'futures.order_book_update': self.handle_order_book_subscription,
|
|
}
|
|
id = self.safe_string(message, 'id')
|
|
if channel in methods:
|
|
subscriptionHash = self.safe_string(client.subscriptions, id)
|
|
subscription = self.safe_value(client.subscriptions, subscriptionHash)
|
|
method = methods[channel]
|
|
method(client, message, subscription)
|
|
if id in client.subscriptions:
|
|
del client.subscriptions[id]
|
|
|
|
def handle_un_subscribe(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "time":1725534679,
|
|
# "time_ms":1725534679786,
|
|
# "id":2,
|
|
# "conn_id":"fac539b443fd7002",
|
|
# "trace_id":"efe1d282b630b4aa266b84bee177791a",
|
|
# "channel":"spot.trades",
|
|
# "event":"unsubscribe",
|
|
# "payload":[
|
|
# "LTC_USDT"
|
|
# ],
|
|
# "result":{
|
|
# "status":"success"
|
|
# },
|
|
# "requestId":"efe1d282b630b4aa266b84bee177791a"
|
|
# }
|
|
#
|
|
id = self.safe_string(message, 'id')
|
|
keys = list(client.subscriptions.keys())
|
|
for i in range(0, len(keys)):
|
|
messageHash = keys[i]
|
|
if not (messageHash in client.subscriptions):
|
|
continue
|
|
# the previous iteration can have deleted the messageHash from the subscriptions
|
|
if messageHash.startswith('unsubscribe'):
|
|
subscription = client.subscriptions[messageHash]
|
|
subId = self.safe_string(subscription, 'id')
|
|
if id != subId:
|
|
continue
|
|
messageHashes = self.safe_list(subscription, 'messageHashes', [])
|
|
subMessageHashes = self.safe_list(subscription, 'subMessageHashes', [])
|
|
for j in range(0, len(messageHashes)):
|
|
unsubHash = messageHashes[j]
|
|
subHash = subMessageHashes[j]
|
|
self.clean_unsubscription(client, subHash, unsubHash)
|
|
self.clean_cache(subscription)
|
|
|
|
def clean_cache(self, subscription: dict):
|
|
topic = self.safe_string(subscription, 'topic', '')
|
|
symbols = self.safe_list(subscription, 'symbols', [])
|
|
symbolsLength = len(symbols)
|
|
if topic == 'ohlcv':
|
|
symbolsAndTimeFrames = self.safe_list(subscription, 'symbolsAndTimeframes', [])
|
|
for i in range(0, len(symbolsAndTimeFrames)):
|
|
symbolAndTimeFrame = symbolsAndTimeFrames[i]
|
|
symbol = self.safe_string(symbolAndTimeFrame, 0)
|
|
timeframe = self.safe_string(symbolAndTimeFrame, 1)
|
|
del self.ohlcvs[symbol][timeframe]
|
|
elif symbolsLength > 0:
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
if topic.endswith('trades'):
|
|
del self.trades[symbol]
|
|
elif topic == 'orderbook':
|
|
del self.orderbooks[symbol]
|
|
elif topic == 'ticker':
|
|
del self.tickers[symbol]
|
|
else:
|
|
if topic.endswith('trades'):
|
|
# don't reset self.myTrades directly here
|
|
# because in c# we need to use a different object
|
|
keys = list(self.trades.keys())
|
|
for i in range(0, len(keys)):
|
|
del self.trades[keys[i]]
|
|
|
|
def handle_message(self, client: Client, message):
|
|
#
|
|
# subscribe
|
|
# {
|
|
# "time": 1649062304,
|
|
# "id": 1649062303,
|
|
# "channel": "spot.candlesticks",
|
|
# "event": "subscribe",
|
|
# "result": {status: "success"}
|
|
# }
|
|
#
|
|
# candlestick
|
|
# {
|
|
# "time": 1649063328,
|
|
# "channel": "spot.candlesticks",
|
|
# "event": "update",
|
|
# "result": {
|
|
# "t": "1649063280",
|
|
# "v": "58932.23174896",
|
|
# "c": "45966.47",
|
|
# "h": "45997.24",
|
|
# "l": "45966.47",
|
|
# "o": "45975.18",
|
|
# "n": "1m_BTC_USDT",
|
|
# "a": "1.281699"
|
|
# }
|
|
# }
|
|
#
|
|
# orders
|
|
# {
|
|
# "time": 1630654851,
|
|
# "channel": "options.orders", or futures.orders or spot.orders
|
|
# "event": "update",
|
|
# "result": [
|
|
# {
|
|
# "contract": "BTC_USDT-20211130-65000-C",
|
|
# "create_time": 1637897000,
|
|
# (...)
|
|
# ]
|
|
# }
|
|
# orderbook
|
|
# {
|
|
# "time": 1649770525,
|
|
# "channel": "spot.order_book_update",
|
|
# "event": "update",
|
|
# "result": {
|
|
# "t": 1649770525653,
|
|
# "e": "depthUpdate",
|
|
# "E": 1649770525,
|
|
# "s": "LTC_USDT",
|
|
# "U": 2622525645,
|
|
# "u": 2622525665,
|
|
# "b": [
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array]
|
|
# ],
|
|
# "a": [
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array], [Array],
|
|
# [Array]
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
# balance update
|
|
#
|
|
# {
|
|
# "time": 1653664351,
|
|
# "channel": "spot.balances",
|
|
# "event": "update",
|
|
# "result": [
|
|
# {
|
|
# "timestamp": "1653664351",
|
|
# "timestamp_ms": "1653664351017",
|
|
# "user": "10406147",
|
|
# "currency": "LTC",
|
|
# "change": "-0.0002000000000000",
|
|
# "total": "0.09986000000000000000",
|
|
# "available": "0.09986000000000000000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
if self.handle_error_message(client, message):
|
|
return
|
|
event = self.safe_string(message, 'event')
|
|
if event == 'subscribe':
|
|
self.handle_subscription_status(client, message)
|
|
return
|
|
if event == 'unsubscribe':
|
|
self.handle_un_subscribe(client, message)
|
|
return
|
|
channel = self.safe_string(message, 'channel', '')
|
|
channelParts = channel.split('.')
|
|
channelType = self.safe_value(channelParts, 1)
|
|
v4Methods: dict = {
|
|
'usertrades': self.handle_my_trades,
|
|
'candlesticks': self.handle_ohlcv,
|
|
'orders': self.handle_order,
|
|
'positions': self.handle_positions,
|
|
'tickers': self.handle_ticker,
|
|
'book_ticker': self.handle_bid_ask,
|
|
'trades': self.handle_trades,
|
|
'order_book_update': self.handle_order_book,
|
|
'balances': self.handle_balance,
|
|
'liquidates': self.handle_liquidation,
|
|
}
|
|
method = self.safe_value(v4Methods, channelType)
|
|
if method is not None:
|
|
method(client, message)
|
|
requestId = self.safe_string(message, 'request_id')
|
|
if requestId == 'authenticated':
|
|
self.handle_authentication_message(client, message)
|
|
return
|
|
if requestId is not None:
|
|
data = self.safe_dict(message, 'data')
|
|
# use safeValue may be Array or an Object
|
|
result = self.safe_value(data, 'result')
|
|
ack = self.safe_bool(message, 'ack')
|
|
if ack is not True:
|
|
client.resolve(result, requestId)
|
|
|
|
def get_url_by_market(self, market):
|
|
baseUrl = self.urls['api'][market['type']]
|
|
if market['contract']:
|
|
return baseUrl['usdt'] if market['linear'] else baseUrl['btc']
|
|
else:
|
|
return baseUrl
|
|
|
|
def get_type_by_market(self, market: Market):
|
|
if market['spot']:
|
|
return 'spot'
|
|
elif market['option']:
|
|
return 'options'
|
|
else:
|
|
return 'futures'
|
|
|
|
def get_url_by_market_type(self, type: MarketType, isInverse=False):
|
|
api = self.urls['api']
|
|
url = api[type]
|
|
if (type == 'swap') or (type == 'future'):
|
|
return url['btc'] if isInverse else url['usdt']
|
|
else:
|
|
return url
|
|
|
|
def get_market_type_by_url(self, url: str):
|
|
findBy: dict = {
|
|
'op-': 'option',
|
|
'delivery': 'future',
|
|
'fx': 'swap',
|
|
}
|
|
keys = list(findBy.keys())
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
value = findBy[key]
|
|
if url.find(key) >= 0:
|
|
return value
|
|
return 'spot'
|
|
|
|
def request_id(self):
|
|
# their support said that reqid must be an int32, not documented
|
|
reqid = self.sum(self.safe_integer(self.options, 'reqid', 0), 1)
|
|
self.options['reqid'] = reqid
|
|
return reqid
|
|
|
|
async def subscribe_public(self, url, messageHash, payload, channel, params={}, subscription=None):
|
|
requestId = self.request_id()
|
|
time = self.seconds()
|
|
request: dict = {
|
|
'id': requestId,
|
|
'time': time,
|
|
'channel': channel,
|
|
'event': 'subscribe',
|
|
'payload': payload,
|
|
}
|
|
if subscription is not None:
|
|
client = self.client(url)
|
|
if not (messageHash in client.subscriptions):
|
|
tempSubscriptionHash = str(requestId)
|
|
client.subscriptions[tempSubscriptionHash] = messageHash
|
|
message = self.extend(request, params)
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
async def subscribe_public_multiple(self, url, messageHashes, payload, channel, params={}):
|
|
requestId = self.request_id()
|
|
time = self.seconds()
|
|
request: dict = {
|
|
'id': requestId,
|
|
'time': time,
|
|
'channel': channel,
|
|
'event': 'subscribe',
|
|
'payload': payload,
|
|
}
|
|
message = self.extend(request, params)
|
|
return await self.watch_multiple(url, messageHashes, message, messageHashes)
|
|
|
|
async def un_subscribe_public_multiple(self, url, topic, symbols, messageHashes, subMessageHashes, payload, channel, params={}):
|
|
requestId = self.request_id()
|
|
time = self.seconds()
|
|
request: dict = {
|
|
'id': requestId,
|
|
'time': time,
|
|
'channel': channel,
|
|
'event': 'unsubscribe',
|
|
'payload': payload,
|
|
}
|
|
sub = {
|
|
'id': str(requestId),
|
|
'topic': topic,
|
|
'unsubscribe': True,
|
|
'messageHashes': messageHashes,
|
|
'subMessageHashes': subMessageHashes,
|
|
'symbols': symbols,
|
|
}
|
|
message = self.extend(request, params)
|
|
return await self.watch_multiple(url, messageHashes, message, messageHashes, sub)
|
|
|
|
async def authenticate(self, url, messageType):
|
|
channel = messageType + '.login'
|
|
client = self.client(url)
|
|
messageHash = 'authenticated'
|
|
future = client.reusableFuture(messageHash)
|
|
authenticated = self.safe_value(client.subscriptions, messageHash)
|
|
if authenticated is None:
|
|
return await self.request_private(url, {}, channel, messageHash)
|
|
return future
|
|
|
|
def handle_authentication_message(self, client: Client, message):
|
|
messageHash = 'authenticated'
|
|
future = self.safe_value(client.futures, messageHash)
|
|
future.resolve(True)
|
|
|
|
async def request_private(self, url, reqParams, channel, requestId: Str = None):
|
|
self.check_required_credentials()
|
|
# uid is required for some subscriptions only so it's not a part of required credentials
|
|
event = 'api'
|
|
if requestId is None:
|
|
reqId = self.request_id()
|
|
requestId = str(reqId)
|
|
messageHash = requestId
|
|
time = self.seconds()
|
|
# unfortunately, PHP demands double quotes for the escaped newline symbol
|
|
signatureString = "\n".join([event, channel, self.json(reqParams), str(time)]) # eslint-disable-line quotes
|
|
signature = self.hmac(self.encode(signatureString), self.encode(self.secret), hashlib.sha512, 'hex')
|
|
payload: dict = {
|
|
'req_id': requestId,
|
|
'timestamp': str(time),
|
|
'api_key': self.apiKey,
|
|
'signature': signature,
|
|
'req_param': reqParams,
|
|
}
|
|
if (channel == 'spot.order_place') or (channel == 'futures.order_place'):
|
|
payload['req_header'] = {
|
|
'X-Gate-Channel-Id': 'ccxt',
|
|
}
|
|
request: dict = {
|
|
'id': requestId,
|
|
'time': time,
|
|
'channel': channel,
|
|
'event': event,
|
|
'payload': payload,
|
|
}
|
|
return await self.watch(url, messageHash, request, messageHash, requestId)
|
|
|
|
async def subscribe_private(self, url, messageHash, payload, channel, params, requiresUid=False):
|
|
self.check_required_credentials()
|
|
# uid is required for some subscriptions only so it's not a part of required credentials
|
|
if requiresUid:
|
|
if self.uid is None or len(self.uid) == 0:
|
|
raise ArgumentsRequired(self.id + ' requires uid to subscribe')
|
|
idArray = [self.uid]
|
|
if payload is None:
|
|
payload = idArray
|
|
else:
|
|
payload = self.array_concat(idArray, payload)
|
|
time = self.seconds()
|
|
event = 'subscribe'
|
|
signaturePayload = 'channel=' + channel + '&' + 'event=' + event + '&' + 'time=' + str(time)
|
|
signature = self.hmac(self.encode(signaturePayload), self.encode(self.secret), hashlib.sha512, 'hex')
|
|
auth: dict = {
|
|
'method': 'api_key',
|
|
'KEY': self.apiKey,
|
|
'SIGN': signature,
|
|
}
|
|
requestId = self.request_id()
|
|
request: dict = {
|
|
'id': requestId,
|
|
'time': time,
|
|
'channel': channel,
|
|
'event': event,
|
|
'auth': auth,
|
|
}
|
|
if payload is not None:
|
|
request['payload'] = payload
|
|
client = self.client(url)
|
|
if not (messageHash in client.subscriptions):
|
|
tempSubscriptionHash = str(requestId)
|
|
# in case of authenticationError we will throw
|
|
client.subscriptions[tempSubscriptionHash] = messageHash
|
|
message = self.extend(request, params)
|
|
return await self.watch(url, messageHash, message, messageHash, messageHash)
|