# -*- 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, ArrayCacheByTimestamp from ccxt.base.types import Any, Balances, Int, Order, OrderBook, Str, Ticker, Trade from ccxt.async_support.base.ws.client import Client from typing import List from ccxt.base.errors import NotSupported class bitrue(ccxt.async_support.bitrue): def describe(self) -> Any: return self.deep_extend(super(bitrue, self).describe(), { 'has': { 'ws': True, 'watchBalance': True, 'watchTicker': True, 'watchTickers': False, 'watchTrades': True, 'watchMyTrades': False, 'watchOrders': True, 'watchOrderBook': True, 'watchOHLCV': True, }, 'urls': { 'api': { 'open': 'https://open.bitrue.com', 'ws': { 'public': 'wss://ws.bitrue.com/market/ws', 'futurePublic': 'wss://fmarket-ws.bitrue.com/kline-api/ws', 'private': 'wss://wsapi.bitrue.com', }, }, }, 'api': { 'open': { 'v1': { 'private': { 'post': { 'poseidon/api/v1/listenKey': 1, }, 'put': { 'poseidon/api/v1/listenKey/{listenKey}': 1, }, 'delete': { 'poseidon/api/v1/listenKey/{listenKey}': 1, }, }, }, }, }, 'options': { 'listenKeyRefreshRate': 1800000, # 30 mins 'ws': { 'gunzip': True, }, 'futuresTimeframes': { '1m': '1min', '5m': '5min', '15m': '15min', '30m': '30min', '1h': '60min', '2h': '2h', '4h': '4h', '1d': '1day', '1w': '1week', }, }, }) async def watch_balance(self, params={}) -> Balances: """ watch balance and get the amount of funds available for trading or funds locked in orders https://github.com/Bitrue-exchange/Spot-official-api-docs#balance-update :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `balance structure ` """ url = await self.authenticate() messageHash = 'balance' message: dict = { 'event': 'sub', 'params': { 'channel': 'user_balance_update', }, } request = self.deep_extend(message, params) return await self.watch(url, messageHash, request, messageHash) def handle_balance(self, client: Client, message): # # { # "e": "BALANCE", # "x": "OutboundAccountPositionTradeEvent", # "E": 1657799510175, # "I": "302274978401288200", # "i": 1657799510175, # "B": [{ # "a": "btc", # "F": "0.0006000000000000", # "T": 1657799510000, # "f": "0.0006000000000000", # "t": 0 # }, # { # "a": "usdt", # "T": 0, # "L": "0.0000000000000000", # "l": "-11.8705317318000000", # "t": 1657799510000 # } # ], # "u": 1814396 # } # # { # "e": "BALANCE", # "x": "OutboundAccountPositionOrderEvent", # "E": 1670051332478, # "I": "353662845694083072", # "i": 1670051332478, # "B": [ # { # "a": "eth", # "F": "0.0400000000000000", # "T": 1670051332000, # "f": "-0.0100000000000000", # "L": "0.0100000000000000", # "l": "0.0100000000000000", # "t": 1670051332000 # } # ], # "u": 2285311 # } # balances = self.safe_value(message, 'B', []) self.parse_ws_balances(balances) messageHash = 'balance' client.resolve(self.balance, messageHash) def parse_ws_balances(self, balances): # # [{ # "a": "btc", # "F": "0.0006000000000000", # "T": 1657799510000, # "f": "0.0006000000000000", # "t": 0 # }, # { # "a": "usdt", # "T": 0, # "L": "0.0000000000000000", # "l": "-11.8705317318000000", # "t": 1657799510000 # }] # self.balance['info'] = balances for i in range(0, len(balances)): balance = balances[i] currencyId = self.safe_string(balance, 'a') code = self.safe_currency_code(currencyId) account = self.account() free = self.safe_string(balance, 'F') used = self.safe_string(balance, 'L') balanceUpdateTime = self.safe_integer(balance, 'T', 0) lockBalanceUpdateTime = self.safe_integer(balance, 't', 0) updateFree = balanceUpdateTime != 0 updateUsed = lockBalanceUpdateTime != 0 if updateFree or updateUsed: if updateFree: account['free'] = free if updateUsed: account['used'] = used self.balance[code] = account self.balance = self.safe_balance(self.balance) async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ watches information on user orders https://github.com/Bitrue-exchange/Spot-official-api-docs#order-update :param str symbol: :param int [since]: timestamp in ms of the earliest order :param int [limit]: the maximum amount of orders to return :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: A dictionary of `order structure ` indexed by market symbols """ await self.load_markets() if symbol is not None: market = self.market(symbol) symbol = market['symbol'] url = await self.authenticate() messageHash = 'orders' message: dict = { 'event': 'sub', 'params': { 'channel': 'user_order_update', }, } request = self.deep_extend(message, params) orders = await self.watch(url, messageHash, request, messageHash) if self.newUpdates: limit = orders.getLimit(symbol, limit) return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True) def handle_order(self, client: Client, message): # # { # "e": "ORDER", # "i": 16122802798, # "E": 1657882521876, # "I": "302623154710888464", # "u": 1814396, # "s": "btcusdt", # "S": 2, # "o": 1, # "q": "0.0005", # "p": "60000", # "X": 0, # "x": 1, # "z": "0", # "n": "0", # "N": "usdt", # "O": 1657882521876, # "L": "0", # "l": "0", # "Y": "0" # } # parsed = self.parse_ws_order(message) if self.orders is None: limit = self.safe_integer(self.options, 'ordersLimit', 1000) self.orders = ArrayCacheBySymbolById(limit) orders = self.orders orders.append(parsed) messageHash = 'orders' client.resolve(self.orders, messageHash) def parse_ws_order(self, order, market=None): # # { # "e": "ORDER", # "i": 16122802798, # "E": 1657882521876, # "I": "302623154710888464", # "u": 1814396, # "s": "btcusdt", # "S": 2, # "o": 1, # "q": "0.0005", # "p": "60000", # "X": 0, # "x": 1, # "z": "0", # "n": "0", # "N": "usdt", # "O": 1657882521876, # "L": "0", # "l": "0", # "Y": "0" # } # timestamp = self.safe_integer(order, 'E') marketId = self.safe_string_upper(order, 's') typeId = self.safe_string(order, 'o') sideId = self.safe_integer(order, 'S') # 1: buy # 2: sell side = 'buy' if (sideId == 1) else 'sell' statusId = self.safe_string(order, 'X') feeCurrencyId = self.safe_string(order, 'N') return self.safe_order({ 'info': order, 'id': self.safe_string(order, 'i'), 'clientOrderId': self.safe_string(order, 'c'), 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'lastTradeTimestamp': self.safe_integer(order, 'T'), 'symbol': self.safe_symbol(marketId, market), 'type': self.parse_ws_order_type(typeId), 'timeInForce': None, 'postOnly': None, 'side': side, 'price': self.safe_string(order, 'p'), 'triggerPrice': None, 'amount': self.safe_string(order, 'q'), 'cost': self.safe_string(order, 'Y'), 'average': None, 'filled': self.safe_string(order, 'z'), 'remaining': None, 'status': self.parse_ws_order_status(statusId), 'fee': { 'currency': self.safe_currency_code(feeCurrencyId), 'cost': self.safe_number(order, 'n'), }, }, market) async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook: await self.load_markets() market = self.market(symbol) symbol = market['symbol'] messageHash = 'orderbook:' + symbol url = None channel = None cbId = None if market['swap']: baseIdLower = self.safe_string_lower(market, 'baseId') quoteIdLower = self.safe_string_lower(market, 'quoteId') wsId = 'e_' + baseIdLower + quoteIdLower channel = 'market_' + wsId + '_depth_step0' cbId = wsId url = self.urls['api']['ws']['futurePublic'] else: marketIdLowercase = market['id'].lower() channel = 'market_' + marketIdLowercase + '_simple_depth_step0' cbId = marketIdLowercase url = self.urls['api']['ws']['public'] message: dict = { 'event': 'sub', 'params': { 'cb_id': cbId, 'channel': channel, }, } request = self.deep_extend(message, params) return await self.watch(url, messageHash, request, messageHash) def handle_order_book(self, client: Client, message): # # { # "channel": "market_ethbtc_simple_depth_step0", # "ts": 1670056708670, # "tick": { # "buys": [ # [ # "0.075170", # "67.153" # ], # [ # "0.075169", # "17.195" # ], # [ # "0.075166", # "29.788" # ], # ] # "asks": [ # [ # "0.075171", # "0.256" # ], # [ # "0.075172", # "0.160" # ], # ] # } # } # channel = self.safe_string(message, 'channel') parts = channel.split('_') channelKind = self.safe_string(parts, 1) isFutures = (channelKind == 'e') market = None if isFutures: wsBaseQuote = self.safe_string_lower(parts, 2) market = self.find_swap_market_by_ws_base_quote(wsBaseQuote) else: marketId = self.safe_string_upper(parts, 1) market = self.safe_market(marketId) symbol = market['symbol'] timestamp = self.safe_integer(message, 'ts') tick = self.safe_value(message, 'tick', {}) parseable = tick if isFutures: rawAsks = self.safe_list(tick, 'asks', []) rawBuys = self.safe_list(tick, 'buys', []) parseable = { 'asks': self.parse_contract_bids_asks(rawAsks, symbol), 'buys': self.parse_contract_bids_asks(rawBuys, symbol), } if not (symbol in self.orderbooks): self.orderbooks[symbol] = self.order_book() orderbook = self.orderbooks[symbol] snapshot = self.parse_order_book(parseable, symbol, timestamp, 'buys', 'asks') orderbook.reset(snapshot) messageHash = 'orderbook:' + symbol client.resolve(orderbook, messageHash) def find_swap_market_by_ws_base_quote(self, wsBaseQuote: str): symbols = list(self.markets.keys()) for i in range(0, len(symbols)): candidate = self.markets[symbols[i]] if not candidate['swap']: continue baseId = self.safe_string_lower(candidate, 'baseId', '') quoteId = self.safe_string_lower(candidate, 'quoteId', '') if baseId + quoteId == wsBaseQuote: return candidate return None def parse_contract_bids_asks(self, bidsAsks, symbol: str): result = [] for i in range(0, len(bidsAsks)): level = bidsAsks[i] price = self.safe_number(level, 0) rawAmount = self.safe_number(level, 1) amount = self.convert_from_raw_quantity(symbol, rawAmount) result.append([price, amount]) return result def convert_from_raw_quantity(self, symbol: str, rawQuantity): if rawQuantity is None: return None market = self.market(symbol) if not market['contract']: return rawQuantity contractSize = self.safe_number(market, 'contractSize', 1) return rawQuantity * contractSize async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]: """ watches public trades for a swap(futures) market https://www.bitrue.com/api_docs_includes_file/futures/index.html#websocket-market-data :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'] if not market['swap']: raise NotSupported(self.id + ' watchTrades is only supported for swap markets') baseIdLower = self.safe_string_lower(market, 'baseId') quoteIdLower = self.safe_string_lower(market, 'quoteId') wsId = 'e_' + baseIdLower + quoteIdLower channel = 'market_' + wsId + '_trade_ticker' messageHash = 'trades:' + symbol url = self.urls['api']['ws']['futurePublic'] message: dict = { 'event': 'sub', 'params': { 'cb_id': wsId, 'channel': channel, }, } request = self.deep_extend(message, params) trades = await self.watch(url, messageHash, request, messageHash) if self.newUpdates: limit = trades.getLimit(symbol, limit) return self.filter_by_since_limit(trades, since, limit, 'timestamp', True) def handle_trades(self, client: Client, message): # # { # "event_rep": "", # "channel": "market_e_btcusdt_trade_ticker", # "ts": 1721743391000, # "status": "ok", # "tick": { # "data": [ # { # "amount": "1666656191.2", # "ds": "2024-07-23 22:03:11", # "price": "66008.8", # "side": "SELL", # "ts": 1721743391398, # "vol": "25249" # } # ] # } # } # channel = self.safe_string(message, 'channel') parts = channel.split('_') wsBaseQuote = self.safe_string_lower(parts, 2) market = self.find_swap_market_by_ws_base_quote(wsBaseQuote) if market is None: return symbol = market['symbol'] tick = self.safe_value(message, 'tick', {}) data = self.safe_list(tick, 'data', []) appended = False stored = self.safe_value(self.trades, symbol) for i in range(0, len(data)): if stored is None: limit = self.safe_integer(self.options, 'tradesLimit', 1000) stored = ArrayCache(limit) self.trades[symbol] = stored trade = self.parse_ws_trade(data[i], market) stored.append(trade) appended = True if appended: messageHash = 'trades:' + symbol client.resolve(stored, messageHash) def parse_ws_trade(self, trade, market=None): symbol = market['symbol'] timestamp = self.safe_integer(trade, 'ts') sideLower = self.safe_string_lower(trade, 'side') priceString = self.safe_string(trade, 'price') rawVol = self.safe_number(trade, 'vol') baseAmount = self.convert_from_raw_quantity(symbol, rawVol) return self.safe_trade({ 'info': trade, 'id': None, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'order': None, 'type': None, 'side': sideLower, 'takerOrMaker': 'taker', 'price': priceString, 'amount': self.number_to_string(baseAmount), 'cost': None, 'fee': None, }, market) async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]: """ watches OHLCV candles for a swap(futures) market https://www.bitrue.com/api_docs_includes_file/futures/index.html#websocket-market-data :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'] if not market['swap']: raise NotSupported(self.id + ' watchOHLCV is only supported for swap markets') futuresTimeframes = self.safe_dict(self.options, 'futuresTimeframes', {}) interval = self.safe_string(futuresTimeframes, timeframe) if interval is None: raise NotSupported(self.id + ' watchOHLCV does not support timeframe ' + timeframe) baseIdLower = self.safe_string_lower(market, 'baseId') quoteIdLower = self.safe_string_lower(market, 'quoteId') wsId = 'e_' + baseIdLower + quoteIdLower channel = 'market_' + wsId + '_kline_' + interval messageHash = 'ohlcv:' + symbol + ':' + timeframe url = self.urls['api']['ws']['futurePublic'] message: dict = { 'event': 'sub', 'params': { 'cb_id': wsId, 'channel': channel, }, } request = self.deep_extend(message, params) ohlcv = await self.watch(url, messageHash, request, messageHash) if self.newUpdates: limit = ohlcv.getLimit(symbol, limit) return self.filter_by_since_limit(ohlcv, since, limit, 0, True) def handle_ohlcv(self, client: Client, message): # # { # "channel": "market_e_btcusdt_kline_1min", # "data": [], # "tick": { # "amount": 396539282326.3, # "close": 19517.1, # "ds": "2022-07-13 14:00:00", # "high": 19556.5, # "id": 1657692000, # "low": 19465.1, # "open": 19507.3, # "vol": 20325940 # }, # "ts": 1657696418000, # "status": "ok" # } # channel = self.safe_string(message, 'channel') parts = channel.split('_') wsBaseQuote = self.safe_string_lower(parts, 2) market = self.find_swap_market_by_ws_base_quote(wsBaseQuote) if market is None: return symbol = market['symbol'] wsInterval = self.safe_string(parts, 4) futuresTimeframes = self.safe_dict(self.options, 'futuresTimeframes', {}) timeframe = self.find_timeframe(wsInterval, futuresTimeframes) tick = self.safe_value(message, 'tick') if tick is None: return parsed = self.parse_ws_ohlcv(tick, market) if not (symbol in self.ohlcvs): self.ohlcvs[symbol] = {} if not (timeframe in self.ohlcvs[symbol]): limit = self.safe_integer(self.options, 'OHLCVLimit', 1000) self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit) stored = self.ohlcvs[symbol][timeframe] stored.append(parsed) messageHash = 'ohlcv:' + symbol + ':' + timeframe client.resolve(stored, messageHash) def parse_ws_ohlcv(self, tick, market=None) -> list: symbol = market['symbol'] idSeconds = self.safe_integer(tick, 'id') timestamp = None if (idSeconds is None) else idSeconds * 1000 open = self.safe_number(tick, 'open') high = self.safe_number(tick, 'high') low = self.safe_number(tick, 'low') close = self.safe_number(tick, 'close') rawVol = self.safe_number(tick, 'vol') baseVolume = self.convert_from_raw_quantity(symbol, rawVol) return [timestamp, open, high, low, close, baseVolume] async def watch_ticker(self, symbol: str, params={}) -> Ticker: """ watches a 24h ticker for a swap(futures) market https://www.bitrue.com/api_docs_includes_file/futures/index.html#websocket-market-data :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'] if not market['swap']: raise NotSupported(self.id + ' watchTicker is only supported for swap markets') baseIdLower = self.safe_string_lower(market, 'baseId') quoteIdLower = self.safe_string_lower(market, 'quoteId') wsId = 'e_' + baseIdLower + quoteIdLower channel = 'market_' + wsId + '_ticker' messageHash = 'ticker:' + symbol url = self.urls['api']['ws']['futurePublic'] message: dict = { 'event': 'sub', 'params': { 'cb_id': wsId, 'channel': channel, }, } request = self.deep_extend(message, params) return await self.watch(url, messageHash, request, messageHash) def handle_ticker(self, client: Client, message): # # { # "channel": "market_e_btcusdt_ticker", # "ts": 1506584998239, # "tick": { # "amount": 123.1221, # "vol": 1212.12211, # "open": 2233.22, # "close": 1221.11, # "high": 22322.22, # "low": 2321.22, # "rose": -0.2922 # }, # "status": "ok" # } # channel = self.safe_string(message, 'channel') parts = channel.split('_') wsBaseQuote = self.safe_string_lower(parts, 2) market = self.find_swap_market_by_ws_base_quote(wsBaseQuote) if market is None: return symbol = market['symbol'] tick = self.safe_value(message, 'tick') if tick is None: return timestamp = self.safe_integer(message, 'ts') parsed = self.parse_ws_ticker(tick, market, timestamp) self.tickers[symbol] = parsed messageHash = 'ticker:' + symbol client.resolve(parsed, messageHash) def parse_ws_ticker(self, tick, market, timestamp: Int = None) -> Ticker: symbol = market['symbol'] rawVol = self.safe_number(tick, 'vol') rawAmount = self.safe_number(tick, 'amount') baseVolume = self.convert_from_raw_quantity(symbol, rawVol) quoteVolume = self.convert_from_raw_quantity(symbol, rawAmount) close = self.safe_number(tick, 'close') rose = self.safe_number(tick, 'rose') percentage = None if (rose is None) else rose * 100 return self.safe_ticker({ 'info': tick, 'symbol': symbol, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'high': self.safe_number(tick, 'high'), 'low': self.safe_number(tick, 'low'), 'bid': None, 'bidVolume': None, 'ask': None, 'askVolume': None, 'vwap': None, 'open': self.safe_number(tick, 'open'), 'close': close, 'last': close, 'previousClose': None, 'change': None, 'percentage': percentage, 'average': None, 'baseVolume': baseVolume, 'quoteVolume': quoteVolume, }, market) def parse_ws_order_type(self, typeId): types: dict = { '1': 'limit', '2': 'market', '3': 'limit', } return self.safe_string(types, typeId, typeId) def parse_ws_order_status(self, status): statuses: dict = { '0': 'open', # The order has not been accepted by the engine. '1': 'open', # The order has been accepted by the engine. '2': 'closed', # The order has been completed. '3': 'open', # A part of the order has been filled. '4': 'canceled', # The order has been canceled. '7': 'open', # Stop order placed. } return self.safe_string(statuses, status, status) def handle_ping(self, client: Client, message): self.spawn(self.pong, client, message) async def pong(self, client, message): # # { # "ping": 1670057540627 # } # time = self.safe_integer(message, 'ping') pong: dict = { 'pong': time, } await client.send(pong) def handle_message(self, client: Client, message): if 'channel' in message: channel = self.safe_string(message, 'channel') if channel.find('_depth_step') > -1: self.handle_order_book(client, message) elif channel.find('_trade_ticker') > -1: self.handle_trades(client, message) elif channel.find('_kline_') > -1: self.handle_ohlcv(client, message) elif channel.find('_ticker') > -1: self.handle_ticker(client, message) elif 'ping' in message: self.handle_ping(client, message) else: event = self.safe_string(message, 'e') handlers: dict = { 'BALANCE': self.handle_balance, 'ORDER': self.handle_order, } handler = self.safe_value(handlers, event) if handler is not None: handler(client, message) async def authenticate(self, params={}): listenKey = self.safe_value(self.options, 'listenKey') if listenKey is None: response = await self.openV1PrivatePostPoseidonApiV1ListenKey(params) # # { # "msg": "succ", # "code": 200, # "data": { # "listenKey": "7d1ec51340f499d85bb33b00a96ef680bda28869d5c3374a444c5ca4847d1bf0" # } # } # data = self.safe_value(response, 'data', {}) key = self.safe_string(data, 'listenKey') self.options['listenKey'] = key self.options['listenKeyUrl'] = self.urls['api']['ws']['private'] + '/stream?listenKey=' + key refreshTimeout = self.safe_integer(self.options, 'listenKeyRefreshRate', 1800000) self.delay(refreshTimeout, self.keep_alive_listen_key) return self.options['listenKeyUrl'] async def keep_alive_listen_key(self, params={}): listenKey = self.safe_string(self.options, 'listenKey') request: dict = { 'listenKey': listenKey, } try: await self.openV1PrivatePutPoseidonApiV1ListenKeyListenKey(self.extend(request, params)) # # ಠ_ಠ # { # "msg": "succ", # "code": "200" # } # except Exception as error: self.options['listenKey'] = None self.options['listenKeyUrl'] = None return refreshTimeout = self.safe_integer(self.options, 'listenKeyRefreshRate', 1800000) self.delay(refreshTimeout, self.keep_alive_listen_key)