559 lines
23 KiB
Python
559 lines
23 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
|
|
from ccxt.base.types import Any, Balances, Bool, Int, Order, OrderBook, Str, Ticker, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import NotSupported
|
|
|
|
|
|
class probit(ccxt.async_support.probit):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(probit, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'watchBalance': True,
|
|
'watchTicker': True,
|
|
'watchTickers': False,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': False,
|
|
'watchMyTrades': True,
|
|
'watchOrders': True,
|
|
'watchOrderBook': True,
|
|
'watchOHLCV': False,
|
|
},
|
|
'urls': {
|
|
'api': {
|
|
'ws': 'wss://api.probit.com/api/exchange/v1/ws',
|
|
},
|
|
'test': {
|
|
'ws': 'wss://demo-api.probit.com/api/exchange/v1/ws',
|
|
},
|
|
},
|
|
'options': {
|
|
'watchOrderBook': {
|
|
'filter': 'order_books_l2',
|
|
'interval': 100, # or 500
|
|
},
|
|
},
|
|
'streaming': {
|
|
},
|
|
})
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
watch balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://docs-en.probit.com/reference/balance-1
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.authenticate(params)
|
|
messageHash = 'balance'
|
|
return await self.subscribe_private(messageHash, 'balance', params)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "channel": "balance",
|
|
# "reset": False,
|
|
# "data": {
|
|
# "USDT": {
|
|
# "available": "15",
|
|
# "total": "15"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
messageHash = 'balance'
|
|
self.parse_ws_balance(message)
|
|
client.resolve(self.balance, messageHash)
|
|
|
|
def parse_ws_balance(self, message):
|
|
#
|
|
# {
|
|
# "channel": "balance",
|
|
# "reset": False,
|
|
# "data": {
|
|
# "USDT": {
|
|
# "available": "15",
|
|
# "total": "15"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
reset = self.safe_bool(message, 'reset', False)
|
|
data = self.safe_value(message, 'data', {})
|
|
currencyIds = list(data.keys())
|
|
if reset:
|
|
self.balance = {}
|
|
for i in range(0, len(currencyIds)):
|
|
currencyId = currencyIds[i]
|
|
entry = data[currencyId]
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
account['free'] = self.safe_string(entry, 'available')
|
|
account['total'] = self.safe_string(entry, 'total')
|
|
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://docs-en.probit.com/reference/marketdata
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.interval]: Unit time to synchronize market information(ms). Available units: 100, 500
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
channel = 'ticker'
|
|
return await self.subscribe_public('watchTicker', symbol, 'ticker', channel, params)
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "channel": "marketdata",
|
|
# "market_id": "BTC-USDT",
|
|
# "status": "ok",
|
|
# "lag": 0,
|
|
# "ticker": {
|
|
# "time": "2022-07-21T14:18:04.000Z",
|
|
# "last": "22591.3",
|
|
# "low": "22500.1",
|
|
# "high": "39790.7",
|
|
# "change": "-1224",
|
|
# "base_volume": "1002.32005445",
|
|
# "quote_volume": "23304489.385351021"
|
|
# },
|
|
# "reset": True
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'market_id')
|
|
symbol = self.safe_symbol(marketId)
|
|
ticker = self.safe_value(message, 'ticker', {})
|
|
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
|
|
|
|
https://docs-en.probit.com/reference/trade_history
|
|
|
|
: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
|
|
:param int [params.interval]: Unit time to synchronize market information(ms). Available units: 100, 500
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
channel = 'recent_trades'
|
|
symbol = self.safe_symbol(symbol)
|
|
trades = await self.subscribe_public('watchTrades', symbol, 'trades', channel, params)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "channel": "marketdata",
|
|
# "market_id": "BTC-USDT",
|
|
# "status": "ok",
|
|
# "lag": 0,
|
|
# "recent_trades": [
|
|
# {
|
|
# "id": "BTC-USDT:8010233",
|
|
# "price": "22701.4",
|
|
# "quantity": "0.011011",
|
|
# "time": "2022-07-21T13:40:40.983Z",
|
|
# "side": "buy",
|
|
# "tick_direction": "up"
|
|
# }
|
|
# ...
|
|
# ]
|
|
# "reset": True
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'market_id')
|
|
symbol = self.safe_symbol(marketId)
|
|
market = self.safe_market(marketId)
|
|
trades = self.safe_value(message, 'recent_trades', [])
|
|
if self.safe_bool(message, 'reset', False):
|
|
return # see comment in handleMessage
|
|
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
|
|
|
|
https://docs-en.probit.com/reference/trade_history
|
|
|
|
: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()
|
|
await self.authenticate(params)
|
|
messageHash = 'trades'
|
|
if symbol is not None:
|
|
symbol = self.safe_symbol(symbol)
|
|
messageHash = messageHash + ':' + symbol
|
|
trades = await self.subscribe_private(messageHash, 'trade_history', params)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_my_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "channel": "trade_history",
|
|
# "reset": False,
|
|
# "data": [{
|
|
# "id": "BTC-USDT:8010722",
|
|
# "order_id": "4124999207",
|
|
# "side": "buy",
|
|
# "fee_amount": "0.0134999868096",
|
|
# "fee_currency_id": "USDT",
|
|
# "status": "settled",
|
|
# "price": "23136.7",
|
|
# "quantity": "0.00032416",
|
|
# "cost": "7.499992672",
|
|
# "time": "2022-07-21T17:09:33.056Z",
|
|
# "market_id": "BTC-USDT"
|
|
# }]
|
|
# }
|
|
#
|
|
rawTrades = self.safe_value(message, 'data', [])
|
|
length = len(rawTrades)
|
|
if length == 0:
|
|
return
|
|
if self.safe_bool(message, 'reset', False):
|
|
return # see comment in handleMessage
|
|
messageHash = 'trades'
|
|
stored = self.myTrades
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
stored = ArrayCacheBySymbolById(limit)
|
|
self.myTrades = stored
|
|
trades = self.parse_trades(rawTrades)
|
|
tradeSymbols: dict = {}
|
|
for j in range(0, len(trades)):
|
|
trade = trades[j]
|
|
# don't include 'executed' state, because it's just blanket state of the trade, emited before actual trade event
|
|
if self.safe_string(trade['info'], 'status') == 'executed':
|
|
continue
|
|
tradeSymbols[trade['symbol']] = True
|
|
stored.append(trade)
|
|
unique = list(tradeSymbols.keys())
|
|
uniqueLength = len(unique)
|
|
if uniqueLength == 0:
|
|
return
|
|
for i in range(0, len(unique)):
|
|
symbol = unique[i]
|
|
symbolSpecificMessageHash = messageHash + ':' + symbol
|
|
client.resolve(stored, symbolSpecificMessageHash)
|
|
client.resolve(stored, messageHash)
|
|
|
|
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
watches information on an order made by the user
|
|
|
|
https://docs-en.probit.com/reference/open_order
|
|
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param int [since]: timestamp in ms of the earliest order to watch
|
|
:param int [limit]: the maximum amount of orders to watch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.channel]: choose what channel to use. Can open_order or order_history.
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.authenticate(params)
|
|
messageHash = 'orders'
|
|
if symbol is not None:
|
|
symbol = self.safe_symbol(symbol)
|
|
messageHash = messageHash + ':' + symbol
|
|
orders = await self.subscribe_private(messageHash, 'open_order', params)
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
|
|
|
|
def handle_orders(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "channel": "order_history",
|
|
# "reset": True,
|
|
# "data": [{
|
|
# "id": "4124999207",
|
|
# "user_id": "633dc56a-621b-4680-8a4e-85a823499b6d",
|
|
# "market_id": "BTC-USDT",
|
|
# "type": "market",
|
|
# "side": "buy",
|
|
# "limit_price": "0",
|
|
# "time_in_force": "ioc",
|
|
# "filled_cost": "7.499992672",
|
|
# "filled_quantity": "0.00032416",
|
|
# "open_quantity": "0",
|
|
# "status": "filled",
|
|
# "time": "2022-07-21T17:09:33.056Z",
|
|
# "client_order_id": '',
|
|
# "cost": "7.5"
|
|
# },
|
|
# ...
|
|
# ]
|
|
# }
|
|
#
|
|
rawOrders = self.safe_value(message, 'data', [])
|
|
length = len(rawOrders)
|
|
if length == 0:
|
|
return
|
|
messageHash = 'orders'
|
|
reset = self.safe_bool(message, 'reset', False)
|
|
stored = self.orders
|
|
if stored is None or reset:
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
stored = ArrayCacheBySymbolById(limit)
|
|
self.orders = stored
|
|
orderSymbols: dict = {}
|
|
for i in range(0, len(rawOrders)):
|
|
rawOrder = rawOrders[i]
|
|
order = self.parse_order(rawOrder)
|
|
orderSymbols[order['symbol']] = True
|
|
stored.append(order)
|
|
unique = list(orderSymbols.keys())
|
|
for i in range(0, len(unique)):
|
|
symbol = unique[i]
|
|
symbolSpecificMessageHash = messageHash + ':' + symbol
|
|
client.resolve(stored, symbolSpecificMessageHash)
|
|
client.resolve(stored, 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
|
|
|
|
https://docs-en.probit.com/reference/marketdata
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
channel = None
|
|
channel, params = self.handle_option_and_params(params, 'watchOrderBook', 'filter', 'order_books')
|
|
orderbook = await self.subscribe_public('watchOrderBook', symbol, 'orderbook', channel, params)
|
|
return orderbook.limit()
|
|
|
|
async def subscribe_private(self, messageHash, channel, params):
|
|
url = self.urls['api']['ws']
|
|
subscribe: dict = {
|
|
'type': 'subscribe',
|
|
'channel': channel,
|
|
}
|
|
request = self.extend(subscribe, params)
|
|
subscribeHash = messageHash
|
|
return await self.watch(url, messageHash, request, subscribeHash)
|
|
|
|
async def subscribe_public(self, methodName: str, symbol: str, dataType, filter, params={}):
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
url = self.urls['api']['ws']
|
|
client = self.client(url)
|
|
subscribeHash = 'marketdata:' + symbol
|
|
messageHash = dataType + ':' + symbol
|
|
filters = {}
|
|
if subscribeHash in client.subscriptions:
|
|
# already subscribed
|
|
filters = client.subscriptions[subscribeHash]
|
|
if not (filter in filters):
|
|
# resubscribe
|
|
del client.subscriptions[subscribeHash]
|
|
filters[filter] = True
|
|
keys = list(filters.keys())
|
|
interval = None
|
|
interval, params = self.handle_option_and_params(params, methodName, 'interval', 100)
|
|
request: dict = {
|
|
'type': 'subscribe',
|
|
'channel': 'marketdata',
|
|
'market_id': market['id'],
|
|
'filter': keys,
|
|
'interval': interval,
|
|
}
|
|
request = self.extend(request, params)
|
|
return await self.watch(url, messageHash, request, subscribeHash, filters)
|
|
|
|
def handle_order_book(self, client: Client, message, orderBook):
|
|
#
|
|
# {
|
|
# "channel": "marketdata",
|
|
# "market_id": "BTC-USDT",
|
|
# "status": "ok",
|
|
# "lag": 0,
|
|
# "order_books": [
|
|
# {side: "buy", price: '1420.7', quantity: "0.057"},
|
|
# ...
|
|
# ],
|
|
# "reset": True
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'market_id')
|
|
symbol = self.safe_symbol(marketId)
|
|
dataBySide = self.group_by(orderBook, 'side')
|
|
messageHash = 'orderbook:' + symbol
|
|
# orderbook = self.safe_value(self.orderbooks, symbol)
|
|
if not (symbol in self.orderbooks):
|
|
self.orderbooks[symbol] = self.order_book({})
|
|
orderbook = self.orderbooks[symbol]
|
|
reset = self.safe_bool(message, 'reset', False)
|
|
if reset:
|
|
snapshot = self.parse_order_book(dataBySide, symbol, None, 'buy', 'sell', 'price', 'quantity')
|
|
orderbook.reset(snapshot)
|
|
else:
|
|
self.handle_delta(orderbook, dataBySide)
|
|
client.resolve(orderbook, messageHash)
|
|
|
|
def handle_bid_asks(self, bookSide, bidAsks):
|
|
for i in range(0, len(bidAsks)):
|
|
bidAsk = bidAsks[i]
|
|
parsed = self.parse_bid_ask(bidAsk, 'price', 'quantity')
|
|
bookSide.storeArray(parsed)
|
|
|
|
def handle_delta(self, orderbook, delta):
|
|
storedBids = orderbook['bids']
|
|
storedAsks = orderbook['asks']
|
|
asks = self.safe_value(delta, 'sell', [])
|
|
bids = self.safe_value(delta, 'buy', [])
|
|
self.handle_bid_asks(storedBids, bids)
|
|
self.handle_bid_asks(storedAsks, asks)
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
#
|
|
# {
|
|
# "errorCode": "INVALID_ARGUMENT",
|
|
# "message": '',
|
|
# "details": {
|
|
# "interval": "invalid"
|
|
# }
|
|
# }
|
|
#
|
|
code = self.safe_string(message, 'errorCode')
|
|
errMessage = self.safe_string(message, 'message', '')
|
|
details = self.safe_value(message, 'details')
|
|
feedback = self.id + ' ' + code + ' ' + errMessage + ' ' + self.json(details)
|
|
if 'exact' in self.exceptions:
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
if 'broad' in self.exceptions:
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], errMessage, feedback)
|
|
raise ExchangeError(feedback)
|
|
|
|
def handle_authenticate(self, client: Client, message):
|
|
#
|
|
# {type: "authorization", result: "ok"}
|
|
#
|
|
result = self.safe_string(message, 'result')
|
|
future = client.subscriptions['authenticated']
|
|
if result == 'ok':
|
|
messageHash = 'authenticated'
|
|
client.resolve(message, messageHash)
|
|
else:
|
|
future.reject(message)
|
|
del client.subscriptions['authenticated']
|
|
|
|
def handle_market_data(self, client: Client, message):
|
|
ticker = self.safe_value(message, 'ticker')
|
|
if ticker is not None:
|
|
self.handle_ticker(client, message)
|
|
trades = self.safe_value(message, 'recent_trades', [])
|
|
tradesLength = len(trades)
|
|
if tradesLength:
|
|
self.handle_trades(client, message)
|
|
orderBook = self.safe_value_n(message, ['order_books', 'order_books_l1', 'order_books_l2', 'order_books_l3', 'order_books_l4'], [])
|
|
orderBookLength = len(orderBook)
|
|
if orderBookLength:
|
|
self.handle_order_book(client, message, orderBook)
|
|
|
|
def handle_message(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "errorCode": "INVALID_ARGUMENT",
|
|
# "message": '',
|
|
# "details": {
|
|
# "interval": "invalid"
|
|
# }
|
|
# }
|
|
#
|
|
# Note about 'reset' field
|
|
# 'reset': True field - it happens once after initial subscription, which just returns old items by the moment of subscription(like "fetchMyTrades" does)
|
|
#
|
|
errorCode = self.safe_string(message, 'errorCode')
|
|
if errorCode is not None:
|
|
self.handle_error_message(client, message)
|
|
return
|
|
type = self.safe_string(message, 'type')
|
|
if type == 'authorization':
|
|
self.handle_authenticate(client, message)
|
|
return
|
|
handlers: dict = {
|
|
'marketdata': self.handle_market_data,
|
|
'balance': self.handle_balance,
|
|
'trade_history': self.handle_my_trades,
|
|
'open_order': self.handle_orders,
|
|
'order_history': self.handle_orders,
|
|
}
|
|
channel = self.safe_string(message, 'channel')
|
|
handler = self.safe_value(handlers, channel)
|
|
if handler is not None:
|
|
handler(client, message)
|
|
return
|
|
error = NotSupported(self.id + ' handleMessage: unknown message: ' + self.json(message))
|
|
client.reject(error)
|
|
|
|
async def authenticate(self, params={}):
|
|
url = self.urls['api']['ws']
|
|
client = self.client(url)
|
|
messageHash = 'authenticated'
|
|
expires = self.safe_integer(self.options, 'expires', 0)
|
|
future = self.safe_value(client.subscriptions, messageHash)
|
|
if (future is None) or (self.milliseconds() > expires):
|
|
response = await self.sign_in()
|
|
#
|
|
# {
|
|
# "access_token": "0ttDv/2hTTn3bLi8GP1gKaneiEQ6+0hOBenPrxNQt2s=",
|
|
# "token_type": "bearer",
|
|
# "expires_in": 900
|
|
# }
|
|
#
|
|
accessToken = self.safe_string(response, 'access_token')
|
|
request: dict = {
|
|
'type': 'authorization',
|
|
'token': accessToken,
|
|
}
|
|
future = await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
client.subscriptions[messageHash] = future
|
|
return future
|