# -*- 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, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade from ccxt.async_support.base.ws.client import Client from typing import List from ccxt.base.errors import ExchangeError from ccxt.base.errors import AuthenticationError from ccxt.base.errors import NotSupported from ccxt.base.precise import Precise class woo(ccxt.async_support.woo): def describe(self) -> Any: return self.deep_extend(super(woo, self).describe(), { 'has': { 'ws': True, 'watchBalance': True, 'watchMyTrades': True, 'watchOHLCV': True, 'watchOrderBook': True, 'watchOrders': True, 'watchTicker': True, 'watchTickers': True, 'watchBidsAsks': True, 'watchTrades': True, 'watchTradesForSymbols': False, 'watchPositions': True, 'unWatchTicker': True, 'unWatchTickers': True, 'unWatchOrderBook': True, 'unWatchOHLCV': True, 'unWatchTrades': True, }, 'urls': { 'api': { 'ws': { 'public': 'wss://wss.woox.io/ws/stream', 'private': 'wss://wss.woox.io/v2/ws/private/stream', }, }, 'test': { 'ws': { 'public': 'wss://wss.staging.woox.io/ws/stream', 'private': 'wss://wss.staging.woox.io/v2/ws/private/stream', }, }, }, 'requiredCredentials': { 'apiKey': True, 'secret': True, 'uid': True, }, 'options': { 'tradesLimit': 1000, 'ordersLimit': 1000, 'requestId': {}, 'watchPositions': { 'fetchPositionsSnapshot': True, # or False 'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates }, }, 'streaming': { 'ping': self.ping, 'keepAlive': 9000, }, 'exceptions': { 'ws': { 'exact': { 'Auth is needed.': AuthenticationError, }, }, }, }) def request_id(self, url): options = self.safe_value(self.options, 'requestId', {}) previousValue = self.safe_integer(options, url, 0) newValue = self.sum(previousValue, 1) self.options['requestId'][url] = newValue return newValue async def watch_public(self, messageHash, message): urlUid = '/' + self.uid if (self.uid) else '' url = self.urls['api']['ws']['public'] + urlUid requestId = self.request_id(url) subscribe: dict = { 'id': requestId, } request = self.extend(subscribe, message) return await self.watch(url, messageHash, request, messageHash, subscribe) async def unwatch_public(self, subHash: str, symbol: str, topic: str, params={}) -> Any: urlUid = '/' + self.uid if (self.uid) else '' url = self.urls['api']['ws']['public'] + urlUid requestId = self.request_id(url) unsubHash = 'unsubscribe::' + subHash message: dict = { 'id': requestId, 'event': 'unsubscribe', 'topic': subHash, } subscription: dict = { 'id': str(requestId), 'unsubscribe': True, 'symbols': [symbol], 'topic': topic, 'subMessageHashes': [subHash], 'unsubMessageHashes': [unsubHash], } symbolsAndTimeframes = self.safe_list(params, 'symbolsAndTimeframes') if symbolsAndTimeframes is not None: subscription['symbolsAndTimeframes'] = symbolsAndTimeframes params = self.omit(params, 'symbolsAndTimeframes') return await self.watch(url, unsubHash, self.extend(message, params), unsubHash, subscription) async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: """ https://docs.woox.io/#orderbookupdate https://docs.woox.io/#orderbook 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 :param str [params.method]: either(default) 'orderbook' or 'orderbookupdate', default is 'orderbook' :returns dict: A dictionary of `order book structures ` indexed by market symbols """ await self.load_markets() method = None method, params = self.handle_option_and_params(params, 'watchOrderBook', 'method', 'orderbook') market = self.market(symbol) topic = market['id'] + '@' + method urlUid = '/' + self.uid if (self.uid) else '' url = self.urls['api']['ws']['public'] + urlUid requestId = self.request_id(url) request: dict = { 'event': 'subscribe', 'topic': topic, 'id': requestId, } subscription: dict = { 'id': str(requestId), 'name': method, 'symbol': market['symbol'], 'limit': limit, 'params': params, } if method == 'orderbookupdate': subscription['method'] = self.handle_order_book_subscription orderbook = await self.watch(url, topic, self.extend(request, params), topic, subscription) return orderbook.limit() async def un_watch_order_book(self, symbol: str, params={}) -> Any: """ unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data https://docs.woox.io/#orderbookupdate https://docs.woox.io/#orderbook :param str symbol: unified symbol of the market :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() method = None method, params = self.handle_option_and_params(params, 'watchOrderBook', 'method', 'orderbook') market = self.market(symbol) subHash = market['id'] + '@' + method topic = 'orderbook' return await self.unwatch_public(subHash, market['symbol'], topic, params) def handle_order_book(self, client: Client, message): # # { # "topic": "PERP_BTC_USDT@orderbookupdate", # "ts": 1722500373999, # "data": { # "symbol": "PERP_BTC_USDT", # "prevTs": 1722500373799, # "bids": [ # [ # 0.30891, # 2469.98 # ] # ], # "asks": [ # [ # 0.31075, # 2379.63 # ] # ] # } # } # data = self.safe_dict(message, 'data') marketId = self.safe_string(data, 'symbol') market = self.safe_market(marketId) symbol = market['symbol'] topic = self.safe_string(message, 'topic') method = self.safe_string(topic.split('@'), 1) if method == 'orderbookupdate': if not (symbol in self.orderbooks): return orderbook = self.orderbooks[symbol] timestamp = self.safe_integer(orderbook, 'timestamp') if timestamp is None: orderbook.cache.append(message) else: try: ts = self.safe_integer(message, 'ts') if ts > timestamp: self.handle_order_book_message(client, message, orderbook) client.resolve(orderbook, topic) except Exception as e: del self.orderbooks[symbol] del client.subscriptions[topic] client.reject(e, topic) else: if not (symbol in self.orderbooks): defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) subscription = client.subscriptions[topic] limit = self.safe_integer(subscription, 'limit', defaultLimit) self.orderbooks[symbol] = self.order_book({}, limit) orderbook = self.orderbooks[symbol] timestamp = self.safe_integer(message, 'ts') snapshot = self.parse_order_book(data, symbol, timestamp, 'bids', 'asks') orderbook.reset(snapshot) client.resolve(orderbook, topic) def handle_order_book_subscription(self, client: Client, message, subscription): defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) limit = self.safe_integer(subscription, 'limit', defaultLimit) symbol = self.safe_string(subscription, 'symbol') # watchOrderBook if symbol in self.orderbooks: del self.orderbooks[symbol] self.orderbooks[symbol] = self.order_book({}, limit) self.spawn(self.fetch_order_book_snapshot, client, message, subscription) async def fetch_order_book_snapshot(self, client, message, subscription): symbol = self.safe_string(subscription, 'symbol') messageHash = self.safe_string(message, 'topic') try: defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000) limit = self.safe_integer(subscription, 'limit', defaultLimit) params = self.safe_value(subscription, 'params') snapshot = await self.fetch_rest_order_book_safe(symbol, limit, params) if self.safe_value(self.orderbooks, symbol) is None: # if the orderbook is dropped before the snapshot is received return orderbook = self.orderbooks[symbol] orderbook.reset(snapshot) messages = orderbook.cache for i in range(0, len(messages)): messageItem = messages[i] ts = self.safe_integer(messageItem, 'ts') if ts < orderbook['timestamp']: continue else: self.handle_order_book_message(client, messageItem, orderbook) self.orderbooks[symbol] = orderbook client.resolve(orderbook, messageHash) except Exception as e: del client.subscriptions[messageHash] client.reject(e, messageHash) def handle_order_book_message(self, client: Client, message, orderbook): data = self.safe_dict(message, 'data') self.handle_deltas(orderbook['asks'], self.safe_value(data, 'asks', [])) self.handle_deltas(orderbook['bids'], self.safe_value(data, 'bids', [])) timestamp = self.safe_integer(message, 'ts') orderbook['timestamp'] = timestamp orderbook['datetime'] = self.iso8601(timestamp) return orderbook def handle_delta(self, bookside, delta): price = self.safe_float_2(delta, 'price', 0) amount = self.safe_float_2(delta, 'quantity', 1) bookside.store(price, amount) def handle_deltas(self, bookside, deltas): for i in range(0, len(deltas)): self.handle_delta(bookside, deltas[i]) 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 :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() name = 'ticker' market = self.market(symbol) symbol = market['symbol'] topic = market['id'] + '@' + name request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) return await self.watch_public(topic, message) 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 a specific market :param str symbol: unified symbol of the market to fetch the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() method = None method, params = self.handle_option_and_params(params, 'watchTicker', 'method', 'ticker') market = self.market(symbol) subHash = market['id'] + '@' + method topic = 'ticker' return await self.unwatch_public(subHash, market['symbol'], topic, params) def parse_ws_ticker(self, ticker, market=None): # # { # "symbol": "PERP_BTC_USDT", # "open": 19441.5, # "close": 20147.07, # "high": 20761.87, # "low": 19320.54, # "volume": 2481.103, # "amount": 50037935.0286, # "count": 3689 # } # return self.safe_ticker({ 'symbol': self.safe_symbol(None, market), 'timestamp': None, 'datetime': None, 'high': self.safe_string(ticker, 'high'), 'low': self.safe_string(ticker, 'low'), 'bid': None, 'bidVolume': None, 'ask': None, 'askVolume': None, 'vwap': None, 'open': self.safe_string(ticker, 'open'), 'close': self.safe_string(ticker, 'close'), 'last': None, 'previousClose': None, 'change': None, 'percentage': None, 'average': None, 'baseVolume': self.safe_string(ticker, 'volume'), 'quoteVolume': self.safe_string(ticker, 'amount'), 'info': ticker, }, market) def handle_ticker(self, client: Client, message): # # { # "topic": "PERP_BTC_USDT@ticker", # "ts": 1657120017000, # "data": { # "symbol": "PERP_BTC_USDT", # "open": 19441.5, # "close": 20147.07, # "high": 20761.87, # "low": 19320.54, # "volume": 2481.103, # "amount": 50037935.0286, # "count": 3689 # } # } # data = self.safe_value(message, 'data') topic = self.safe_value(message, 'topic') marketId = self.safe_string(data, 'symbol') market = self.safe_market(marketId) timestamp = self.safe_integer(message, 'ts') data['date'] = timestamp ticker = self.parse_ws_ticker(data, market) ticker['symbol'] = market['symbol'] self.tickers[market['symbol']] = ticker client.resolve(ticker, topic) return message async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: """ https://docs.woox.io/#24h-tickers watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list :param str[] symbols: unified symbol of the market to fetch the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() symbols = self.market_symbols(symbols) name = 'tickers' topic = name request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) tickers = await self.watch_public(topic, message) return self.filter_by_array(tickers, 'symbol', symbols) async def un_watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: """ https://docs.woox.io/#24h-tickers stops watching a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list :param str[] symbols: unified symbol of the market to stop fetching the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() if symbols is not None: raise NotSupported(self.id + ' unWatchTickers() does not support a symbols argument. Only unwatch all tickers at once') topic = 'ticker' subHash = 'tickers' return await self.unwatch_public(subHash, None, topic, params) def handle_tickers(self, client: Client, message): # # { # "topic":"tickers", # "ts":1618820615000, # "data":[ # { # "symbol":"SPOT_OKB_USDT", # "open":16.297, # "close":17.183, # "high":24.707, # "low":11.997, # "volume":0, # "amount":0, # "count":0 # }, # { # "symbol":"SPOT_XRP_USDT", # "open":1.3515, # "close":1.43794, # "high":1.96674, # "low":0.39264, # "volume":750127.1, # "amount":985440.5122, # "count":396 # }, # ... # ] # } # topic = self.safe_value(message, 'topic') data = self.safe_value(message, 'data') timestamp = self.safe_integer(message, 'ts') result = [] for i in range(0, len(data)): marketId = self.safe_string(data[i], 'symbol') market = self.safe_market(marketId) ticker = self.parse_ws_ticker(self.extend(data[i], {'date': timestamp}), market) self.tickers[market['symbol']] = ticker result.append(ticker) client.resolve(result, topic) async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers: """ https://docs.woox.io/#bbos watches best bid & ask for symbols :param str[] [symbols]: unified symbol of the market to fetch the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() symbols = self.market_symbols(symbols) name = 'bbos' topic = name request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) bidsasks = await self.watch_public(topic, message) if self.newUpdates: return bidsasks return self.filter_by_array(self.bidsasks, 'symbol', symbols) async def un_watch_bids_asks(self, symbols: Strings = None, params={}) -> Any: """ https://docs.woox.io/#bbos unWatches best bid & ask for symbols :param str[] [symbols]: unified symbol of the market to fetch the ticker for(not used by woo) :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() if symbols is not None: raise NotSupported(self.id + ' unWatchBidsAsks() does not support a symbols argument. Only unwatch all bidsAsks at once') subHash = 'bbos' topic = 'bidsasks' return await self.unwatch_public(subHash, None, topic, params) def handle_bid_ask(self, client: Client, message): # # { # "topic": "bbos", # "ts": 1618822376000, # "data": [ # { # "symbol": "SPOT_FIL_USDT", # "ask": 159.0318, # "askSize": 370.43, # "bid": 158.9158, # "bidSize": 16 # } # ] # } # topic = self.safe_string(message, 'topic') data = self.safe_list(message, 'data', []) timestamp = self.safe_integer(message, 'ts') result: dict = {} for i in range(0, len(data)): ticker = self.safe_dict(data, i) ticker['ts'] = timestamp parsedTicker = self.parse_ws_bid_ask(ticker) symbol = parsedTicker['symbol'] self.bidsasks[symbol] = parsedTicker result[symbol] = parsedTicker client.resolve(result, topic) def parse_ws_bid_ask(self, ticker, market=None): marketId = self.safe_string(ticker, 'symbol') market = self.safe_market(marketId, market) symbol = self.safe_string(market, 'symbol') timestamp = self.safe_integer(ticker, 'ts') return self.safe_ticker({ 'symbol': symbol, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'ask': self.safe_string(ticker, 'ask'), 'askVolume': self.safe_string(ticker, 'askSize'), 'bid': self.safe_string(ticker, 'bid'), 'bidVolume': self.safe_string(ticker, 'bidSize'), 'info': ticker, }, market) 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://docs.woox.io/#k-line :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() if (timeframe != '1m') and (timeframe != '5m') and (timeframe != '15m') and (timeframe != '30m') and (timeframe != '1h') and (timeframe != '1d') and (timeframe != '1w') and (timeframe != '1M'): raise ExchangeError(self.id + ' watchOHLCV timeframe argument must be 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M') market = self.market(symbol) interval = self.safe_string(self.timeframes, timeframe, timeframe) name = 'kline' topic = market['id'] + '@' + name + '_' + interval request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) ohlcv = await self.watch_public(topic, message) if self.newUpdates: limit = ohlcv.getLimit(market['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://docs.woox.io/#k-line :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) topic = 'ohlcv' name = 'kline' subHash = market['id'] + '@' + name + '_' + interval params['symbolsAndTimeframes'] = [[market['symbol'], timeframe]] return await self.unwatch_public(subHash, market['symbol'], topic, params) def handle_ohlcv(self, client: Client, message): # # { # "topic":"SPOT_BTC_USDT@kline_1m", # "ts":1618822432146, # "data":{ # "symbol":"SPOT_BTC_USDT", # "type":"1m", # "open":56948.97, # "close":56891.76, # "high":56948.97, # "low":56889.06, # "volume":44.00947568, # "amount":2504584.9, # "startTime":1618822380000, # "endTime":1618822440000 # } # } # data = self.safe_value(message, 'data') topic = self.safe_value(message, 'topic') marketId = self.safe_string(data, 'symbol') market = self.safe_market(marketId) symbol = market['symbol'] interval = self.safe_string(data, 'type') timeframe = self.find_timeframe(interval) parsed = [ self.safe_integer(data, 'startTime'), self.safe_float(data, 'open'), self.safe_float(data, 'high'), self.safe_float(data, 'low'), self.safe_float(data, 'close'), self.safe_float(data, 'volume'), ] 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 stored.append(parsed) client.resolve(stored, topic) async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: """ watches information on multiple trades made in a market https://docs.woox.io/#trade :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 ` """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] topic = market['id'] + '@trade' request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) trades = await self.watch_public(topic, message) if self.newUpdates: limit = trades.getLimit(market['symbol'], limit) return self.filter_by_symbol_since_limit(trades, symbol, since, limit, 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://docs.woox.io/#trade :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' subHash = market['id'] + '@trade' return await self.unwatch_public(subHash, market['symbol'], topic, params) def handle_trade(self, client: Client, message): # # { # "topic":"SPOT_ADA_USDT@trade", # "ts":1618820361552, # "data":{ # "symbol":"SPOT_ADA_USDT", # "price":1.27988, # "size":300, # "side":"BUY", # "source":0 # } # } # topic = self.safe_string(message, 'topic') timestamp = self.safe_integer(message, 'ts') data = self.safe_value(message, 'data') marketId = self.safe_string(data, 'symbol') market = self.safe_market(marketId) symbol = market['symbol'] trade = self.parse_ws_trade(self.extend(data, {'timestamp': timestamp}), market) tradesArray = self.safe_value(self.trades, symbol) if tradesArray is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) tradesArray = ArrayCache(limit) tradesArray.append(trade) self.trades[symbol] = tradesArray client.resolve(tradesArray, topic) def parse_ws_trade(self, trade, market=None): # # { # "symbol":"SPOT_ADA_USDT", # "timestamp":1618820361552, # "price":1.27988, # "size":300, # "side":"BUY", # "source":0 # } # private trade # { # "msgType": 0, # execution report # "symbol": "SPOT_BTC_USDT", # "clientOrderId": 0, # "orderId": 54774393, # "type": "MARKET", # "side": "BUY", # "quantity": 0.0, # "price": 0.0, # "tradeId": 56201985, # "executedPrice": 23534.06, # "executedQuantity": 0.00040791, # "fee": 2.1E-7, # "feeAsset": "BTC", # "totalExecutedQuantity": 0.00040791, # "avgPrice": 23534.06, # "status": "FILLED", # "reason": "", # "orderTag": "default", # "totalFee": 2.1E-7, # "feeCurrency": "BTC", # "totalRebate": 0, # "rebateCurrency": "USDT", # "visible": 0.0, # "timestamp": 1675406261689, # "reduceOnly": False, # "maker": False # } # marketId = self.safe_string(trade, 'symbol') market = self.safe_market(marketId, market) symbol = market['symbol'] price = self.safe_string_2(trade, 'executedPrice', 'price') amount = self.safe_string_2(trade, 'executedQuantity', 'size') cost = Precise.string_mul(price, amount) side = self.safe_string_lower(trade, 'side') timestamp = self.safe_integer(trade, 'timestamp') maker = self.safe_bool(trade, 'marker') takerOrMaker = None if maker is not None: takerOrMaker = 'maker' if maker else 'taker' type = self.safe_string_lower(trade, 'type') fee = None feeCost = self.safe_number(trade, 'fee') if feeCost is not None: fee = { 'cost': feeCost, 'currency': self.safe_currency_code(self.safe_string(trade, 'feeCurrency')), } return self.safe_trade({ 'id': self.safe_string(trade, 'tradeId'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'side': side, 'price': price, 'amount': amount, 'cost': cost, 'order': self.safe_string(trade, 'orderId'), 'takerOrMaker': takerOrMaker, 'type': type, 'fee': fee, 'info': trade, }, market) def check_required_uid(self, error=True): if not self.uid: if error: raise AuthenticationError(self.id + ' requires `uid` credential(woox calls it `application_id`)') else: return False return True async def authenticate(self, params={}): self.check_required_credentials() url = self.urls['api']['ws']['private'] + '/' + self.uid client = self.client(url) messageHash = 'authenticated' event = 'auth' future = client.reusableFuture(messageHash) authenticated = self.safe_value(client.subscriptions, messageHash) if authenticated is None: ts = str(self.nonce()) auth = '|' + ts signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256) request: dict = { 'event': event, 'params': { 'apikey': self.apiKey, 'sign': signature, 'timestamp': ts, }, } message = self.extend(request, params) self.watch(url, messageHash, message, messageHash, message) return await future async def watch_private(self, messageHash, message, params={}): await self.authenticate(params) url = self.urls['api']['ws']['private'] + '/' + self.uid requestId = self.request_id(url) subscribe: dict = { 'id': requestId, } request = self.extend(subscribe, message) return await self.watch(url, messageHash, request, messageHash, subscribe) async def watch_private_multiple(self, messageHashes, message, params={}): await self.authenticate(params) url = self.urls['api']['ws']['private'] + '/' + self.uid requestId = self.request_id(url) subscribe: dict = { 'id': requestId, } request = self.extend(subscribe, message) return await self.watch_multiple(url, messageHashes, request, messageHashes, subscribe) async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ https://docs.woox.io/#executionreport https://docs.woox.io/#algoexecutionreportv2 watches information on multiple orders made by the user :param str symbol: unified market symbol of the market orders were made in :param int [since]: the earliest time in ms to fetch orders for :param int [limit]: the maximum number of order structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param bool [params.trigger]: True if trigger order :returns dict[]: a list of `order structures ` """ await self.load_markets() trigger = self.safe_bool_2(params, 'stop', 'trigger', False) topic = 'algoexecutionreportv2' if (trigger) else 'executionreport' params = self.omit(params, ['stop', 'trigger']) messageHash = topic if symbol is not None: market = self.market(symbol) symbol = market['symbol'] messageHash += ':' + symbol request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) orders = await self.watch_private(messageHash, message) if self.newUpdates: limit = orders.getLimit(symbol, limit) return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: """ https://docs.woox.io/#executionreport https://docs.woox.io/#algoexecutionreportv2 watches information on multiple trades made by the user :param str symbol: unified market symbol of the market orders were made in :param int [since]: the earliest time in ms to fetch orders for :param int [limit]: the maximum number of order structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :param bool [params.trigger]: True if trigger order :returns dict[]: a list of `trade structures ` """ await self.load_markets() trigger = self.safe_bool_2(params, 'stop', 'trigger', False) topic = 'algoexecutionreportv2' if (trigger) else 'executionreport' params = self.omit(params, ['stop', 'trigger']) messageHash = 'myTrades' if symbol is not None: market = self.market(symbol) symbol = market['symbol'] messageHash += ':' + symbol request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) trades = await self.watch_private(messageHash, message) if self.newUpdates: limit = trades.getLimit(symbol, limit) return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) def parse_ws_order(self, order, market=None): # # { # "symbol": "PERP_BTC_USDT", # "clientOrderId": 0, # "orderId": 52952826, # "type": "LIMIT", # "side": "SELL", # "quantity": 0.01, # "price": 22000, # "tradeId": 0, # "executedPrice": 0, # "executedQuantity": 0, # "fee": 0, # "feeAsset": "USDT", # "totalExecutedQuantity": 0, # "status": "NEW", # "reason": '', # "orderTag": "default", # "totalFee": 0, # "visible": 0.01, # "timestamp": 1657515556799, # "reduceOnly": False, # "maker": False # } # { # "symbol": "SPOT_BTC_USDT", # "rootAlgoOrderId": 2573778, # "parentAlgoOrderId": 0, # "algoOrderId": 2573778, # "clientOrderId": 0, # "orderTag": "default", # "algoType": "STOP_LOSS", # "side": "SELL", # "quantity": 0.00011, # "triggerPrice": 98566.67, # "triggerStatus": "USELESS", # "price": 0, # "type": "MARKET", # "triggerTradePrice": 0, # "triggerTime": 0, # "tradeId": 0, # "executedPrice": 0, # "executedQuantity": 0, # "fee": 0, # "reason": "", # "feeAsset": "", # "totalExecutedQuantity": 0, # "averageExecutedPrice": 0, # "totalFee": 0, # "timestamp": 1761030467426, # "visibleQuantity": 0, # "reduceOnly": False, # "triggerPriceType": "MARKET_PRICE", # "positionSide": "BOTH", # "feeCurrency": "", # "totalRebate": 0.0, # "rebateCurrency": "", # "triggered": False, # "maker": False, # "activated": False, # "isTriggered": False, # "isMaker": False, # "isActivated": False, # "rootAlgoStatus": "NEW", # "algoStatus": "NEW" # } # orderId = self.safe_string_2(order, 'orderId', 'algoOrderId') marketId = self.safe_string(order, 'symbol') market = self.market(marketId) symbol = market['symbol'] timestamp = self.safe_integer(order, 'timestamp') fee = { 'cost': self.safe_string(order, 'totalFee'), 'currency': self.safe_string(order, 'feeAsset'), } priceString = self.safe_string(order, 'price') price = self.safe_number(order, 'price') avgPrice = self.safe_number(order, 'avgPrice') if Precise.string_eq(priceString, '0') and (avgPrice is not None): price = avgPrice amount = self.safe_float(order, 'quantity') side = self.safe_string_lower(order, 'side') type = self.safe_string_lower(order, 'type') filled = self.safe_number(order, 'totalExecutedQuantity') totalExecQuantity = self.safe_float(order, 'totalExecutedQuantity') remaining = amount if amount >= totalExecQuantity: remaining -= totalExecQuantity rawStatus = self.safe_string_2(order, 'status', 'algoStatus') status = self.parse_order_status(rawStatus) trades = None clientOrderId = self.safe_string(order, 'clientOrderId') triggerPrice = self.safe_string(order, 'triggerPrice') return self.safe_order({ 'info': order, 'symbol': symbol, 'id': orderId, 'clientOrderId': clientOrderId, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'lastTradeTimestamp': timestamp, 'type': type, 'timeInForce': None, 'postOnly': None, 'side': side, 'price': price, 'stopPrice': triggerPrice, 'triggerPrice': triggerPrice, 'reduceOnly': self.safe_bool(order, 'reduceOnly'), 'amount': amount, 'cost': None, 'average': avgPrice, 'filled': filled, 'remaining': remaining, 'status': status, 'fee': fee, 'trades': trades, }) def handle_order_update(self, client: Client, message): # # { # "topic": "executionreport", # "ts": 1657515556799, # "data": { # "symbol": "PERP_BTC_USDT", # "clientOrderId": 0, # "orderId": 52952826, # "type": "LIMIT", # "side": "SELL", # "quantity": 0.01, # "price": 22000, # "tradeId": 0, # "executedPrice": 0, # "executedQuantity": 0, # "fee": 0, # "feeAsset": "USDT", # "totalExecutedQuantity": 0, # "status": "NEW", # "reason": '', # "orderTag": "default", # "totalFee": 0, # "visible": 0.01, # "timestamp": 1657515556799, # "reduceOnly": False, # "maker": False # } # } # topic = self.safe_string(message, 'topic') data = self.safe_value(message, 'data') if isinstance(data, list): # algoexecutionreportv2 for i in range(0, len(data)): order = data[i] tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) if tradeId is not None: self.handle_my_trade(client, order) self.handle_order(client, order, topic) else: # executionreport tradeId = self.omit_zero(self.safe_string(data, 'tradeId')) if tradeId is not None: self.handle_my_trade(client, data) self.handle_order(client, data, topic) def handle_order(self, client: Client, message, topic): parsed = self.parse_ws_order(message) symbol = self.safe_string(parsed, 'symbol') orderId = self.safe_string(parsed, 'id') if symbol is not None: if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) cachedOrders = self.orders orders = self.safe_value(cachedOrders.hashmap, symbol, {}) order = self.safe_value(orders, orderId) if order is not None: fee = self.safe_value(order, 'fee') if fee is not None: parsed['fee'] = fee fees = self.safe_value(order, 'fees') if fees is not None: parsed['fees'] = fees parsed['trades'] = self.safe_value(order, 'trades') parsed['timestamp'] = self.safe_integer(order, 'timestamp') parsed['datetime'] = self.safe_string(order, 'datetime') cachedOrders.append(parsed) client.resolve(self.orders, topic) messageHashSymbol = topic + ':' + symbol client.resolve(self.orders, messageHashSymbol) def handle_my_trade(self, client: Client, message): # # { # "msgType": 0, # execution report # "symbol": "SPOT_BTC_USDT", # "clientOrderId": 0, # "orderId": 54774393, # "type": "MARKET", # "side": "BUY", # "quantity": 0.0, # "price": 0.0, # "tradeId": 56201985, # "executedPrice": 23534.06, # "executedQuantity": 0.00040791, # "fee": 2.1E-7, # "feeAsset": "BTC", # "totalExecutedQuantity": 0.00040791, # "avgPrice": 23534.06, # "status": "FILLED", # "reason": "", # "orderTag": "default", # "totalFee": 2.1E-7, # "feeCurrency": "BTC", # "totalRebate": 0, # "rebateCurrency": "USDT", # "visible": 0.0, # "timestamp": 1675406261689, # "reduceOnly": False, # "maker": False # } # myTrades = self.myTrades if myTrades is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) myTrades = ArrayCacheBySymbolById(limit) trade = self.parse_ws_trade(message) myTrades.append(trade) messageHash = 'myTrades:' + trade['symbol'] client.resolve(myTrades, messageHash) messageHash = 'myTrades' client.resolve(myTrades, messageHash) async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]: """ https://docs.woox.io/#position-push watch all open positions :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() messageHashes = [] symbols = self.market_symbols(symbols) if not self.is_empty(symbols): for i in range(0, len(symbols)): symbol = symbols[i] messageHashes.append('positions::' + symbol) else: messageHashes.append('positions') url = self.urls['api']['ws']['private'] + '/' + self.uid client = self.client(url) self.set_positions_cache(client, symbols) fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True) awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True) if fetchPositionsSnapshot and awaitPositionsSnapshot and self.positions is None: snapshot = await client.future('fetchPositionsSnapshot') return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True) request: dict = { 'event': 'subscribe', 'topic': 'position', } newPositions = await self.watch_private_multiple(messageHashes, request, params) if self.newUpdates: return newPositions return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True) def set_positions_cache(self, client: Client, type, symbols: Strings = None): fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False) if fetchPositionsSnapshot: messageHash = 'fetchPositionsSnapshot' if not (messageHash in client.futures): client.future(messageHash) self.spawn(self.load_positions_snapshot, client, messageHash) else: self.positions = ArrayCacheBySymbolBySide() async def load_positions_snapshot(self, client, messageHash): positions = await self.fetch_positions() self.positions = ArrayCacheBySymbolBySide() cache = self.positions for i in range(0, len(positions)): position = positions[i] contracts = self.safe_number(position, 'contracts', 0) if contracts > 0: cache.append(position) # don't remove the future from the .futures cache future = client.futures[messageHash] future.resolve(cache) client.resolve(cache, 'positions') def handle_positions(self, client, message): # # { # "topic":"position", # "ts":1705292345255, # "data":{ # "positions":{ # "PERP_LTC_USDT":{ # "holding":1, # "pendingLongQty":0, # "pendingShortQty":0, # "averageOpenPrice":71.53, # "pnl24H":0, # "fee24H":0.07153, # "settlePrice":71.53, # "markPrice":71.32098452065145, # "version":7886, # "openingTime":1705292304267, # "pnl24HPercentage":0, # "adlQuantile":1, # "positionSide":"BOTH" # } # } # } # } # data = self.safe_value(message, 'data', {}) rawPositions = self.safe_value(data, 'positions', {}) postitionsIds = list(rawPositions.keys()) if self.positions is None: self.positions = ArrayCacheBySymbolBySide() cache = self.positions newPositions = [] for i in range(0, len(postitionsIds)): marketId = postitionsIds[i] market = self.safe_market(marketId) rawPosition = rawPositions[marketId] position = self.parse_position(rawPosition, market) newPositions.append(position) cache.append(position) messageHash = 'positions::' + market['symbol'] client.resolve(position, messageHash) client.resolve(newPositions, 'positions') async def watch_balance(self, params={}) -> Balances: """ https://docs.woox.io/#balance watch balance and get the amount of funds available for trading or funds locked in orders :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `balance structure ` """ await self.load_markets() topic = 'balance' messageHash = topic request: dict = { 'event': 'subscribe', 'topic': topic, } message = self.extend(request, params) return await self.watch_private(messageHash, message) def handle_balance(self, client, message): # # { # "topic": "balance", # "ts": 1695716888789, # "data": { # "balances": { # "USDT": { # "holding": 266.56059176, # "frozen": 0, # "interest": 0, # "pendingShortQty": 0, # "pendingExposure": 0, # "pendingLongQty": 0, # "pendingLongExposure": 0, # "version": 37, # "staked": 0, # "unbonding": 0, # "vault": 0, # "averageOpenPrice": 0, # "pnl24H": 0, # "fee24H": 0, # "markPrice": 1, # "pnl24HPercentage": 0 # } # } # # } # data = self.safe_value(message, 'data') balances = self.safe_value(data, 'balances') keys = list(balances.keys()) ts = self.safe_integer(message, 'ts') self.balance['info'] = data self.balance['timestamp'] = ts self.balance['datetime'] = self.iso8601(ts) for i in range(0, len(keys)): key = keys[i] value = balances[key] code = self.safe_currency_code(key) account = self.balance[code] if (code in self.balance) else self.account() total = self.safe_string(value, 'holding') used = self.safe_string(value, 'frozen') account['total'] = total account['used'] = used account['free'] = Precise.string_sub(total, used) self.balance[code] = account self.balance = self.safe_balance(self.balance) client.resolve(self.balance, 'balance') def handle_error_message(self, client: Client, message) -> Bool: # # {"id":"1","event":"subscribe","success":false,"ts":1710780997216,"errorMsg":"Auth is needed."} # if not ('success' in message): return False success = self.safe_bool(message, 'success') if success: return False errorMessage = self.safe_string(message, 'errorMsg') try: if errorMessage is not None: feedback = self.id + ' ' + self.json(message) self.throw_exactly_matched_exception(self.exceptions['exact'], errorMessage, feedback) return False except Exception as error: if isinstance(error, AuthenticationError): messageHash = 'authenticated' client.reject(error, messageHash) if messageHash in client.subscriptions: del client.subscriptions[messageHash] else: client.reject(error) return True def handle_un_subscription(self, client: Client, message): # # { # "id": "2", # "event": "unsubscribe", # "success": True, # "ts": 1759568478343, # "data": "SPOT_BTC_USDT@orderbook" # } # subscribeHash = self.safe_string(message, 'data') unsubscribeHash = 'unsubscribe::' + subscribeHash subscription = self.safe_dict(client.subscriptions, unsubscribeHash, {}) subMessageHashes = self.safe_list(subscription, 'subMessageHashes', []) unsubMessageHashes = self.safe_list(subscription, 'unsubMessageHashes', []) for i in range(0, len(subMessageHashes)): subHash = subMessageHashes[i] unsubHash = unsubMessageHashes[i] self.clean_unsubscription(client, subHash, unsubHash) self.clean_cache(subscription) def handle_message(self, client: Client, message): if self.handle_error_message(client, message): return methods: dict = { 'ping': self.handle_ping, 'pong': self.handle_pong, 'subscribe': self.handle_subscribe, 'unsubscribe': self.handle_un_subscription, 'orderbook': self.handle_order_book, 'orderbookupdate': self.handle_order_book, 'ticker': self.handle_ticker, 'tickers': self.handle_tickers, 'kline': self.handle_ohlcv, 'auth': self.handle_auth, 'executionreport': self.handle_order_update, 'algoexecutionreportv2': self.handle_order_update, 'trade': self.handle_trade, 'balance': self.handle_balance, 'position': self.handle_positions, 'bbos': self.handle_bid_ask, } event = self.safe_string(message, 'event') method = self.safe_value(methods, event) if method is not None: method(client, message) return topic = self.safe_string(message, 'topic') if topic is not None: method = self.safe_value(methods, topic) if method is not None: method(client, message) return splitTopic = topic.split('@') splitLength = len(splitTopic) if splitLength == 2: name = self.safe_string(splitTopic, 1) method = self.safe_value(methods, name) if method is not None: method(client, message) return splitName = name.split('_') splitNameLength = len(splitTopic) if splitNameLength == 2: method = self.safe_value(methods, self.safe_string(splitName, 0)) if method is not None: method(client, message) def ping(self, client: Client): return {'event': 'ping'} def handle_ping(self, client: Client, message): return {'event': 'pong'} def handle_pong(self, client: Client, message): # # {event: "pong", ts: 1657117026090} # client.lastPong = self.milliseconds() return message def handle_subscribe(self, client: Client, message): # # { # "id": "666888", # "event": "subscribe", # "success": True, # "ts": 1657117712212 # } # id = self.safe_string(message, 'id') subscriptionsById = self.index_by(client.subscriptions, 'id') subscription = self.safe_value(subscriptionsById, id, {}) method = self.safe_value(subscription, 'method') if method is not None: method(client, message, subscription) return message def handle_auth(self, client: Client, message): # # { # "event": "auth", # "success": True, # "ts": 1657463158812 # } # messageHash = 'authenticated' success = self.safe_value(message, 'success') if success: # client.resolve(message, messageHash) future = self.safe_value(client.futures, 'authenticated') future.resolve(True) else: error = AuthenticationError(self.json(message)) client.reject(error, messageHash) # allows further authentication attempts if messageHash in client.subscriptions: del client.subscriptions['authenticated']