822 lines
31 KiB
Python
822 lines
31 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, 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 <https://docs.ccxt.com/?id=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 <https://docs.ccxt.com/?id=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 <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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)
|