# -*- 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, Market, Order, OrderBook, Position, Str, Strings, Ticker, 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 BadSymbol from ccxt.base.errors import NetworkError from ccxt.base.errors import InvalidNonce from ccxt.base.errors import ChecksumError class htx(ccxt.async_support.htx): def describe(self) -> Any: return self.deep_extend(super(htx, self).describe(), { 'has': { 'ws': True, 'createOrderWs': False, 'editOrderWs': False, 'fetchOpenOrdersWs': False, 'fetchOrderWs': False, 'cancelOrderWs': False, 'cancelOrdersWs': False, 'cancelAllOrdersWs': False, 'fetchTradesWs': False, 'fetchBalanceWs': False, 'watchOrderBook': True, 'watchOrders': True, 'watchTickers': False, 'watchTicker': True, 'watchTrades': True, 'watchTradesForSymbols': False, 'watchMyTrades': True, 'watchBalance': True, 'watchOHLCV': True, 'unwatchTicker': True, 'unwatchOHLCV': True, 'unwatchTrades': True, 'unwatchOrderBook': True, }, 'urls': { 'api': { 'ws': { 'api': { 'spot': { 'public': 'wss://{hostname}/ws', 'private': 'wss://{hostname}/ws/v2', 'feed': 'wss://{hostname}/feed', }, 'future': { 'linear': { 'public': 'wss://api.hbdm.com/linear-swap-ws', 'private': 'wss://api.hbdm.com/linear-swap-notification', }, 'inverse': { 'public': 'wss://api.hbdm.com/ws', 'private': 'wss://api.hbdm.com/notification', }, }, 'swap': { 'inverse': { 'public': 'wss://api.hbdm.com/swap-ws', 'private': 'wss://api.hbdm.com/swap-notification', }, 'linear': { 'public': 'wss://api.hbdm.com/linear-swap-ws', 'private': 'wss://api.hbdm.com/linear-swap-notification', }, }, }, # these settings work faster for clients hosted on AWS 'api-aws': { 'spot': { 'public': 'wss://api-aws.huobi.pro/ws', 'private': 'wss://api-aws.huobi.pro/ws/v2', 'feed': 'wss://{hostname}/feed', }, 'future': { 'linear': { 'public': 'wss://api.hbdm.vn/linear-swap-ws', 'private': 'wss://api.hbdm.vn/linear-swap-notification', }, 'inverse': { 'public': 'wss://api.hbdm.vn/ws', 'private': 'wss://api.hbdm.vn/notification', }, }, 'swap': { 'linear': { 'public': 'wss://api.hbdm.vn/linear-swap-ws', 'private': 'wss://api.hbdm.vn/linear-swap-notification', }, 'inverse': { 'public': 'wss://api.hbdm.vn/swap-ws', 'private': 'wss://api.hbdm.vn/swap-notification', }, }, }, }, }, }, 'options': { 'tradesLimit': 1000, 'OHLCVLimit': 1000, 'api': 'api', # or api-aws for clients hosted on AWS 'watchOrderBook': { 'maxRetries': 3, 'checksum': True, 'depth': 150, # 150 or 20 }, 'ws': { 'gunzip': True, }, 'watchTicker': { 'name': 'market.{marketId}.detail', # 'market.{marketId}.bbo' or 'market.{marketId}.ticker' }, }, 'exceptions': { 'ws': { 'exact': { 'bad-request': BadRequest, # { ts: 1586323747018, status: 'error', 'err-code': 'bad-request', err-msg': 'invalid mbp.150.symbol linkusdt', id: '2'} '2002': AuthenticationError, # {action: 'sub', code: 2002, ch: 'accounts.update#2', message: 'invalid.auth.state'} '2021': BadRequest, '2001': BadSymbol, # {action: 'sub', code: 2001, ch: 'orders#2ltcusdt', message: 'invalid.symbol'} '2011': BadSymbol, # {op: 'sub', cid: '1649149285', topic: 'orders_cross.ltc-usdt', 'err-code': 2011, 'err-msg': "Contract doesn't exist.", ts: 1649149287637} '2040': BadRequest, # {op: 'sub', cid: '1649152947', 'err-code': 2040, 'err-msg': 'Missing required parameter.', ts: 1649152948684} '4007': BadRequest, # {op: 'sub', cid: '1', topic: 'accounts_unify.USDT', 'err-code': 4007, 'err-msg': 'Non - single account user is not available, please check through the cross and isolated account asset interface', ts: 1698419318540} }, }, }, }) def request_id(self): requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) self.options['requestId'] = requestId return str(requestId) async def watch_ticker(self, symbol: str, params={}) -> Ticker: """ watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market https://www.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003 :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 ` """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] options = self.safe_dict(self.options, 'watchTicker', {}) topic = self.safe_string(options, 'name', 'market.{marketId}.detail') if topic == 'market.{marketId}.ticker' and market['type'] != 'spot': raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead') messageHash = self.implode_params(topic, {'marketId': market['id']}) url = self.get_url_by_market_type(market['type'], market['linear']) return await self.subscribe_public(url, symbol, messageHash, None, params) async def un_watch_ticker(self, symbol: str, params={}) -> Any: """ unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list https://www.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003 :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 ` """ await self.load_markets() market = self.market(symbol) topic = 'ticker' options = self.safe_dict(self.options, 'watchTicker', {}) channel = self.safe_string(options, 'name', 'market.{marketId}.detail') if channel == 'market.{marketId}.ticker' and market['type'] != 'spot': raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead') subMessageHash = self.implode_params(channel, {'marketId': market['id']}) return await self.unsubscribe_public(market, subMessageHash, topic, params) def handle_ticker(self, client: Client, message): # # "market.btcusdt.detail" # { # "ch": "market.btcusdt.detail", # "ts": 1583494163784, # "tick": { # "id": 209988464418, # "low": 8988, # "high": 9155.41, # "open": 9078.91, # "close": 9136.46, # "vol": 237813910.5928412, # "amount": 26184.202558551195, # "version": 209988464418, # "count": 265673 # } # } # "market.btcusdt.bbo" # { # "ch": "market.btcusdt.bbo", # "ts": 1671941599613, # "tick": { # "seqId": 161499562790, # "ask": 16829.51, # "askSize": 0.707776, # "bid": 16829.5, # "bidSize": 1.685945, # "quoteTime": 1671941599612, # "symbol": "btcusdt" # } # } # tick = self.safe_value(message, 'tick', {}) ch = self.safe_string(message, 'ch') parts = ch.split('.') marketId = self.safe_string(parts, 1) market = self.safe_market(marketId) ticker = self.parse_ticker(tick, market) timestamp = self.safe_value(message, 'ts') ticker['timestamp'] = timestamp ticker['datetime'] = self.iso8601(timestamp) symbol = ticker['symbol'] self.tickers[symbol] = ticker client.resolve(ticker, ch) return message 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 https://www.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003 :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 ` """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] messageHash = 'market.' + market['id'] + '.trade.detail' url = self.get_url_by_market_type(market['type'], market['linear']) trades = await self.subscribe_public(url, symbol, messageHash, None, params) if self.newUpdates: limit = trades.getLimit(symbol, limit) return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) async def un_watch_trades(self, symbol: str, params={}) -> Any: """ unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list https://www.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003 :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 ` """ await self.load_markets() market = self.market(symbol) topic = 'trades' options = self.safe_dict(self.options, 'watchTrades', {}) channel = self.safe_string(options, 'name', 'market.{marketId}.trade.detail') subMessageHash = self.implode_params(channel, {'marketId': market['id']}) return await self.unsubscribe_public(market, subMessageHash, topic, params) def handle_trades(self, client: Client, message): # # { # "ch": "market.btcusdt.trade.detail", # "ts": 1583495834011, # "tick": { # "id": 105004645372, # "ts": 1583495833751, # "data": [ # { # "id": 1.050046453727319e+22, # "ts": 1583495833751, # "tradeId": 102090727790, # "amount": 0.003893, # "price": 9150.01, # "direction": "sell" # } # ] # } # } # tick = self.safe_value(message, 'tick', {}) data = self.safe_value(tick, 'data', {}) ch = self.safe_string(message, 'ch') parts = ch.split('.') marketId = self.safe_string(parts, 1) market = self.safe_market(marketId) symbol = market['symbol'] tradesCache = self.safe_value(self.trades, symbol) if tradesCache is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) tradesCache = ArrayCache(limit) self.trades[symbol] = tradesCache for i in range(0, len(data)): trade = self.parse_trade(data[i], market) tradesCache.append(trade) client.resolve(tradesCache, ch) return message 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 https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003 :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'] interval = self.safe_string(self.timeframes, timeframe, timeframe) messageHash = 'market.' + market['id'] + '.kline.' + interval url = self.get_url_by_market_type(market['type'], market['linear']) ohlcv = await self.subscribe_public(url, symbol, messageHash, None, params) if self.newUpdates: limit = ohlcv.getLimit(symbol, limit) return self.filter_by_since_limit(ohlcv, since, limit, 0, True) async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any: """ unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003 :param str symbol: unified symbol of the market :param str timeframe: the length of time each candle represents :param dict [params]: extra parameters specific to the exchange API endpoint :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00' :returns int[][]: A list of candles ordered, open, high, low, close, volume """ await self.load_markets() market = self.market(symbol) interval = self.safe_string(self.timeframes, timeframe, timeframe) subMessageHash = 'market.' + market['id'] + '.kline.' + interval topic = 'ohlcv' params['symbolsAndTimeframes'] = [[market['symbol'], timeframe]] return await self.unsubscribe_public(market, subMessageHash, topic, params) def handle_ohlcv(self, client: Client, message): # # { # "ch": "market.btcusdt.kline.1min", # "ts": 1583501786794, # "tick": { # "id": 1583501760, # "open": 9094.5, # "close": 9094.51, # "low": 9094.5, # "high": 9094.51, # "amount": 0.44639786263800907, # "vol": 4059.76919054, # "count": 16 # } # } # ch = self.safe_string(message, 'ch') parts = ch.split('.') marketId = self.safe_string(parts, 1) market = self.safe_market(marketId) symbol = market['symbol'] interval = self.safe_string(parts, 3) timeframe = self.find_timeframe(interval) 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][timeframe] = stored tick = self.safe_value(message, 'tick') parsed = self.parse_ohlcv(tick, market) stored.append(parsed) client.resolve(stored, ch) async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: """ https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-market-depth-data watches 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 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 ` indexed by market symbols """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] allowedLimits = [5, 20, 150, 400] # 2) 5-level/20-level incremental MBP is a tick by tick feed, # which means whenever there is an order book change at that level, it pushes an update # 150-levels/400-level incremental MBP feed is based on the gap # between two snapshots at 100ms interval. options = self.safe_dict(self.options, 'watchOrderBook', {}) if limit is None: limit = self.safe_integer(options, 'depth', 150) if not self.in_array(limit, allowedLimits): raise ExchangeError(self.id + ' watchOrderBook market accepts limits of 5, 20, 150 or 400 only') messageHash = None if market['spot']: messageHash = 'market.' + market['id'] + '.mbp.' + self.number_to_string(limit) else: messageHash = 'market.' + market['id'] + '.depth.size_' + self.number_to_string(limit) + '.high_freq' url = self.get_url_by_market_type(market['type'], market['linear'], False, True) method = self.handle_order_book_subscription if not market['spot']: params = self.extend(params) params['data_type'] = 'incremental' method = None orderbook = await self.subscribe_public(url, symbol, messageHash, method, params) return orderbook.limit() async def un_watch_order_book(self, symbol: str, params={}) -> Any: """ unsubscribe from the orderbook channel https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-market-depth-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 :param int [params.limit]: orderbook limit, default is None :returns dict: A dictionary of `order book structures ` indexed by market symbols """ await self.load_markets() market = self.market(symbol) topic = 'orderbook' options = self.safe_dict(self.options, 'watchOrderBook', {}) depth = self.safe_integer(options, 'depth', 150) subMessageHash = None if market['spot']: subMessageHash = 'market.' + market['id'] + '.mbp.' + self.number_to_string(depth) else: subMessageHash = 'market.' + market['id'] + '.depth.size_' + self.number_to_string(depth) + '.high_freq' if not (market['spot']): params['data_type'] = 'incremental' return await self.unsubscribe_public(market, subMessageHash, topic, params) def handle_order_book_snapshot(self, client: Client, message, subscription): # # { # "id": 1583473663565, # "rep": "market.btcusdt.mbp.150", # "status": "ok", # "ts": 1698359289261, # "data": { # "seqNum": 104999417756, # "bids": [ # [9058.27, 0], # [9058.43, 0], # [9058.99, 0], # ], # "asks": [ # [9084.27, 0.2], # [9085.69, 0], # [9085.81, 0], # ] # } # } # symbol = self.safe_string(subscription, 'symbol') messageHash = self.safe_string(subscription, 'messageHash') id = self.safe_string(message, 'id') lastTimestamp = self.safe_integer(subscription, 'lastTimestamp') try: orderbook = self.orderbooks[symbol] data = self.safe_value(message, 'data') messages = orderbook.cache firstMessage = self.safe_value(messages, 0, {}) snapshot = self.parse_order_book(data, symbol) tick = self.safe_value(firstMessage, 'tick') sequence = self.safe_integer(tick, 'prevSeqNum') nonce = self.safe_integer(data, 'seqNum') snapshot['nonce'] = nonce snapshotTimestamp = self.safe_integer(message, 'ts') subscription['lastTimestamp'] = snapshotTimestamp snapshotLimit = self.safe_integer(subscription, 'limit') snapshotOrderBook = self.order_book(snapshot, snapshotLimit) client.resolve(snapshotOrderBook, id) if (sequence is None) or (nonce < sequence): maxAttempts = self.handle_option('watchOrderBook', 'maxRetries', 3) numAttempts = self.safe_integer(subscription, 'numAttempts', 0) # retry to synchronize if we have not reached maxAttempts yet if numAttempts < maxAttempts: # safety guard if messageHash in client.subscriptions: numAttempts = self.sum(numAttempts, 1) delayTime = self.sum(1000, lastTimestamp - snapshotTimestamp) subscription['numAttempts'] = numAttempts client.subscriptions[messageHash] = subscription self.delay(delayTime, self.watch_order_book_snapshot, client, message, subscription) else: # raise upon failing to synchronize in maxAttempts raise InvalidNonce(self.id + ' failed to synchronize WebSocket feed with the snapshot for symbol ' + symbol + ' in ' + str(maxAttempts) + ' attempts') else: orderbook.reset(snapshot) # unroll the accumulated deltas for i in range(0, len(messages)): self.handle_order_book_message(client, messages[i]) orderbook.cache = [] self.orderbooks[symbol] = orderbook client.resolve(orderbook, messageHash) except Exception as e: del client.subscriptions[messageHash] del self.orderbooks[symbol] client.reject(e, messageHash) async def watch_order_book_snapshot(self, client, message, subscription): messageHash = self.safe_string(subscription, 'messageHash') symbol = self.safe_string(subscription, 'symbol') limit = self.safe_integer(subscription, 'limit') timestamp = self.safe_integer(message, 'ts') params = self.safe_value(subscription, 'params') attempts = self.safe_integer(subscription, 'numAttempts', 0) market = self.market(symbol) url = self.get_url_by_market_type(market['type'], market['linear'], False, True) requestId = self.request_id() request: dict = { 'req': messageHash, 'id': requestId, } # self is a temporary subscription by a specific requestId # it has a very short lifetime until the snapshot is received over ws snapshotSubscription: dict = { 'id': requestId, 'messageHash': messageHash, 'symbol': symbol, 'limit': limit, 'params': params, 'numAttempts': attempts, 'lastTimestamp': timestamp, 'method': self.handle_order_book_snapshot, } try: orderbook = await self.watch(url, requestId, request, requestId, snapshotSubscription) return orderbook.limit() except Exception as e: del client.subscriptions[messageHash] client.reject(e, messageHash) return None def handle_delta(self, bookside, delta): price = self.safe_float(delta, 0) amount = self.safe_float(delta, 1) bookside.store(price, amount) def handle_deltas(self, bookside, deltas): for i in range(0, len(deltas)): self.handle_delta(bookside, deltas[i]) def handle_order_book_message(self, client: Client, message): # spot markets # # { # "ch": "market.btcusdt.mbp.150", # "ts": 1583472025885, # "tick": { # "seqNum": 104998984994, # "prevSeqNum": 104998984977, # "bids": [ # [9058.27, 0], # [9058.43, 0], # [9058.99, 0], # ], # "asks": [ # [9084.27, 0.2], # [9085.69, 0], # [9085.81, 0], # ] # } # } # # non-spot market update # # { # "ch":"market.BTC220218.depth.size_150.high_freq", # "tick":{ # "asks":[], # "bids":[ # [43445.74,1], # [43444.48,0], # [40593.92,9] # ], # "ch":"market.BTC220218.depth.size_150.high_freq", # "event":"update", # "id":152727500274, # "mrid":152727500274, # "ts":1645023376098, # "version":37536690 # }, # "ts":1645023376098 # } # non-spot market snapshot # # { # "ch":"market.BTC220218.depth.size_150.high_freq", # "tick":{ # "asks":[ # [43445.74,1], # [43444.48,0], # [40593.92,9] # ], # "bids":[ # [43445.74,1], # [43444.48,0], # [40593.92,9] # ], # "ch":"market.BTC220218.depth.size_150.high_freq", # "event":"snapshot", # "id":152727500274, # "mrid":152727500274, # "ts":1645023376098, # "version":37536690 # }, # "ts":1645023376098 # } # ch = self.safe_value(message, 'ch') parts = ch.split('.') marketId = self.safe_string(parts, 1) market = self.safe_market(marketId) symbol = market['symbol'] orderbook = self.orderbooks[symbol] tick = self.safe_value(message, 'tick', {}) seqNum = self.safe_integer(tick, 'seqNum') prevSeqNum = self.safe_integer(tick, 'prevSeqNum') event = self.safe_string(tick, 'event') version = self.safe_integer(tick, 'version') timestamp = self.safe_integer(message, 'ts') if event == 'snapshot': snapshot = self.parse_order_book(tick, symbol, timestamp) orderbook.reset(snapshot) orderbook['nonce'] = version if (prevSeqNum is not None) and prevSeqNum > orderbook['nonce']: checksum = self.handle_option('watchOrderBook', 'checksum', True) if checksum: raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol)) spotConditon = market['spot'] and (prevSeqNum == orderbook['nonce']) nonSpotCondition = market['contract'] and (version - 1 == orderbook['nonce']) if spotConditon or nonSpotCondition: asks = self.safe_value(tick, 'asks', []) bids = self.safe_value(tick, 'bids', []) self.handle_deltas(orderbook['asks'], asks) self.handle_deltas(orderbook['bids'], bids) orderbook['nonce'] = seqNum if spotConditon else version orderbook['timestamp'] = timestamp orderbook['datetime'] = self.iso8601(timestamp) def handle_order_book(self, client: Client, message): # # deltas # # spot markets # # { # "ch": "market.btcusdt.mbp.150", # "ts": 1583472025885, # "tick": { # "seqNum": 104998984994, # "prevSeqNum": 104998984977, # "bids": [ # [9058.27, 0], # [9058.43, 0], # [9058.99, 0], # ], # "asks": [ # [9084.27, 0.2], # [9085.69, 0], # [9085.81, 0], # ] # } # } # # non spot markets # # { # "ch":"market.BTC220218.depth.size_150.high_freq", # "tick":{ # "asks":[], # "bids":[ # [43445.74,1], # [43444.48,0], # [40593.92,9] # ], # "ch":"market.BTC220218.depth.size_150.high_freq", # "event":"update", # "id":152727500274, # "mrid":152727500274, # "ts":1645023376098, # "version":37536690 # }, # "ts":1645023376098 # } # messageHash = self.safe_string(message, 'ch') tick = self.safe_dict(message, 'tick') event = self.safe_string(tick, 'event') ch = self.safe_string(message, 'ch') parts = ch.split('.') marketId = self.safe_string(parts, 1) symbol = self.safe_symbol(marketId) if not (symbol in self.orderbooks): size = self.safe_string(parts, 3) sizeParts = size.split('_') limit = self.safe_integer(sizeParts, 1) self.orderbooks[symbol] = self.order_book({}, limit) orderbook = self.orderbooks[symbol] if (event is None) and (orderbook['nonce'] is None): orderbook.cache.append(message) else: self.handle_order_book_message(client, message) client.resolve(orderbook, messageHash) def handle_order_book_subscription(self, client: Client, message, subscription): symbol = self.safe_string(subscription, 'symbol') market = self.market(symbol) limit = self.safe_integer(subscription, 'limit') self.orderbooks[symbol] = self.order_book({}, limit) if market['spot']: self.spawn(self.watch_order_book_snapshot, client, message, subscription) 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 https://www.htx.com/en-us/opend/newApiPages/?id=7ec53dd5-7773-11ed-9966-0242ac110003 :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 ` """ self.check_required_credentials() await self.load_markets() type = None marketId = '*' # wildcard market = None messageHash = None channel = None trades = None subType = None if symbol is not None: market = self.market(symbol) symbol = market['symbol'] type = market['type'] subType = 'linear' if market['linear'] else 'inverse' marketId = market['lowercaseId'] else: type = self.safe_string(self.options, 'defaultType', 'spot') type = self.safe_string(params, 'type', type) subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear') subType = self.safe_string(params, 'subType', subType) params = self.omit(params, ['type', 'subType']) if type == 'spot': mode = None if mode is None: mode = self.safe_string_2(self.options, 'watchMyTrades', 'mode', '0') mode = self.safe_string(params, 'mode', mode) params = self.omit(params, 'mode') messageHash = 'trade.clearing' + '#' + marketId + '#' + mode channel = messageHash else: channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params) channel = self.safe_string(channelAndMessageHash, 0) orderMessageHash = self.safe_string(channelAndMessageHash, 1) # we will take advantage of the order messageHash because already handles stuff # like symbol/margin/subtype/type variations messageHash = orderMessageHash + ':' + 'trade' trades = await self.subscribe_private(channel, messageHash, type, subType, params) if self.newUpdates: limit = trades.getLimit(symbol, limit) return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) def get_order_channel_and_message_hash(self, type, subType, market=None, params={}): messageHash = None channel = None orderType = self.safe_string(self.options, 'orderType', 'orders') # orders or matchOrders orderType = self.safe_string(params, 'orderType', orderType) params = self.omit(params, 'orderType') marketCode = market['lowercaseId'].lower() if (market is not None) else None baseId = market['baseId'] if (market is not None) else None prefix = orderType messageHash = prefix if subType == 'linear': # USDT Margined Contracts Example: LTC/USDT:USDT marginMode = self.safe_string(params, 'margin', 'cross') marginPrefix = prefix + '_cross' if (marginMode == 'cross') else prefix messageHash = marginPrefix if marketCode is not None: messageHash += '.' + marketCode channel = messageHash else: channel = marginPrefix + '.' + '*' elif type == 'future': # inverse futures Example: BCH/USD:BCH-220408 if baseId is not None: channel = prefix + '.' + baseId.lower() messageHash = channel else: channel = prefix + '.' + '*' else: # inverse swaps: Example: BTC/USD:BTC if marketCode is not None: channel = prefix + '.' + marketCode messageHash = channel else: channel = prefix + '.' + '*' return [channel, messageHash] 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 https://www.htx.com/en-us/opend/newApiPages/?id=7ec53c8f-7773-11ed-9966-0242ac110003 :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 dict[]: a list of `order structures ` """ await self.load_markets() type = None subType = None market = None suffix = '*' # wildcard if symbol is not None: market = self.market(symbol) symbol = market['symbol'] type = market['type'] suffix = market['lowercaseId'] subType = 'linear' if market['linear'] else 'inverse' else: type = self.safe_string(self.options, 'defaultType', 'spot') type = self.safe_string(params, 'type', type) subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear') subType = self.safe_string(params, 'subType', subType) params = self.omit(params, ['type', 'subType']) messageHash = None channel = None if type == 'spot': messageHash = 'orders' + '#' + suffix channel = messageHash else: channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params) channel = self.safe_string(channelAndMessageHash, 0) messageHash = self.safe_string(channelAndMessageHash, 1) orders = await self.subscribe_private(channel, messageHash, type, subType, params) 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): # # spot # # for new order creation # # { # "action":"push", # "ch":"orders#btcusdt", # or "orders#*" for global subscriptions # "data": { # "orderStatus": "submitted", # "eventType": "creation", # "totalTradeAmount": 0 # for "submitted" order status # "orderCreateTime": 1645116048355, # only when `submitted` status # "orderSource": "spot-web", # "accountId": 44234548, # "orderPrice": "100", # "orderSize": "0.05", # "symbol": "ethusdt", # "type": "buy-limit", # "orderId": "478861479986886", # "clientOrderId": '', # } # } # # for filled order, additional fields are present: # # "orderStatus": "filled", # "eventType": "trade", # "totalTradeAmount": "5.9892649859", # "tradePrice": "0.676669", # "tradeVolume": "8.8511", # "tradeTime": 1760427775894, # "aggressor": False, # "execAmt": "8.8511", # "tradeId": 100599712781, # "remainAmt": "0", # # spot wrapped trade # # { # "action": "push", # "ch": "orders#ltcusdt", # "data": { # "tradePrice": "130.01", # "tradeVolume": "0.0385", # "tradeTime": 1648714741525, # "aggressor": True, # "execAmt": "0.0385", # "orderSource": "spot-web", # "orderSize": "0.0385", # "remainAmt": "0", # "tradeId": 101541578884, # "symbol": "ltcusdt", # "type": "sell-market", # "eventType": "trade", # "clientOrderId": '', # "orderStatus": "filled", # "orderId": 509835753860328 # } # } # # non spot order # # { # "contract_type": "swap", # "pair": "LTC-USDT", # "business_type": "swap", # "op": "notify", # "topic": "orders_cross.ltc-usdt", # "ts": 1650354508696, # "symbol": "LTC", # "contract_code": "LTC-USDT", # "volume": 1, # "price": 110.34, # "order_price_type": "lightning", # "direction": "sell", # "offset": "close", # "status": 6, # "lever_rate": 1, # "order_id": "966002354015051776", # "order_id_str": "966002354015051776", # "client_order_id": null, # "order_source": "web", # "order_type": 1, # "created_at": 1650354508649, # "trade_volume": 1, # "trade_turnover": 11.072, # "fee": -0.005536, # "trade_avg_price": 110.72, # "margin_frozen": 0, # "profit": -0.045, # "trade": [ # { # "trade_fee": -0.005536, # "fee_asset": "USDT", # "real_profit": 0.473, # "profit": -0.045, # "trade_id": 86678766507, # "id": "86678766507-966002354015051776-1", # "trade_volume": 1, # "trade_price": 110.72, # "trade_turnover": 11.072, # "created_at": 1650354508656, # "role": "taker" # } # ], # "canceled_at": 0, # "fee_asset": "USDT", # "margin_asset": "USDT", # "uid": "359305390", # "liquidation_type": "0", # "margin_mode": "cross", # "margin_account": "USDT", # "is_tpsl": 0, # "real_profit": 0.473, # "trade_partition": "USDT", # "reduce_only": 1 # } # # messageHash = self.safe_string_2(message, 'ch', 'topic') data = self.safe_value(message, 'data') marketId = self.safe_string(message, 'contract_code') if marketId is None: marketId = self.safe_string(data, 'symbol') market = self.safe_market(marketId) parsedOrder = None if data is not None: # spot updates eventType = self.safe_string(data, 'eventType') if eventType == 'trade': # when a spot order is filled we get an update message # with the trade info parsedTrade = self.parse_order_trade(data, market) # inject trade in existing order by faking an order object orderId = self.safe_string(parsedTrade, 'order') trades = [parsedTrade] status = self.parse_order_status(self.safe_string_2(data, 'orderStatus', 'status', 'closed')) filled = self.safe_string(data, 'execAmt') remaining = self.safe_string(data, 'remainAmt') order: dict = { 'id': orderId, 'trades': trades, 'status': status, 'symbol': market['symbol'], 'filled': self.parse_number(filled), 'remaining': self.parse_number(remaining), 'price': self.safe_number(data, 'orderPrice'), 'amount': self.safe_number(data, 'orderSize'), 'info': data, } parsedOrder = order else: parsedOrder = self.parse_ws_order(data, market) else: # contract branch parsedOrder = self.parse_ws_order(message, market) rawTrades = self.safe_value(message, 'trade', []) tradesLength = len(rawTrades) if tradesLength > 0: tradesObject: dict = { 'trades': rawTrades, 'ch': messageHash, 'symbol': marketId, } # inject order params in every trade extendTradeParams: dict = { 'order': self.safe_string(parsedOrder, 'id'), 'type': self.safe_string(parsedOrder, 'type'), 'side': self.safe_string(parsedOrder, 'side'), } # trades arrive inside an order update # we're forwarding them to handleMyTrade # so they can be properly resolved self.handle_my_trade(client, tradesObject, extendTradeParams) if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) cachedOrders = self.orders cachedOrders.append(parsedOrder) client.resolve(self.orders, messageHash) # when we make a global subscription(for contracts only) our message hash can't have a symbol/currency attached # so we're removing it here genericMessageHash = messageHash.replace('.' + market['lowercaseId'], '') lowerCaseBaseId = self.safe_string_lower(market, 'baseId') genericMessageHash = genericMessageHash.replace('.' + lowerCaseBaseId, '') client.resolve(self.orders, genericMessageHash) def parse_ws_order(self, order, market=None): # # spot # # { # "orderSource": "spot-web", # "orderCreateTime": 1645116048355, # creating only # "accountId": 44234548, # "orderPrice": "100", # "orderSize": "0.05", # "orderValue": "3.71676361", # market-buy only # "symbol": "ethusdt", # "type": "buy-limit", # "orderId": "478861479986886", # "eventType": "creation", # "clientOrderId": '', # "orderStatus": "submitted" # "lastActTime":1645118621810 # except creating # "execAmt":"0" # } # # swap order # # { # "contract_type": "swap", # "pair": "LTC-USDT", # "business_type": "swap", # "op": "notify", # "topic": "orders_cross.ltc-usdt", # "ts": 1648717911384, # "symbol": "LTC", # "contract_code": "LTC-USDT", # "volume": 1, # "price": 129.13, # "order_price_type": "lightning", # "direction": "sell", # "offset": "close", # "status": 6, # "lever_rate": 5, # "order_id": "959137967397068800", # "order_id_str": "959137967397068800", # "client_order_id": null, # "order_source": "web", # "order_type": 1, # "created_at": 1648717911344, # "trade_volume": 1, # "trade_turnover": 12.952, # "fee": -0.006476, # "trade_avg_price": 129.52, # "margin_frozen": 0, # "profit": -0.005, # "trade": [ # { # "trade_fee": -0.006476, # "fee_asset": "USDT", # "real_profit": -0.005, # "profit": -0.005, # "trade_id": 83619995370, # "id": "83619995370-959137967397068800-1", # "trade_volume": 1, # "trade_price": 129.52, # "trade_turnover": 12.952, # "created_at": 1648717911352, # "role": "taker" # } # ], # "canceled_at": 0, # "fee_asset": "USDT", # "margin_asset": "USDT", # "uid": "359305390", # "liquidation_type": "0", # "margin_mode": "cross", # "margin_account": "USDT", # "is_tpsl": 0, # "real_profit": -0.005, # "trade_partition": "USDT", # "reduce_only": 1 # } # # { # "op":"notify", # "topic":"orders.ada", # "ts":1604388667226, # "symbol":"ADA", # "contract_type":"quarter", # "contract_code":"ADA201225", # "volume":1, # "price":0.0905, # "order_price_type":"post_only", # "direction":"sell", # "offset":"open", # "status":6, # "lever_rate":20, # "order_id":773207641127878656, # "order_id_str":"773207641127878656", # "client_order_id":null, # "order_source":"web", # "order_type":1, # "created_at":1604388667146, # "trade_volume":1, # "trade_turnover":10, # "fee":-0.022099447513812154, # "trade_avg_price":0.0905, # "margin_frozen":0, # "profit":0, # "trade":[], # "canceled_at":0, # "fee_asset":"ADA", # "uid":"123456789", # "liquidation_type":"0", # "is_tpsl": 0, # "real_profit": 0 # } # lastTradeTimestamp = self.safe_integer_2(order, 'lastActTime', 'ts') created = self.safe_integer(order, 'orderCreateTime') marketId = self.safe_string_2(order, 'contract_code', 'symbol') market = self.safe_market(marketId, market) symbol = self.safe_symbol(marketId, market) amount = self.safe_string_2(order, 'orderSize', 'volume') status = self.parse_order_status(self.safe_string_2(order, 'orderStatus', 'status')) id = self.safe_string_2(order, 'orderId', 'order_id') clientOrderId = self.safe_string_2(order, 'clientOrderId', 'client_order_id') price = self.safe_string_2(order, 'orderPrice', 'price') filled = self.safe_string(order, 'execAmt') typeSide = self.safe_string(order, 'type') feeCost = self.safe_string(order, 'fee') fee = None if feeCost is not None: feeCurrencyId = self.safe_string(order, 'fee_asset') fee = { 'cost': feeCost, 'currency': self.safe_currency_code(feeCurrencyId), } avgPrice = self.safe_string(order, 'trade_avg_price') rawTrades = self.safe_value(order, 'trade') typeSideParts = [] if typeSide is not None: typeSideParts = typeSide.split('-') type = self.safe_string_lower(typeSideParts, 1) if type is None: type = self.safe_string(order, 'order_price_type') side = self.safe_string_lower(typeSideParts, 0) if side is None: side = self.safe_string(order, 'direction') cost = self.safe_string(order, 'orderValue') return self.safe_order({ 'info': order, 'id': id, 'clientOrderId': clientOrderId, 'timestamp': created, 'datetime': self.iso8601(created), 'lastTradeTimestamp': lastTradeTimestamp, 'status': status, 'symbol': symbol, 'type': type, 'timeInForce': None, 'postOnly': None, 'side': side, 'price': price, 'amount': amount, 'filled': filled, 'remaining': None, 'cost': cost, 'fee': fee, 'average': avgPrice, 'trades': rawTrades, }, market) def parse_order_trade(self, trade, market=None): # spot private wrapped trade # # { # "tradePrice": "130.01", # "tradeVolume": "0.0385", # "tradeTime": 1648714741525, # "aggressor": True, # "execAmt": "0.0385", # "orderSource": "spot-web", # "orderSize": "0.0385", # "remainAmt": "0", # "tradeId": 101541578884, # "symbol": "ltcusdt", # "type": "sell-market", # "eventType": "trade", # "clientOrderId": '', # "orderStatus": "filled", # "orderId": 509835753860328 # } # market = self.safe_market(None, market) symbol = market['symbol'] tradeId = self.safe_string(trade, 'tradeId') price = self.safe_string(trade, 'tradePrice') amount = self.safe_string(trade, 'tradeVolume') order = self.safe_string(trade, 'orderId') timestamp = self.safe_integer(trade, 'tradeTime') type = self.safe_string(trade, 'type') side = None if type is not None: typeParts = type.split('-') side = typeParts[0] type = typeParts[1] aggressor = self.safe_value(trade, 'aggressor') takerOrMaker = None if aggressor is not None: takerOrMaker = 'taker' if aggressor else 'maker' return self.safe_trade({ 'info': trade, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'id': tradeId, 'order': order, 'type': type, 'takerOrMaker': takerOrMaker, 'side': side, 'price': price, 'amount': amount, 'cost': None, 'fee': None, }, market) async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: """ https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7de1c-77b5-11ed-9966-0242ac110003 https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7df0f-77b5-11ed-9966-0242ac110003 https://www.huobi.com/en-in/opend/newApiPages/?id=28c34a7d-77ae-11ed-9966-0242ac110003 https://www.huobi.com/en-in/opend/newApiPages/?id=5d5156b5-77b6-11ed-9966-0242ac110003 watch all open positions. Note: huobi has one channel for each marginMode and type :param str[]|None symbols: list of unified market symbols @param since @param limit :param dict params: extra parameters specific to the exchange API endpoint :returns dict[]: a list of `position structure ` """ await self.load_markets() market = None messageHash = '' if not self.is_empty(symbols): market = self.get_market_from_symbols(symbols) messageHash = '::' + ','.join(symbols) type = None subType = None if market is not None: type = market['type'] subType = 'linear' if market['linear'] else 'inverse' else: type, params = self.handle_market_type_and_params('watchPositions', market, params) if type == 'spot': type = 'future' subType, params = self.handle_option_and_params(params, 'watchPositions', 'subType', subType) symbols = self.market_symbols(symbols) marginMode = None marginMode, params = self.handle_margin_mode_and_params('watchPositions', params, 'cross') isLinear = (subType == 'linear') url = self.get_url_by_market_type(type, isLinear, True) messageHash = marginMode + ':positions' + messageHash channel = 'positions_cross.*' if (marginMode == 'cross') else 'positions.*' newPositions = await self.subscribe_private(channel, messageHash, type, subType, params) if self.newUpdates: return newPositions return self.filter_by_symbols_since_limit(self.positions[url][marginMode], symbols, since, limit, False) def handle_positions(self, client, message): # # { # op: 'notify', # topic: 'positions_cross', # ts: 1696767149650, # event: 'snapshot', # data: [ # { # contract_type: 'swap', # pair: 'BTC-USDT', # business_type: 'swap', # liquidation_price: null, # symbol: 'BTC', # contract_code: 'BTC-USDT', # volume: 1, # available: 1, # frozen: 0, # cost_open: 27802.2, # cost_hold: 27802.2, # profit_unreal: 0.0175, # profit_rate: 0.000629446590557581, # profit: 0.0175, # margin_asset: 'USDT', # position_margin: 27.8197, # lever_rate: 1, # direction: 'buy', # last_price: 27819.7, # margin_mode: 'cross', # margin_account: 'USDT', # trade_partition: 'USDT', # position_mode: 'dual_side' # }, # ] # } # url = client.url topic = self.safe_string(message, 'topic', '') marginMode = 'cross' if (topic == 'positions_cross') else 'isolated' if self.positions is None: self.positions = {} clientPositions = self.safe_value(self.positions, url) if clientPositions is None: self.positions[url] = {} clientMarginModePositions = self.safe_value(clientPositions, marginMode) if clientMarginModePositions is None: self.positions[url][marginMode] = ArrayCacheBySymbolBySide() cache = self.positions[url][marginMode] rawPositions = self.safe_value(message, 'data', []) newPositions = [] timestamp = self.safe_integer(message, 'ts') for i in range(0, len(rawPositions)): rawPosition = rawPositions[i] position = self.parse_position(rawPosition) position['timestamp'] = timestamp position['datetime'] = self.iso8601(timestamp) newPositions.append(position) cache.append(position) messageHashes = self.find_message_hashes(client, marginMode + ':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, marginMode + ':positions') async def watch_balance(self, params={}) -> Balances: """ watch balance and get the amount of funds available for trading or funds locked in orders https://www.htx.com/en-us/opend/newApiPages/?id=7ec52e28-7773-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=10000084-77b7-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=8cb7dcca-77b5-11ed-9966-0242ac110003 https://www.htx.com/en-us/opend/newApiPages/?id=28c34995-77ae-11ed-9966-0242ac110003 :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `balance structure ` """ type = None type, params = self.handle_market_type_and_params('watchBalance', None, params) subType = None subType, params = self.handle_sub_type_and_params('watchBalance', None, params, 'linear') isUnifiedAccount = self.safe_value_2(params, 'isUnifiedAccount', 'unified', False) params = self.omit(params, ['isUnifiedAccount', 'unified']) await self.load_markets() messageHash = None channel = None marginMode = None if type == 'spot': mode = self.safe_string_2(self.options, 'watchBalance', 'mode', '2') mode = self.safe_string(params, 'mode', mode) messageHash = 'accounts.update' + '#' + mode channel = messageHash else: symbol = self.safe_string(params, 'symbol') currency = self.safe_string(params, 'currency') market = self.market(symbol) if (symbol is not None) else None currencyCode = self.currency(currency) if (currency is not None) else None marginMode = self.safe_string(params, 'margin', 'cross') params = self.omit(params, ['currency', 'symbol', 'margin']) prefix = 'accounts' messageHash = prefix if subType == 'linear': if isUnifiedAccount: # usdt contracts account prefix = 'accounts_unify' messageHash = prefix channel = prefix + '.' + 'usdt' else: # usdt contracts account prefix = prefix + '_cross' if (marginMode == 'cross') else prefix messageHash = prefix if marginMode == 'isolated': # isolated margin only allows filtering by symbol3 if symbol is not None: messageHash += '.' + market['id'] channel = messageHash else: # subscribe to all channel = prefix + '.' + '*' else: # cross margin if currencyCode is not None: channel = prefix + '.' + currencyCode['id'] messageHash = channel else: # subscribe to all channel = prefix + '.' + '*' elif type == 'future': # inverse futures account if currencyCode is not None: messageHash += '.' + currencyCode['id'] channel = messageHash else: # subscribe to all channel = prefix + '.' + '*' else: # inverse swaps account if market is not None: messageHash += '.' + market['id'] channel = messageHash else: # subscribe to all channel = prefix + '.' + '*' subscriptionParams: dict = { 'type': type, 'subType': subType, 'margin': marginMode, } # we are differentiating the channel from the messageHash for global subscriptions(*) # because huobi returns a different topic than the topic sent. Example: we send # "accounts.*" and "accounts" is returned so we're setting channel = "accounts.*" and # messageHash = "accounts" allowing handleBalance to freely resolve the topic in the message return await self.subscribe_private(channel, messageHash, type, subType, params, subscriptionParams) def handle_balance(self, client: Client, message): # spot # # { # "action": "push", # "ch": "accounts.update#0", # "data": { # "currency": "btc", # "accountId": 123456, # "balance": "23.111", # "available": "2028.699426619837209087", # "changeType": "transfer", # "accountType":"trade", # "seqNum": "86872993928", # "changeTime": 1568601800000 # } # } # # inverse future # # { # "op":"notify", # "topic":"accounts.ada", # "ts":1604388667226, # "event":"order.match", # "data":[ # { # "symbol":"ADA", # "margin_balance":446.417641681222726716, # "margin_static":445.554085945257745136, # "margin_position":11.049723756906077348, # "margin_frozen":0, # "margin_available":435.367917924316649368, # "profit_real":21.627049781983019459, # "profit_unreal":0.86355573596498158, # "risk_rate":40.000796572150656768, # "liquidation_price":0.018674308027108984, # "withdraw_available":423.927036163274725677, # "lever_rate":20, # "adjust_factor":0.4 # } # ], # "uid":"123456789" # } # # usdt / linear future, swap # # { # "op":"notify", # "topic":"accounts.btc-usdt", # or "accounts" for global subscriptions # "ts":1603711370689, # "event":"order.open", # "data":[ # { # "margin_mode":"cross", # "margin_account":"USDT", # "margin_asset":"USDT", # "margin_balance":30.959342395, # "margin_static":30.959342395, # "margin_position":0, # "margin_frozen":10, # "profit_real":0, # "profit_unreal":0, # "withdraw_available":20.959342395, # "risk_rate":153.796711975, # "position_mode":"dual_side", # "contract_detail":[ # { # "symbol":"LTC", # "contract_code":"LTC-USDT", # "margin_position":0, # "margin_frozen":0, # "margin_available":20.959342395, # "profit_unreal":0, # "liquidation_price":null, # "lever_rate":1, # "adjust_factor":0.01, # "contract_type":"swap", # "pair":"LTC-USDT", # "business_type":"swap", # "trade_partition":"USDT" # }, # ], # "futures_contract_detail":[], # } # ] # } # # inverse future # # { # "op":"notify", # "topic":"accounts.ada", # "ts":1604388667226, # "event":"order.match", # "data":[ # { # "symbol":"ADA", # "margin_balance":446.417641681222726716, # "margin_static":445.554085945257745136, # "margin_position":11.049723756906077348, # "margin_frozen":0, # "margin_available":435.367917924316649368, # "profit_real":21.627049781983019459, # "profit_unreal":0.86355573596498158, # "risk_rate":40.000796572150656768, # "liquidation_price":0.018674308027108984, # "withdraw_available":423.927036163274725677, # "lever_rate":20, # "adjust_factor":0.4 # } # ], # "uid":"123456789" # } # channel = self.safe_string(message, 'ch') data = self.safe_value(message, 'data', []) timestamp = self.safe_integer(data, 'changeTime', self.safe_integer(message, 'ts')) self.balance['timestamp'] = timestamp self.balance['datetime'] = self.iso8601(timestamp) self.balance['info'] = data if channel is not None: # spot balance currencyId = self.safe_string(data, 'currency') code = self.safe_currency_code(currencyId) account = self.account() account['free'] = self.safe_string(data, 'available') account['total'] = self.safe_string(data, 'balance') self.balance[code] = account self.balance = self.safe_balance(self.balance) client.resolve(self.balance, channel) else: # contract balance dataLength = len(data) if dataLength == 0: return first = self.safe_value(data, 0, {}) topic = self.safe_string(message, 'topic') splitTopic = topic.split('.') messageHash = self.safe_string(splitTopic, 0) subscription = self.safe_value_2(client.subscriptions, messageHash, messageHash + '.*') if subscription is None: # if subscription not found means that we subscribed to a specific currency/symbol # and we use the first data entry to find it # Example: topic = 'accounts' # client.subscription hash = 'accounts.usdt' # we do 'accounts' + '.' + data[0]]['margin_asset'] to get it currencyId = self.safe_string_2(first, 'margin_asset', 'symbol') messageHash += '.' + currencyId.lower() subscription = self.safe_value(client.subscriptions, messageHash) type = self.safe_string(subscription, 'type') subType = self.safe_string(subscription, 'subType') if topic == 'accounts_unify': # { # "margin_asset": "USDT", # "margin_static": 10, # "cross_margin_static": 10, # "margin_balance": 10, # "cross_profit_unreal": 0, # "margin_frozen": 0, # "withdraw_available": 10, # "cross_risk_rate": null, # "cross_swap": [], # "cross_future": [], # "isolated_swap": [] # } marginAsset = self.safe_string(first, 'margin_asset') code = self.safe_currency_code(marginAsset) marginFrozen = self.safe_string(first, 'margin_frozen') unifiedAccount = self.account() unifiedAccount['free'] = self.safe_string(first, 'withdraw_available') unifiedAccount['used'] = marginFrozen self.balance[code] = unifiedAccount self.balance = self.safe_balance(self.balance) client.resolve(self.balance, 'accounts_unify') elif subType == 'linear': margin = self.safe_string(subscription, 'margin') if margin == 'cross': fieldName = 'futures_contract_detail' if (type == 'future') else 'contract_detail' balances = self.safe_value(first, fieldName, []) balancesLength = len(balances) if balancesLength > 0: for i in range(0, len(balances)): balance = balances[i] marketId = self.safe_string_2(balance, 'contract_code', 'margin_account') market = self.safe_market(marketId) currencyId = self.safe_string(balance, 'margin_asset') currency = self.safe_currency(currencyId) code = self.safe_string(market, 'settle', currency['code']) # the exchange outputs positions for delisted markets # https://www.huobi.com/support/en-us/detail/74882968522337 # we skip it if the market was delisted if code is not None: account = self.account() account['free'] = self.safe_string_2(balance, 'margin_balance', 'margin_available') account['used'] = self.safe_string(balance, 'margin_frozen') accountsByCode: dict = {} accountsByCode[code] = account symbol = market['symbol'] self.balance[symbol] = self.safe_balance(accountsByCode) else: # isolated margin for i in range(0, len(data)): isolatedBalance = data[i] account = self.account() account['free'] = self.safe_string(isolatedBalance, 'margin_balance', 'margin_available') account['used'] = self.safe_string(isolatedBalance, 'margin_frozen') currencyId = self.safe_string_2(isolatedBalance, 'margin_asset', 'symbol') code = self.safe_currency_code(currencyId) self.balance[code] = account self.balance = self.safe_balance(self.balance) else: # inverse branch for i in range(0, len(data)): balance = data[i] currencyId = self.safe_string(balance, 'symbol') code = self.safe_currency_code(currencyId) account = self.account() account['free'] = self.safe_string(balance, 'margin_available') account['used'] = self.safe_string(balance, 'margin_frozen') self.balance[code] = account self.balance = self.safe_balance(self.balance) client.resolve(self.balance, messageHash) def handle_subscription_status(self, client: Client, message): # # { # "id": 1583414227, # "status": "ok", # "subbed": "market.btcusdt.mbp.150", # "ts": 1583414229143 # } # # unsubscribe # { # "id": "2", # "status": "ok", # "unsubbed": "market.BTC-USDT-251003.detail", # "ts": 1759329276980 # } # id = self.safe_string(message, 'id') subscriptionsById = self.index_by(client.subscriptions, 'id') subscription = self.safe_dict(subscriptionsById, id) if subscription is not None: method = self.safe_value(subscription, 'method') if method is not None: method(client, message, subscription) # return; commented out to clean up # clean up if id in client.subscriptions: del client.subscriptions[id] if 'unsubbed' in message: self.handle_un_subscription(client, subscription) def handle_un_subscription(self, client: Client, subscription: dict): messageHashes = self.safe_list(subscription, 'messageHashes', []) subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) for i in range(0, len(messageHashes)): unsubHash = messageHashes[i] subHash = subMessageHashes[i] self.clean_unsubscription(client, subHash, unsubHash) self.clean_cache(subscription) def handle_system_status(self, client: Client, message): # # todo: answer the question whether handleSystemStatus should be renamed # and unified for any usage pattern that # involves system status and maintenance updates # # { # "id": "1578090234088", # connectId # "type": "welcome", # } # return message def handle_subject(self, client: Client, message): # spot # { # "ch": "market.btcusdt.mbp.150", # "ts": 1583472025885, # "tick": { # "seqNum": 104998984994, # "prevSeqNum": 104998984977, # "bids": [ # [9058.27, 0], # [9058.43, 0], # [9058.99, 0], # ], # "asks": [ # [9084.27, 0.2], # [9085.69, 0], # [9085.81, 0], # ] # } # } # non spot # # { # "ch":"market.BTC220218.depth.size_150.high_freq", # "tick":{ # "asks":[], # "bids":[ # [43445.74,1], # [43444.48,0], # [40593.92,9] # ], # "ch":"market.BTC220218.depth.size_150.high_freq", # "event":"update", # "id":152727500274, # "mrid":152727500274, # "ts":1645023376098, # "version":37536690 # }, # "ts":1645023376098 # } # # spot private trade # # { # "action":"push", # "ch":"trade.clearing#ltcusdt#1", # "data":{ # "eventType":"trade", # "symbol":"ltcusdt", # # ... # }, # } # # spot order # # { # "action":"push", # "ch":"orders#btcusdt", # "data": { # "orderSide":"buy", # "lastActTime":1583853365586, # "clientOrderId":"abc123", # "orderStatus":"rejected", # "symbol":"btcusdt", # "eventType":"trigger", # "errCode": 2002, # "errMessage":"invalid.client.order.id(NT)" # } # } # # contract order # # { # "op":"notify", # "topic":"orders.ada", # "ts":1604388667226, # # ? # } # ch = self.safe_value(message, 'ch', '') parts = ch.split('.') type = self.safe_string(parts, 0) if type == 'market': methodName = self.safe_string(parts, 2) methods: dict = { 'depth': self.handle_order_book, 'mbp': self.handle_order_book, 'detail': self.handle_ticker, 'bbo': self.handle_ticker, 'ticker': self.handle_ticker, 'trade': self.handle_trades, 'kline': self.handle_ohlcv, } method = self.safe_value(methods, methodName) if method is not None: method(client, message) return # private spot subjects privateParts = ch.split('#') privateType = self.safe_string(privateParts, 0, '') if privateType == 'trade.clearing': self.handle_my_trade(client, message) return if privateType.find('accounts.update') >= 0: self.handle_balance(client, message) return if privateType == 'orders': self.handle_order(client, message) return # private contract subjects op = self.safe_string(message, 'op') if op == 'notify': topic = self.safe_string(message, 'topic', '') if topic.find('orders') >= 0: self.handle_order(client, message) if topic.find('account') >= 0: self.handle_balance(client, message) if topic.find('positions') >= 0: self.handle_positions(client, message) async def pong(self, client, message): # # {ping: 1583491673714} # {action: "ping", data: {ts: 1645108204665}} # {op: "ping", ts: "1645202800015"} # try: ping = self.safe_integer(message, 'ping') if ping is not None: await client.send({'pong': ping}) return action = self.safe_string(message, 'action') if action == 'ping': data = self.safe_value(message, 'data') pingTs = self.safe_integer(data, 'ts') await client.send({'action': 'pong', 'data': {'ts': pingTs}}) return op = self.safe_string(message, 'op') if op == 'ping': pingTs = self.safe_integer(message, 'ts') await client.send({'op': 'pong', 'ts': pingTs}) except Exception as e: error = NetworkError(self.id + ' pong failed ' + self.json(e)) client.reset(error) def handle_ping(self, client: Client, message): self.spawn(self.pong, client, message) def handle_authenticate(self, client: Client, message): # # spot # # { # "action": "req", # "code": 200, # "ch": "auth", # "data": {} # } # # non spot # # { # "op": "auth", # "type": "api", # "err-code": 0, # "ts": 1645200307319, # "data": {"user-id": "35930539"} # } # promise = client.futures['auth'] promise.resolve(message) def handle_error_message(self, client: Client, message) -> Bool: # # { # "action": "sub", # "code": 2002, # "ch": "accounts.update#2", # "message": "invalid.auth.state" # } # # { # "ts": 1586323747018, # "status": "error", # 'err-code': "bad-request", # 'err-msg': "invalid mbp.150.symbol linkusdt", # "id": "2" # } # # { # "op": "sub", # "cid": "1", # "topic": "accounts_unify.USDT", # "err-code": 4007, # 'err-msg': "Non - single account user is not available, please check through the cross and isolated account asset interface", # "ts": 1698419490189 # } # { # "action":"req", # "code":2002, # "ch":"auth", # "message":"auth.fail" # } # status = self.safe_string(message, 'status') if status == 'error': id = self.safe_string(message, 'id') subscriptionsById = self.index_by(client.subscriptions, 'id') subscription = self.safe_value(subscriptionsById, id) if subscription is not None: errorCode = self.safe_string(message, 'err-code') try: self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], errorCode, self.json(message)) raise ExchangeError(self.json(message)) except Exception as e: messageHash = self.safe_string(subscription, 'messageHash') client.reject(e, messageHash) client.reject(e, id) if id in client.subscriptions: del client.subscriptions[id] return False code = self.safe_string_2(message, 'code', 'err-code') if code is not None and ((code != '200') and (code != '0')): feedback = self.id + ' ' + self.json(message) try: self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback) raise ExchangeError(feedback) except Exception as e: if isinstance(e, AuthenticationError): client.reject(e, 'auth') method = 'auth' if method in client.subscriptions: del client.subscriptions[method] return False else: client.reject(e) return True def handle_message(self, client: Client, message): if self.handle_error_message(client, message): # # {"id":1583414227,"status":"ok","subbed":"market.btcusdt.mbp.150","ts":1583414229143} # # first ping format # # {"ping": 1645106821667} # # second ping format # # {"action":"ping","data":{"ts":1645106821667}} # # third pong format # # # auth spot # # { # "action": "req", # "code": 200, # "ch": "auth", # "data": {} # } # # auth non spot # # { # "op": "auth", # "type": "api", # "err-code": 0, # "ts": 1645200307319, # "data": {"user-id": "35930539"} # } # # trade # # { # "action":"push", # "ch":"trade.clearing#ltcusdt#1", # "data":{ # "eventType":"trade", # # ? # } # } # if 'id' in message: self.handle_subscription_status(client, message) return if 'action' in message: action = self.safe_string(message, 'action') if action == 'ping': self.handle_ping(client, message) return if action == 'sub': self.handle_subscription_status(client, message) return if 'ch' in message: if message['ch'] == 'auth': self.handle_authenticate(client, message) return else: # route by channel aka topic aka subject self.handle_subject(client, message) return if 'op' in message: op = self.safe_string(message, 'op') if op == 'ping': self.handle_ping(client, message) return if op == 'auth': self.handle_authenticate(client, message) return if op == 'sub': self.handle_subscription_status(client, message) return if op == 'notify': self.handle_subject(client, message) return if 'ping' in message: self.handle_ping(client, message) def handle_my_trade(self, client: Client, message, extendParams={}): # # spot # # { # "action":"push", # "ch":"trade.clearing#ltcusdt#1", # "data":{ # "eventType":"trade", # "symbol":"ltcusdt", # "orderId":"478862728954426", # "orderSide":"buy", # "orderType":"buy-market", # "accountId":44234548, # "source":"spot-web", # "orderValue":"5.01724137", # "orderCreateTime":1645124660365, # "orderStatus":"filled", # "feeCurrency":"ltc", # "tradePrice":"118.89", # "tradeVolume":"0.042200701236437042", # "aggressor":true, # "tradeId":101539740584, # "tradeTime":1645124660368, # "transactFee":"0.000041778694224073", # "feeDeduct":"0", # "feeDeductType":"" # } # } # # contract # # { # "symbol": "ADA/USDT:USDT" # "ch": "orders_cross.ada-usdt" # "trades": [ # { # "trade_fee":-0.022099447513812154, # "fee_asset":"ADA", # "trade_id":113913755890, # "id":"113913755890-773207641127878656-1", # "trade_volume":1, # "trade_price":0.0905, # "trade_turnover":10, # "created_at":1604388667194, # "profit":0, # "real_profit": 0, # "role":"maker" # } # ], # } # if self.myTrades is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) self.myTrades = ArrayCacheBySymbolById(limit) cachedTrades = self.myTrades messageHash = self.safe_string(message, 'ch') if messageHash is not None: data = self.safe_value(message, 'data') if data is not None: parsed = self.parse_ws_trade(data) symbol = self.safe_string(parsed, 'symbol') if symbol is not None: cachedTrades.append(parsed) client.resolve(self.myTrades, messageHash) else: # self trades object is artificially created # in handleOrder rawTrades = self.safe_value(message, 'trades', []) marketId = self.safe_value(message, 'symbol') market = self.market(marketId) for i in range(0, len(rawTrades)): trade = rawTrades[i] parsedTrade = self.parse_trade(trade, market) # add extra params(side, type, ...) coming from the order parsedTrade = self.extend(parsedTrade, extendParams) cachedTrades.append(parsedTrade) # messageHash here is the orders one, so # we have to recreate the trades messageHash = orderMessageHash + ':' + 'trade' tradesHash = messageHash + ':' + 'trade' client.resolve(self.myTrades, tradesHash) # when we make an global order sub we have to send the channel like self # ch = orders_cross.* and we store messageHash = 'orders_cross' # however it is returned with the specific order update symbol: ch = orders_cross.btc-usd # since self is a global sub, our messageHash does not specify any symbol(ex: orders_cross:trade) # so we must remove it genericOrderHash = messageHash.replace('.' + market['lowercaseId'], '') lowerCaseBaseId = self.safe_string_lower(market, 'baseId') genericOrderHash = genericOrderHash.replace('.' + lowerCaseBaseId, '') genericTradesHash = genericOrderHash + ':' + 'trade' client.resolve(self.myTrades, genericTradesHash) def parse_ws_trade(self, trade, market=None): # spot private # # { # "eventType":"trade", # "symbol":"ltcusdt", # "orderId":"478862728954426", # "orderSide":"buy", # "orderType":"buy-market", # "accountId":44234548, # "source":"spot-web", # "orderValue":"5.01724137", # "orderCreateTime":1645124660365, # "orderStatus":"filled", # "feeCurrency":"ltc", # "tradePrice":"118.89", # "tradeVolume":"0.042200701236437042", # "aggressor":true, # "tradeId":101539740584, # "tradeTime":1645124660368, # "transactFee":"0.000041778694224073", # "feeDeduct":"0", # "feeDeductType":"" # } # symbol = self.safe_symbol(self.safe_string(trade, 'symbol')) side = self.safe_string_2(trade, 'side', 'orderSide') tradeId = self.safe_string(trade, 'tradeId') price = self.safe_string(trade, 'tradePrice') amount = self.safe_string(trade, 'tradeVolume') order = self.safe_string(trade, 'orderId') timestamp = self.safe_integer(trade, 'tradeTime') market = self.market(symbol) orderType = self.safe_string(trade, 'orderType') aggressor = self.safe_value(trade, 'aggressor') takerOrMaker = None if aggressor is not None: takerOrMaker = 'taker' if aggressor else 'maker' type = None orderTypeParts = [] if orderType is not None: orderTypeParts = orderType.split('-') type = self.safe_string(orderTypeParts, 1) fee = None feeCurrency = self.safe_currency_code(self.safe_string(trade, 'feeCurrency')) if feeCurrency is not None: fee = { 'cost': self.safe_string(trade, 'transactFee'), 'currency': feeCurrency, } return self.safe_trade({ 'info': trade, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'id': tradeId, 'order': order, 'type': type, 'takerOrMaker': takerOrMaker, 'side': side, 'price': price, 'amount': amount, 'cost': None, 'fee': fee, }, market) def get_url_by_market_type(self, type, isLinear=True, isPrivate=False, isFeed=False): api = self.safe_string(self.options, 'api', 'api') hostname: dict = {'hostname': self.hostname} hostnameURL = None url = None if type == 'spot': if isPrivate: hostnameURL = self.urls['api']['ws'][api]['spot']['private'] else: if isFeed: hostnameURL = self.urls['api']['ws'][api]['spot']['feed'] else: hostnameURL = self.urls['api']['ws'][api]['spot']['public'] url = self.implode_params(hostnameURL, hostname) else: baseUrl = self.urls['api']['ws'][api][type] subTypeUrl = baseUrl['linear'] if isLinear else baseUrl['inverse'] url = subTypeUrl['private'] if isPrivate else subTypeUrl['public'] return url async def subscribe_public(self, url, symbol, messageHash, method=None, params={}): requestId = self.request_id() request: dict = { 'sub': messageHash, 'id': requestId, } subscription: dict = { 'id': requestId, 'messageHash': messageHash, 'symbol': symbol, 'params': params, } if method is not None: subscription['method'] = method return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) async def unsubscribe_public(self, market: Market, subMessageHash: str, topic: str, params={}): requestId = self.request_id() request: dict = { 'unsub': subMessageHash, 'id': requestId, } messageHash = 'unsubscribe::' + subMessageHash isFeed = (topic == 'orderbook') url = self.get_url_by_market_type(market['type'], market['linear'], False, isFeed) subscription: dict = { 'unsubscribe': True, 'id': requestId, 'subMessageHashes': [subMessageHash], 'messageHashes': [messageHash], 'symbols': [market['symbol']], 'topic': topic, } symbolsAndTimeframes = self.safe_list(params, 'symbolsAndTimeframes') if symbolsAndTimeframes is not None: subscription['symbolsAndTimeframes'] = symbolsAndTimeframes params = self.omit(params, 'symbolsAndTimeframes') return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription) async def subscribe_private(self, channel, messageHash, type, subtype, params={}, subscriptionParams={}): requestId = self.request_id() subscription: dict = { 'id': requestId, 'messageHash': messageHash, 'params': params, } extendedSubsription = self.extend(subscription, subscriptionParams) request = None if type == 'spot': request = { 'action': 'sub', 'ch': channel, } else: request = { 'op': 'sub', 'topic': channel, 'cid': requestId, } isLinear = subtype == 'linear' url = self.get_url_by_market_type(type, isLinear, True) hostname = self.urls['hostnames']['spot'] if (type == 'spot') else self.urls['hostnames']['contract'] authParams: dict = { 'type': type, 'url': url, 'hostname': hostname, } await self.authenticate(authParams) return await self.watch(url, messageHash, self.extend(request, params), channel, extendedSubsription) async def authenticate(self, params={}): url = self.safe_string(params, 'url') hostname = self.safe_string(params, 'hostname') type = self.safe_string(params, 'type') if url is None or hostname is None or type is None: raise ArgumentsRequired(self.id + ' authenticate requires a url, hostname and type argument') self.check_required_credentials() messageHash = 'auth' relativePath = url.replace('wss://' + hostname, '') client = self.client(url) future = client.reusableFuture(messageHash) authenticated = self.safe_value(client.subscriptions, messageHash) if authenticated is None: timestamp = self.ymdhms(self.milliseconds(), 'T') signatureParams = None if type == 'spot': signatureParams = { 'accessKey': self.apiKey, 'signatureMethod': 'HmacSHA256', 'signatureVersion': '2.1', 'timestamp': timestamp, } else: signatureParams = { 'AccessKeyId': self.apiKey, 'SignatureMethod': 'HmacSHA256', 'SignatureVersion': '2', 'Timestamp': timestamp, } signatureParams = self.keysort(signatureParams) auth = self.urlencode(signatureParams, True) # True required in go payload = "\n".join(['GET', hostname, relativePath, auth]) # eslint-disable-line quotes signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64') request = None if type == 'spot': newParams: dict = { 'authType': 'api', 'accessKey': self.apiKey, 'signatureMethod': 'HmacSHA256', 'signatureVersion': '2.1', 'timestamp': timestamp, 'signature': signature, } request = { 'params': newParams, 'action': 'req', 'ch': 'auth', } else: request = { 'op': 'auth', 'type': 'api', 'AccessKeyId': self.apiKey, 'SignatureMethod': 'HmacSHA256', 'SignatureVersion': '2', 'Timestamp': timestamp, 'Signature': signature, } requestId = self.request_id() subscription: dict = { 'id': requestId, 'messageHash': messageHash, 'params': params, } self.watch(url, messageHash, request, messageHash, subscription) return await future