# -*- 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 import hashlib from ccxt.base.types import Any, Balances, Int, Market, Order, OrderBook, Str, Strings, Ticker, Tickers, Trade from ccxt.async_support.base.ws.client import Client from typing import List from ccxt.base.errors import NotSupported class exmo(ccxt.async_support.exmo): def describe(self) -> Any: return self.deep_extend(super(exmo, self).describe(), { 'has': { 'ws': True, 'watchBalance': True, 'watchTicker': True, 'watchTickers': True, 'watchTrades': True, 'watchMyTrades': True, 'watchOrders': True, 'watchOrderBook': True, 'watchOHLCV': False, }, 'urls': { 'api': { 'ws': { 'public': 'wss://ws-api.exmo.com:443/v1/public', 'spot': 'wss://ws-api.exmo.com:443/v1/private', 'margin': 'wss://ws-api.exmo.com:443/v1/margin/private', }, }, }, 'options': { }, 'streaming': { }, 'exceptions': { }, }) def request_id(self): requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1) self.options['requestId'] = requestId return requestId async def watch_balance(self, params={}) -> Balances: """ watch balance and get the amount of funds available for trading or funds locked in orders :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `balance structure ` """ await self.authenticate(params) type, query = self.handle_market_type_and_params('watchBalance', None, params) messageHash = 'balance:' + type url = self.urls['api']['ws'][type] subscribe: dict = { 'method': 'subscribe', 'topics': [type + '/wallet'], 'id': self.request_id(), } request = self.deep_extend(subscribe, query) return await self.watch(url, messageHash, request, messageHash, request) def handle_balance(self, client: Client, message): # # spot # { # "ts": 1654208766007, # "event": "snapshot", # "topic": "spot/wallet", # "data": { # "balances": { # "ADA": "0", # "ALGO": "0", # ... # }, # "reserved": { # "ADA": "0", # "ALGO": "0", # ... # } # } # } # # margin # { # "ts": 1624370076651, # "event": "snapshot", # "topic": "margin/wallets", # "data": { # "RUB": { # "balance": "1000000", # "used": "0", # "free": "1000000" # }, # "USD": { # "balance": "1000000", # "used": "1831.925", # "free": "998168.075" # } # } # } # { # "ts": 1624370185720, # "event": "update", # "topic": "margin/wallets", # "data": { # "USD": { # "balance": "1000123", # "used": "1831.925", # "free": "998291.075" # } # } # } # topic = self.safe_string(message, 'topic') parts = topic.split('/') type = self.safe_string(parts, 0) if type == 'spot': self.parse_spot_balance(message) elif type == 'margin': self.parse_margin_balance(message) messageHash = 'balance:' + type client.resolve(self.balance, messageHash) def parse_spot_balance(self, message): # # { # "balances": { # "BTC": "3", # "USD": "1000", # "RUB": "0" # }, # "reserved": { # "BTC": "0.5", # "DASH": "0", # "RUB": "0" # } # } # event = self.safe_string(message, 'event') data = self.safe_value(message, 'data') self.balance['info'] = data if event == 'snapshot': balances = self.safe_value(data, 'balances', {}) reserved = self.safe_value(data, 'reserved', {}) currencies = list(balances.keys()) for i in range(0, len(currencies)): currencyId = currencies[i] code = self.safe_currency_code(currencyId) account = self.account() account['free'] = self.safe_string(balances, currencyId) account['used'] = self.safe_string(reserved, currencyId) self.balance[code] = account elif event == 'update': currencyId = self.safe_string(data, 'currency') code = self.safe_currency_code(currencyId) account = self.account() account['free'] = self.safe_string(data, 'balance') account['used'] = self.safe_string(data, 'reserved') self.balance[code] = account self.balance = self.safe_balance(self.balance) def parse_margin_balance(self, message): # # { # "RUB": { # "balance": "1000000", # "used": "0", # "free": "1000000" # }, # "USD": { # "balance": "1000000", # "used": "1831.925", # "free": "998168.075" # } # } # data = self.safe_value(message, 'data') self.balance['info'] = data currencies = list(data.keys()) for i in range(0, len(currencies)): currencyId = currencies[i] code = self.safe_currency_code(currencyId) wallet = self.safe_value(data, currencyId) account = self.account() account['free'] = self.safe_string(wallet, 'free') account['used'] = self.safe_string(wallet, 'used') account['total'] = self.safe_string(wallet, 'balance') self.balance[code] = account self.balance = self.safe_balance(self.balance) 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://documenter.getpostman.com/view/10287440/SzYXWKPi#fd8f47bc-8517-43c0-bb60-1d61a86d4471 :param str symbol: unified symbol of the market to fetch the ticker for :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `ticker structure ` """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] url = self.urls['api']['ws']['public'] messageHash = 'ticker:' + symbol message: dict = { 'method': 'subscribe', 'topics': [ 'spot/ticker:' + market['id'], ], 'id': self.request_id(), } request = self.deep_extend(message, params) return await self.watch(url, messageHash, request, messageHash, request) async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers: """ watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list https://documenter.getpostman.com/view/10287440/SzYXWKPi#fd8f47bc-8517-43c0-bb60-1d61a86d4471 :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, None, False) messageHashes = [] args = [] for i in range(0, len(symbols)): market = self.market(symbols[i]) messageHashes.append('ticker:' + market['symbol']) args.append('spot/ticker:' + market['id']) url = self.urls['api']['ws']['public'] message: dict = { 'method': 'subscribe', 'topics': args, 'id': self.request_id(), } request = self.deep_extend(message, params) await self.watch_multiple(url, messageHashes, request, messageHashes, request) return self.filter_by_array(self.tickers, 'symbol', symbols) def handle_ticker(self, client: Client, message): # # spot # { # "ts": 1654205085473, # "event": "update", # "topic": "spot/ticker:BTC_USDT", # "data": { # "buy_price": "30285.84", # "sell_price": "30299.97", # "last_trade": "30295.01", # "high": "30386.7", # "low": "29542.76", # "avg": "29974.16178449", # "vol": "118.79538518", # "vol_curr": "3598907.38200826", # "updated": 1654205084 # } # } # topic = self.safe_string(message, 'topic') topicParts = topic.split(':') marketId = self.safe_string(topicParts, 1) symbol = self.safe_symbol(marketId) ticker = self.safe_value(message, 'data', {}) market = self.safe_market(marketId) parsedTicker = self.parse_ticker(ticker, market) messageHash = 'ticker:' + symbol self.tickers[symbol] = parsedTicker client.resolve(parsedTicker, messageHash) async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: """ get the list of most recent trades for a particular symbol :param str symbol: unified symbol of the market to fetch trades for :param int [since]: timestamp in ms of the earliest trade to fetch :param int [limit]: the maximum amount of trades to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: a list of `trade structures ` """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] url = self.urls['api']['ws']['public'] messageHash = 'trades:' + symbol message: dict = { 'method': 'subscribe', 'topics': [ 'spot/trades:' + market['id'], ], 'id': self.request_id(), } request = self.deep_extend(message, params) trades = await self.watch(url, messageHash, request, messageHash, request) return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) def handle_trades(self, client: Client, message): # # { # "ts": 1654206084001, # "event": "update", # "topic": "spot/trades:BTC_USDT", # "data": [{ # "trade_id": 389704729, # "type": "sell", # "price": "30310.95", # "quantity": "0.0197", # "amount": "597.125715", # "date": 1654206083 # }] # } # topic = self.safe_string(message, 'topic') parts = topic.split(':') marketId = self.safe_string(parts, 1) symbol = self.safe_symbol(marketId) market = self.safe_market(marketId) trades = self.safe_value(message, 'data', []) messageHash = 'trades:' + symbol stored = self.safe_value(self.trades, symbol) if stored is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) stored = ArrayCache(limit) self.trades[symbol] = stored for i in range(0, len(trades)): trade = trades[i] parsed = self.parse_trade(trade, market) stored.append(parsed) self.trades[symbol] = stored client.resolve(self.trades[symbol], messageHash) async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]: """ get the list of trades associated with the user :param str symbol: unified symbol of the market to fetch trades for :param int [since]: timestamp in ms of the earliest trade to fetch :param int [limit]: the maximum amount of trades to fetch :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: a list of `trade structures ` """ await self.load_markets() await self.authenticate(params) type, query = self.handle_market_type_and_params('watchMyTrades', None, params) url = self.urls['api']['ws'][type] messageHash = None if symbol is None: messageHash = 'myTrades:' + type else: market = self.market(symbol) symbol = market['symbol'] messageHash = 'myTrades:' + market['symbol'] message: dict = { 'method': 'subscribe', 'topics': [ type + '/user_trades', ], 'id': self.request_id(), } request = self.deep_extend(message, query) trades = await self.watch(url, messageHash, request, messageHash, request) return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True) def handle_my_trades(self, client: Client, message): # # spot # { # "ts": 1654210290219, # "event": "update", # "topic": "spot/user_trades", # "data": { # "trade_id": 389715807, # "type": "buy", # "price": "30527.77", # "quantity": "0.0001", # "amount": "3.052777", # "date": 1654210290, # "order_id": 27352777112, # "client_id": 0, # "pair": "BTC_USDT", # "exec_type": "taker", # "commission_amount": "0.0000001", # "commission_currency": "BTC", # "commission_percent": "0.1" # } # } # # margin # { # "ts":1624369720168, # "event":"snapshot", # "topic":"margin/user_trades", # "data":[ # { # "trade_id":"692844278081167054", # "trade_dt":"1624369773990729200", # "type":"buy", # "order_id":"692844278081167033", # "pair":"BTC_USD", # "quantity":"0.1", # "price":"36638.5", # "is_maker":false # } # ] # } # { # "ts":1624370368612, # "event":"update", # "topic":"margin/user_trades", # "data":{ # "trade_id":"692844278081167693", # "trade_dt":"1624370368569092500", # "type":"buy", # "order_id":"692844278081167674", # "pair":"BTC_USD", # "quantity":"0.1", # "price":"36638.5", # "is_maker":false # } # } # topic = self.safe_string(message, 'topic') parts = topic.split('/') type = self.safe_string(parts, 0) messageHash = 'myTrades:' + type event = self.safe_string(message, 'event') rawTrades = [] myTrades = None if self.myTrades is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) myTrades = ArrayCacheBySymbolById(limit) self.myTrades = myTrades else: myTrades = self.myTrades if event == 'snapshot': rawTrades = self.safe_value(message, 'data', []) elif event == 'update': rawTrade = self.safe_value(message, 'data', {}) rawTrades = [rawTrade] trades = self.parse_trades(rawTrades) symbols: dict = {} for j in range(0, len(trades)): trade = trades[j] myTrades.append(trade) symbols[trade['symbol']] = True symbolKeys = list(symbols.keys()) for i in range(0, len(symbolKeys)): symbol = symbolKeys[i] symbolSpecificMessageHash = 'myTrades:' + symbol client.resolve(myTrades, symbolSpecificMessageHash) client.resolve(myTrades, messageHash) async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: """ watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data :param str symbol: unified symbol of the market to fetch the order book for :param int [limit]: the maximum amount of order book entries to return :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: A dictionary of `order book structures ` indexed by market symbols """ await self.load_markets() market = self.market(symbol) symbol = market['symbol'] url = self.urls['api']['ws']['public'] messageHash = 'orderbook:' + symbol params = self.omit(params, 'aggregation') subscribe: dict = { 'method': 'subscribe', 'id': self.request_id(), 'topics': [ 'spot/order_book_updates:' + market['id'], ], } request = self.deep_extend(subscribe, params) orderbook = await self.watch(url, messageHash, request, messageHash) return orderbook.limit() def handle_order_book(self, client: Client, message): # # { # "ts": 1574427585174, # "event": "snapshot", # "topic": "spot/order_book_updates:BTC_USD", # "data": { # "ask": [ # ["100", "3", "300"], # ["200", "4", "800"] # ], # "bid": [ # ["99", "2", "198"], # ["98", "1", "98"] # ] # } # } # # { # "ts": 1574427585174, # "event": "update", # "topic": "spot/order_book_updates:BTC_USD", # "data": { # "ask": [ # ["100", "1", "100"], # ["200", "2", "400"] # ], # "bid": [ # ["99", "1", "99"], # ["98", "0", "0"] # ] # } # } # topic = self.safe_string(message, 'topic') parts = topic.split(':') marketId = self.safe_string(parts, 1) symbol = self.safe_symbol(marketId) orderBook = self.safe_value(message, 'data', {}) messageHash = 'orderbook:' + symbol timestamp = self.safe_integer(message, 'ts') if not (symbol in self.orderbooks): self.orderbooks[symbol] = self.order_book({}) orderbook = self.orderbooks[symbol] event = self.safe_string(message, 'event') if event == 'snapshot': snapshot = self.parse_order_book(orderBook, symbol, timestamp, 'bid', 'ask') orderbook.reset(snapshot) else: asks = self.safe_list(orderBook, 'ask', []) bids = self.safe_list(orderBook, 'bid', []) self.handle_deltas(orderbook['asks'], asks) self.handle_deltas(orderbook['bids'], bids) orderbook['timestamp'] = timestamp orderbook['datetime'] = self.iso8601(timestamp) client.resolve(orderbook, messageHash) def handle_delta(self, bookside, delta): bidAsk = self.parse_bid_ask(delta, 0, 1) bookside.storeArray(bidAsk) def handle_deltas(self, bookside, deltas): for i in range(0, len(deltas)): self.handle_delta(bookside, deltas[i]) async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ https://documenter.getpostman.com/view/10287440/SzYXWKPi#85f7bc03-b1c9-4cd2-bd22-8fd422272825 https://documenter.getpostman.com/view/10287440/SzYXWKPi#95e4ed18-1791-4e6d-83ad-cbfe9be1051c 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 :returns dict[]: a list of `order structures ` """ await self.load_markets() await self.authenticate(params) type, query = self.handle_market_type_and_params('watchOrders', None, params) url = self.urls['api']['ws'][type] messageHash = None if symbol is None: messageHash = 'orders:' + type else: market = self.market(symbol) symbol = market['symbol'] messageHash = 'orders:' + market['symbol'] message: dict = { 'method': 'subscribe', 'topics': [ type + '/orders', ], 'id': self.request_id(), } request = self.deep_extend(message, query) orders = await self.watch(url, messageHash, request, messageHash, request) return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) def handle_orders(self, client: Client, message): # # spot # { # "ts": 1574427585174, # "event": "snapshot", # "topic": "spot/orders", # "data": [ # { # "order_id": "14", # "client_id":"100500", # "created": "1574427585", # "pair": "BTC_USD", # "price": "7750", # "quantity": "0.1", # "amount": "775", # "original_quantity": "0.1", # "original_amount": "775", # "type": "sell", # "status": "open" # } # ] # } # # margin # { # "ts":1624371281773, # "event":"snapshot", # "topic":"margin/orders", # "data":[ # { # "order_id":"692844278081168665", # "created":"1624371250919761600", # "type":"limit_buy", # "previous_type":"limit_buy", # "pair":"BTC_USD", # "leverage":"2", # "price":"10000", # "stop_price":"0", # "distance":"0", # "trigger_price":"10000", # "init_quantity":"0.1", # "quantity":"0.1", # "funding_currency":"USD", # "funding_quantity":"1000", # "funding_rate":"0", # "client_id":"111111", # "expire":0, # "src":1, # "comment":"comment1", # "updated":1624371250938136600, # "status":"active" # } # ] # } # topic = self.safe_string(message, 'topic') parts = topic.split('/') type = self.safe_string(parts, 0) messageHash = 'orders:' + type event = self.safe_string(message, 'event') if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) cachedOrders = self.orders rawOrders = [] if event == 'snapshot': rawOrders = self.safe_value(message, 'data', []) elif event == 'update': rawOrder = self.safe_dict(message, 'data', {}) rawOrders.append(rawOrder) symbols: dict = {} for j in range(0, len(rawOrders)): order = self.parse_ws_order(rawOrders[j]) cachedOrders.append(order) symbols[order['symbol']] = True symbolKeys = list(symbols.keys()) for i in range(0, len(symbolKeys)): symbol = symbolKeys[i] symbolSpecificMessageHash = 'orders:' + symbol client.resolve(cachedOrders, symbolSpecificMessageHash) client.resolve(cachedOrders, messageHash) def parse_ws_order(self, order: dict, market: Market = None) -> Order: # # { # order_id: '43226756791', # client_id: 0, # created: '1730371416', # type: 'market_buy', # pair: 'TRX_USD', # quantity: '0', # original_quantity: '30', # status: 'cancelled', # last_trade_id: '726480870', # last_trade_price: '0.17', # last_trade_quantity: '30' # } # id = self.safe_string(order, 'order_id') timestamp = self.safe_timestamp(order, 'created') orderType = self.safe_string(order, 'type') side = self.parseSide(orderType) marketId = self.safe_string(order, 'pair') market = self.safe_market(marketId, market) symbol = market['symbol'] amount = self.safe_string(order, 'quantity') if amount is None: amountField = 'in_amount' if (side == 'buy') else 'out_amount' amount = self.safe_string(order, amountField) price = self.safe_string(order, 'price') clientOrderId = self.omit_zero(self.safe_string(order, 'client_id')) triggerPrice = self.omit_zero(self.safe_string(order, 'stop_price')) type = None if (orderType != 'buy') and (orderType != 'sell'): type = orderType trades = None if 'last_trade_id' in order: trade = self.parse_ws_trade(order, market) trades = [trade] return self.safe_order({ 'id': id, 'clientOrderId': clientOrderId, 'datetime': self.iso8601(timestamp), 'timestamp': timestamp, 'lastTradeTimestamp': None, 'status': self.parseStatus(self.safe_string(order, 'status')), 'symbol': symbol, 'type': type, 'timeInForce': None, 'postOnly': None, 'side': side, 'price': price, 'stopPrice': triggerPrice, 'triggerPrice': triggerPrice, 'cost': None, 'amount': self.safe_string(order, 'original_quantity'), 'filled': None, 'remaining': self.safe_string(order, 'quantity'), 'average': None, 'trades': trades, 'fee': None, 'info': order, }, market) def parse_ws_trade(self, trade: dict, market: Market = None) -> Trade: id = self.safe_string(trade, 'order_id') orderType = self.safe_string(trade, 'type') side = self.parseSide(orderType) marketId = self.safe_string(trade, 'pair') market = self.safe_market(marketId, market) symbol = market['symbol'] type = None if (orderType != 'buy') and (orderType != 'sell'): type = orderType return self.safe_trade({ 'id': self.safe_string(trade, 'last_trade_id'), 'symbol': symbol, 'order': id, 'type': type, 'side': side, 'price': self.safe_string(trade, 'last_trade_price'), 'amount': self.safe_string(trade, 'last_trade_quantity'), 'cost': None, 'fee': None, }, market) def handle_message(self, client: Client, message): # # { # "ts": 1654206362552, # "event": "info", # "code": 1, # "message": "connection established", # "session_id": "7548931b-c2a4-45dd-8d71-877881a7251a" # } # # { # "ts": 1654206491399, # "event": "subscribed", # "id": 1, # "topic": "spot/ticker:BTC_USDT" # } event = self.safe_string(message, 'event') events: dict = { 'logged_in': self.handle_authentication_message, 'info': self.handle_info, 'subscribed': self.handle_subscribed, } eventHandler = self.safe_value(events, event) if eventHandler is not None: eventHandler(client, message) return if (event == 'update') or (event == 'snapshot'): topic = self.safe_string(message, 'topic') if topic is not None: parts = topic.split(':') channel = self.safe_string(parts, 0) handlers: dict = { 'spot/ticker': self.handle_ticker, 'spot/wallet': self.handle_balance, 'margin/wallet': self.handle_balance, 'margin/wallets': self.handle_balance, 'spot/trades': self.handle_trades, 'margin/trades': self.handle_trades, 'spot/order_book_updates': self.handle_order_book, 'spot/orders': self.handle_orders, 'margin/orders': self.handle_orders, 'spot/user_trades': self.handle_my_trades, 'margin/user_trades': self.handle_my_trades, } handler = self.safe_value(handlers, channel) if handler is not None: handler(client, message) return raise NotSupported(self.id + ' received an unsupported message: ' + self.json(message)) def handle_subscribed(self, client: Client, message): # # { # "method": "subscribe", # "id": 2, # "topics": ["spot/orders"] # } # return message def handle_info(self, client: Client, message): # # { # "ts": 1654215731659, # "event": "info", # "code": 1, # "message": "connection established", # "session_id": "4c496262-e259-4c27-b805-f20b46209c17" # } # return message def handle_authentication_message(self, client: Client, message): # # { # "method": "login", # "id": 1, # "api_key": "K-************************", # "sign": "******************************************************************", # "nonce": 1654215729887 # } # messageHash = 'authenticated' client.resolve(message, messageHash) async def authenticate(self, params={}): messageHash = 'authenticated' type, query = self.handle_market_type_and_params('authenticate', None, params) url = self.urls['api']['ws'][type] client = self.client(url) future = self.safe_value(client.subscriptions, messageHash) if future is None: time = self.milliseconds() self.check_required_credentials() requestId = self.request_id() signData = self.apiKey + str(time) sign = self.hmac(self.encode(signData), self.encode(self.secret), hashlib.sha512, 'base64') request: dict = { 'method': 'login', 'id': requestId, 'api_key': self.apiKey, 'sign': sign, 'nonce': time, } message = self.extend(request, query) future = await self.watch(url, messageHash, message, messageHash) client.subscriptions[messageHash] = future return future