Files
ccxt_with_mt5/ccxt/pro/htx.py
lz_db 0fab423a18 add
2025-11-16 12:31:03 +08:00

2410 lines
104 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, 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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=public-trades>`
"""
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=trade-structure>`
"""
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 <https://docs.ccxt.com/#/?id=order-structure>`
"""
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 <https://docs.ccxt.com/en/latest/manual.html#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 <https://docs.ccxt.com/#/?id=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