1471 lines
59 KiB
Python
1471 lines
59 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, 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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
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 <https://docs.ccxt.com/en/latest/manual.html#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 <https://docs.ccxt.com/#/?id=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']
|