1030 lines
45 KiB
Python
1030 lines
45 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
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Int, Order, OrderBook, 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 ArgumentsRequired
|
|
from ccxt.base.errors import NotSupported
|
|
|
|
|
|
class deribit(ccxt.async_support.deribit):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(deribit, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'watchBalance': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchBidsAsks': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
'watchMyTrades': True,
|
|
'watchOrders': True,
|
|
'watchOrderBook': True,
|
|
'watchOrderBookForSymbols': True,
|
|
'watchOHLCV': True,
|
|
'watchOHLCVForSymbols': True,
|
|
},
|
|
'urls': {
|
|
'test': {
|
|
'ws': 'wss://test.deribit.com/ws/api/v2',
|
|
},
|
|
'api': {
|
|
'ws': 'wss://www.deribit.com/ws/api/v2',
|
|
},
|
|
},
|
|
'options': {
|
|
'ws': {
|
|
'timeframes': {
|
|
'1m': '1',
|
|
'3m': '3',
|
|
'5m': '5',
|
|
'15m': '15',
|
|
'30m': '30',
|
|
'1h': '60',
|
|
'2h': '120',
|
|
'4h': '180',
|
|
'6h': '360',
|
|
'12h': '720',
|
|
'1d': '1D',
|
|
},
|
|
# watchTrades replacement
|
|
'watchTradesForSymbols': {
|
|
'interval': '100ms', # 100ms, agg2, raw
|
|
},
|
|
# watchOrderBook replacement
|
|
'watchOrderBookForSymbols': {
|
|
'interval': '100ms', # 100ms, agg2, raw
|
|
'useDepthEndpoint': False, # if True, it will use the {books.group.depth.interval} endpoint instead of the {books.interval} endpoint
|
|
'depth': '20', # 1, 10, 20
|
|
'group': 'none', # none, 1, 2, 5, 10, 25, 100, 250
|
|
},
|
|
},
|
|
'currencies': ['BTC', 'ETH', 'SOL', 'USDC'],
|
|
},
|
|
'streaming': {
|
|
},
|
|
'exceptions': {
|
|
},
|
|
})
|
|
|
|
def request_id(self):
|
|
requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1)
|
|
self.options['requestId'] = requestId
|
|
return requestId
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
|
|
https://docs.deribit.com/#user-portfolio-currency
|
|
|
|
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.authenticate(params)
|
|
messageHash = 'balance'
|
|
url = self.urls['api']['ws']
|
|
currencies = self.safe_value(self.options, 'currencies', [])
|
|
channels = []
|
|
for i in range(0, len(currencies)):
|
|
currencyCode = currencies[i]
|
|
channels.append('user.portfolio.' + currencyCode)
|
|
subscribe: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'private/subscribe',
|
|
'params': {
|
|
'channels': channels,
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
request = self.deep_extend(subscribe, params)
|
|
return await self.watch(url, messageHash, request, messageHash, request)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# subscription
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "user.portfolio.btc",
|
|
# "data": {
|
|
# "total_pl": 0,
|
|
# "session_upl": 0,
|
|
# "session_rpl": 0,
|
|
# "projected_maintenance_margin": 0,
|
|
# "projected_initial_margin": 0,
|
|
# "projected_delta_total": 0,
|
|
# "portfolio_margining_enabled": False,
|
|
# "options_vega": 0,
|
|
# "options_value": 0,
|
|
# "options_theta": 0,
|
|
# "options_session_upl": 0,
|
|
# "options_session_rpl": 0,
|
|
# "options_pl": 0,
|
|
# "options_gamma": 0,
|
|
# "options_delta": 0,
|
|
# "margin_balance": 0.0015,
|
|
# "maintenance_margin": 0,
|
|
# "initial_margin": 0,
|
|
# "futures_session_upl": 0,
|
|
# "futures_session_rpl": 0,
|
|
# "futures_pl": 0,
|
|
# "fee_balance": 0,
|
|
# "estimated_liquidation_ratio_map": {},
|
|
# "estimated_liquidation_ratio": 0,
|
|
# "equity": 0.0015,
|
|
# "delta_total_map": {},
|
|
# "delta_total": 0,
|
|
# "currency": "BTC",
|
|
# "balance": 0.0015,
|
|
# "available_withdrawal_funds": 0.0015,
|
|
# "available_funds": 0.0015
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_value(message, 'params', {})
|
|
data = self.safe_value(params, 'data', {})
|
|
self.balance['info'] = data
|
|
currencyId = self.safe_string(data, 'currency')
|
|
currencyCode = self.safe_currency_code(currencyId)
|
|
balance = self.parse_balance(data)
|
|
self.balance[currencyCode] = balance
|
|
messageHash = 'balance'
|
|
client.resolve(self.balance, messageHash)
|
|
|
|
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
|
|
https://docs.deribit.com/#ticker-instrument_name-interval
|
|
|
|
watches a price ticker, a statistical calculation with the information 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
|
|
:param str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
url = self.urls['api']['ws']
|
|
interval = self.safe_string(params, 'interval', '100ms')
|
|
params = self.omit(params, 'interval')
|
|
await self.load_markets()
|
|
if interval == 'raw':
|
|
await self.authenticate()
|
|
channel = 'ticker.' + market['id'] + '.' + interval
|
|
message: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'public/subscribe',
|
|
'params': {
|
|
'channels': ['ticker.' + market['id'] + '.' + interval],
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
request = self.deep_extend(message, params)
|
|
return await self.watch(url, channel, request, channel, request)
|
|
|
|
async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
|
|
https://docs.deribit.com/#ticker-instrument_name-interval
|
|
|
|
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
|
|
:param str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
url = self.urls['api']['ws']
|
|
interval = self.safe_string(params, 'interval', '100ms')
|
|
params = self.omit(params, 'interval')
|
|
await self.load_markets()
|
|
if interval == 'raw':
|
|
await self.authenticate()
|
|
channels = []
|
|
for i in range(0, len(symbols)):
|
|
market = self.market(symbols[i])
|
|
channels.append('ticker.' + market['id'] + '.' + interval)
|
|
message: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'public/subscribe',
|
|
'params': {
|
|
'channels': channels,
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
request = self.deep_extend(message, params)
|
|
newTickers = await self.watch_multiple(url, channels, request, channels, request)
|
|
if self.newUpdates:
|
|
tickers: dict = {}
|
|
tickers[newTickers['symbol']] = newTickers
|
|
return tickers
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "ticker.BTC_USDC-PERPETUAL.raw",
|
|
# "data": {
|
|
# "timestamp": 1655393725040,
|
|
# "stats": [Object],
|
|
# "state": "open",
|
|
# "settlement_price": 21729.5891,
|
|
# "open_interest": 164.501,
|
|
# "min_price": 20792.9376,
|
|
# "max_price": 21426.225,
|
|
# "mark_price": 21109.555,
|
|
# "last_price": 21132,
|
|
# "instrument_name": "BTC_USDC-PERPETUAL",
|
|
# "index_price": 21122.3937,
|
|
# "funding_8h": -0.00022427,
|
|
# "estimated_delivery_price": 21122.3937,
|
|
# "current_funding": -0.00010782,
|
|
# "best_bid_price": 21106,
|
|
# "best_bid_amount": 1.143,
|
|
# "best_ask_price": 21113,
|
|
# "best_ask_amount": 0.327
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_value(message, 'params', {})
|
|
data = self.safe_value(params, 'data', {})
|
|
marketId = self.safe_string(data, 'instrument_name')
|
|
symbol = self.safe_symbol(marketId)
|
|
ticker = self.parse_ticker(data)
|
|
messageHash = self.safe_string(params, 'channel')
|
|
self.tickers[symbol] = ticker
|
|
client.resolve(ticker, messageHash)
|
|
|
|
async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
|
|
https://docs.deribit.com/#quote-instrument_name
|
|
|
|
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, None, False)
|
|
url = self.urls['api']['ws']
|
|
channels = []
|
|
for i in range(0, len(symbols)):
|
|
market = self.market(symbols[i])
|
|
channels.append('quote.' + market['id'])
|
|
message: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'public/subscribe',
|
|
'params': {
|
|
'channels': channels,
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
request = self.deep_extend(message, params)
|
|
newTickers = await self.watch_multiple(url, channels, request, channels, request)
|
|
if self.newUpdates:
|
|
tickers: dict = {}
|
|
tickers[newTickers['symbol']] = newTickers
|
|
return tickers
|
|
return self.filter_by_array(self.bidsasks, 'symbol', symbols)
|
|
|
|
def handle_bid_ask(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "quote.BTC_USDT",
|
|
# "data": {
|
|
# "best_bid_amount": 0.026,
|
|
# "best_ask_amount": 0.026,
|
|
# "best_bid_price": 63908,
|
|
# "best_ask_price": 63940,
|
|
# "instrument_name": "BTC_USDT",
|
|
# "timestamp": 1727765131750
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_dict(message, 'params', {})
|
|
data = self.safe_dict(params, 'data', {})
|
|
ticker = self.parse_ws_bid_ask(data)
|
|
symbol = ticker['symbol']
|
|
self.bidsasks[symbol] = ticker
|
|
messageHash = self.safe_string(params, 'channel')
|
|
client.resolve(ticker, messageHash)
|
|
|
|
def parse_ws_bid_ask(self, ticker, market=None):
|
|
marketId = self.safe_string(ticker, 'instrument_name')
|
|
market = self.safe_market(marketId, market)
|
|
symbol = self.safe_string(market, 'symbol')
|
|
timestamp = self.safe_integer(ticker, 'timestamp')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'ask': self.safe_string(ticker, 'best_ask_price'),
|
|
'askVolume': self.safe_string(ticker, 'best_ask_amount'),
|
|
'bid': self.safe_string(ticker, 'best_bid_price'),
|
|
'bidVolume': self.safe_string(ticker, 'best_bid_amount'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
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.deribit.com/#trades-instrument_name-interval
|
|
|
|
: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 str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
params['callerMethodName'] = 'watchTrades'
|
|
return await self.watch_trades_for_symbols([symbol], since, limit, params)
|
|
|
|
async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a list of symbols
|
|
|
|
https://docs.deribit.com/#trades-instrument_name-interval
|
|
|
|
:param str[] symbols: 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>`
|
|
"""
|
|
interval = None
|
|
interval, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'interval', '100ms')
|
|
if interval == 'raw':
|
|
await self.authenticate()
|
|
trades = await self.watch_multiple_wrapper('trades', interval, symbols, params)
|
|
if self.newUpdates:
|
|
first = self.safe_dict(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
def handle_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "trades.BTC_USDC-PERPETUAL.100ms",
|
|
# "data": [{
|
|
# "trade_seq": 501899,
|
|
# "trade_id": "USDC-2436803",
|
|
# "timestamp": 1655397355998,
|
|
# "tick_direction": 2,
|
|
# "price": 21026,
|
|
# "mark_price": 21019.9719,
|
|
# "instrument_name": "BTC_USDC-PERPETUAL",
|
|
# "index_price": 21031.7847,
|
|
# "direction": "buy",
|
|
# "amount": 0.049
|
|
# }]
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_dict(message, 'params', {})
|
|
channel = self.safe_string(params, 'channel', '')
|
|
parts = channel.split('.')
|
|
marketId = self.safe_string(parts, 1)
|
|
interval = self.safe_string(parts, 2)
|
|
symbol = self.safe_symbol(marketId)
|
|
market = self.safe_market(marketId)
|
|
trades = self.safe_list(params, 'data', [])
|
|
if self.safe_value(self.trades, symbol) is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
self.trades[symbol] = ArrayCache(limit)
|
|
stored = self.trades[symbol]
|
|
for i in range(0, len(trades)):
|
|
trade = trades[i]
|
|
parsed = self.parse_trade(trade, market)
|
|
stored.append(parsed)
|
|
self.trades[symbol] = stored
|
|
messageHash = 'trades|' + symbol + '|' + interval
|
|
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.deribit.com/#user-trades-instrument_name-interval
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for. Use 'any' to watch all trades
|
|
: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 str [params.interval]: specify aggregation and frequency of notifications. Possible values: 100ms, raw
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.authenticate(params)
|
|
if symbol is not None:
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
url = self.urls['api']['ws']
|
|
interval = self.safe_string(params, 'interval', 'raw')
|
|
params = self.omit(params, 'interval')
|
|
channel = 'user.trades.any.any.' + interval
|
|
message: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'private/subscribe',
|
|
'params': {
|
|
'channels': [channel],
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
request = self.deep_extend(message, params)
|
|
trades = await self.watch(url, channel, request, channel, request)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_my_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "user.trades.any.any.raw",
|
|
# "data": [{
|
|
# "trade_seq": 149546319,
|
|
# "trade_id": "219381310",
|
|
# "timestamp": 1655421193564,
|
|
# "tick_direction": 0,
|
|
# "state": "filled",
|
|
# "self_trade": False,
|
|
# "reduce_only": False,
|
|
# "profit_loss": 0,
|
|
# "price": 20236.5,
|
|
# "post_only": False,
|
|
# "order_type": "market",
|
|
# "order_id": "46108941243",
|
|
# "matching_id": null,
|
|
# "mark_price": 20233.96,
|
|
# "liquidity": "T",
|
|
# "instrument_name": "BTC-PERPETUAL",
|
|
# "index_price": 20253.31,
|
|
# "fee_currency": "BTC",
|
|
# "fee": 2.5e-7,
|
|
# "direction": "buy",
|
|
# "amount": 10
|
|
# }]
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_value(message, 'params', {})
|
|
channel = self.safe_string(params, 'channel', '')
|
|
trades = self.safe_value(params, 'data', [])
|
|
cachedTrades = self.myTrades
|
|
if cachedTrades is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
cachedTrades = ArrayCacheBySymbolById(limit)
|
|
parsed = self.parse_trades(trades)
|
|
marketIds: dict = {}
|
|
for i in range(0, len(parsed)):
|
|
trade = parsed[i]
|
|
cachedTrades.append(trade)
|
|
symbol = trade['symbol']
|
|
marketIds[symbol] = True
|
|
client.resolve(cachedTrades, channel)
|
|
|
|
async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
|
|
https://docs.deribit.com/#book-instrument_name-group-depth-interval
|
|
|
|
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.interval]: Frequency of notifications. Events will be aggregated over self interval. Possible values: 100ms, raw
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
params['callerMethodName'] = 'watchOrderBook'
|
|
return await self.watch_order_book_for_symbols([symbol], limit, params)
|
|
|
|
async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://docs.deribit.com/#book-instrument_name-group-depth-interval
|
|
|
|
:param str[] symbols: unified array of symbols
|
|
: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
|
|
"""
|
|
interval = None
|
|
interval, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'interval', '100ms')
|
|
if interval == 'raw':
|
|
await self.authenticate()
|
|
descriptor = ''
|
|
useDepthEndpoint = None # for more info, see comment in .options
|
|
useDepthEndpoint, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'useDepthEndpoint', False)
|
|
if useDepthEndpoint:
|
|
depth = None
|
|
depth, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'depth', '20')
|
|
group = None
|
|
group, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'group', 'none')
|
|
descriptor = group + '.' + depth + '.' + interval
|
|
else:
|
|
descriptor = interval
|
|
orderbook = await self.watch_multiple_wrapper('book', descriptor, symbols, params)
|
|
return orderbook.limit()
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# snapshot
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "book.BTC_USDC-PERPETUAL.raw",
|
|
# "data": {
|
|
# "type": "snapshot",
|
|
# "timestamp": 1655395057025,
|
|
# "instrument_name": "BTC_USDC-PERPETUAL",
|
|
# "change_id": 1550694837,
|
|
# "bids": [
|
|
# ["new", 20987, 0.487],
|
|
# ["new", 20986, 0.238],
|
|
# ],
|
|
# "asks": [
|
|
# ["new", 20999, 0.092],
|
|
# ["new", 21000, 1.238],
|
|
# ]
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
# change
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "book.BTC_USDC-PERPETUAL.raw",
|
|
# "data": {
|
|
# "type": "change",
|
|
# "timestamp": 1655395168086,
|
|
# "prev_change_id": 1550724481,
|
|
# "instrument_name": "BTC_USDC-PERPETUAL",
|
|
# "change_id": 1550724483,
|
|
# "bids": [
|
|
# ["new", 20977, 0.109],
|
|
# ["delete", 20975, 0]
|
|
# ],
|
|
# "asks": []
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_value(message, 'params', {})
|
|
data = self.safe_value(params, 'data', {})
|
|
channel = self.safe_string(params, 'channel')
|
|
parts = channel.split('.')
|
|
descriptor = ''
|
|
partsLength = len(parts)
|
|
isDetailed = partsLength == 5
|
|
if isDetailed:
|
|
group = self.safe_string(parts, 2)
|
|
depth = self.safe_string(parts, 3)
|
|
interval = self.safe_string(parts, 4)
|
|
descriptor = group + '.' + depth + '.' + interval
|
|
else:
|
|
interval = self.safe_string(parts, 2)
|
|
descriptor = interval
|
|
marketId = self.safe_string(data, 'instrument_name')
|
|
symbol = self.safe_symbol(marketId)
|
|
timestamp = self.safe_integer(data, 'timestamp')
|
|
if not (symbol in self.orderbooks):
|
|
self.orderbooks[symbol] = self.counted_order_book()
|
|
storedOrderBook = self.orderbooks[symbol]
|
|
asks = self.safe_list(data, 'asks', [])
|
|
bids = self.safe_list(data, 'bids', [])
|
|
self.handle_deltas(storedOrderBook['asks'], asks)
|
|
self.handle_deltas(storedOrderBook['bids'], bids)
|
|
storedOrderBook['nonce'] = timestamp
|
|
storedOrderBook['timestamp'] = timestamp
|
|
storedOrderBook['datetime'] = self.iso8601(timestamp)
|
|
storedOrderBook['symbol'] = symbol
|
|
self.orderbooks[symbol] = storedOrderBook
|
|
messageHash = 'book|' + symbol + '|' + descriptor
|
|
client.resolve(storedOrderBook, messageHash)
|
|
|
|
def clean_order_book(self, data):
|
|
bids = self.safe_list(data, 'bids', [])
|
|
asks = self.safe_list(data, 'asks', [])
|
|
cleanedBids = []
|
|
for i in range(0, len(bids)):
|
|
cleanedBids.append([bids[i][1], bids[i][2]])
|
|
cleanedAsks = []
|
|
for i in range(0, len(asks)):
|
|
cleanedAsks.append([asks[i][1], asks[i][2]])
|
|
data['bids'] = cleanedBids
|
|
data['asks'] = cleanedAsks
|
|
return data
|
|
|
|
def handle_delta(self, bookside, delta):
|
|
price = delta[1]
|
|
amount = delta[2]
|
|
if delta[0] == 'new' or delta[0] == 'change':
|
|
bookside.storeArray([price, amount, 1])
|
|
elif delta[0] == 'delete':
|
|
bookside.storeArray([price, amount, 0])
|
|
|
|
def handle_deltas(self, bookside, deltas):
|
|
for i in range(0, len(deltas)):
|
|
self.handle_delta(bookside, deltas[i])
|
|
|
|
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
|
|
https://docs.deribit.com/#user-orders-instrument_name-raw
|
|
|
|
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
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.authenticate(params)
|
|
if symbol is not None:
|
|
symbol = self.symbol(symbol)
|
|
url = self.urls['api']['ws']
|
|
currency = self.safe_string(params, 'currency', 'any')
|
|
interval = self.safe_string(params, 'interval', 'raw')
|
|
kind = self.safe_string(params, 'kind', 'any')
|
|
params = self.omit(params, 'interval', 'currency', 'kind')
|
|
channel = 'user.orders.' + kind + '.' + currency + '.' + interval
|
|
message: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'private/subscribe',
|
|
'params': {
|
|
'channels': [channel],
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
request = self.deep_extend(message, params)
|
|
orders = await self.watch(url, channel, request, channel, request)
|
|
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):
|
|
# Does not return a snapshot of current orders
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "user.orders.any.any.raw",
|
|
# "data": {
|
|
# "web": True,
|
|
# "time_in_force": "good_til_cancelled",
|
|
# "replaced": False,
|
|
# "reduce_only": False,
|
|
# "profit_loss": 0,
|
|
# "price": 50000,
|
|
# "post_only": False,
|
|
# "order_type": "limit",
|
|
# "order_state": "open",
|
|
# "order_id": "46094375191",
|
|
# "max_show": 10,
|
|
# "last_update_timestamp": 1655401625037,
|
|
# "label": '',
|
|
# "is_liquidation": False,
|
|
# "instrument_name": "BTC-PERPETUAL",
|
|
# "filled_amount": 0,
|
|
# "direction": "sell",
|
|
# "creation_timestamp": 1655401625037,
|
|
# "commission": 0,
|
|
# "average_price": 0,
|
|
# "api": False,
|
|
# "amount": 10
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
if self.orders is None:
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
self.orders = ArrayCacheBySymbolById(limit)
|
|
params = self.safe_value(message, 'params', {})
|
|
channel = self.safe_string(params, 'channel', '')
|
|
data = self.safe_value(params, 'data', {})
|
|
orders = []
|
|
if isinstance(data, list):
|
|
orders = self.parse_orders(data)
|
|
else:
|
|
order = self.parse_order(data)
|
|
orders = [order]
|
|
cachedOrders = self.orders
|
|
for i in range(0, len(orders)):
|
|
cachedOrders.append(orders[i])
|
|
client.resolve(self.orders, channel)
|
|
|
|
async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
|
|
https://docs.deribit.com/#chart-trades-instrument_name-resolution
|
|
|
|
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
: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()
|
|
symbol = self.symbol(symbol)
|
|
ohlcvs = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params)
|
|
return ohlcvs[symbol][timeframe]
|
|
|
|
async def watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://docs.deribit.com/#chart-trades-instrument_name-resolution
|
|
|
|
:param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]
|
|
: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
|
|
"""
|
|
symbolsLength = len(symbolsAndTimeframes)
|
|
if symbolsLength == 0 or not isinstance(symbolsAndTimeframes[0], list):
|
|
raise ArgumentsRequired(self.id + " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]")
|
|
symbol, timeframe, candles = await self.watch_multiple_wrapper('chart.trades', None, symbolsAndTimeframes, params)
|
|
if self.newUpdates:
|
|
limit = candles.getLimit(symbol, limit)
|
|
filtered = self.filter_by_since_limit(candles, since, limit, 0, True)
|
|
return self.create_ohlcv_object(symbol, timeframe, filtered)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "chart.trades.BTC_USDC-PERPETUAL.1",
|
|
# "data": {
|
|
# "volume": 0,
|
|
# "tick": 1655403420000,
|
|
# "open": 20951,
|
|
# "low": 20951,
|
|
# "high": 20951,
|
|
# "cost": 0,
|
|
# "close": 20951
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
params = self.safe_dict(message, 'params', {})
|
|
channel = self.safe_string(params, 'channel', '')
|
|
parts = channel.split('.')
|
|
marketId = self.safe_string(parts, 2)
|
|
rawTimeframe = self.safe_string(parts, 3)
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
wsOptions = self.safe_dict(self.options, 'ws', {})
|
|
timeframes = self.safe_dict(wsOptions, 'timeframes', {})
|
|
unifiedTimeframe = self.find_timeframe(rawTimeframe, timeframes)
|
|
self.ohlcvs[symbol] = self.safe_dict(self.ohlcvs, symbol, {})
|
|
if self.safe_value(self.ohlcvs[symbol], unifiedTimeframe) is None:
|
|
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
|
self.ohlcvs[symbol][unifiedTimeframe] = ArrayCacheByTimestamp(limit)
|
|
stored = self.ohlcvs[symbol][unifiedTimeframe]
|
|
ohlcv = self.safe_dict(params, 'data', {})
|
|
# data contains a single OHLCV candle
|
|
parsed = self.parse_ws_ohlcv(ohlcv, market)
|
|
stored.append(parsed)
|
|
self.ohlcvs[symbol][unifiedTimeframe] = stored
|
|
resolveData = [symbol, unifiedTimeframe, stored]
|
|
messageHash = 'chart.trades|' + symbol + '|' + rawTimeframe
|
|
client.resolve(resolveData, messageHash)
|
|
|
|
def parse_ws_ohlcv(self, ohlcv, market=None) -> list:
|
|
#
|
|
# {
|
|
# "c": "28909.0",
|
|
# "o": "28915.4",
|
|
# "h": "28915.4",
|
|
# "l": "28896.1",
|
|
# "v": "27.6919",
|
|
# "T": 1696687499999,
|
|
# "t": 1696687440000
|
|
# }
|
|
#
|
|
return [
|
|
self.safe_integer(ohlcv, 'tick'),
|
|
self.safe_number(ohlcv, 'open'),
|
|
self.safe_number(ohlcv, 'high'),
|
|
self.safe_number(ohlcv, 'low'),
|
|
self.safe_number(ohlcv, 'close'),
|
|
self.safe_number(ohlcv, 'volume'),
|
|
]
|
|
|
|
async def watch_multiple_wrapper(self, channelName: str, channelDescriptor: Str, symbolsArray=None, params={}):
|
|
await self.load_markets()
|
|
url = self.urls['api']['ws']
|
|
rawSubscriptions = []
|
|
messageHashes = []
|
|
isOHLCV = (channelName == 'chart.trades')
|
|
symbols = self.get_list_from_object_values(symbolsArray, 0) if isOHLCV else symbolsArray
|
|
self.market_symbols(symbols, None, False)
|
|
for i in range(0, len(symbolsArray)):
|
|
current = symbolsArray[i]
|
|
market = None
|
|
if isOHLCV:
|
|
market = self.market(current[0])
|
|
unifiedTf = current[1]
|
|
rawTf = self.safe_string(self.timeframes, unifiedTf, unifiedTf)
|
|
channelDescriptor = rawTf
|
|
else:
|
|
market = self.market(current)
|
|
message = channelName + '.' + market['id'] + '.' + channelDescriptor
|
|
rawSubscriptions.append(message)
|
|
messageHashes.append(channelName + '|' + market['symbol'] + '|' + channelDescriptor)
|
|
request: dict = {
|
|
'jsonrpc': '2.0',
|
|
'method': 'public/subscribe',
|
|
'params': {
|
|
'channels': rawSubscriptions,
|
|
},
|
|
'id': self.request_id(),
|
|
}
|
|
extendedRequest = self.deep_extend(request, params)
|
|
maxMessageByteLimit = 32768 - 1 # 'Message Too Big: limit 32768B'
|
|
jsonedText = self.json(extendedRequest)
|
|
if len(jsonedText) >= maxMessageByteLimit:
|
|
raise ExchangeError(self.id + ' requested subscription length over limit, try to reduce symbols amount')
|
|
return await self.watch_multiple(url, messageHashes, extendedRequest, rawSubscriptions)
|
|
|
|
def handle_message(self, client: Client, message):
|
|
#
|
|
# error
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": 1,
|
|
# "error": {
|
|
# "message": "Invalid params",
|
|
# "data": {
|
|
# "reason": "invalid format",
|
|
# "param": "nonce"
|
|
# },
|
|
# "code": -32602
|
|
# },
|
|
# "usIn": "1655391709417993",
|
|
# "usOut": "1655391709418049",
|
|
# "usDiff": 56,
|
|
# "testnet": False
|
|
# }
|
|
#
|
|
# subscribe
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": 2,
|
|
# "result": ["ticker.BTC_USDC-PERPETUAL.raw"],
|
|
# "usIn": "1655393625889396",
|
|
# "usOut": "1655393625889518",
|
|
# "usDiff": 122,
|
|
# "testnet": False
|
|
# }
|
|
#
|
|
# notification
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "method": "subscription",
|
|
# "params": {
|
|
# "channel": "ticker.BTC_USDC-PERPETUAL.raw",
|
|
# "data": {
|
|
# "timestamp": 1655393724752,
|
|
# "stats": [Object],
|
|
# "state": "open",
|
|
# "settlement_price": 21729.5891,
|
|
# "open_interest": 164.501,
|
|
# "min_price": 20792.9001,
|
|
# "max_price": 21426.1864,
|
|
# "mark_price": 21109.4757,
|
|
# "last_price": 21132,
|
|
# "instrument_name": "BTC_USDC-PERPETUAL",
|
|
# "index_price": 21122.3937,
|
|
# "funding_8h": -0.00022427,
|
|
# "estimated_delivery_price": 21122.3937,
|
|
# "current_funding": -0.00011158,
|
|
# "best_bid_price": 21106,
|
|
# "best_bid_amount": 1.143,
|
|
# "best_ask_price": 21113,
|
|
# "best_ask_amount": 0.402
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
error = self.safe_value(message, 'error')
|
|
if error is not None:
|
|
raise ExchangeError(self.id + ' ' + self.json(error))
|
|
params = self.safe_value(message, 'params')
|
|
channel = self.safe_string(params, 'channel')
|
|
if channel is not None:
|
|
parts = channel.split('.')
|
|
channelId = self.safe_string(parts, 0)
|
|
userHandlers: dict = {
|
|
'trades': self.handle_my_trades,
|
|
'portfolio': self.handle_balance,
|
|
'orders': self.handle_orders,
|
|
}
|
|
handlers: dict = {
|
|
'ticker': self.handle_ticker,
|
|
'quote': self.handle_bid_ask,
|
|
'book': self.handle_order_book,
|
|
'trades': self.handle_trades,
|
|
'chart': self.handle_ohlcv,
|
|
'user': self.safe_value(userHandlers, self.safe_string(parts, 1)),
|
|
}
|
|
handler = self.safe_value(handlers, channelId)
|
|
if handler is not None:
|
|
handler(client, message)
|
|
return
|
|
raise NotSupported(self.id + ' no handler found for self message ' + self.json(message))
|
|
result = self.safe_value(message, 'result', {})
|
|
accessToken = self.safe_string(result, 'access_token')
|
|
if accessToken is not None:
|
|
self.handle_authentication_message(client, message)
|
|
|
|
def handle_authentication_message(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "jsonrpc": "2.0",
|
|
# "id": 1,
|
|
# "result": {
|
|
# "token_type": "bearer",
|
|
# "scope": "account:read_write block_trade:read_write connection custody:read_write mainaccount name:ccxt trade:read_write wallet:read_write",
|
|
# "refresh_token": "1686927372328.1EzFBRmt.logRQWXkPA1oE_Tk0gRsls9Hau7YN6a321XUBnxvR4x6cryhbkKcniUJU-czA8_zKXrqQGpQmfoDwhLIjIsWCvRuu6otbg-LKWlrtTX1GQqLcPaTTHAdZGTMV-HM8HiS03QBd9MIXWRfF53sKj2hdR9nZPZ6MH1XrkpAZPB_peuEEB9wlcc3elzWEZFtCmiy1fnQ8TPHwAJMt3nuUmEcMLt_-F554qrsg_-I66D9xMiifJj4dBemdPfV_PkGPRIwIoKlxDjyv2-xfCw-4eKyo6Hu1m2h6gT1DPOTxSXcBgfBQjpi-_uY3iAIj7U6xjC46PHthEdquhEuCTZl7UfCRZSAWwZA",
|
|
# "expires_in": 31536000,
|
|
# "access_token": "1686923272328.1CkwEx-u.qHradpIulmuoeboKMEi8PkQ1_4DF8yFE2zywBTtkD32sruVC53b1HwL5OWRuh2nYAndXff4xuXIMRkkEfMAFCeq24prihxxinoS8DDVkKBxedGx4CUPJFeXjmh7wuRGqQOLg1plXOpbF3fwF2KPEkAuETwcpcVY6K9HUVjutNRfxFe2TR7CvuS9x8TATvoPeu7H1ezYl-LkKSaRifdTXuwituXgp4oDbPRyQLniEBWuYF9rY7qbABxuOJlXI1VZ63u7Bh0mGWei-KeVeqHGNpy6OgrFRPXPxa9_U7vaxCyHW3zZ9959TQ1QUMLWtUX-NLBEv3BT5eCieW9HORYIOKfsgkpd3"
|
|
# },
|
|
# "usIn": "1655391872327712",
|
|
# "usOut": "1655391872328515",
|
|
# "usDiff": 803,
|
|
# "testnet": False
|
|
# }
|
|
#
|
|
messageHash = 'authenticated'
|
|
client.resolve(message, messageHash)
|
|
return message
|
|
|
|
async def authenticate(self, params={}):
|
|
url = self.urls['api']['ws']
|
|
client = self.client(url)
|
|
time = self.milliseconds()
|
|
timeString = self.number_to_string(time)
|
|
nonce = timeString
|
|
messageHash = 'authenticated'
|
|
future = self.safe_value(client.subscriptions, messageHash)
|
|
if future is None:
|
|
self.check_required_credentials()
|
|
requestId = self.request_id()
|
|
lineBreak = "\n" # eslint-disable-line quotes
|
|
signature = self.hmac(self.encode(timeString + lineBreak + nonce + lineBreak), self.encode(self.secret), hashlib.sha256)
|
|
request: dict = {
|
|
'jsonrpc': '2.0',
|
|
'id': requestId,
|
|
'method': 'public/auth',
|
|
'params': {
|
|
'grant_type': 'client_signature',
|
|
'client_id': self.apiKey,
|
|
'timestamp': time,
|
|
'signature': signature,
|
|
'nonce': nonce,
|
|
'data': '',
|
|
},
|
|
}
|
|
future = await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
client.subscriptions[messageHash] = future
|
|
return future
|