406 lines
16 KiB
Python
406 lines
16 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, ArrayCacheByTimestamp
|
|
from ccxt.base.types import Any, Int, OrderBook, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
|
|
|
|
class dydx(ccxt.async_support.dydx):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(dydx, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'watchBalance': False,
|
|
'watchTicker': False,
|
|
'watchTickers': False,
|
|
'watchTrades': True,
|
|
'watchOrderBook': True,
|
|
'watchOHLCV': True,
|
|
},
|
|
'urls': {
|
|
'test': {
|
|
'ws': 'wss://indexer.v4testnet.dydx.exchange/v4/ws',
|
|
},
|
|
'api': {
|
|
'ws': 'wss://indexer.dydx.trade/v4/ws',
|
|
},
|
|
},
|
|
'options': {},
|
|
'streaming': {},
|
|
'exceptions': {},
|
|
})
|
|
|
|
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.dydx.xyz/indexer-client/websockets#trades
|
|
|
|
: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://github.com/ccxt/ccxt/wiki/Manual#public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
url = self.urls['api']['ws']
|
|
market = self.market(symbol)
|
|
messageHash = 'trade:' + market['symbol']
|
|
request: dict = {
|
|
'type': 'subscribe',
|
|
'channel': 'v4_trades',
|
|
'id': market['id'],
|
|
}
|
|
trades = await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def un_watch_trades(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
unsubscribes from the trades channel
|
|
|
|
https://docs.dydx.xyz/indexer-client/websockets#trades
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
: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()
|
|
url = self.urls['api']['ws']
|
|
market = self.market(symbol)
|
|
messageHash = 'trade:' + market['symbol']
|
|
request: dict = {
|
|
'type': 'unsubscribe',
|
|
'channel': 'v4_trades',
|
|
'id': market['id'],
|
|
}
|
|
return await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
|
|
def handle_trades(self, client, message):
|
|
#
|
|
# {
|
|
# "type": "subscribed",
|
|
# "connection_id": "9011edff-d8f7-47fc-bbc6-0c7b5ba7dfae",
|
|
# "message_id": 3,
|
|
# "channel": "v4_trades",
|
|
# "id": "BTC-USD",
|
|
# "contents": {
|
|
# "trades": [
|
|
# {
|
|
# "id": "02b6148d0000000200000005",
|
|
# "side": "BUY",
|
|
# "size": "0.024",
|
|
# "price": "114581",
|
|
# "type": "LIMIT",
|
|
# "createdAt": "2025-08-04T00:42:07.119Z",
|
|
# "createdAtHeight": "45487245"
|
|
# }
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'id')
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
content = self.safe_dict(message, 'contents')
|
|
rawTrades = self.safe_list(content, 'trades', [])
|
|
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
|
|
parsedTrades = self.parse_trades(rawTrades, market)
|
|
for i in range(0, len(parsedTrades)):
|
|
parsed = parsedTrades[i]
|
|
stored.append(parsed)
|
|
messageHash = 'trade' + ':' + symbol
|
|
client.resolve(stored, messageHash)
|
|
|
|
def parse_ws_trade(self, trade, market=None):
|
|
#
|
|
# {
|
|
# "id": "02b6148d0000000200000003",
|
|
# "side": "BUY",
|
|
# "size": "0.024",
|
|
# "price": "114581",
|
|
# "type": "LIMIT",
|
|
# "createdAt": "2025-08-04T00:42:07.118Z",
|
|
# "createdAtHeight": "45487244"
|
|
# }
|
|
#
|
|
timestamp = self.parse8601(self.safe_string(trade, 'createdAt'))
|
|
return self.safe_trade({
|
|
'id': self.safe_string(trade, 'id'),
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': self.safe_string(market, 'symbol'),
|
|
'order': None,
|
|
'type': self.safe_string_lower(trade, 'type'),
|
|
'side': self.safe_string_lower(trade, 'side'),
|
|
'takerOrMaker': None,
|
|
'price': self.safe_string(trade, 'price'),
|
|
'amount': self.safe_string(trade, 'size'),
|
|
'cost': None,
|
|
'fee': None,
|
|
}, market)
|
|
|
|
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.dydx.xyz/indexer-client/websockets#orders
|
|
|
|
: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
|
|
"""
|
|
await self.load_markets()
|
|
url = self.urls['api']['ws']
|
|
market = self.market(symbol)
|
|
messageHash = 'orderbook:' + market['symbol']
|
|
request: dict = {
|
|
'type': 'subscribe',
|
|
'channel': 'v4_orderbook',
|
|
'id': market['id'],
|
|
}
|
|
orderbook = await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
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.dydx.xyz/indexer-client/websockets#orders
|
|
|
|
:param str symbol: unified array of symbols
|
|
: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()
|
|
url = self.urls['api']['ws']
|
|
market = self.market(symbol)
|
|
messageHash = 'orderbook:' + market['symbol']
|
|
request: dict = {
|
|
'type': 'unsubscribe',
|
|
'channel': 'v4_orderbook',
|
|
'id': market['id'],
|
|
}
|
|
return await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "type": "subscribed",
|
|
# "connection_id": "7af140fb-b33d-4f0e-8f4c-30f16337b360",
|
|
# "message_id": 1,
|
|
# "channel": "v4_orderbook",
|
|
# "id": "BTC-USD",
|
|
# "contents": {
|
|
# "bids": [
|
|
# {
|
|
# "price": "114623",
|
|
# "size": "0.1112"
|
|
# }
|
|
# ],
|
|
# "asks": [
|
|
# {
|
|
# "price": "114624",
|
|
# "size": "0.0872"
|
|
# }
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
marketId = self.safe_string(message, 'id')
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
content = self.safe_dict(message, 'contents')
|
|
orderbook = self.safe_value(self.orderbooks, symbol)
|
|
if orderbook is None:
|
|
orderbook = self.order_book()
|
|
orderbook['symbol'] = symbol
|
|
asks = self.safe_list(content, 'asks', [])
|
|
bids = self.safe_list(content, 'bids', [])
|
|
self.handle_deltas(orderbook['asks'], asks)
|
|
self.handle_deltas(orderbook['bids'], bids)
|
|
orderbook['nonce'] = self.safe_integer(message, 'message_id')
|
|
messageHash = 'orderbook:' + symbol
|
|
self.orderbooks[symbol] = orderbook
|
|
client.resolve(orderbook, messageHash)
|
|
|
|
def handle_delta(self, bookside, delta):
|
|
if isinstance(delta, list):
|
|
price = self.safe_float(delta, 0)
|
|
amount = self.safe_float(delta, 1)
|
|
bookside.store(price, amount)
|
|
else:
|
|
bidAsk = self.parse_bid_ask(delta, 'price', 'size')
|
|
bookside.storeArray(bidAsk)
|
|
|
|
async def watch_ohlcv(self, symbol: str, timeframe='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.dydx.xyz/indexer-client/websockets#candles
|
|
|
|
: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()
|
|
url = self.urls['api']['ws']
|
|
market = self.market(symbol)
|
|
messageHash = 'ohlcv:' + market['symbol']
|
|
resolution = self.safe_string(self.timeframes, timeframe, timeframe)
|
|
request: dict = {
|
|
'type': 'subscribe',
|
|
'channel': 'v4_candles',
|
|
'id': market['id'] + '/' + resolution,
|
|
}
|
|
ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
if self.newUpdates:
|
|
limit = ohlcv.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
|
|
|
|
async def un_watch_ohlcv(self, symbol: str, timeframe='1m', params={}) -> Any:
|
|
"""
|
|
unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://docs.dydx.xyz/indexer-client/websockets#candles
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
: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()
|
|
url = self.urls['api']['ws']
|
|
market = self.market(symbol)
|
|
messageHash = 'ohlcv:' + market['symbol']
|
|
resolution = self.safe_string(self.timeframes, timeframe, timeframe)
|
|
request: dict = {
|
|
'type': 'unsubscribe',
|
|
'channel': 'v4_candles',
|
|
'id': market['id'] + '/' + resolution,
|
|
}
|
|
return await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "type": "subscribed",
|
|
# "connection_id": "e00b6e27-590c-4e91-a24d-b0645289434b",
|
|
# "message_id": 1,
|
|
# "channel": "v4_candles",
|
|
# "id": "BTC-USD/1MIN",
|
|
# "contents": {
|
|
# "candles": [
|
|
# {
|
|
# "startedAt": "2025-08-05T03:40:00.000Z",
|
|
# "ticker": "BTC-USD",
|
|
# "resolution": "1MIN",
|
|
# "low": "114249",
|
|
# "high": "114256",
|
|
# "open": "114256",
|
|
# "close": "114249",
|
|
# "baseTokenVolume": "0.4726",
|
|
# "usdVolume": "53996.1818",
|
|
# "trades": 7,
|
|
# "startingOpenInterest": "501.7424",
|
|
# "orderbookMidPriceOpen": "114255.5",
|
|
# "orderbookMidPriceClose": "114255.5"
|
|
# }
|
|
# ]
|
|
# }
|
|
# }
|
|
# {
|
|
# "type": "channel_data",
|
|
# "connection_id": "e00b6e27-590c-4e91-a24d-b0645289434b",
|
|
# "message_id": 3,
|
|
# "id": "BTC-USD/1MIN",
|
|
# "channel": "v4_candles",
|
|
# "version": "1.0.0",
|
|
# "contents": {
|
|
# "startedAt": "2025-08-05T03:40:00.000Z",
|
|
# "ticker": "BTC-USD",
|
|
# "resolution": "1MIN",
|
|
# "low": "114249",
|
|
# "high": "114262",
|
|
# "open": "114256",
|
|
# "close": "114261",
|
|
# "baseTokenVolume": "0.4753",
|
|
# "usdVolume": "54304.6873",
|
|
# "trades": 9,
|
|
# "startingOpenInterest": "501.7424",
|
|
# "orderbookMidPriceOpen": "114255.5",
|
|
# "orderbookMidPriceClose": "114255.5"
|
|
# }
|
|
# }
|
|
#
|
|
id = self.safe_string(message, 'id')
|
|
part = id.split('/')
|
|
interval = self.safe_string(part, 1)
|
|
timeframe = self.find_timeframe(interval)
|
|
marketId = self.safe_string(part, 0)
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
content = self.safe_dict(message, 'contents')
|
|
candles = self.safe_list(content, 'candles')
|
|
messageHash = 'ohlcv:' + symbol
|
|
ohlcv = self.safe_dict(candles, 0, content)
|
|
parsed = self.parse_ohlcv(ohlcv, market)
|
|
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, messageHash)
|
|
|
|
def handle_error_message(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "type": "error",
|
|
# "message": "....",
|
|
# "connection_id": "9011edff-d8f7-47fc-bbc6-0c7b5ba7dfae",
|
|
# "message_id": 4
|
|
# }
|
|
#
|
|
try:
|
|
msg = self.safe_string(message, 'message')
|
|
raise ExchangeError(self.id + ' ' + msg)
|
|
except Exception as e:
|
|
client.reject(e)
|
|
return True
|
|
|
|
def handle_message(self, client: Client, message):
|
|
type = self.safe_string(message, 'type')
|
|
if type == 'error':
|
|
self.handle_error_message(client, message)
|
|
return
|
|
if type is not None:
|
|
topic = self.safe_string(message, 'channel')
|
|
methods: dict = {
|
|
'v4_trades': self.handle_trades,
|
|
'v4_orderbook': self.handle_order_book,
|
|
'v4_candles': self.handle_ohlcv,
|
|
}
|
|
method = self.safe_value(methods, topic)
|
|
if method is not None:
|
|
method(client, message)
|