4202 lines
206 KiB
Python
4202 lines
206 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, Int, Liquidation, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import NotSupported
|
|
from ccxt.base.errors import ChecksumError
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class binance(ccxt.async_support.binance):
|
|
|
|
def describe(self) -> Any:
|
|
superDescribe = super(binance, self).describe()
|
|
return self.deep_extend(superDescribe, self.describe_data())
|
|
|
|
def describe_data(self):
|
|
return {
|
|
'has': {
|
|
'ws': True,
|
|
'watchBalance': True,
|
|
'watchLiquidations': True,
|
|
'watchLiquidationsForSymbols': True,
|
|
'watchMyLiquidations': True,
|
|
'watchMyLiquidationsForSymbols': True,
|
|
'watchBidsAsks': True,
|
|
'watchMyTrades': True,
|
|
'watchOHLCV': True,
|
|
'watchOHLCVForSymbols': True,
|
|
'watchOrderBook': True,
|
|
'watchOrderBookForSymbols': True,
|
|
'watchOrders': True,
|
|
'watchOrdersForSymbols': True,
|
|
'watchPositions': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchMarkPrices': True,
|
|
'watchMarkPrice': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
'createOrderWs': True,
|
|
'editOrderWs': True,
|
|
'cancelOrderWs': True,
|
|
'cancelOrdersWs': False,
|
|
'cancelAllOrdersWs': True,
|
|
'fetchBalanceWs': True,
|
|
'fetchDepositsWs': False,
|
|
'fetchMarketsWs': False,
|
|
'fetchMyTradesWs': True,
|
|
'fetchOHLCVWs': True,
|
|
'fetchOrderBookWs': True,
|
|
'fetchOpenOrdersWs': True,
|
|
'fetchOrderWs': True,
|
|
'fetchOrdersWs': True,
|
|
'fetchPositionWs': True,
|
|
'fetchPositionForSymbolWs': True,
|
|
'fetchPositionsWs': True,
|
|
'fetchTickerWs': True,
|
|
'fetchTradesWs': True,
|
|
'fetchTradingFeesWs': False,
|
|
'fetchWithdrawalsWs': False,
|
|
'unWatchTicker': True,
|
|
'unWatchTickers': True,
|
|
'unWatchOHLCV': True,
|
|
'unWatchOHLCVForSymbols': True,
|
|
'unWatchOrderBook': True,
|
|
'unWatchOrderBookForSymbols': True,
|
|
'unWatchTrades': True,
|
|
'unWatchTradesForSymbols': True,
|
|
'unWatchMyTrades': False,
|
|
'unWatchOrders': False,
|
|
'unWatchPositions': False,
|
|
'unWatchMarkPrices': True,
|
|
'unWatchMarkPrice': True,
|
|
},
|
|
'urls': {
|
|
'test': {
|
|
'ws': {
|
|
'spot': 'wss://stream.testnet.binance.vision/ws',
|
|
'margin': 'wss://stream.testnet.binance.vision/ws',
|
|
'future': 'wss://fstream.binancefuture.com/ws',
|
|
'delivery': 'wss://dstream.binancefuture.com/ws',
|
|
'ws-api': {
|
|
'spot': 'wss://ws-api.testnet.binance.vision/ws-api/v3',
|
|
'future': 'wss://testnet.binancefuture.com/ws-fapi/v1',
|
|
'delivery': 'wss://testnet.binancefuture.com/ws-dapi/v1',
|
|
},
|
|
},
|
|
},
|
|
'demo': {
|
|
'ws': {
|
|
'spot': 'wss://demo-stream.binance.com/ws',
|
|
'margin': 'wss://demo-stream.binance.com/ws',
|
|
'future': 'wss://fstream.binancefuture.com/ws',
|
|
'delivery': 'wss://dstream.binancefuture.com/ws',
|
|
'ws-api': {
|
|
'spot': 'wss://demo-ws-api.binance.com/ws-api/v3',
|
|
'future': 'wss://testnet.binancefuture.com/ws-fapi/v1',
|
|
'delivery': 'wss://testnet.binancefuture.com/ws-dapi/v1',
|
|
},
|
|
},
|
|
},
|
|
'api': {
|
|
'ws': {
|
|
'spot': 'wss://stream.binance.com:9443/ws',
|
|
'margin': 'wss://stream.binance.com:9443/ws',
|
|
'future': 'wss://fstream.binance.com/ws',
|
|
'delivery': 'wss://dstream.binance.com/ws',
|
|
'ws-api': {
|
|
'spot': 'wss://ws-api.binance.com:443/ws-api/v3',
|
|
'future': 'wss://ws-fapi.binance.com/ws-fapi/v1',
|
|
'delivery': 'wss://ws-dapi.binance.com/ws-dapi/v1',
|
|
},
|
|
'papi': 'wss://fstream.binance.com/pm/ws',
|
|
},
|
|
},
|
|
'doc': 'https://developers.binance.com/en',
|
|
},
|
|
'streaming': {
|
|
'keepAlive': 180000,
|
|
},
|
|
'options': {
|
|
'returnRateLimits': False,
|
|
'streamLimits': {
|
|
'spot': 50, # max 1024
|
|
'margin': 50, # max 1024
|
|
'future': 50, # max 200
|
|
'delivery': 50, # max 200
|
|
},
|
|
'subscriptionLimitByStream': {
|
|
'spot': 200,
|
|
'margin': 200,
|
|
'future': 200,
|
|
'delivery': 200,
|
|
},
|
|
'streamBySubscriptionsHash': self.create_safe_dictionary(),
|
|
'streamIndex': -1,
|
|
# get updates every 1000ms or 100ms
|
|
# or every 0ms in real-time for futures
|
|
'watchOrderBookRate': 100,
|
|
'liquidationsLimit': 1000,
|
|
'myLiquidationsLimit': 1000,
|
|
'tradesLimit': 1000,
|
|
'ordersLimit': 1000,
|
|
'OHLCVLimit': 1000,
|
|
'requestId': self.create_safe_dictionary(),
|
|
'watchOrderBookLimit': 1000, # default limit
|
|
'watchTrades': {
|
|
'name': 'trade', # 'trade' or 'aggTrade'
|
|
},
|
|
'watchTicker': {
|
|
'name': 'ticker', # ticker or miniTicker or ticker_<window_size>
|
|
},
|
|
'watchTickers': {
|
|
'name': 'ticker', # ticker or miniTicker or ticker_<window_size>
|
|
},
|
|
'watchOHLCV': {
|
|
'name': 'kline', # or indexPriceKline or markPriceKline(coin-m futures)
|
|
},
|
|
'watchOrderBook': {
|
|
'maxRetries': 3,
|
|
'checksum': True,
|
|
},
|
|
'watchBalance': {
|
|
'fetchBalanceSnapshot': False, # or True
|
|
'awaitBalanceSnapshot': True, # whether to wait for the balance snapshot before providing updates
|
|
},
|
|
'watchLiquidationsForSymbols': {
|
|
'defaultType': 'swap',
|
|
},
|
|
'watchPositions': {
|
|
'fetchPositionsSnapshot': True, # or False
|
|
'awaitPositionsSnapshot': True, # whether to wait for the positions snapshot before providing updates
|
|
},
|
|
'wallet': 'wb', # wb = wallet balance, cw = cross balance
|
|
'listenKeyRefreshRate': 1200000, # 20 mins
|
|
'ws': {
|
|
'cost': 5,
|
|
},
|
|
'tickerChannelsMap': {
|
|
'24hrTicker': 'ticker',
|
|
'24hrMiniTicker': 'miniTicker',
|
|
'markPriceUpdate': 'markPrice',
|
|
# rolling window tickers
|
|
'1hTicker': 'ticker_1h',
|
|
'4hTicker': 'ticker_4h',
|
|
'1dTicker': 'ticker_1d',
|
|
'bookTicker': 'bookTicker',
|
|
},
|
|
},
|
|
}
|
|
|
|
def request_id(self, url):
|
|
options = self.safe_dict(self.options, 'requestId', self.create_safe_dictionary())
|
|
previousValue = self.safe_integer(options, url, 0)
|
|
newValue = self.sum(previousValue, 1)
|
|
self.options['requestId'][url] = newValue
|
|
return newValue
|
|
|
|
def is_spot_url(self, client: Client):
|
|
return(client.url.find('/stream') > -1) or (client.url.find('demo-stream') > -1)
|
|
|
|
def stream(self, type: Str, subscriptionHash: Str, numSubscriptions=1):
|
|
streamBySubscriptionsHash = self.safe_dict(self.options, 'streamBySubscriptionsHash', self.create_safe_dictionary())
|
|
stream = self.safe_string(streamBySubscriptionsHash, subscriptionHash)
|
|
if stream is None:
|
|
streamIndex = self.safe_integer(self.options, 'streamIndex', -1)
|
|
streamLimits = self.safe_value(self.options, 'streamLimits')
|
|
streamLimit = self.safe_integer(streamLimits, type)
|
|
streamIndex = streamIndex + 1
|
|
normalizedIndex = streamIndex % streamLimit
|
|
self.options['streamIndex'] = streamIndex
|
|
stream = self.number_to_string(normalizedIndex)
|
|
self.options['streamBySubscriptionsHash'][subscriptionHash] = stream
|
|
subscriptionsByStreams = self.safe_value(self.options, 'numSubscriptionsByStream')
|
|
if subscriptionsByStreams is None:
|
|
self.options['numSubscriptionsByStream'] = self.create_safe_dictionary()
|
|
subscriptionsByStream = self.safe_integer(self.options['numSubscriptionsByStream'], stream, 0)
|
|
newNumSubscriptions = subscriptionsByStream + numSubscriptions
|
|
subscriptionLimitByStream = self.safe_integer(self.options['subscriptionLimitByStream'], type, 200)
|
|
if newNumSubscriptions > subscriptionLimitByStream:
|
|
raise BadRequest(self.id + ' reached the limit of subscriptions by stream. Increase the number of streams, or increase the stream limit or subscription limit by stream if the exchange allows.')
|
|
self.options['numSubscriptionsByStream'][stream] = subscriptionsByStream + numSubscriptions
|
|
return stream
|
|
|
|
async def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
|
|
"""
|
|
watch the public liquidations of a trading pair
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Liquidation-Order-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Liquidation-Order-Streams
|
|
|
|
:param str symbol: unified CCXT market symbol
|
|
:param int [since]: the earliest time in ms to fetch liquidations for
|
|
:param int [limit]: the maximum number of liquidation structures to retrieve
|
|
:param dict [params]: exchange specific parameters for the bitmex api endpoint
|
|
:returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
|
|
"""
|
|
return await self.watch_liquidations_for_symbols([symbol], since, limit, params)
|
|
|
|
async def watch_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
|
|
"""
|
|
watch the public liquidations of a trading pair
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Liquidation-Order-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Liquidation-Order-Streams
|
|
|
|
:param str[] symbols: list of unified market symbols
|
|
:param int [since]: the earliest time in ms to fetch liquidations for
|
|
:param int [limit]: the maximum number of liquidation structures to retrieve
|
|
:param dict [params]: exchange specific parameters for the bitmex api endpoint
|
|
:returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
subscriptionHashes = []
|
|
messageHashes = []
|
|
streamHash = 'liquidations'
|
|
symbols = self.market_symbols(symbols, None, True, True)
|
|
if self.is_empty(symbols):
|
|
subscriptionHashes.append('!' + 'forceOrder@arr')
|
|
messageHashes.append('liquidations')
|
|
else:
|
|
for i in range(0, len(symbols)):
|
|
market = self.market(symbols[i])
|
|
subscriptionHashes.append(market['lowercaseId'] + '@forceOrder')
|
|
messageHashes.append('liquidations::' + symbols[i])
|
|
streamHash += '::' + ','.join(symbols)
|
|
firstMarket = self.get_market_from_symbols(symbols)
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchLiquidationsForSymbols', firstMarket, params)
|
|
if type == 'spot':
|
|
raise BadRequest(self.id + ' watchLiquidationsForSymbols is not supported for spot symbols')
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchLiquidationsForSymbols', firstMarket, params)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
numSubscriptions = len(subscriptionHashes)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, numSubscriptions)
|
|
requestId = self.request_id(url)
|
|
request = {
|
|
'method': 'SUBSCRIBE',
|
|
'params': subscriptionHashes,
|
|
'id': requestId,
|
|
}
|
|
subscribe = {
|
|
'id': requestId,
|
|
}
|
|
newLiquidations = await self.watch_multiple(url, messageHashes, self.extend(request, params), subscriptionHashes, subscribe)
|
|
if self.newUpdates:
|
|
return newLiquidations
|
|
return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True)
|
|
|
|
def handle_liquidation(self, client: Client, message):
|
|
#
|
|
# future
|
|
# {
|
|
# "e":"forceOrder",
|
|
# "E":1698871323061,
|
|
# "o":{
|
|
# "s":"BTCUSDT",
|
|
# "S":"BUY",
|
|
# "o":"LIMIT",
|
|
# "f":"IOC",
|
|
# "q":"1.437",
|
|
# "p":"35100.81",
|
|
# "ap":"34959.70",
|
|
# "X":"FILLED",
|
|
# "l":"1.437",
|
|
# "z":"1.437",
|
|
# "T":1698871323059
|
|
# }
|
|
# }
|
|
# delivery
|
|
# {
|
|
# "e":"forceOrder", # Event Type
|
|
# "E": 1591154240950, # Event Time
|
|
# "o":{
|
|
# "s":"BTCUSD_200925", # Symbol
|
|
# "ps": "BTCUSD", # Pair
|
|
# "S":"SELL", # Side
|
|
# "o":"LIMIT", # Order Type
|
|
# "f":"IOC", # Time in Force
|
|
# "q":"1", # Original Quantity
|
|
# "p":"9425.5", # Price
|
|
# "ap":"9496.5", # Average Price
|
|
# "X":"FILLED", # Order Status
|
|
# "l":"1", # Order Last Filled Quantity
|
|
# "z":"1", # Order Filled Accumulated Quantity
|
|
# "T": 1591154240949, # Order Trade Time
|
|
# }
|
|
# }
|
|
#
|
|
rawLiquidation = self.safe_value(message, 'o', {})
|
|
marketId = self.safe_string(rawLiquidation, 's')
|
|
market = self.safe_market(marketId, None, '', 'contract')
|
|
symbol = market['symbol']
|
|
liquidation = self.parse_ws_liquidation(rawLiquidation, market)
|
|
liquidations = self.safe_value(self.liquidations, symbol)
|
|
if liquidations is None:
|
|
limit = self.safe_integer(self.options, 'liquidationsLimit', 1000)
|
|
liquidations = ArrayCache(limit)
|
|
liquidations.append(liquidation)
|
|
self.liquidations[symbol] = liquidations
|
|
client.resolve([liquidation], 'liquidations')
|
|
client.resolve([liquidation], 'liquidations::' + symbol)
|
|
|
|
def parse_ws_liquidation(self, liquidation, market=None):
|
|
#
|
|
# future
|
|
# {
|
|
# "s":"BTCUSDT",
|
|
# "S":"BUY",
|
|
# "o":"LIMIT",
|
|
# "f":"IOC",
|
|
# "q":"1.437",
|
|
# "p":"35100.81",
|
|
# "ap":"34959.70",
|
|
# "X":"FILLED",
|
|
# "l":"1.437",
|
|
# "z":"1.437",
|
|
# "T":1698871323059
|
|
# }
|
|
# delivery
|
|
# {
|
|
# "s":"BTCUSD_200925", # Symbol
|
|
# "ps": "BTCUSD", # Pair
|
|
# "S":"SELL", # Side
|
|
# "o":"LIMIT", # Order Type
|
|
# "f":"IOC", # Time in Force
|
|
# "q":"1", # Original Quantity
|
|
# "p":"9425.5", # Price
|
|
# "ap":"9496.5", # Average Price
|
|
# "X":"FILLED", # Order Status
|
|
# "l":"1", # Order Last Filled Quantity
|
|
# "z":"1", # Order Filled Accumulated Quantity
|
|
# "T": 1591154240949, # Order Trade Time
|
|
# }
|
|
# myLiquidation
|
|
# {
|
|
# "s":"BTCUSDT", # Symbol
|
|
# "c":"TEST", # Client Order Id
|
|
# # special client order id:
|
|
# # starts with "autoclose-": liquidation order
|
|
# # "adl_autoclose": ADL auto close order
|
|
# # "settlement_autoclose-": settlement order for delisting or delivery
|
|
# "S":"SELL", # Side
|
|
# "o":"TRAILING_STOP_MARKET", # Order Type
|
|
# "f":"GTC", # Time in Force
|
|
# "q":"0.001", # Original Quantity
|
|
# "p":"0", # Original Price
|
|
# "ap":"0", # Average Price
|
|
# "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order
|
|
# "x":"NEW", # Execution Type
|
|
# "X":"NEW", # Order Status
|
|
# "i":8886774, # Order Id
|
|
# "l":"0", # Order Last Filled Quantity
|
|
# "z":"0", # Order Filled Accumulated Quantity
|
|
# "L":"0", # Last Filled Price
|
|
# "N":"USDT", # Commission Asset, will not push if no commission
|
|
# "n":"0", # Commission, will not push if no commission
|
|
# "T":1568879465650, # Order Trade Time
|
|
# "t":0, # Trade Id
|
|
# "b":"0", # Bids Notional
|
|
# "a":"9.91", # Ask Notional
|
|
# "m":false, # Is self trade the maker side?
|
|
# "R":false, # Is self reduce only
|
|
# "wt":"CONTRACT_PRICE", # Stop Price Working Type
|
|
# "ot":"TRAILING_STOP_MARKET",// Original Order Type
|
|
# "ps":"LONG", # Position Side
|
|
# "cp":false, # If Close-All, pushed with conditional order
|
|
# "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order
|
|
# "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
|
# "pP": False, # If price protection is turned on
|
|
# "si": 0, # ignore
|
|
# "ss": 0, # ignore
|
|
# "rp":"0", # Realized Profit of the trade
|
|
# "V":"EXPIRE_TAKER", # STP mode
|
|
# "pm":"OPPONENT", # Price match mode
|
|
# "gtd":0 # TIF GTD order auto cancel time
|
|
# }
|
|
#
|
|
marketId = self.safe_string(liquidation, 's')
|
|
market = self.safe_market(marketId, market, None, 'swap')
|
|
timestamp = self.safe_integer(liquidation, 'T')
|
|
return self.safe_liquidation({
|
|
'info': liquidation,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'contracts': self.safe_number(liquidation, 'l'),
|
|
'contractSize': self.safe_number(market, 'contractSize'),
|
|
'price': self.safe_number(liquidation, 'ap'),
|
|
'side': self.safe_string_lower(liquidation, 'S'),
|
|
'baseValue': None,
|
|
'quoteValue': None,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
})
|
|
|
|
async def watch_my_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
|
|
"""
|
|
watch the private liquidations of a trading pair
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update
|
|
|
|
:param str symbol: unified CCXT market symbol
|
|
:param int [since]: the earliest time in ms to fetch liquidations for
|
|
:param int [limit]: the maximum number of liquidation structures to retrieve
|
|
:param dict [params]: exchange specific parameters for the bitmex api endpoint
|
|
:returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
|
|
"""
|
|
return self.watch_my_liquidations_for_symbols([symbol], since, limit, params)
|
|
|
|
async def watch_my_liquidations_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
|
|
"""
|
|
watch the private liquidations of a trading pair
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/user-data-streams/Event-Order-Update
|
|
|
|
:param str[] symbols: list of unified market symbols
|
|
:param int [since]: the earliest time in ms to fetch liquidations for
|
|
:param int [limit]: the maximum number of liquidation structures to retrieve
|
|
:param dict [params]: exchange specific parameters for the bitmex api endpoint
|
|
:returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, True, True, True)
|
|
market = self.get_market_from_symbols(symbols)
|
|
messageHashes = ['myLiquidations']
|
|
if not self.is_empty(symbols):
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
messageHashes.append('myLiquidations::' + symbol)
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchMyLiquidationsForSymbols', market, params)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchMyLiquidationsForSymbols', market, params)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
await self.authenticate(params)
|
|
url = self.urls['api']['ws'][type] + '/' + self.options[type]['listenKey']
|
|
message = None
|
|
newLiquidations = await self.watch_multiple(url, messageHashes, message, [type])
|
|
if self.newUpdates:
|
|
return newLiquidations
|
|
return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit)
|
|
|
|
def handle_my_liquidation(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "s":"BTCUSDT", # Symbol
|
|
# "c":"TEST", # Client Order Id
|
|
# # special client order id:
|
|
# # starts with "autoclose-": liquidation order
|
|
# # "adl_autoclose": ADL auto close order
|
|
# # "settlement_autoclose-": settlement order for delisting or delivery
|
|
# "S":"SELL", # Side
|
|
# "o":"TRAILING_STOP_MARKET", # Order Type
|
|
# "f":"GTC", # Time in Force
|
|
# "q":"0.001", # Original Quantity
|
|
# "p":"0", # Original Price
|
|
# "ap":"0", # Average Price
|
|
# "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order
|
|
# "x":"NEW", # Execution Type
|
|
# "X":"NEW", # Order Status
|
|
# "i":8886774, # Order Id
|
|
# "l":"0", # Order Last Filled Quantity
|
|
# "z":"0", # Order Filled Accumulated Quantity
|
|
# "L":"0", # Last Filled Price
|
|
# "N":"USDT", # Commission Asset, will not push if no commission
|
|
# "n":"0", # Commission, will not push if no commission
|
|
# "T":1568879465650, # Order Trade Time
|
|
# "t":0, # Trade Id
|
|
# "b":"0", # Bids Notional
|
|
# "a":"9.91", # Ask Notional
|
|
# "m":false, # Is self trade the maker side?
|
|
# "R":false, # Is self reduce only
|
|
# "wt":"CONTRACT_PRICE", # Stop Price Working Type
|
|
# "ot":"TRAILING_STOP_MARKET",// Original Order Type
|
|
# "ps":"LONG", # Position Side
|
|
# "cp":false, # If Close-All, pushed with conditional order
|
|
# "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order
|
|
# "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
|
# "pP": False, # If price protection is turned on
|
|
# "si": 0, # ignore
|
|
# "ss": 0, # ignore
|
|
# "rp":"0", # Realized Profit of the trade
|
|
# "V":"EXPIRE_TAKER", # STP mode
|
|
# "pm":"OPPONENT", # Price match mode
|
|
# "gtd":0 # TIF GTD order auto cancel time
|
|
# }
|
|
#
|
|
orderType = self.safe_string(message, 'o')
|
|
if orderType != 'LIQUIDATION':
|
|
return
|
|
marketId = self.safe_string(message, 's')
|
|
market = self.safe_market(marketId, None, None, 'swap')
|
|
symbol = self.safe_symbol(marketId, market)
|
|
liquidation = self.parse_ws_liquidation(message, market)
|
|
myLiquidations = self.safe_value(self.myLiquidations, symbol)
|
|
if myLiquidations is None:
|
|
limit = self.safe_integer(self.options, 'myLiquidationsLimit', 1000)
|
|
myLiquidations = ArrayCache(limit)
|
|
myLiquidations.append(liquidation)
|
|
self.myLiquidations[symbol] = myLiquidations
|
|
client.resolve([liquidation], 'myLiquidations')
|
|
client.resolve([liquidation], 'myLiquidations::' + symbol)
|
|
|
|
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://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
|
|
: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
|
|
"""
|
|
#
|
|
# todo add support for <levels>-snapshots(depth)
|
|
# https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md#partial-book-depth-streams # <symbol>@depth<levels>@100ms or <symbol>@depth<levels>(1000ms)
|
|
# valid <levels> are 5, 10, or 20
|
|
#
|
|
# default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000
|
|
#
|
|
# notice the differences between trading futures and spot trading
|
|
# the algorithms use different urls in step 1
|
|
# delta caching and merging also differs in steps 4, 5, 6
|
|
#
|
|
# spot/margin
|
|
# https://binance-docs.github.io/apidocs/spot/en/#how-to-manage-a-local-order-book-correctly
|
|
#
|
|
# 1. Open a stream to wss://stream.binance.com:9443/ws/bnbbtc@depth.
|
|
# 2. Buffer the events you receive from the stream.
|
|
# 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 .
|
|
# 4. Drop any event where u is <= lastUpdateId in the snapshot.
|
|
# 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1.
|
|
# 6. While listening to the stream, each new event's U should be equal to the previous event's u+1.
|
|
# 7. The data in each event is the absolute quantity for a price level.
|
|
# 8. If the quantity is 0, remove the price level.
|
|
# 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal.
|
|
#
|
|
# futures
|
|
# https://binance-docs.github.io/apidocs/futures/en/#how-to-manage-a-local-order-book-correctly
|
|
#
|
|
# 1. Open a stream to wss://fstream.binance.com/stream?streams=btcusdt@depth.
|
|
# 2. Buffer the events you receive from the stream. For same price, latest received update covers the previous one.
|
|
# 3. Get a depth snapshot from https://fapi.binance.com/fapi/v1/depth?symbol=BTCUSDT&limit=1000 .
|
|
# 4. Drop any event where u is < lastUpdateId in the snapshot.
|
|
# 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId
|
|
# 6. While listening to the stream, each new event's pu should be equal to the previous event's u, otherwise initialize the process from step 3.
|
|
# 7. The data in each event is the absolute quantity for a price level.
|
|
# 8. If the quantity is 0, remove the price level.
|
|
# 9. Receiving an event that removes a price level that is not in your local order book can happen and is normal.
|
|
#
|
|
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://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
|
|
: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
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False, True, True)
|
|
firstMarket = self.market(symbols[0])
|
|
type = firstMarket['type']
|
|
if firstMarket['contract']:
|
|
type = 'future' if firstMarket['linear'] else 'delivery'
|
|
name = 'depth'
|
|
streamHash = 'multipleOrderbook'
|
|
if symbols is not None:
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength > 200:
|
|
raise BadRequest(self.id + ' watchOrderBookForSymbols() accepts 200 symbols at most. To watch more symbols call watchOrderBookForSymbols() multiple times')
|
|
streamHash += '::' + ','.join(symbols)
|
|
watchOrderBookRate = self.safe_string(self.options, 'watchOrderBookRate', '100')
|
|
subParams = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
messageHashes.append('orderbook::' + symbol)
|
|
subscriptionHash = market['lowercaseId'] + '@' + name
|
|
symbolHash = subscriptionHash + '@' + watchOrderBookRate + 'ms'
|
|
subParams.append(symbolHash)
|
|
messageHashesLength = len(messageHashes)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, messageHashesLength)
|
|
requestId = self.request_id(url)
|
|
request: dict = {
|
|
'method': 'SUBSCRIBE',
|
|
'params': subParams,
|
|
'id': requestId,
|
|
}
|
|
subscription: dict = {
|
|
'id': str(requestId),
|
|
'name': name,
|
|
'symbols': symbols,
|
|
'method': self.handle_order_book_subscription,
|
|
'limit': limit,
|
|
'type': type,
|
|
'params': params,
|
|
}
|
|
orderbook = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscription)
|
|
return orderbook.limit()
|
|
|
|
async def un_watch_order_book_for_symbols(self, symbols: List[str], params={}) -> Any:
|
|
"""
|
|
unWatches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
|
|
:param str[] symbols: 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()
|
|
symbols = self.market_symbols(symbols, None, False, True, True)
|
|
firstMarket = self.market(symbols[0])
|
|
type = firstMarket['type']
|
|
if firstMarket['contract']:
|
|
type = 'future' if firstMarket['linear'] else 'delivery'
|
|
name = 'depth'
|
|
streamHash = 'multipleOrderbook'
|
|
if symbols is not None:
|
|
streamHash += '::' + ','.join(symbols)
|
|
watchOrderBookRate = self.safe_string(self.options, 'watchOrderBookRate', '100')
|
|
subParams = []
|
|
subMessageHashes = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
subMessageHashes.append('orderbook::' + symbol)
|
|
messageHashes.append('unsubscribe:orderbook:' + symbol)
|
|
subscriptionHash = market['lowercaseId'] + '@' + name
|
|
symbolHash = subscriptionHash + '@' + watchOrderBookRate + 'ms'
|
|
subParams.append(symbolHash)
|
|
messageHashesLength = len(subMessageHashes)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, messageHashesLength)
|
|
requestId = self.request_id(url)
|
|
request: dict = {
|
|
'method': 'UNSUBSCRIBE',
|
|
'params': subParams,
|
|
'id': requestId,
|
|
}
|
|
subscription: dict = {
|
|
'unsubscribe': True,
|
|
'id': str(requestId),
|
|
'symbols': symbols,
|
|
'subMessageHashes': subMessageHashes,
|
|
'messageHashes': messageHashes,
|
|
'topic': 'orderbook',
|
|
}
|
|
return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscription)
|
|
|
|
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://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#partial-book-depth-streams
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#diff-depth-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Partial-Book-Depth-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Diff-Book-Depth-Streams
|
|
|
|
: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
|
|
"""
|
|
return await self.un_watch_order_book_for_symbols([symbol], params)
|
|
|
|
async def fetch_order_book_ws(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#order-book
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/websocket-api/Order-Book
|
|
|
|
: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()
|
|
market = self.market(symbol)
|
|
payload: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
if limit is not None:
|
|
payload['limit'] = limit
|
|
marketType = self.get_market_type('fetchOrderBookWs', market, params)
|
|
if marketType != 'future':
|
|
raise BadRequest(self.id + ' fetchOrderBookWs only supports swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][marketType]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'createOrderWs', 'returnRateLimits', False)
|
|
payload['returnRateLimits'] = returnRateLimits
|
|
params = self.omit(params, 'test')
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'depth',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_fetch_order_book,
|
|
}
|
|
orderbook = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
orderbook['symbol'] = market['symbol']
|
|
return orderbook
|
|
|
|
def handle_fetch_order_book(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "id":"51e2affb-0aba-4821-ba75-f2625006eb43",
|
|
# "status":200,
|
|
# "result":{
|
|
# "lastUpdateId":1027024,
|
|
# "E":1589436922972,
|
|
# "T":1589436922959,
|
|
# "bids":[
|
|
# [
|
|
# "4.00000000",
|
|
# "431.00000000"
|
|
# ]
|
|
# ],
|
|
# "asks":[
|
|
# [
|
|
# "4.00000200",
|
|
# "12.00000000"
|
|
# ]
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_dict(message, 'result')
|
|
timestamp = self.safe_integer(result, 'T')
|
|
orderbook = self.parse_order_book(result, None, timestamp)
|
|
orderbook['nonce'] = self.safe_integer_2(result, 'lastUpdateId', 'u')
|
|
client.resolve(orderbook, messageHash)
|
|
|
|
async def fetch_order_book_snapshot(self, client, message, subscription):
|
|
symbol = self.safe_string(subscription, 'symbol')
|
|
messageHash = 'orderbook::' + symbol
|
|
try:
|
|
defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000)
|
|
type = self.safe_value(subscription, 'type')
|
|
limit = self.safe_integer(subscription, 'limit', defaultLimit)
|
|
params = self.safe_value(subscription, 'params')
|
|
# 3. Get a depth snapshot from https://www.binance.com/api/v1/depth?symbol=BNBBTC&limit=1000 .
|
|
# todo: self is a synch blocking call - make it async
|
|
# default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000
|
|
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)
|
|
# unroll the accumulated deltas
|
|
messages = orderbook.cache
|
|
orderbook.cache = []
|
|
for i in range(0, len(messages)):
|
|
messageItem = messages[i]
|
|
U = self.safe_integer(messageItem, 'U')
|
|
u = self.safe_integer(messageItem, 'u')
|
|
pu = self.safe_integer(messageItem, 'pu')
|
|
if type == 'future':
|
|
# 4. Drop any event where u is < lastUpdateId in the snapshot
|
|
if u < orderbook['nonce']:
|
|
continue
|
|
# 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId
|
|
if (U <= orderbook['nonce']) and (u >= orderbook['nonce']) or (pu == orderbook['nonce']):
|
|
self.handle_order_book_message(client, messageItem, orderbook)
|
|
else:
|
|
# 4. Drop any event where u is <= lastUpdateId in the snapshot
|
|
if u <= orderbook['nonce']:
|
|
continue
|
|
# 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1
|
|
if ((U - 1) <= orderbook['nonce']) and ((u - 1) >= orderbook['nonce']):
|
|
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_delta(self, bookside, delta):
|
|
price = self.safe_float(delta, 0)
|
|
amount = self.safe_float(delta, 1)
|
|
bookside.store(price, amount)
|
|
|
|
def handle_deltas(self, bookside, deltas):
|
|
for i in range(0, len(deltas)):
|
|
self.handle_delta(bookside, deltas[i])
|
|
|
|
def handle_order_book_message(self, client: Client, message, orderbook):
|
|
u = self.safe_integer(message, 'u')
|
|
self.handle_deltas(orderbook['asks'], self.safe_value(message, 'a', []))
|
|
self.handle_deltas(orderbook['bids'], self.safe_value(message, 'b', []))
|
|
orderbook['nonce'] = u
|
|
timestamp = self.safe_integer(message, 'E')
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
return orderbook
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# initial snapshot is fetched with ccxt's fetchOrderBook
|
|
# the feed does not include a snapshot, just the deltas
|
|
#
|
|
# {
|
|
# "e": "depthUpdate", # Event type
|
|
# "E": 1577554482280, # Event time
|
|
# "s": "BNBBTC", # Symbol
|
|
# "U": 157, # First update ID in event
|
|
# "u": 160, # Final update ID in event
|
|
# "b": [ # bids
|
|
# ["0.0024", "10"], # price, size
|
|
# ],
|
|
# "a": [ # asks
|
|
# ["0.0026", "100"], # price, size
|
|
# ]
|
|
# }
|
|
#
|
|
isSpot = self.is_spot_url(client)
|
|
marketType = 'spot' if (isSpot) else 'swap'
|
|
marketId = self.safe_string(message, 's')
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
symbol = market['symbol']
|
|
messageHash = 'orderbook::' + symbol
|
|
if not (symbol in self.orderbooks):
|
|
#
|
|
# https://github.com/ccxt/ccxt/issues/6672
|
|
#
|
|
# Sometimes Binance sends the first delta before the subscription
|
|
# confirmation arrives. At that point the orderbook is not
|
|
# initialized yet and the snapshot has not been requested yet
|
|
# therefore it is safe to drop these premature messages.
|
|
#
|
|
return
|
|
orderbook = self.orderbooks[symbol]
|
|
nonce = self.safe_integer(orderbook, 'nonce')
|
|
if nonce is None:
|
|
# 2. Buffer the events you receive from the stream.
|
|
orderbook.cache.append(message)
|
|
else:
|
|
try:
|
|
U = self.safe_integer(message, 'U')
|
|
u = self.safe_integer(message, 'u')
|
|
pu = self.safe_integer(message, 'pu')
|
|
if pu is None:
|
|
# spot
|
|
# 4. Drop any event where u is <= lastUpdateId in the snapshot
|
|
if u > orderbook['nonce']:
|
|
timestamp = self.safe_integer(orderbook, 'timestamp')
|
|
conditional = None
|
|
if timestamp is None:
|
|
# 5. The first processed event should have U <= lastUpdateId+1 AND u >= lastUpdateId+1
|
|
conditional = ((U - 1) <= orderbook['nonce']) and ((u - 1) >= orderbook['nonce'])
|
|
else:
|
|
# 6. While listening to the stream, each new event's U should be equal to the previous event's u+1.
|
|
conditional = ((U - 1) == orderbook['nonce'])
|
|
if conditional:
|
|
self.handle_order_book_message(client, message, orderbook)
|
|
if nonce < orderbook['nonce']:
|
|
client.resolve(orderbook, messageHash)
|
|
else:
|
|
checksum = self.handle_option('watchOrderBook', 'checksum', True)
|
|
if checksum:
|
|
# todo: client.reject from handleOrderBookMessage properly
|
|
raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol))
|
|
else:
|
|
# future
|
|
# 4. Drop any event where u is < lastUpdateId in the snapshot
|
|
if u >= orderbook['nonce']:
|
|
# 5. The first processed event should have U <= lastUpdateId AND u >= lastUpdateId
|
|
# 6. While listening to the stream, each new event's pu should be equal to the previous event's u, otherwise initialize the process from step 3
|
|
if (U <= orderbook['nonce']) or (pu == orderbook['nonce']):
|
|
self.handle_order_book_message(client, message, orderbook)
|
|
if nonce <= orderbook['nonce']:
|
|
client.resolve(orderbook, messageHash)
|
|
else:
|
|
checksum = self.handle_option('watchOrderBook', 'checksum', True)
|
|
if checksum:
|
|
# todo: client.reject from handleOrderBookMessage properly
|
|
raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol))
|
|
except Exception as e:
|
|
del self.orderbooks[symbol]
|
|
del client.subscriptions[messageHash]
|
|
client.reject(e, messageHash)
|
|
|
|
def handle_order_book_subscription(self, client: Client, message, subscription):
|
|
defaultLimit = self.safe_integer(self.options, 'watchOrderBookLimit', 1000)
|
|
# messageHash = self.safe_string(subscription, 'messageHash')
|
|
symbolOfSubscription = self.safe_string(subscription, 'symbol') # watchOrderBook
|
|
symbols = self.safe_value(subscription, 'symbols', [symbolOfSubscription]) # watchOrderBookForSymbols
|
|
limit = self.safe_integer(subscription, 'limit', defaultLimit)
|
|
# handle list of symbols
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
if symbol in self.orderbooks:
|
|
del self.orderbooks[symbol]
|
|
self.orderbooks[symbol] = self.order_book({}, limit)
|
|
subscription = self.extend(subscription, {'symbol': symbol})
|
|
# fetch the snapshot in a separate async call
|
|
self.spawn(self.fetch_order_book_snapshot, client, message, subscription)
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "result": null,
|
|
# "id": 1574649734450
|
|
# }
|
|
#
|
|
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)
|
|
isUnSubMessage = self.safe_bool(subscription, 'unsubscribe', False)
|
|
if isUnSubMessage:
|
|
self.handle_un_subscription(client, subscription)
|
|
return message
|
|
|
|
def handle_un_subscription(self, client: Client, subscription: dict):
|
|
messageHashes = self.safe_list(subscription, 'messageHashes', [])
|
|
subMessageHashes = self.safe_list(subscription, 'subMessageHashes', [])
|
|
for j in range(0, len(messageHashes)):
|
|
unsubHash = messageHashes[j]
|
|
subHash = subMessageHashes[j]
|
|
self.clean_unsubscription(client, subHash, unsubHash)
|
|
self.clean_cache(subscription)
|
|
|
|
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://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
|
|
: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
|
|
:param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade'
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False, True, True)
|
|
streamHash = 'multipleTrades'
|
|
if symbols is not None:
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength > 200:
|
|
raise BadRequest(self.id + ' watchTradesForSymbols() accepts 200 symbols at most. To watch more symbols call watchTradesForSymbols() multiple times')
|
|
streamHash += '::' + ','.join(symbols)
|
|
name = None
|
|
name, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'name', 'trade')
|
|
params = self.omit(params, 'callerMethodName')
|
|
firstMarket = self.market(symbols[0])
|
|
type = firstMarket['type']
|
|
if firstMarket['contract']:
|
|
type = 'future' if firstMarket['linear'] else 'delivery'
|
|
messageHashes = []
|
|
subParams = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
messageHashes.append('trade::' + symbol)
|
|
rawHash = market['lowercaseId'] + '@' + name
|
|
subParams.append(rawHash)
|
|
query = self.omit(params, 'type')
|
|
subParamsLength = len(subParams)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, subParamsLength)
|
|
requestId = self.request_id(url)
|
|
request: dict = {
|
|
'method': 'SUBSCRIBE',
|
|
'params': subParams,
|
|
'id': requestId,
|
|
}
|
|
subscribe: dict = {
|
|
'id': requestId,
|
|
}
|
|
trades = await self.watch_multiple(url, messageHashes, self.extend(request, query), messageHashes, subscribe)
|
|
if self.newUpdates:
|
|
first = self.safe_value(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
|
|
async def un_watch_trades_for_symbols(self, symbols: List[str], params={}) -> Any:
|
|
"""
|
|
unsubscribes from the trades channel
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade'
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False, True, True)
|
|
streamHash = 'multipleTrades'
|
|
if symbols is not None:
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength > 200:
|
|
raise BadRequest(self.id + ' watchTradesForSymbols() accepts 200 symbols at most. To watch more symbols call watchTradesForSymbols() multiple times')
|
|
streamHash += '::' + ','.join(symbols)
|
|
name = None
|
|
name, params = self.handle_option_and_params(params, 'watchTradesForSymbols', 'name', 'trade')
|
|
params = self.omit(params, 'callerMethodName')
|
|
firstMarket = self.market(symbols[0])
|
|
type = firstMarket['type']
|
|
if firstMarket['contract']:
|
|
type = 'future' if firstMarket['linear'] else 'delivery'
|
|
subMessageHashes = []
|
|
subParams = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
subMessageHashes.append('trade::' + symbol)
|
|
messageHashes.append('unsubscribe:trade:' + symbol)
|
|
rawHash = market['lowercaseId'] + '@' + name
|
|
subParams.append(rawHash)
|
|
query = self.omit(params, 'type')
|
|
subParamsLength = len(subParams)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, streamHash, subParamsLength)
|
|
requestId = self.request_id(url)
|
|
request: dict = {
|
|
'method': 'UNSUBSCRIBE',
|
|
'params': subParams,
|
|
'id': requestId,
|
|
}
|
|
subscription: dict = {
|
|
'unsubscribe': True,
|
|
'id': str(requestId),
|
|
'subMessageHashes': subMessageHashes,
|
|
'messageHashes': messageHashes,
|
|
'symbols': symbols,
|
|
'topic': 'trades',
|
|
}
|
|
return await self.watch_multiple(url, messageHashes, self.extend(request, query), messageHashes, subscription)
|
|
|
|
async def un_watch_trades(self, symbol: str, params={}) -> Any:
|
|
"""
|
|
unsubscribes from the trades channel
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade'
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
return await self.un_watch_trades_for_symbols([symbol], params)
|
|
|
|
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://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#aggregate-trades
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Aggregate-Trade-Streams
|
|
|
|
: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.name]: the name of the method to call, 'trade' or 'aggTrade', default is 'trade'
|
|
: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)
|
|
|
|
def parse_ws_trade(self, trade, market=None) -> Trade:
|
|
#
|
|
# public watchTrades
|
|
#
|
|
# {
|
|
# "e": "trade", # event type
|
|
# "E": 1579481530911, # event time
|
|
# "s": "ETHBTC", # symbol
|
|
# "t": 158410082, # trade id
|
|
# "p": "0.01914100", # price
|
|
# "q": "0.00700000", # quantity
|
|
# "b": 586187049, # buyer order id
|
|
# "a": 586186710, # seller order id
|
|
# "T": 1579481530910, # trade time
|
|
# "m": False, # is the buyer the market maker
|
|
# "M": True # binance docs say it should be ignored
|
|
# }
|
|
#
|
|
# {
|
|
# "e": "aggTrade", # Event type
|
|
# "E": 123456789, # Event time
|
|
# "s": "BNBBTC", # Symbol
|
|
# "a": 12345, # Aggregate trade ID
|
|
# "p": "0.001", # Price
|
|
# "q": "100", # Quantity
|
|
# "f": 100, # First trade ID
|
|
# "l": 105, # Last trade ID
|
|
# "T": 123456785, # Trade time
|
|
# "m": True, # Is the buyer the market maker?
|
|
# "M": True # Ignore
|
|
# }
|
|
#
|
|
# private watchMyTrades spot
|
|
#
|
|
# {
|
|
# "e": "executionReport",
|
|
# "E": 1611063861489,
|
|
# "s": "BNBUSDT",
|
|
# "c": "m4M6AD5MF3b1ERe65l4SPq",
|
|
# "S": "BUY",
|
|
# "o": "MARKET",
|
|
# "f": "GTC",
|
|
# "q": "2.00000000",
|
|
# "p": "0.00000000",
|
|
# "P": "0.00000000",
|
|
# "F": "0.00000000",
|
|
# "g": -1,
|
|
# "C": '',
|
|
# "x": "TRADE",
|
|
# "X": "PARTIALLY_FILLED",
|
|
# "r": "NONE",
|
|
# "i": 1296882607,
|
|
# "l": "0.33200000",
|
|
# "z": "0.33200000",
|
|
# "L": "46.86600000",
|
|
# "n": "0.00033200",
|
|
# "N": "BNB",
|
|
# "T": 1611063861488,
|
|
# "t": 109747654,
|
|
# "I": 2696953381,
|
|
# "w": False,
|
|
# "m": False,
|
|
# "M": True,
|
|
# "O": 1611063861488,
|
|
# "Z": "15.55951200",
|
|
# "Y": "15.55951200",
|
|
# "Q": "0.00000000"
|
|
# }
|
|
#
|
|
# private watchMyTrades future/delivery
|
|
#
|
|
# {
|
|
# "s": "BTCUSDT",
|
|
# "c": "pb2jD6ZQHpfzSdUac8VqMK",
|
|
# "S": "SELL",
|
|
# "o": "MARKET",
|
|
# "f": "GTC",
|
|
# "q": "0.001",
|
|
# "p": "0",
|
|
# "ap": "33468.46000",
|
|
# "sp": "0",
|
|
# "x": "TRADE",
|
|
# "X": "FILLED",
|
|
# "i": 13351197194,
|
|
# "l": "0.001",
|
|
# "z": "0.001",
|
|
# "L": "33468.46",
|
|
# "n": "0.00027086",
|
|
# "N": "BNB",
|
|
# "T": 1612095165362,
|
|
# "t": 458032604,
|
|
# "b": "0",
|
|
# "a": "0",
|
|
# "m": False,
|
|
# "R": False,
|
|
# "wt": "CONTRACT_PRICE",
|
|
# "ot": "MARKET",
|
|
# "ps": "BOTH",
|
|
# "cp": False,
|
|
# "rp": "0.00335000",
|
|
# "pP": False,
|
|
# "si": 0,
|
|
# "ss": 0
|
|
# }
|
|
#
|
|
executionType = self.safe_string(trade, 'x')
|
|
isTradeExecution = (executionType == 'TRADE')
|
|
if not isTradeExecution:
|
|
return self.parse_trade(trade, market)
|
|
id = self.safe_string_2(trade, 't', 'a')
|
|
timestamp = self.safe_integer(trade, 'T')
|
|
price = self.safe_string_2(trade, 'L', 'p')
|
|
amount = self.safe_string(trade, 'q')
|
|
if isTradeExecution:
|
|
amount = self.safe_string(trade, 'l', amount)
|
|
cost = self.safe_string(trade, 'Y')
|
|
if cost is None:
|
|
if (price is not None) and (amount is not None):
|
|
cost = Precise.string_mul(price, amount)
|
|
marketId = self.safe_string(trade, 's')
|
|
marketType = 'contract' if ('ps' in trade) else 'spot'
|
|
symbol = self.safe_symbol(marketId, None, None, marketType)
|
|
side = self.safe_string_lower(trade, 'S')
|
|
takerOrMaker = None
|
|
orderId = self.safe_string(trade, 'i')
|
|
if 'm' in trade:
|
|
if side is None:
|
|
side = 'sell' if trade['m'] else 'buy' # self is reversed intentionally
|
|
takerOrMaker = 'maker' if trade['m'] else 'taker'
|
|
fee = None
|
|
feeCost = self.safe_string(trade, 'n')
|
|
if feeCost is not None:
|
|
feeCurrencyId = self.safe_string(trade, 'N')
|
|
feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
|
|
fee = {
|
|
'cost': feeCost,
|
|
'currency': feeCurrencyCode,
|
|
}
|
|
type = self.safe_string_lower(trade, 'o')
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'order': orderId,
|
|
'type': type,
|
|
'takerOrMaker': takerOrMaker,
|
|
'side': side,
|
|
'price': price,
|
|
'amount': amount,
|
|
'cost': cost,
|
|
'fee': fee,
|
|
})
|
|
|
|
def handle_trade(self, client: Client, message):
|
|
# the trade streams push raw trade information in real-time
|
|
# each trade has a unique buyer and seller
|
|
isSpot = self.is_spot_url(client)
|
|
marketType = 'spot' if (isSpot) else 'contract'
|
|
marketId = self.safe_string(message, 's')
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
symbol = market['symbol']
|
|
messageHash = 'trade::' + symbol
|
|
trade = self.parse_ws_trade(message, 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, messageHash)
|
|
|
|
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://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
|
|
: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
|
|
: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)
|
|
symbol = market['symbol']
|
|
params['callerMethodName'] = 'watchOHLCV'
|
|
result = await self.watch_ohlcv_for_symbols([[symbol, timeframe]], since, limit, params)
|
|
return result[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://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
|
|
: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
|
|
: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()
|
|
klineType = None
|
|
klineType, params = self.handle_param_string_2(params, 'channel', 'name', 'kline')
|
|
symbols = self.get_list_from_object_values(symbolsAndTimeframes, 0)
|
|
marketSymbols = self.market_symbols(symbols, None, False, False, True)
|
|
firstMarket = self.market(marketSymbols[0])
|
|
type = firstMarket['type']
|
|
if firstMarket['contract']:
|
|
type = 'future' if firstMarket['linear'] else 'delivery'
|
|
isSpot = (type == 'spot')
|
|
timezone = None
|
|
timezone, params = self.handle_param_string(params, 'timezone', None)
|
|
isUtc8 = (timezone is not None) and ((timezone == '+08:00') or Precise.string_eq(timezone, '8'))
|
|
rawHashes = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbolsAndTimeframes)):
|
|
symAndTf = symbolsAndTimeframes[i]
|
|
symbolString = symAndTf[0]
|
|
timeframeString = symAndTf[1]
|
|
interval = self.safe_string(self.timeframes, timeframeString, timeframeString)
|
|
market = self.market(symbolString)
|
|
marketId = market['lowercaseId']
|
|
if klineType == 'indexPriceKline':
|
|
# weird behavior for index price kline we can't use the perp suffix
|
|
marketId = marketId.replace('_perp', '')
|
|
shouldUseUTC8 = (isUtc8 and isSpot)
|
|
suffix = '@+08:00'
|
|
utcSuffix = suffix if shouldUseUTC8 else ''
|
|
rawHashes.append(marketId + '@' + klineType + '_' + interval + utcSuffix)
|
|
messageHashes.append('ohlcv::' + market['symbol'] + '::' + timeframeString)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, 'multipleOHLCV')
|
|
requestId = self.request_id(url)
|
|
request = {
|
|
'method': 'SUBSCRIBE',
|
|
'params': rawHashes,
|
|
'id': requestId,
|
|
}
|
|
subscribe = {
|
|
'id': requestId,
|
|
}
|
|
params = self.omit(params, 'callerMethodName')
|
|
res = await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscribe)
|
|
symbol, timeframe, candles = res
|
|
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)
|
|
|
|
async def un_watch_ohlcv_for_symbols(self, symbolsAndTimeframes: List[List[str]], params={}) -> Any:
|
|
"""
|
|
unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
|
|
:param str[][] symbolsAndTimeframes: array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]
|
|
: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()
|
|
klineType = None
|
|
klineType, params = self.handle_param_string_2(params, 'channel', 'name', 'kline')
|
|
symbols = self.get_list_from_object_values(symbolsAndTimeframes, 0)
|
|
marketSymbols = self.market_symbols(symbols, None, False, False, True)
|
|
firstMarket = self.market(marketSymbols[0])
|
|
type = firstMarket['type']
|
|
if firstMarket['contract']:
|
|
type = 'future' if firstMarket['linear'] else 'delivery'
|
|
isSpot = (type == 'spot')
|
|
timezone = None
|
|
timezone, params = self.handle_param_string(params, 'timezone', None)
|
|
isUtc8 = (timezone is not None) and ((timezone == '+08:00') or Precise.string_eq(timezone, '8'))
|
|
rawHashes = []
|
|
subMessageHashes = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbolsAndTimeframes)):
|
|
symAndTf = symbolsAndTimeframes[i]
|
|
symbolString = symAndTf[0]
|
|
timeframeString = symAndTf[1]
|
|
interval = self.safe_string(self.timeframes, timeframeString, timeframeString)
|
|
market = self.market(symbolString)
|
|
marketId = market['lowercaseId']
|
|
if klineType == 'indexPriceKline':
|
|
# weird behavior for index price kline we can't use the perp suffix
|
|
marketId = marketId.replace('_perp', '')
|
|
shouldUseUTC8 = (isUtc8 and isSpot)
|
|
suffix = '@+08:00'
|
|
utcSuffix = suffix if shouldUseUTC8 else ''
|
|
rawHashes.append(marketId + '@' + klineType + '_' + interval + utcSuffix)
|
|
subMessageHashes.append('ohlcv::' + market['symbol'] + '::' + timeframeString)
|
|
messageHashes.append('unsubscribe::ohlcv::' + market['symbol'] + '::' + timeframeString)
|
|
url = self.urls['api']['ws'][type] + '/' + self.stream(type, 'multipleOHLCV')
|
|
requestId = self.request_id(url)
|
|
request = {
|
|
'method': 'UNSUBSCRIBE',
|
|
'params': rawHashes,
|
|
'id': requestId,
|
|
}
|
|
subscribe = {
|
|
'unsubscribe': True,
|
|
'id': str(requestId),
|
|
'symbols': symbols,
|
|
'symbolsAndTimeframes': symbolsAndTimeframes,
|
|
'subMessageHashes': subMessageHashes,
|
|
'messageHashes': messageHashes,
|
|
'topic': 'ohlcv',
|
|
}
|
|
params = self.omit(params, 'callerMethodName')
|
|
return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscribe)
|
|
|
|
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://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Kline-Candlestick-Streams
|
|
|
|
: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()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
params['callerMethodName'] = 'watchOHLCV'
|
|
return await self.un_watch_ohlcv_for_symbols([[symbol, timeframe]], params)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "e": "kline",
|
|
# "E": 1579482921215,
|
|
# "s": "ETHBTC",
|
|
# "k": {
|
|
# "t": 1579482900000,
|
|
# "T": 1579482959999,
|
|
# "s": "ETHBTC",
|
|
# "i": "1m",
|
|
# "f": 158411535,
|
|
# "L": 158411550,
|
|
# "o": "0.01913200",
|
|
# "c": "0.01913500",
|
|
# "h": "0.01913700",
|
|
# "l": "0.01913200",
|
|
# "v": "5.08400000",
|
|
# "n": 16,
|
|
# "x": False,
|
|
# "q": "0.09728060",
|
|
# "V": "3.30200000",
|
|
# "Q": "0.06318500",
|
|
# "B": "0"
|
|
# }
|
|
# }
|
|
#
|
|
event = self.safe_string(message, 'e')
|
|
eventMap: dict = {
|
|
'indexPrice_kline': 'indexPriceKline',
|
|
'markPrice_kline': 'markPriceKline',
|
|
}
|
|
event = self.safe_string(eventMap, event, event)
|
|
kline = self.safe_value(message, 'k')
|
|
marketId = self.safe_string_2(kline, 's', 'ps')
|
|
if event == 'indexPriceKline':
|
|
# indexPriceKline doesn't have the _PERP suffix
|
|
marketId = self.safe_string(message, 'ps')
|
|
interval = self.safe_string(kline, 'i')
|
|
# use a reverse lookup in a static map instead
|
|
unifiedTimeframe = self.find_timeframe(interval)
|
|
parsed = [
|
|
self.safe_integer(kline, 't'),
|
|
self.safe_float(kline, 'o'),
|
|
self.safe_float(kline, 'h'),
|
|
self.safe_float(kline, 'l'),
|
|
self.safe_float(kline, 'c'),
|
|
self.safe_float(kline, 'v'),
|
|
]
|
|
isSpot = self.is_spot_url(client)
|
|
marketType = 'spot' if (isSpot) else 'contract'
|
|
symbol = self.safe_symbol(marketId, None, None, marketType)
|
|
messageHash = 'ohlcv::' + symbol + '::' + unifiedTimeframe
|
|
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
|
stored = self.safe_value(self.ohlcvs[symbol], unifiedTimeframe)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
|
stored = ArrayCacheByTimestamp(limit)
|
|
self.ohlcvs[symbol][unifiedTimeframe] = stored
|
|
stored.append(parsed)
|
|
resolveData = [symbol, unifiedTimeframe, stored]
|
|
client.resolve(resolveData, messageHash)
|
|
|
|
async def fetch_ticker_ws(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches 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
|
|
:param str [params.method]: method to use can be ticker.price or ticker.book
|
|
:param boolean [params.returnRateLimits]: return the rate limits for the exchange
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
payload: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
type = self.get_market_type('fetchTickerWs', market, params)
|
|
if type != 'future':
|
|
raise BadRequest(self.id + ' fetchTickerWs only supports swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
subscription: dict = {
|
|
'method': self.handle_ticker_ws,
|
|
}
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchTickerWs', 'returnRateLimits', False)
|
|
payload['returnRateLimits'] = returnRateLimits
|
|
params = self.omit(params, 'test')
|
|
method = None
|
|
method, params = self.handle_option_and_params(params, 'fetchTickerWs', 'method', 'ticker.book')
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': method,
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
ticker = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
return ticker
|
|
|
|
async def fetch_ohlcv_ws(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
query historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#klines
|
|
|
|
:param str symbol: unified symbol of the market to query 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
|
|
:param int params['until']: timestamp in ms of the earliest candle to fetch
|
|
|
|
EXCHANGE SPECIFIC PARAMETERS
|
|
:param str params['timeZone']: default=0(UTC)
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
marketType = self.get_market_type('fetchOHLCVWs', market, params)
|
|
if marketType != 'spot' and marketType != 'future':
|
|
raise BadRequest(self.id + ' fetchOHLCVWs only supports spot or swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][marketType]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchOHLCVWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
'interval': self.timeframes[timeframe],
|
|
}
|
|
until = self.safe_integer(params, 'until')
|
|
params = self.omit(params, 'until')
|
|
if since is not None:
|
|
payload['startTime'] = since
|
|
if limit is not None:
|
|
payload['limit'] = limit
|
|
if until is not None:
|
|
payload['endTime'] = until
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'klines',
|
|
'params': self.extend(payload, params),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_fetch_ohlcv,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
def handle_fetch_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "id": "1dbbeb56-8eea-466a-8f6e-86bdcfa2fc0b",
|
|
# "status": 200,
|
|
# "result": [
|
|
# [
|
|
# 1655971200000, # Kline open time
|
|
# "0.01086000", # Open price
|
|
# "0.01086600", # High price
|
|
# "0.01083600", # Low price
|
|
# "0.01083800", # Close price
|
|
# "2290.53800000", # Volume
|
|
# 1655974799999, # Kline close time
|
|
# "24.85074442", # Quote asset volume
|
|
# 2283, # Number of trades
|
|
# "1171.64000000", # Taker buy base asset volume
|
|
# "12.71225884", # Taker buy quote asset volume
|
|
# "0" # Unused field, ignore
|
|
# ]
|
|
# ],
|
|
# "rateLimits": [
|
|
# {
|
|
# "rateLimitType": "REQUEST_WEIGHT",
|
|
# "interval": "MINUTE",
|
|
# "intervalNum": 1,
|
|
# "limit": 6000,
|
|
# "count": 2
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
result = self.safe_list(message, 'result')
|
|
parsed = self.parse_ohlcvs(result)
|
|
# use a reverse lookup in a static map instead
|
|
messageHash = self.safe_string(message, 'id')
|
|
client.resolve(parsed, messageHash)
|
|
|
|
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
|
|
: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.name]: stream to use can be ticker or miniTicker
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
tickers = await self.watch_tickers([symbol], self.extend(params, {'callerMethodName': 'watchTicker'}))
|
|
return tickers[symbol]
|
|
|
|
async def watch_mark_price(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
watches a mark price for a specific market
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream
|
|
|
|
: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 boolean [params.use1sFreq]: *default is True* if set to True, the mark price will be updated every second, otherwise every 3 seconds
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
tickers = await self.watch_mark_prices([symbol], self.extend(params, {'callerMethodName': 'watchMarkPrice'}))
|
|
return tickers[symbol]
|
|
|
|
async def watch_mark_prices(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
watches the mark price for all markets
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream-for-All-market
|
|
|
|
: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 boolean [params.use1sFreq]: *default is True* if set to True, the mark price will be updated every second, otherwise every 3 seconds
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
channelName = None
|
|
# for now watchmarkPrice uses the same messageHash
|
|
# so it's impossible to watch both at the same time
|
|
# refactor self to use different messageHashes
|
|
channelName, params = self.handle_option_and_params(params, 'watchMarkPrices', 'name', 'markPrice')
|
|
newTickers = await self.watch_multi_ticker_helper('watchMarkPrices', channelName, symbols, params)
|
|
if self.newUpdates:
|
|
return newTickers
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
|
|
: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>`
|
|
"""
|
|
channelName = None
|
|
channelName, params = self.handle_option_and_params(params, 'watchTickers', 'name', 'ticker')
|
|
if channelName == 'bookTicker':
|
|
raise BadRequest(self.id + ' deprecation notice - to subscribe for bids-asks, use watch_bids_asks() method instead')
|
|
newTickers = await self.watch_multi_ticker_helper('watchTickers', channelName, symbols, params)
|
|
if self.newUpdates:
|
|
return newTickers
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
async def un_watch_tickers(self, symbols: Strings = None, 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://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
|
|
: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>`
|
|
"""
|
|
channelName = None
|
|
channelName, params = self.handle_option_and_params(params, 'watchTickers', 'name', 'ticker')
|
|
if channelName == 'bookTicker':
|
|
raise BadRequest(self.id + ' deprecation notice - to subscribe for bids-asks, use watch_bids_asks() method instead')
|
|
return await self.watch_multi_ticker_helper('unWatchTickers', channelName, symbols, params, True)
|
|
|
|
async def un_watch_mark_prices(self, symbols: Strings = None, 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://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream
|
|
|
|
: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>`
|
|
"""
|
|
channelName = None
|
|
channelName, params = self.handle_option_and_params(params, 'watchMarkPrices', 'name', 'markPrice')
|
|
await self.load_markets()
|
|
return await self.watch_multi_ticker_helper('unWatchMarkPrices', channelName, symbols, params, True)
|
|
|
|
async def un_watch_mark_price(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://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Mark-Price-Stream
|
|
|
|
: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>`
|
|
"""
|
|
return await self.un_watch_mark_prices([symbol], params)
|
|
|
|
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 all markets of a specific list
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream
|
|
https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#all-market-mini-tickers-stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Market-Mini-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/Individual-Symbol-Ticker-Streams
|
|
|
|
: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>`
|
|
"""
|
|
return await self.un_watch_tickers([symbol], params)
|
|
|
|
async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
watches best bid & ask for symbols
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#symbol-order-book-ticker
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/websocket-market-streams/All-Book-Tickers-Stream
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/websocket-market-streams/All-Book-Tickers-Stream
|
|
|
|
: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, True, False, True)
|
|
result = await self.watch_multi_ticker_helper('watchBidsAsks', 'bookTicker', symbols, params)
|
|
if self.newUpdates:
|
|
return result
|
|
return self.filter_by_array(self.bidsasks, 'symbol', symbols)
|
|
|
|
async def watch_multi_ticker_helper(self, methodName, channelName: str, symbols: Strings = None, params={}, isUnsubscribe: bool = False):
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, True, False, True)
|
|
isBidAsk = (channelName == 'bookTicker')
|
|
isMarkPrice = (channelName == 'markPrice')
|
|
use1sFreq = self.safe_bool(params, 'use1sFreq', True)
|
|
firstMarket = None
|
|
marketType = None
|
|
symbolsDefined = (symbols is not None)
|
|
if symbolsDefined:
|
|
firstMarket = self.market(symbols[0])
|
|
defaultMarket = 'swap' if (isMarkPrice) else None
|
|
marketType, params = self.handle_market_type_and_params(methodName, firstMarket, params, defaultMarket)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params(methodName, firstMarket, params)
|
|
rawMarketType = None
|
|
if self.isLinear(marketType, subType):
|
|
rawMarketType = 'future'
|
|
elif self.isInverse(marketType, subType):
|
|
rawMarketType = 'delivery'
|
|
elif marketType == 'spot':
|
|
rawMarketType = marketType
|
|
else:
|
|
raise NotSupported(self.id + ' ' + methodName + '() does not support options markets')
|
|
if isMarkPrice and not self.in_array(marketType, ['swap', 'future']):
|
|
raise NotSupported(self.id + ' ' + methodName + '() does not support ' + marketType + ' markets yet')
|
|
subscriptionArgs = []
|
|
messageHashes = []
|
|
unsubscribeMessageHashes = []
|
|
suffix = ''
|
|
if isMarkPrice:
|
|
suffix = '@1s' if (use1sFreq) else ''
|
|
unifiedPrefix: Str = None
|
|
if isBidAsk:
|
|
unifiedPrefix = 'bidask'
|
|
elif isMarkPrice:
|
|
unifiedPrefix = 'markPrice'
|
|
else:
|
|
unifiedPrefix = 'ticker'
|
|
if symbolsDefined:
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
subscriptionArgs.append(market['lowercaseId'] + '@' + channelName + suffix)
|
|
messageHashes.append(unifiedPrefix + ':' + channelName + '@' + symbol)
|
|
if isUnsubscribe:
|
|
unsubscribeMessageHashes.append('unsubscribe::' + unifiedPrefix + ':' + channelName + '@' + symbol)
|
|
else:
|
|
if isBidAsk:
|
|
if marketType == 'spot':
|
|
raise ArgumentsRequired(self.id + ' ' + methodName + '() requires symbols for self channel for spot markets')
|
|
subscriptionArgs.append('!' + channelName)
|
|
elif isMarkPrice:
|
|
subscriptionArgs.append('!' + channelName + '@arr' + suffix)
|
|
else:
|
|
subscriptionArgs.append('!' + channelName + '@arr')
|
|
messageHashes.append(unifiedPrefix + 's:' + channelName)
|
|
unsubscribeMessageHashes.append('unsubscribe::' + channelName)
|
|
streamHash = channelName
|
|
if symbolsDefined:
|
|
streamHash = channelName + '::' + ','.join(symbols)
|
|
url = self.urls['api']['ws'][rawMarketType] + '/' + self.stream(rawMarketType, streamHash)
|
|
requestId = self.request_id(url)
|
|
request: dict = {
|
|
'method': 'UNSUBSCRIBE' if isUnsubscribe else 'SUBSCRIBE',
|
|
'params': subscriptionArgs,
|
|
'id': requestId,
|
|
}
|
|
hashes = messageHashes
|
|
subscription: dict = {
|
|
'id': requestId,
|
|
}
|
|
if isUnsubscribe:
|
|
subscription = {
|
|
'unsubscribe': True,
|
|
'id': str(requestId),
|
|
'subMessageHashes': messageHashes,
|
|
'messageHashes': unsubscribeMessageHashes,
|
|
'symbols': symbols,
|
|
'topic': 'ticker',
|
|
}
|
|
hashes = unsubscribeMessageHashes
|
|
result = await self.watch_multiple(url, hashes, self.deep_extend(request, params), hashes, subscription)
|
|
if isUnsubscribe:
|
|
return result
|
|
# for efficiency, we have two type of returned structure here - if symbols array was provided, then individual
|
|
# ticker dict comes in, otherwise all-tickers dict comes in
|
|
if not symbolsDefined:
|
|
return result
|
|
else:
|
|
newDict: dict = {}
|
|
newDict[result['symbol']] = result
|
|
return newDict
|
|
|
|
def parse_ws_ticker(self, message, marketType):
|
|
# markPrice
|
|
# {
|
|
# "e": "markPriceUpdate", # Event type
|
|
# "E": 1562305380000, # Event time
|
|
# "s": "BTCUSDT", # Symbol
|
|
# "p": "11794.15000000", # Mark price
|
|
# "i": "11784.62659091", # Index price
|
|
# "P": "11784.25641265", # Estimated Settle Price, only useful in the last hour before the settlement starts
|
|
# "r": "0.00038167", # Funding rate
|
|
# "T": 1562306400000 # Next funding time
|
|
# }
|
|
#
|
|
# ticker
|
|
# {
|
|
# "e": "24hrTicker", # event type
|
|
# "E": 1579485598569, # event time
|
|
# "s": "ETHBTC", # symbol
|
|
# "p": "-0.00004000", # price change
|
|
# "P": "-0.209", # price change percent
|
|
# "w": "0.01920495", # weighted average price
|
|
# "x": "0.01916500", # the price of the first trade before the 24hr rolling window
|
|
# "c": "0.01912500", # last(closing) price
|
|
# "Q": "0.10400000", # last quantity
|
|
# "b": "0.01912200", # best bid
|
|
# "B": "4.10400000", # best bid quantity
|
|
# "a": "0.01912500", # best ask
|
|
# "A": "0.00100000", # best ask quantity
|
|
# "o": "0.01916500", # open price
|
|
# "h": "0.01956500", # high price
|
|
# "l": "0.01887700", # low price
|
|
# "v": "173518.11900000", # base volume
|
|
# "q": "3332.40703994", # quote volume
|
|
# "O": 1579399197842, # open time
|
|
# "C": 1579485597842, # close time
|
|
# "F": 158251292, # first trade id
|
|
# "L": 158414513, # last trade id
|
|
# "n": 163222, # total number of trades
|
|
# }
|
|
#
|
|
# miniTicker
|
|
# {
|
|
# "e": "24hrMiniTicker",
|
|
# "E": 1671617114585,
|
|
# "s": "MOBBUSD",
|
|
# "c": "0.95900000",
|
|
# "o": "0.91200000",
|
|
# "h": "1.04000000",
|
|
# "l": "0.89400000",
|
|
# "v": "2109995.32000000",
|
|
# "q": "2019254.05788000"
|
|
# }
|
|
# fetchTickerWs
|
|
# {
|
|
# "symbol":"BTCUSDT",
|
|
# "price":"72606.70",
|
|
# "time":1712526204284
|
|
# }
|
|
# fetchTickerWs - ticker.book
|
|
# {
|
|
# "lastUpdateId":1027024,
|
|
# "symbol":"BTCUSDT",
|
|
# "bidPrice":"4.00000000",
|
|
# "bidQty":"431.00000000",
|
|
# "askPrice":"4.00000200",
|
|
# "askQty":"9.00000000",
|
|
# "time":1589437530011,
|
|
# }
|
|
#
|
|
marketId = self.safe_string_2(message, 's', 'symbol')
|
|
symbol = self.safe_symbol(marketId, None, None, marketType)
|
|
event = self.safe_string(message, 'e', 'bookTicker')
|
|
if event == '24hrTicker':
|
|
event = 'ticker'
|
|
if event == 'markPriceUpdate':
|
|
# handle self separately because some fields clash with the ticker fields
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': self.safe_integer(message, 'E'),
|
|
'datetime': self.iso8601(self.safe_integer(message, 'E')),
|
|
'info': message,
|
|
'markPrice': self.safe_string(message, 'p'),
|
|
'indexPrice': self.safe_string(message, 'i'),
|
|
})
|
|
timestamp = None
|
|
if event == 'bookTicker':
|
|
# take the event timestamp, if available, for spot tickers it is not
|
|
timestamp = self.safe_integer_2(message, 'E', 'time')
|
|
else:
|
|
# take the timestamp of the closing price for candlestick streams
|
|
timestamp = self.safe_integer_n(message, ['C', 'E', 'time'])
|
|
market = self.safe_market(marketId, None, None, marketType)
|
|
last = self.safe_string_2(message, 'c', 'price')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': self.safe_string(message, 'h'),
|
|
'low': self.safe_string(message, 'l'),
|
|
'bid': self.safe_string_2(message, 'b', 'bidPrice'),
|
|
'bidVolume': self.safe_string_2(message, 'B', 'bidQty'),
|
|
'ask': self.safe_string_2(message, 'a', 'askPrice'),
|
|
'askVolume': self.safe_string_2(message, 'A', 'askQty'),
|
|
'vwap': self.safe_string(message, 'w'),
|
|
'open': self.safe_string(message, 'o'),
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': self.safe_string(message, 'x'), # previous day close
|
|
'change': self.safe_string(message, 'p'),
|
|
'percentage': self.safe_string(message, 'P'),
|
|
'average': None,
|
|
'baseVolume': self.safe_string(message, 'v'),
|
|
'quoteVolume': self.safe_string(message, 'q'),
|
|
'info': message,
|
|
}, market)
|
|
|
|
def handle_ticker_ws(self, client: Client, message):
|
|
#
|
|
# ticker.price
|
|
# {
|
|
# "id":"1",
|
|
# "status":200,
|
|
# "result":{
|
|
# "symbol":"BTCUSDT",
|
|
# "price":"73178.50",
|
|
# "time":1712527052374
|
|
# }
|
|
# }
|
|
# ticker.book
|
|
# {
|
|
# "id":"9d32157c-a556-4d27-9866-66760a174b57",
|
|
# "status":200,
|
|
# "result":{
|
|
# "lastUpdateId":1027024,
|
|
# "symbol":"BTCUSDT",
|
|
# "bidPrice":"4.00000000",
|
|
# "bidQty":"431.00000000",
|
|
# "askPrice":"4.00000200",
|
|
# "askQty":"9.00000000",
|
|
# "time":1589437530011 # Transaction time
|
|
# }
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_value(message, 'result', {})
|
|
ticker = self.parse_ws_ticker(result, 'future')
|
|
client.resolve(ticker, messageHash)
|
|
|
|
def handle_bids_asks(self, client: Client, message):
|
|
#
|
|
# arrives one symbol dict or array of symbol dicts
|
|
#
|
|
# {
|
|
# "u": 7488717758,
|
|
# "s": "BTCUSDT",
|
|
# "b": "28621.74000000",
|
|
# "B": "1.43278800",
|
|
# "a": "28621.75000000",
|
|
# "A": "2.52500800"
|
|
# }
|
|
#
|
|
self.handle_tickers_and_bids_asks(client, message, 'bidasks')
|
|
|
|
def handle_tickers(self, client: Client, message):
|
|
#
|
|
# arrives one symbol dict or array of symbol dicts
|
|
#
|
|
# {
|
|
# "e": "24hrTicker", # event type
|
|
# "E": 1579485598569, # event time
|
|
# "s": "ETHBTC", # symbol
|
|
# "p": "-0.00004000", # price change
|
|
# "P": "-0.209", # price change percent
|
|
# "w": "0.01920495", # weighted average price
|
|
# "x": "0.01916500", # the price of the first trade before the 24hr rolling window
|
|
# "c": "0.01912500", # last(closing) price
|
|
# "Q": "0.10400000", # last quantity
|
|
# "b": "0.01912200", # best bid
|
|
# "B": "4.10400000", # best bid quantity
|
|
# "a": "0.01912500", # best ask
|
|
# "A": "0.00100000", # best ask quantity
|
|
# "o": "0.01916500", # open price
|
|
# "h": "0.01956500", # high price
|
|
# "l": "0.01887700", # low price
|
|
# "v": "173518.11900000", # base volume
|
|
# "q": "3332.40703994", # quote volume
|
|
# "O": 1579399197842, # open time
|
|
# "C": 1579485597842, # close time
|
|
# "F": 158251292, # first trade id
|
|
# "L": 158414513, # last trade id
|
|
# "n": 163222, # total number of trades
|
|
# }
|
|
#
|
|
self.handle_tickers_and_bids_asks(client, message, 'tickers')
|
|
|
|
def handle_mark_prices(self, client: Client, message):
|
|
self.handle_tickers_and_bids_asks(client, message, 'markPrices')
|
|
|
|
def handle_tickers_and_bids_asks(self, client: Client, message, methodType):
|
|
isSpot = self.is_spot_url(client)
|
|
marketType = 'spot' if (isSpot) else 'contract'
|
|
isBidAsk = (methodType == 'bidasks')
|
|
isMarkPrice = (methodType == 'markPrices')
|
|
unifiedPrefix: Str = None
|
|
if isBidAsk:
|
|
unifiedPrefix = 'bidask'
|
|
elif isMarkPrice:
|
|
unifiedPrefix = 'markPrice'
|
|
else:
|
|
unifiedPrefix = 'ticker'
|
|
channelName = None
|
|
resolvedMessageHashes = []
|
|
rawTickers = []
|
|
newTickers: dict = {}
|
|
if isinstance(message, list):
|
|
rawTickers = message
|
|
else:
|
|
rawTickers.append(message)
|
|
for i in range(0, len(rawTickers)):
|
|
ticker = rawTickers[i]
|
|
event = self.safe_string(ticker, 'e')
|
|
if isBidAsk:
|
|
event = 'bookTicker' # in `handleMessage`, bookTicker doesn't have identifier, so manually set here
|
|
channelName = self.safe_string(self.options['tickerChannelsMap'], event, event)
|
|
if channelName is None:
|
|
continue
|
|
parsedTicker = self.parse_ws_ticker(ticker, marketType)
|
|
symbol = parsedTicker['symbol']
|
|
newTickers[symbol] = parsedTicker
|
|
if isBidAsk:
|
|
self.bidsasks[symbol] = parsedTicker
|
|
else:
|
|
self.tickers[symbol] = parsedTicker
|
|
messageHash = unifiedPrefix + ':' + channelName + '@' + symbol
|
|
resolvedMessageHashes.append(messageHash)
|
|
client.resolve(parsedTicker, messageHash)
|
|
# resolve batch endpoint
|
|
length = len(resolvedMessageHashes)
|
|
if length > 0:
|
|
batchMessageHash = unifiedPrefix + 's:' + channelName
|
|
client.resolve(newTickers, batchMessageHash)
|
|
|
|
def sign_params(self, params={}):
|
|
self.check_required_credentials()
|
|
defaultRecvWindow = self.safe_integer(self.options, 'recvWindow')
|
|
if defaultRecvWindow is not None:
|
|
params['recvWindow'] = defaultRecvWindow
|
|
recvWindow = self.safe_integer(params, 'recvWindow')
|
|
if recvWindow is not None:
|
|
params['recvWindow'] = recvWindow
|
|
extendedParams = self.extend({
|
|
'timestamp': self.nonce(),
|
|
'apiKey': self.apiKey,
|
|
}, params)
|
|
extendedParams = self.keysort(extendedParams)
|
|
query = self.rawencode(extendedParams)
|
|
signature = None
|
|
if self.secret.find('PRIVATE KEY') > -1:
|
|
if len(self.secret) > 120:
|
|
signature = self.rsa(query, self.secret, 'sha256')
|
|
else:
|
|
signature = self.eddsa(self.encode(query), self.secret, 'ed25519')
|
|
else:
|
|
signature = self.hmac(self.encode(query), self.encode(self.secret), hashlib.sha256)
|
|
extendedParams['signature'] = signature
|
|
return extendedParams
|
|
|
|
async def ensure_user_data_stream_ws_subscribe_signature(self, marketType: str = 'spot'):
|
|
"""
|
|
watches best bid & ask for symbols
|
|
@param marketType {string} only support on 'spot'
|
|
|
|
{@link https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/user-data-stream-requests#subscribe-to-user-data-stream-through-signature-subscription-user_data Binance User Data Stream Documentation}
|
|
|
|
:returns: Promise<number> The subscription ID for the user data stream
|
|
"""
|
|
url = self.urls['api']['ws']['ws-api'][marketType]
|
|
client = self.client(url)
|
|
subscriptions = client.subscriptions
|
|
subscriptionsKeys = list(subscriptions.keys())
|
|
accountType = self.get_account_type_from_subscriptions(subscriptionsKeys)
|
|
if accountType == marketType:
|
|
return
|
|
client.subscriptions[marketType] = True
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'userDataStream.subscribe.signature',
|
|
'params': self.sign_params({}),
|
|
}
|
|
subscription: dict = {
|
|
'id': messageHash,
|
|
'method': self.handle_user_data_stream_subscribe,
|
|
'subscription': marketType,
|
|
}
|
|
await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
def handle_user_data_stream_subscribe(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "id": 1,
|
|
# "status": 200,
|
|
# "result": {
|
|
# "subscriptionId": 0
|
|
# }
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
subscriptions = client.subscriptions
|
|
subscriptionsKeys = list(subscriptions.keys())
|
|
accountType = self.get_account_type_from_subscriptions(subscriptionsKeys)
|
|
result = self.safe_dict(message, 'result', {})
|
|
subscriptionId = self.safe_integer(result, 'subscriptionId')
|
|
if subscriptionId is None:
|
|
del client.subscriptions[accountType]
|
|
client.reject(message, accountType)
|
|
client.resolve(message, messageHash)
|
|
|
|
async def authenticate(self, params={}):
|
|
time = self.milliseconds()
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('authenticate', None, params)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('authenticate', None, params)
|
|
isPortfolioMargin = None
|
|
isPortfolioMargin, params = self.handle_option_and_params_2(params, 'authenticate', 'papi', 'portfolioMargin', False)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
# For spot use WebSocket API signature subscription
|
|
if type == 'spot':
|
|
await self.ensure_user_data_stream_ws_subscribe_signature('spot')
|
|
return
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('authenticate', params)
|
|
isIsolatedMargin = (marginMode == 'isolated')
|
|
isCrossMargin = (marginMode == 'cross') or (marginMode is None)
|
|
symbol = self.safe_string(params, 'symbol')
|
|
params = self.omit(params, 'symbol')
|
|
options = self.safe_value(self.options, type, {})
|
|
lastAuthenticatedTime = self.safe_integer(options, 'lastAuthenticatedTime', 0)
|
|
listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000)
|
|
delay = self.sum(listenKeyRefreshRate, 10000)
|
|
if time - lastAuthenticatedTime > delay:
|
|
response = None
|
|
if isPortfolioMargin:
|
|
response = await self.papiPostListenKey(params)
|
|
params = self.extend(params, {'portfolioMargin': True})
|
|
elif type == 'future':
|
|
response = await self.fapiPrivatePostListenKey(params)
|
|
elif type == 'delivery':
|
|
response = await self.dapiPrivatePostListenKey(params)
|
|
elif type == 'margin' and isCrossMargin:
|
|
response = await self.sapiPostUserDataStream(params)
|
|
elif isIsolatedMargin:
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' authenticate() requires a symbol argument for isolated margin mode')
|
|
marketId = self.market_id(symbol)
|
|
params = self.extend(params, {'symbol': marketId})
|
|
response = await self.sapiPostUserDataStreamIsolated(params)
|
|
else:
|
|
response = await self.publicPostUserDataStream(params)
|
|
self.options[type] = self.extend(options, {
|
|
'listenKey': self.safe_string(response, 'listenKey'),
|
|
'lastAuthenticatedTime': time,
|
|
})
|
|
self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params)
|
|
|
|
async def keep_alive_listen_key(self, params={}):
|
|
# https://binance-docs.github.io/apidocs/spot/en/#listen-key-spot
|
|
type = self.safe_string_2(self.options, 'defaultType', 'authenticate', 'spot')
|
|
type = self.safe_string(params, 'type', type)
|
|
isPortfolioMargin = None
|
|
isPortfolioMargin, params = self.handle_option_and_params_2(params, 'keepAliveListenKey', 'papi', 'portfolioMargin', False)
|
|
subTypeInfo = self.handle_sub_type_and_params('keepAliveListenKey', None, params)
|
|
subType = subTypeInfo[0]
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
options = self.safe_value(self.options, type, {})
|
|
listenKey = self.safe_string(options, 'listenKey')
|
|
if listenKey is None:
|
|
# A network error happened: we can't renew a listen key that does not exist.
|
|
return
|
|
request: dict = {}
|
|
symbol = self.safe_string(params, 'symbol')
|
|
params = self.omit(params, ['type', 'symbol'])
|
|
time = self.milliseconds()
|
|
try:
|
|
if isPortfolioMargin:
|
|
await self.papiPutListenKey(self.extend(request, params))
|
|
params = self.extend(params, {'portfolioMargin': True})
|
|
elif type == 'future':
|
|
await self.fapiPrivatePutListenKey(self.extend(request, params))
|
|
elif type == 'delivery':
|
|
await self.dapiPrivatePutListenKey(self.extend(request, params))
|
|
else:
|
|
request['listenKey'] = listenKey
|
|
if type == 'margin':
|
|
request['symbol'] = symbol
|
|
await self.sapiPutUserDataStream(self.extend(request, params))
|
|
else:
|
|
await self.publicPutUserDataStream(self.extend(request, params))
|
|
except Exception as error:
|
|
urlType = type
|
|
if isPortfolioMargin:
|
|
urlType = 'papi'
|
|
url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey']
|
|
client = self.client(url)
|
|
messageHashes = list(client.futures.keys())
|
|
for i in range(0, len(messageHashes)):
|
|
messageHash = messageHashes[i]
|
|
client.reject(error, messageHash)
|
|
self.options[type] = self.extend(options, {
|
|
'listenKey': None,
|
|
'lastAuthenticatedTime': 0,
|
|
})
|
|
return
|
|
self.options[type] = self.extend(options, {
|
|
'listenKey': listenKey,
|
|
'lastAuthenticatedTime': time,
|
|
})
|
|
# whether or not to schedule another listenKey keepAlive request
|
|
clients = list(self.clients.values())
|
|
listenKeyRefreshRate = self.safe_integer(self.options, 'listenKeyRefreshRate', 1200000)
|
|
for i in range(0, len(clients)):
|
|
client = clients[i]
|
|
subscriptionKeys = list(client.subscriptions.keys())
|
|
for j in range(0, len(subscriptionKeys)):
|
|
subscribeType = subscriptionKeys[j]
|
|
if subscribeType == type:
|
|
self.delay(listenKeyRefreshRate, self.keep_alive_listen_key, params)
|
|
return
|
|
|
|
def set_balance_cache(self, client: Client, type, isPortfolioMargin=False):
|
|
if (type in client.subscriptions) and (type in self.balance):
|
|
return
|
|
options = self.safe_value(self.options, 'watchBalance')
|
|
fetchBalanceSnapshot = self.safe_bool(options, 'fetchBalanceSnapshot', False)
|
|
if fetchBalanceSnapshot:
|
|
messageHash = type + ':fetchBalanceSnapshot'
|
|
if not (messageHash in client.futures):
|
|
client.future(messageHash)
|
|
self.spawn(self.load_balance_snapshot, client, messageHash, type, isPortfolioMargin)
|
|
else:
|
|
self.balance[type] = {}
|
|
|
|
async def load_balance_snapshot(self, client, messageHash, type, isPortfolioMargin):
|
|
params: dict = {
|
|
'type': type,
|
|
}
|
|
if isPortfolioMargin:
|
|
params['portfolioMargin'] = True
|
|
response = await self.fetch_balance(params)
|
|
self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {}))
|
|
# don't remove the future from the .futures cache
|
|
future = client.futures[messageHash]
|
|
future.resolve()
|
|
client.resolve(self.balance[type], type + ':balance')
|
|
|
|
async def fetch_balance_ws(self, params={}) -> Balances:
|
|
"""
|
|
fetch balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/account/websocket-api/Futures-Account-Balance
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/account-requests#account-information-user_data
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/account/websocket-api
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str|None [params.type]: 'future', 'delivery', 'savings', 'funding', or 'spot'
|
|
:param str|None [params.marginMode]: 'cross' or 'isolated', for margin trading, uses self.options.defaultMarginMode if not passed, defaults to None/None/None
|
|
:param str[]|None [params.symbols]: unified market symbols, only used in isolated margin mode
|
|
:param str|None [params.method]: method to use. Can be account.balance, account.status, v2/account.balance or v2/account.status
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
type = self.get_market_type('fetchBalanceWs', None, params)
|
|
if type != 'spot' and type != 'future' and type != 'delivery':
|
|
raise BadRequest(self.id + ' fetchBalanceWs only supports spot or swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchBalanceWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
method = None
|
|
method, params = self.handle_option_and_params(params, 'fetchBalanceWs', 'method', 'account.status')
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': method,
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_account_status_ws if (method == 'account.status') else self.handle_balance_ws,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
def handle_balance_ws(self, client: Client, message):
|
|
#
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
rawBalance = None
|
|
if isinstance(message['result'], list):
|
|
# account.balance
|
|
rawBalance = self.safe_list(message, 'result', [])
|
|
else:
|
|
# account.status
|
|
result = self.safe_dict(message, 'result', {})
|
|
rawBalance = self.safe_list(result, 'assets', [])
|
|
parsedBalances = self.parseBalanceCustom(rawBalance)
|
|
client.resolve(parsedBalances, messageHash)
|
|
|
|
def handle_account_status_ws(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {
|
|
# "id": "605a6d20-6588-4cb9-afa0-b0ab087507ba",
|
|
# "status": 200,
|
|
# "result": {
|
|
# "makerCommission": 15,
|
|
# "takerCommission": 15,
|
|
# "buyerCommission": 0,
|
|
# "sellerCommission": 0,
|
|
# "canTrade": True,
|
|
# "canWithdraw": True,
|
|
# "canDeposit": True,
|
|
# "commissionRates": {
|
|
# "maker": "0.00150000",
|
|
# "taker": "0.00150000",
|
|
# "buyer": "0.00000000",
|
|
# "seller": "0.00000000"
|
|
# },
|
|
# "brokered": False,
|
|
# "requireSelfTradePrevention": False,
|
|
# "updateTime": 1660801833000,
|
|
# "accountType": "SPOT",
|
|
# "balances": [{
|
|
# "asset": "BNB",
|
|
# "free": "0.00000000",
|
|
# "locked": "0.00000000"
|
|
# },
|
|
# {
|
|
# "asset": "BTC",
|
|
# "free": "1.3447112",
|
|
# "locked": "0.08600000"
|
|
# },
|
|
# {
|
|
# "asset": "USDT",
|
|
# "free": "1021.21000000",
|
|
# "locked": "0.00000000"
|
|
# }
|
|
# ],
|
|
# "permissions": [
|
|
# "SPOT"
|
|
# ]
|
|
# }
|
|
# }
|
|
# swap
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_dict(message, 'result', {})
|
|
parsedBalances = self.parseBalanceCustom(result)
|
|
client.resolve(parsedBalances, messageHash)
|
|
|
|
async def fetch_position_ws(self, symbol: str, params={}) -> List[Position]:
|
|
"""
|
|
fetch data on an open position
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Position-Information
|
|
|
|
:param str symbol: unified market symbol of the market the position is held in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `position structure <https://docs.ccxt.com/#/?id=position-structure>`
|
|
"""
|
|
return await self.fetch_positions_ws([symbol], params)
|
|
|
|
async def fetch_positions_ws(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Position-Information
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Position-Information
|
|
|
|
:param str[] [symbols]: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.returnRateLimits]: set to True to return rate limit informations, defaults to False.
|
|
:param str|None [params.method]: method to use. Can be account.position or v2/account.position
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/#/?id=position-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
payload: dict = {}
|
|
market = None
|
|
symbols = self.market_symbols(symbols, 'swap', True, True, True)
|
|
if symbols is not None:
|
|
symbolsLength = len(symbols)
|
|
if symbolsLength == 1:
|
|
market = self.market(symbols[0])
|
|
payload['symbol'] = market['id']
|
|
type = self.get_market_type('fetchPositionsWs', market, params)
|
|
if type != 'future' and type != 'delivery':
|
|
raise BadRequest(self.id + ' fetchPositionsWs only supports swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchPositionsWs', 'returnRateLimits', False)
|
|
payload['returnRateLimits'] = returnRateLimits
|
|
method = None
|
|
method, params = self.handle_option_and_params(params, 'fetchPositionsWs', 'method', 'account.position')
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': method,
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_positions_ws,
|
|
}
|
|
result = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
return self.filter_by_array_positions(result, 'symbol', symbols, False)
|
|
|
|
def handle_positions_ws(self, client: Client, message):
|
|
#
|
|
# {
|
|
# id: '1',
|
|
# status: 200,
|
|
# result: [
|
|
# {
|
|
# symbol: 'BTCUSDT',
|
|
# positionAmt: '-0.014',
|
|
# entryPrice: '42901.1',
|
|
# breakEvenPrice: '30138.83333142',
|
|
# markPrice: '71055.98470333',
|
|
# unRealizedProfit: '-394.16838584',
|
|
# liquidationPrice: '137032.02272908',
|
|
# leverage: '123',
|
|
# maxNotionalValue: '50000',
|
|
# marginType: 'cross',
|
|
# isolatedMargin: '0.00000000',
|
|
# isAutoAddMargin: 'false',
|
|
# positionSide: 'BOTH',
|
|
# notional: '-994.78378584',
|
|
# isolatedWallet: '0',
|
|
# updateTime: 1708906343111,
|
|
# isolated: False,
|
|
# adlQuantile: 2
|
|
# },
|
|
# ...
|
|
# ]
|
|
# }
|
|
#
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_list(message, 'result', [])
|
|
positions = []
|
|
for i in range(0, len(result)):
|
|
parsed = self.parse_position_risk(result[i])
|
|
entryPrice = self.safe_string(parsed, 'entryPrice')
|
|
if (entryPrice != '0') and (entryPrice != '0.0') and (entryPrice != '0.00000000'):
|
|
positions.append(parsed)
|
|
client.resolve(positions, messageHash)
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
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
|
|
:param boolean [params.portfolioMargin]: set to True if you would like to watch the balance of a portfolio margin account
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.authenticate(params)
|
|
defaultType = self.safe_string(self.options, 'defaultType', 'spot')
|
|
type = self.safe_string(params, 'type', defaultType)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchBalance', None, params)
|
|
isPortfolioMargin = None
|
|
isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchBalance', 'papi', 'portfolioMargin', False)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
url = ''
|
|
urlType = type
|
|
if type == 'spot':
|
|
# route to WebSocket API connection where the user data stream is subscribed
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
else:
|
|
if isPortfolioMargin:
|
|
urlType = 'papi'
|
|
url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey']
|
|
client = self.client(url)
|
|
self.set_balance_cache(client, type, isPortfolioMargin)
|
|
self.set_positions_cache(client, type, None, isPortfolioMargin)
|
|
options = self.safe_dict(self.options, 'watchBalance')
|
|
fetchBalanceSnapshot = self.safe_bool(options, 'fetchBalanceSnapshot', False)
|
|
awaitBalanceSnapshot = self.safe_bool(options, 'awaitBalanceSnapshot', True)
|
|
if fetchBalanceSnapshot and awaitBalanceSnapshot:
|
|
await client.future(type + ':fetchBalanceSnapshot')
|
|
messageHash = type + ':balance'
|
|
message = None
|
|
return await self.watch(url, messageHash, message, type)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# sent upon a balance update not related to orders
|
|
#
|
|
# {
|
|
# "e": "balanceUpdate",
|
|
# "E": 1629352505586,
|
|
# "a": "IOTX",
|
|
# "d": "0.43750000",
|
|
# "T": 1629352505585
|
|
# }
|
|
#
|
|
# sent upon creating or filling an order
|
|
#
|
|
# {
|
|
# "e": "outboundAccountPosition", # Event type
|
|
# "E": 1564034571105, # Event Time
|
|
# "u": 1564034571073, # Time of last account update
|
|
# "B": [ # Balances Array
|
|
# {
|
|
# "a": "ETH", # Asset
|
|
# "f": "10000.000000", # Free
|
|
# "l": "0.000000" # Locked
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# future/delivery
|
|
#
|
|
# {
|
|
# "e": "ACCOUNT_UPDATE", # Event Type
|
|
# "E": 1564745798939, # Event Time
|
|
# "T": 1564745798938 , # Transaction
|
|
# "i": "SfsR", # Account Alias
|
|
# "a": { # Update Data
|
|
# "m":"ORDER", # Event reason type
|
|
# "B":[ # Balances
|
|
# {
|
|
# "a":"BTC", # Asset
|
|
# "wb":"122624.12345678", # Wallet Balance
|
|
# "cw":"100.12345678" # Cross Wallet Balance
|
|
# },
|
|
# ],
|
|
# "P":[
|
|
# {
|
|
# "s":"BTCUSD_200925", # Symbol
|
|
# "pa":"0", # Position Amount
|
|
# "ep":"0.0", # Entry Price
|
|
# "cr":"200", #(Pre-fee) Accumulated Realized
|
|
# "up":"0", # Unrealized PnL
|
|
# "mt":"isolated", # Margin Type
|
|
# "iw":"0.00000000", # Isolated Wallet(if isolated position)
|
|
# "ps":"BOTH" # Position Side
|
|
# },
|
|
# ]
|
|
# }
|
|
# }
|
|
# externalLockUpdate
|
|
# {
|
|
# "e": "externalLockUpdate", # Event Type
|
|
# "E": 1581557507324, # Event Time
|
|
# "a": "NEO", # Asset
|
|
# "d": "10.00000000", # Delta
|
|
# "T": 1581557507268 # Transaction Time
|
|
# }
|
|
#
|
|
wallet = self.safe_string(self.options, 'wallet', 'wb') # cw for cross wallet
|
|
# each account is connected to a different endpoint
|
|
subscriptions = client.subscriptions
|
|
subscriptionsKeys = list(subscriptions.keys())
|
|
accountType = self.get_account_type_from_subscriptions(subscriptionsKeys)
|
|
messageHash = accountType + ':balance'
|
|
if self.balance[accountType] is None:
|
|
self.balance[accountType] = {}
|
|
self.balance[accountType]['info'] = message
|
|
event = self.safe_string(message, 'e')
|
|
if event == 'balanceUpdate':
|
|
currencyId = self.safe_string(message, 'a')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
delta = self.safe_string(message, 'd')
|
|
if code in self.balance[accountType]:
|
|
previousValue = self.balance[accountType][code]['free']
|
|
if not isinstance(previousValue, str):
|
|
previousValue = self.number_to_string(previousValue)
|
|
account['free'] = Precise.string_add(previousValue, delta)
|
|
else:
|
|
account['free'] = delta
|
|
self.balance[accountType][code] = account
|
|
else:
|
|
message = self.safe_dict(message, 'a', message)
|
|
B = self.safe_list(message, 'B')
|
|
for i in range(0, len(B)):
|
|
entry = B[i]
|
|
currencyId = self.safe_string(entry, 'a')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
account['free'] = self.safe_string(entry, 'f')
|
|
account['used'] = self.safe_string(entry, 'l')
|
|
account['total'] = self.safe_string(entry, wallet)
|
|
self.balance[accountType][code] = account
|
|
timestamp = self.safe_integer(message, 'E')
|
|
self.balance[accountType]['timestamp'] = timestamp
|
|
self.balance[accountType]['datetime'] = self.iso8601(timestamp)
|
|
self.balance[accountType] = self.safe_balance(self.balance[accountType])
|
|
client.resolve(self.balance[accountType], messageHash)
|
|
|
|
def get_account_type_from_subscriptions(self, subscriptions: List[str]) -> str:
|
|
accountType = ''
|
|
for i in range(0, len(subscriptions)):
|
|
subscription = subscriptions[i]
|
|
if (subscription == 'spot') or (subscription == 'margin') or (subscription == 'future') or (subscription == 'delivery'):
|
|
accountType = subscription
|
|
break
|
|
return accountType
|
|
|
|
def get_market_type(self, method, market, params={}):
|
|
type = None
|
|
type, params = self.handle_market_type_and_params(method, market, params)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params(method, market, params)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
return type
|
|
|
|
async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
|
|
"""
|
|
create a trade order
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#place-new-order-trade
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/New-Order
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of currency you want to trade in units of base currency
|
|
:param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean params['test']: test order, default False
|
|
:param boolean params['returnRateLimits']: set to True to return rate limit information, default False
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
marketType = self.get_market_type('createOrderWs', market, params)
|
|
if marketType != 'spot' and marketType != 'future' and marketType != 'delivery':
|
|
raise BadRequest(self.id + ' createOrderWs only supports spot or swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][marketType]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
sor = self.safe_bool_2(params, 'sor', 'SOR', False)
|
|
params = self.omit(params, 'sor', 'SOR')
|
|
payload = self.create_order_request(symbol, type, side, amount, price, params)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'createOrderWs', 'returnRateLimits', False)
|
|
payload['returnRateLimits'] = returnRateLimits
|
|
test = self.safe_bool(params, 'test', False)
|
|
params = self.omit(params, 'test')
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'order.place',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
if test:
|
|
if sor:
|
|
message['method'] = 'sor.order.test'
|
|
else:
|
|
message['method'] = 'order.test'
|
|
subscription: dict = {
|
|
'method': self.handle_order_ws,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
def handle_order_ws(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "id": 1,
|
|
# "status": 200,
|
|
# "result": {
|
|
# "symbol": "BTCUSDT",
|
|
# "orderId": 7663053,
|
|
# "orderListId": -1,
|
|
# "clientOrderId": "x-R4BD3S82d8959d0f5114499487a614",
|
|
# "transactTime": 1687642291434,
|
|
# "price": "25000.00000000",
|
|
# "origQty": "0.00100000",
|
|
# "executedQty": "0.00000000",
|
|
# "cummulativeQuoteQty": "0.00000000",
|
|
# "status": "NEW",
|
|
# "timeInForce": "GTC",
|
|
# "type": "LIMIT",
|
|
# "side": "BUY",
|
|
# "workingTime": 1687642291434,
|
|
# "fills": [],
|
|
# "selfTradePreventionMode": "NONE"
|
|
# },
|
|
# "rateLimits": [
|
|
# {
|
|
# "rateLimitType": "ORDERS",
|
|
# "interval": "SECOND",
|
|
# "intervalNum": 10,
|
|
# "limit": 50,
|
|
# "count": 1
|
|
# },
|
|
# {
|
|
# "rateLimitType": "ORDERS",
|
|
# "interval": "DAY",
|
|
# "intervalNum": 1,
|
|
# "limit": 160000,
|
|
# "count": 1
|
|
# },
|
|
# {
|
|
# "rateLimitType": "REQUEST_WEIGHT",
|
|
# "interval": "MINUTE",
|
|
# "intervalNum": 1,
|
|
# "limit": 1200,
|
|
# "count": 12
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_dict(message, 'result', {})
|
|
order = self.parse_order(result)
|
|
client.resolve(order, messageHash)
|
|
|
|
def handle_orders_ws(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "id": 1,
|
|
# "status": 200,
|
|
# "result": [{
|
|
# "symbol": "BTCUSDT",
|
|
# "orderId": 7665584,
|
|
# "orderListId": -1,
|
|
# "clientOrderId": "x-R4BD3S82b54769abdd3e4b57874c52",
|
|
# "price": "26000.00000000",
|
|
# "origQty": "0.00100000",
|
|
# "executedQty": "0.00000000",
|
|
# "cummulativeQuoteQty": "0.00000000",
|
|
# "status": "NEW",
|
|
# "timeInForce": "GTC",
|
|
# "type": "LIMIT",
|
|
# "side": "BUY",
|
|
# "stopPrice": "0.00000000",
|
|
# "icebergQty": "0.00000000",
|
|
# "time": 1687642884646,
|
|
# "updateTime": 1687642884646,
|
|
# "isWorking": True,
|
|
# "workingTime": 1687642884646,
|
|
# "origQuoteOrderQty": "0.00000000",
|
|
# "selfTradePreventionMode": "NONE"
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "rateLimits": [{
|
|
# "rateLimitType": "REQUEST_WEIGHT",
|
|
# "interval": "MINUTE",
|
|
# "intervalNum": 1,
|
|
# "limit": 1200,
|
|
# "count": 14
|
|
# }]
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_list(message, 'result', [])
|
|
orders = self.parse_orders(result)
|
|
client.resolve(orders, messageHash)
|
|
|
|
async def edit_order_ws(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}) -> Order:
|
|
"""
|
|
edit a trade order
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#cancel-and-replace-order-trade
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Modify-Order
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Modify-Order
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str type: 'market' or 'limit'
|
|
:param str side: 'buy' or 'sell'
|
|
:param float amount: how much of the currency you want to trade in units of the base currency
|
|
:param float|None [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
marketType = self.get_market_type('editOrderWs', market, params)
|
|
if marketType != 'spot' and marketType != 'future' and marketType != 'delivery':
|
|
raise BadRequest(self.id + ' editOrderWs only supports spot or swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][marketType]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
isSwap = (marketType == 'future' or marketType == 'delivery')
|
|
payload = None
|
|
if marketType == 'spot':
|
|
payload = self.editSpotOrderRequest(id, symbol, type, side, amount, price, params)
|
|
elif isSwap:
|
|
payload = self.editContractOrderRequest(id, symbol, type, side, amount, price, params)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'editOrderWs', 'returnRateLimits', False)
|
|
payload['returnRateLimits'] = returnRateLimits
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'order.modify' if (isSwap) else 'order.cancelReplace',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_edit_order_ws,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
def handle_edit_order_ws(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {
|
|
# "id": 1,
|
|
# "status": 200,
|
|
# "result": {
|
|
# "cancelResult": "SUCCESS",
|
|
# "newOrderResult": "SUCCESS",
|
|
# "cancelResponse": {
|
|
# "symbol": "BTCUSDT",
|
|
# "origClientOrderId": "x-R4BD3S82813c5d7ffa594104917de2",
|
|
# "orderId": 7665177,
|
|
# "orderListId": -1,
|
|
# "clientOrderId": "mbrnbQsQhtCXCLY45d5q7S",
|
|
# "price": "26000.00000000",
|
|
# "origQty": "0.00100000",
|
|
# "executedQty": "0.00000000",
|
|
# "cummulativeQuoteQty": "0.00000000",
|
|
# "status": "CANCELED",
|
|
# "timeInForce": "GTC",
|
|
# "type": "LIMIT",
|
|
# "side": "BUY",
|
|
# "selfTradePreventionMode": "NONE"
|
|
# },
|
|
# "newOrderResponse": {
|
|
# "symbol": "BTCUSDT",
|
|
# "orderId": 7665584,
|
|
# "orderListId": -1,
|
|
# "clientOrderId": "x-R4BD3S82b54769abdd3e4b57874c52",
|
|
# "transactTime": 1687642884646,
|
|
# "price": "26000.00000000",
|
|
# "origQty": "0.00100000",
|
|
# "executedQty": "0.00000000",
|
|
# "cummulativeQuoteQty": "0.00000000",
|
|
# "status": "NEW",
|
|
# "timeInForce": "GTC",
|
|
# "type": "LIMIT",
|
|
# "side": "BUY",
|
|
# "workingTime": 1687642884646,
|
|
# "fills": [],
|
|
# "selfTradePreventionMode": "NONE"
|
|
# }
|
|
# },
|
|
# "rateLimits": [{
|
|
# "rateLimitType": "ORDERS",
|
|
# "interval": "SECOND",
|
|
# "intervalNum": 10,
|
|
# "limit": 50,
|
|
# "count": 1
|
|
# },
|
|
# {
|
|
# "rateLimitType": "ORDERS",
|
|
# "interval": "DAY",
|
|
# "intervalNum": 1,
|
|
# "limit": 160000,
|
|
# "count": 3
|
|
# },
|
|
# {
|
|
# "rateLimitType": "REQUEST_WEIGHT",
|
|
# "interval": "MINUTE",
|
|
# "intervalNum": 1,
|
|
# "limit": 1200,
|
|
# "count": 12
|
|
# }
|
|
# ]
|
|
# }
|
|
# swap
|
|
# {
|
|
# "id":"1",
|
|
# "status":200,
|
|
# "result":{
|
|
# "orderId":667061487,
|
|
# "symbol":"LTCUSDT",
|
|
# "status":"NEW",
|
|
# "clientOrderId":"x-xcKtGhcu91a74c818749ee42c0f70",
|
|
# "price":"82.00",
|
|
# "avgPrice":"0.00",
|
|
# "origQty":"1.000",
|
|
# "executedQty":"0.000",
|
|
# "cumQty":"0.000",
|
|
# "cumQuote":"0.00000",
|
|
# "timeInForce":"GTC",
|
|
# "type":"LIMIT",
|
|
# "reduceOnly":false,
|
|
# "closePosition":false,
|
|
# "side":"BUY",
|
|
# "positionSide":"BOTH",
|
|
# "stopPrice":"0.00",
|
|
# "workingType":"CONTRACT_PRICE",
|
|
# "priceProtect":false,
|
|
# "origType":"LIMIT",
|
|
# "priceMatch":"NONE",
|
|
# "selfTradePreventionMode":"NONE",
|
|
# "goodTillDate":0,
|
|
# "updateTime":1712918927511
|
|
# }
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_dict(message, 'result', {})
|
|
newSpotOrder = self.safe_dict(result, 'newOrderResponse')
|
|
order = None
|
|
if newSpotOrder is not None:
|
|
order = self.parse_order(newSpotOrder)
|
|
else:
|
|
order = self.parse_order(result)
|
|
client.resolve(order, messageHash)
|
|
|
|
async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order:
|
|
"""
|
|
cancel multiple orders
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#cancel-order-trade
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Cancel-Order
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Cancel-Order
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified market symbol, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str|None [params.cancelRestrictions]: Supported values: ONLY_NEW - Cancel will succeed if the order status is NEW. ONLY_PARTIALLY_FILLED - Cancel will succeed if order status is PARTIALLY_FILLED.
|
|
:returns dict: an list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
if symbol is None:
|
|
raise BadRequest(self.id + ' cancelOrderWs requires a symbol')
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('cancelOrderWs', market, params)
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'cancelOrderWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
payload['origClientOrderId'] = clientOrderId
|
|
else:
|
|
payload['orderId'] = self.parse_to_int(id)
|
|
params = self.omit(params, ['origClientOrderId', 'clientOrderId'])
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'order.cancel',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_order_ws,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
async def cancel_all_orders_ws(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders in a market
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#cancel-open-orders-trade
|
|
|
|
:param str [symbol]: unified market symbol of the market to cancel orders in
|
|
: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()
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('cancelAllOrdersWs', market, params)
|
|
if type != 'spot':
|
|
raise BadRequest(self.id + ' cancelAllOrdersWs only supports spot markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'cancelAllOrdersWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'openOrders.cancelAll',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_orders_ws,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
async def fetch_order_ws(self, id: str, symbol: Str = None, params={}) -> Order:
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#query-order-user_data
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Query-Order
|
|
https://developers.binance.com/docs/derivatives/coin-margined-futures/trade/websocket-api/Query-Order
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
if symbol is None:
|
|
raise BadRequest(self.id + ' cancelOrderWs requires a symbol')
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('fetchOrderWs', market, params)
|
|
if type != 'spot' and type != 'future' and type != 'delivery':
|
|
raise BadRequest(self.id + ' fetchOrderWs only supports spot or swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchOrderWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
clientOrderId = self.safe_string_2(params, 'origClientOrderId', 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
payload['origClientOrderId'] = clientOrderId
|
|
else:
|
|
payload['orderId'] = self.parse_to_int(id)
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'order.status',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_order_ws,
|
|
}
|
|
return await self.watch(url, messageHash, message, messageHash, subscription)
|
|
|
|
async def fetch_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple orders made by the user
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#order-lists
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int|None [since]: the earliest time in ms to fetch orders for
|
|
:param int|None [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.orderId]: order id to begin at
|
|
:param int [params.startTime]: earliest time in ms to retrieve orders for
|
|
:param int [params.endTime]: latest time in ms to retrieve orders for
|
|
:param int [params.limit]: the maximum number of order structures to retrieve
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
if symbol is None:
|
|
raise BadRequest(self.id + ' fetchOrdersWs requires a symbol')
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('fetchOrdersWs', market, params)
|
|
if type != 'spot':
|
|
raise BadRequest(self.id + ' fetchOrdersWs only supports spot markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchOrderWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'allOrders',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_orders_ws,
|
|
}
|
|
orders = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit)
|
|
|
|
async def fetch_closed_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch closed orders
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#order-lists
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders 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>`
|
|
"""
|
|
orders = await self.fetch_orders_ws(symbol, since, limit, params)
|
|
closedOrders = []
|
|
for i in range(0, len(orders)):
|
|
order = orders[i]
|
|
if order['status'] == 'closed':
|
|
closedOrders.append(order)
|
|
return closedOrders
|
|
|
|
async def fetch_open_orders_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/trading-requests#current-open-orders-user_data
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int|None [since]: the earliest time in ms to fetch open orders for
|
|
:param int|None [limit]: the maximum number of open orders 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()
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('fetchOpenOrdersWs', market, params)
|
|
if type != 'spot' and type != 'future':
|
|
raise BadRequest(self.id + ' fetchOpenOrdersWs only supports spot or swap markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchOrderWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
if symbol is not None:
|
|
payload['symbol'] = self.market_id(symbol)
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'openOrders.status',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_orders_ws,
|
|
}
|
|
orders = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit)
|
|
|
|
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
watches information on multiple orders made by the user
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/user-data-stream#order-update
|
|
https://developers.binance.com/docs/margin_trading/trade-data-stream/Event-Order-Update
|
|
https://developers.binance.com/docs/derivatives/usds-margined-futures/user-data-streams/Event-Order-Update
|
|
|
|
:param str symbol: unified market symbol of the market the 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 str|None [params.marginMode]: 'cross' or 'isolated', for spot margin
|
|
:param boolean [params.portfolioMargin]: set to True if you would like to watch portfolio margin account orders
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
messageHash = 'orders'
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
messageHash += ':' + symbol
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchOrders', market, params)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchOrders', market, params)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
params = self.extend(params, {'type': type, 'symbol': symbol, 'subType': subType}) # needed inside authenticate for isolated margin
|
|
await self.authenticate(params)
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('watchOrders', params)
|
|
urlType = type
|
|
if (type == 'margin') or ((type == 'spot') and (marginMode is not None)):
|
|
urlType = 'spot' # spot-margin shares the same stream spot
|
|
isPortfolioMargin = None
|
|
isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchOrders', 'papi', 'portfolioMargin', False)
|
|
url = ''
|
|
if type == 'spot':
|
|
# route orders to ws-api user data stream
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
else:
|
|
if isPortfolioMargin:
|
|
urlType = 'papi'
|
|
url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey']
|
|
client = self.client(url)
|
|
self.set_balance_cache(client, type, isPortfolioMargin)
|
|
self.set_positions_cache(client, type, None, isPortfolioMargin)
|
|
message = None
|
|
orders = await self.watch(url, messageHash, message, type)
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
|
|
|
|
def parse_ws_order(self, order, market=None):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# "e": "executionReport", # Event type
|
|
# "E": 1499405658658, # Event time
|
|
# "s": "ETHBTC", # Symbol
|
|
# "c": "mUvoqJxFIILMdfAW5iGSOW", # Client order ID
|
|
# "S": "BUY", # Side
|
|
# "o": "LIMIT", # Order type
|
|
# "f": "GTC", # Time in force
|
|
# "q": "1.00000000", # Order quantity
|
|
# "p": "0.10264410", # Order price
|
|
# "P": "0.00000000", # Stop price
|
|
# "F": "0.00000000", # Iceberg quantity
|
|
# "g": -1, # OrderListId
|
|
# "C": null, # Original client order ID; This is the ID of the order being canceled
|
|
# "x": "NEW", # Current execution type
|
|
# "X": "NEW", # Current order status
|
|
# "r": "NONE", # Order reject reason; will be an error code.
|
|
# "i": 4293153, # Order ID
|
|
# "l": "0.00000000", # Last executed quantity
|
|
# "z": "0.00000000", # Cumulative filled quantity
|
|
# "L": "0.00000000", # Last executed price
|
|
# "n": "0", # Commission amount
|
|
# "N": null, # Commission asset
|
|
# "T": 1499405658657, # Transaction time
|
|
# "t": -1, # Trade ID
|
|
# "I": 8641984, # Ignore
|
|
# "w": True, # Is the order on the book?
|
|
# "m": False, # Is self trade the maker side?
|
|
# "M": False, # Ignore
|
|
# "O": 1499405658657, # Order creation time
|
|
# "Z": "0.00000000", # Cumulative quote asset transacted quantity
|
|
# "Y": "0.00000000" # Last quote asset transacted quantity(i.e. lastPrice * lastQty),
|
|
# "Q": "0.00000000" # Quote Order Qty
|
|
# }
|
|
#
|
|
# future
|
|
#
|
|
# {
|
|
# "s":"BTCUSDT", # Symbol
|
|
# "c":"TEST", # Client Order Id
|
|
# # special client order id:
|
|
# # starts with "autoclose-": liquidation order
|
|
# # "adl_autoclose": ADL auto close order
|
|
# "S":"SELL", # Side
|
|
# "o":"TRAILING_STOP_MARKET", # Order Type
|
|
# "f":"GTC", # Time in Force
|
|
# "q":"0.001", # Original Quantity
|
|
# "p":"0", # Original Price
|
|
# "ap":"0", # Average Price
|
|
# "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order
|
|
# "x":"NEW", # Execution Type
|
|
# "X":"NEW", # Order Status
|
|
# "i":8886774, # Order Id
|
|
# "l":"0", # Order Last Filled Quantity
|
|
# "z":"0", # Order Filled Accumulated Quantity
|
|
# "L":"0", # Last Filled Price
|
|
# "N":"USDT", # Commission Asset, will not push if no commission
|
|
# "n":"0", # Commission, will not push if no commission
|
|
# "T":1568879465651, # Order Trade Time
|
|
# "t":0, # Trade Id
|
|
# "b":"0", # Bids Notional
|
|
# "a":"9.91", # Ask Notional
|
|
# "m":false, # Is self trade the maker side?
|
|
# "R":false, # Is self reduce only
|
|
# "wt":"CONTRACT_PRICE", # Stop Price Working Type
|
|
# "ot":"TRAILING_STOP_MARKET", # Original Order Type
|
|
# "ps":"LONG", # Position Side
|
|
# "cp":false, # If Close-All, pushed with conditional order
|
|
# "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order
|
|
# "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
|
# "rp":"0" # Realized Profit of the trade
|
|
# }
|
|
#
|
|
executionType = self.safe_string(order, 'x')
|
|
orderId = self.safe_string(order, 'i')
|
|
marketId = self.safe_string(order, 's')
|
|
marketType = 'contract' if ('ps' in order) else 'spot'
|
|
symbol = self.safe_symbol(marketId, None, None, marketType)
|
|
timestamp = self.safe_integer(order, 'O')
|
|
T = self.safe_integer(order, 'T')
|
|
lastTradeTimestamp = None
|
|
if executionType == 'NEW' or executionType == 'AMENDMENT' or executionType == 'CANCELED':
|
|
if timestamp is None:
|
|
timestamp = T
|
|
elif executionType == 'TRADE':
|
|
lastTradeTimestamp = T
|
|
lastUpdateTimestamp = T
|
|
fee = None
|
|
feeCost = self.safe_string(order, 'n')
|
|
if (feeCost is not None) and (Precise.string_gt(feeCost, '0')):
|
|
feeCurrencyId = self.safe_string(order, 'N')
|
|
feeCurrency = self.safe_currency_code(feeCurrencyId)
|
|
fee = {
|
|
'cost': feeCost,
|
|
'currency': feeCurrency,
|
|
}
|
|
price = self.safe_string(order, 'p')
|
|
amount = self.safe_string(order, 'q')
|
|
side = self.safe_string_lower(order, 'S')
|
|
type = self.safe_string_lower(order, 'o')
|
|
filled = self.safe_string(order, 'z')
|
|
cost = self.safe_string(order, 'Z')
|
|
average = self.safe_string(order, 'ap')
|
|
rawStatus = self.safe_string(order, 'X')
|
|
status = self.parse_order_status(rawStatus)
|
|
trades = None
|
|
clientOrderId = self.safe_string(order, 'C')
|
|
if (clientOrderId is None) or (len(clientOrderId) == 0):
|
|
clientOrderId = self.safe_string(order, 'c')
|
|
stopPrice = self.safe_string_2(order, 'P', 'sp')
|
|
timeInForce = self.safe_string(order, 'f')
|
|
if timeInForce == 'GTX':
|
|
# GTX means "Good Till Crossing" and is an equivalent way of saying Post Only
|
|
timeInForce = 'PO'
|
|
return self.safe_order({
|
|
'info': order,
|
|
'symbol': symbol,
|
|
'id': orderId,
|
|
'clientOrderId': clientOrderId,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': lastTradeTimestamp,
|
|
'lastUpdateTimestamp': lastUpdateTimestamp,
|
|
'type': type,
|
|
'timeInForce': timeInForce,
|
|
'postOnly': None,
|
|
'reduceOnly': self.safe_bool(order, 'R'),
|
|
'side': side,
|
|
'price': price,
|
|
'stopPrice': stopPrice,
|
|
'triggerPrice': stopPrice,
|
|
'amount': amount,
|
|
'cost': cost,
|
|
'average': average,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'status': status,
|
|
'fee': fee,
|
|
'trades': trades,
|
|
})
|
|
|
|
def handle_order_update(self, client: Client, message):
|
|
#
|
|
# spot
|
|
#
|
|
# {
|
|
# "e": "executionReport", # Event type
|
|
# "E": 1499405658658, # Event time
|
|
# "s": "ETHBTC", # Symbol
|
|
# "c": "mUvoqJxFIILMdfAW5iGSOW", # Client order ID
|
|
# "S": "BUY", # Side
|
|
# "o": "LIMIT", # Order type
|
|
# "f": "GTC", # Time in force
|
|
# "q": "1.00000000", # Order quantity
|
|
# "p": "0.10264410", # Order price
|
|
# "P": "0.00000000", # Stop price
|
|
# "F": "0.00000000", # Iceberg quantity
|
|
# "g": -1, # OrderListId
|
|
# "C": null, # Original client order ID; This is the ID of the order being canceled
|
|
# "x": "NEW", # Current execution type
|
|
# "X": "NEW", # Current order status
|
|
# "r": "NONE", # Order reject reason; will be an error code.
|
|
# "i": 4293153, # Order ID
|
|
# "l": "0.00000000", # Last executed quantity
|
|
# "z": "0.00000000", # Cumulative filled quantity
|
|
# "L": "0.00000000", # Last executed price
|
|
# "n": "0", # Commission amount
|
|
# "N": null, # Commission asset
|
|
# "T": 1499405658657, # Transaction time
|
|
# "t": -1, # Trade ID
|
|
# "I": 8641984, # Ignore
|
|
# "w": True, # Is the order on the book?
|
|
# "m": False, # Is self trade the maker side?
|
|
# "M": False, # Ignore
|
|
# "O": 1499405658657, # Order creation time
|
|
# "Z": "0.00000000", # Cumulative quote asset transacted quantity
|
|
# "Y": "0.00000000" # Last quote asset transacted quantity(i.e. lastPrice * lastQty),
|
|
# "Q": "0.00000000" # Quote Order Qty
|
|
# }
|
|
#
|
|
# future
|
|
#
|
|
# {
|
|
# "e":"ORDER_TRADE_UPDATE", # Event Type
|
|
# "E":1568879465651, # Event Time
|
|
# "T":1568879465650, # Trasaction Time
|
|
# "o": {
|
|
# "s":"BTCUSDT", # Symbol
|
|
# "c":"TEST", # Client Order Id
|
|
# # special client order id:
|
|
# # starts with "autoclose-": liquidation order
|
|
# # "adl_autoclose": ADL auto close order
|
|
# "S":"SELL", # Side
|
|
# "o":"TRAILING_STOP_MARKET", # Order Type
|
|
# "f":"GTC", # Time in Force
|
|
# "q":"0.001", # Original Quantity
|
|
# "p":"0", # Original Price
|
|
# "ap":"0", # Average Price
|
|
# "sp":"7103.04", # Stop Price. Please ignore with TRAILING_STOP_MARKET order
|
|
# "x":"NEW", # Execution Type
|
|
# "X":"NEW", # Order Status
|
|
# "i":8886774, # Order Id
|
|
# "l":"0", # Order Last Filled Quantity
|
|
# "z":"0", # Order Filled Accumulated Quantity
|
|
# "L":"0", # Last Filled Price
|
|
# "N":"USDT", # Commission Asset, will not push if no commission
|
|
# "n":"0", # Commission, will not push if no commission
|
|
# "T":1568879465651, # Order Trade Time
|
|
# "t":0, # Trade Id
|
|
# "b":"0", # Bids Notional
|
|
# "a":"9.91", # Ask Notional
|
|
# "m":false, # Is self trade the maker side?
|
|
# "R":false, # Is self reduce only
|
|
# "wt":"CONTRACT_PRICE", # Stop Price Working Type
|
|
# "ot":"TRAILING_STOP_MARKET", # Original Order Type
|
|
# "ps":"LONG", # Position Side
|
|
# "cp":false, # If Close-All, pushed with conditional order
|
|
# "AP":"7476.89", # Activation Price, only puhed with TRAILING_STOP_MARKET order
|
|
# "cr":"5.0", # Callback Rate, only puhed with TRAILING_STOP_MARKET order
|
|
# "rp":"0" # Realized Profit of the trade
|
|
# }
|
|
# }
|
|
#
|
|
e = self.safe_string(message, 'e')
|
|
if e == 'ORDER_TRADE_UPDATE':
|
|
message = self.safe_dict(message, 'o', message)
|
|
self.handle_my_trade(client, message)
|
|
self.handle_order(client, message)
|
|
self.handle_my_liquidation(client, message)
|
|
|
|
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
|
|
"""
|
|
watch all open positions
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param number [since]: since timestamp
|
|
:param number [limit]: limit
|
|
:param dict params: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.portfolioMargin]: set to True if you would like to watch positions in a portfolio margin account
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
messageHash = ''
|
|
symbols = self.market_symbols(symbols)
|
|
if not self.is_empty(symbols):
|
|
market = self.get_market_from_symbols(symbols)
|
|
messageHash = '::' + ','.join(symbols)
|
|
type = None
|
|
type, params = self.handle_market_type_and_params('watchPositions', market, params)
|
|
if type == 'spot' or type == 'margin':
|
|
type = 'future'
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchPositions', market, params)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
marketTypeObject: dict = {}
|
|
marketTypeObject['type'] = type
|
|
marketTypeObject['subType'] = subType
|
|
await self.authenticate(self.extend(marketTypeObject, params))
|
|
messageHash = type + ':positions' + messageHash
|
|
isPortfolioMargin = None
|
|
isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchPositions', 'papi', 'portfolioMargin', False)
|
|
urlType = type
|
|
if isPortfolioMargin:
|
|
urlType = 'papi'
|
|
url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey']
|
|
client = self.client(url)
|
|
self.set_balance_cache(client, type, isPortfolioMargin)
|
|
self.set_positions_cache(client, type, symbols, isPortfolioMargin)
|
|
fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True)
|
|
awaitPositionsSnapshot = self.handle_option('watchPositions', 'awaitPositionsSnapshot', True)
|
|
cache = self.safe_value(self.positions, type)
|
|
if fetchPositionsSnapshot and awaitPositionsSnapshot and cache is None:
|
|
snapshot = await client.future(type + ':fetchPositionsSnapshot')
|
|
return self.filter_by_symbols_since_limit(snapshot, symbols, since, limit, True)
|
|
newPositions = await self.watch(url, messageHash, None, type)
|
|
if self.newUpdates:
|
|
return newPositions
|
|
return self.filter_by_symbols_since_limit(cache, symbols, since, limit, True)
|
|
|
|
def set_positions_cache(self, client: Client, type, symbols: Strings = None, isPortfolioMargin=False):
|
|
if type == 'spot':
|
|
return
|
|
if self.positions is None:
|
|
self.positions = {}
|
|
if type in self.positions:
|
|
return
|
|
fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False)
|
|
if fetchPositionsSnapshot:
|
|
messageHash = type + ':fetchPositionsSnapshot'
|
|
if not (messageHash in client.futures):
|
|
client.future(messageHash)
|
|
self.spawn(self.load_positions_snapshot, client, messageHash, type, isPortfolioMargin)
|
|
else:
|
|
self.positions[type] = ArrayCacheBySymbolBySide()
|
|
|
|
async def load_positions_snapshot(self, client, messageHash, type, isPortfolioMargin):
|
|
params: dict = {
|
|
'type': type,
|
|
}
|
|
if isPortfolioMargin:
|
|
params['portfolioMargin'] = True
|
|
positions = await self.fetch_positions(None, params)
|
|
self.positions[type] = ArrayCacheBySymbolBySide()
|
|
cache = self.positions[type]
|
|
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, type + ':position')
|
|
|
|
def handle_positions(self, client, message):
|
|
#
|
|
# {
|
|
# e: 'ACCOUNT_UPDATE',
|
|
# T: 1667881353112,
|
|
# E: 1667881353115,
|
|
# a: {
|
|
# B: [{
|
|
# a: 'USDT',
|
|
# wb: '1127.95750089',
|
|
# cw: '1040.82091149',
|
|
# bc: '0'
|
|
# }],
|
|
# P: [{
|
|
# s: 'BTCUSDT',
|
|
# pa: '-0.089',
|
|
# ep: '19700.03933',
|
|
# cr: '-1260.24809979',
|
|
# up: '1.53058860',
|
|
# mt: 'isolated',
|
|
# iw: '87.13658940',
|
|
# ps: 'BOTH',
|
|
# ma: 'USDT'
|
|
# }],
|
|
# m: 'ORDER'
|
|
# }
|
|
# }
|
|
#
|
|
# each account is connected to a different endpoint
|
|
# and has exactly one subscriptionhash which is the account type
|
|
subscriptions = client.subscriptions
|
|
subscriptionsKeys = list(subscriptions.keys())
|
|
accountType = self.get_account_type_from_subscriptions(subscriptionsKeys)
|
|
if self.positions is None:
|
|
self.positions = {}
|
|
if not (accountType in self.positions):
|
|
self.positions[accountType] = ArrayCacheBySymbolBySide()
|
|
cache = self.positions[accountType]
|
|
data = self.safe_dict(message, 'a', {})
|
|
rawPositions = self.safe_list(data, 'P', [])
|
|
newPositions = []
|
|
for i in range(0, len(rawPositions)):
|
|
rawPosition = rawPositions[i]
|
|
position = self.parse_ws_position(rawPosition)
|
|
timestamp = self.safe_integer(message, 'E')
|
|
position['timestamp'] = timestamp
|
|
position['datetime'] = self.iso8601(timestamp)
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
messageHashes = self.find_message_hashes(client, accountType + ':positions::')
|
|
for i in range(0, len(messageHashes)):
|
|
messageHash = messageHashes[i]
|
|
parts = messageHash.split('::')
|
|
symbolsString = parts[1]
|
|
symbols = symbolsString.split(',')
|
|
positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
|
|
if not self.is_empty(positions):
|
|
client.resolve(positions, messageHash)
|
|
client.resolve(newPositions, accountType + ':positions')
|
|
|
|
def parse_ws_position(self, position, market=None):
|
|
#
|
|
# {
|
|
# "s": "BTCUSDT", # Symbol
|
|
# "pa": "0", # Position Amount
|
|
# "ep": "0.00000", # Entry Price
|
|
# "cr": "200", #(Pre-fee) Accumulated Realized
|
|
# "up": "0", # Unrealized PnL
|
|
# "mt": "isolated", # Margin Type
|
|
# "iw": "0.00000000", # Isolated Wallet(if isolated position)
|
|
# "ps": "BOTH" # Position Side
|
|
# }
|
|
#
|
|
marketId = self.safe_string(position, 's')
|
|
contracts = self.safe_string(position, 'pa')
|
|
contractsAbs = Precise.string_abs(self.safe_string(position, 'pa'))
|
|
positionSide = self.safe_string_lower(position, 'ps')
|
|
hedged = True
|
|
if positionSide == 'both':
|
|
hedged = False
|
|
if not Precise.string_eq(contracts, '0'):
|
|
if Precise.string_lt(contracts, '0'):
|
|
positionSide = 'short'
|
|
else:
|
|
positionSide = 'long'
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': self.safe_symbol(marketId, None, None, 'swap'),
|
|
'notional': None,
|
|
'marginMode': self.safe_string(position, 'mt'),
|
|
'liquidationPrice': None,
|
|
'entryPrice': self.safe_number(position, 'ep'),
|
|
'unrealizedPnl': self.safe_number(position, 'up'),
|
|
'percentage': None,
|
|
'contracts': self.parse_number(contractsAbs),
|
|
'contractSize': None,
|
|
'markPrice': None,
|
|
'side': positionSide,
|
|
'hedged': hedged,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'maintenanceMargin': None,
|
|
'maintenanceMarginPercentage': None,
|
|
'collateral': None,
|
|
'initialMargin': None,
|
|
'initialMarginPercentage': None,
|
|
'leverage': None,
|
|
'marginRatio': None,
|
|
})
|
|
|
|
async def fetch_my_trades_ws(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/account-requests#account-trade-history-user_data
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int|None [since]: the earliest time in ms to fetch trades for
|
|
:param int|None [limit]: the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.endTime]: the latest time in ms to fetch trades for
|
|
:param int [params.fromId]: first trade Id to fetch
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
if symbol is None:
|
|
raise BadRequest(self.id + ' fetchMyTradesWs requires a symbol')
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('fetchMyTradesWs', market, params)
|
|
if type != 'spot' and type != 'future':
|
|
raise BadRequest(self.id + ' fetchMyTradesWs does not support ' + type + ' markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchMyTradesWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
if since is not None:
|
|
payload['startTime'] = since
|
|
if limit is not None:
|
|
payload['limit'] = limit
|
|
fromId = self.safe_integer(params, 'fromId')
|
|
if fromId is not None and since is not None:
|
|
raise BadRequest(self.id + ' fetchMyTradesWs does not support fetching by both fromId and since parameters at the same time')
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'myTrades',
|
|
'params': self.sign_params(self.extend(payload, params)),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_trades_ws,
|
|
}
|
|
trades = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit)
|
|
|
|
async def fetch_trades_ws(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://developers.binance.com/docs/binance-spot-api-docs/websocket-api/market-data-requests#recent-trades
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trades structures to retrieve, default=500, max=1000
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
|
|
EXCHANGE SPECIFIC PARAMETERS
|
|
:param int [params.fromId]: trade ID to begin at
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
type = self.get_market_type('fetchTradesWs', market, params)
|
|
if type != 'spot' and type != 'future':
|
|
raise BadRequest(self.id + ' fetchTradesWs does not support ' + type + ' markets')
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
requestId = self.request_id(url)
|
|
messageHash = str(requestId)
|
|
returnRateLimits = False
|
|
returnRateLimits, params = self.handle_option_and_params(params, 'fetchTradesWs', 'returnRateLimits', False)
|
|
payload: dict = {
|
|
'symbol': self.market_id(symbol),
|
|
'returnRateLimits': returnRateLimits,
|
|
}
|
|
if limit is not None:
|
|
payload['limit'] = limit
|
|
message: dict = {
|
|
'id': messageHash,
|
|
'method': 'trades.historical',
|
|
'params': self.extend(payload, params),
|
|
}
|
|
subscription: dict = {
|
|
'method': self.handle_trades_ws,
|
|
}
|
|
trades = await self.watch(url, messageHash, message, messageHash, subscription)
|
|
return self.filter_by_since_limit(trades, since, limit)
|
|
|
|
def handle_trades_ws(self, client: Client, message):
|
|
#
|
|
# fetchMyTradesWs
|
|
#
|
|
# {
|
|
# "id": "f4ce6a53-a29d-4f70-823b-4ab59391d6e8",
|
|
# "status": 200,
|
|
# "result": [
|
|
# {
|
|
# "symbol": "BTCUSDT",
|
|
# "id": 1650422481,
|
|
# "orderId": 12569099453,
|
|
# "orderListId": -1,
|
|
# "price": "23416.10000000",
|
|
# "qty": "0.00635000",
|
|
# "quoteQty": "148.69223500",
|
|
# "commission": "0.00000000",
|
|
# "commissionAsset": "BNB",
|
|
# "time": 1660801715793,
|
|
# "isBuyer": False,
|
|
# "isMaker": True,
|
|
# "isBestMatch": True
|
|
# },
|
|
# ...
|
|
# ],
|
|
# }
|
|
#
|
|
# fetchTradesWs
|
|
#
|
|
# {
|
|
# "id": "f4ce6a53-a29d-4f70-823b-4ab59391d6e8",
|
|
# "status": 200,
|
|
# "result": [
|
|
# {
|
|
# "id": 0,
|
|
# "price": "0.00005000",
|
|
# "qty": "40.00000000",
|
|
# "quoteQty": "0.00200000",
|
|
# "time": 1500004800376,
|
|
# "isBuyerMaker": True,
|
|
# "isBestMatch": True
|
|
# }
|
|
# ...
|
|
# ],
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'id')
|
|
result = self.safe_list(message, 'result', [])
|
|
trades = self.parse_trades(result)
|
|
client.resolve(trades, messageHash)
|
|
|
|
async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
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 boolean [params.portfolioMargin]: set to True if you would like to watch trades in a portfolio margin account
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
type = None
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
type, params = self.handle_market_type_and_params('watchMyTrades', market, params)
|
|
subType = None
|
|
subType, params = self.handle_sub_type_and_params('watchMyTrades', market, params)
|
|
if self.isLinear(type, subType):
|
|
type = 'future'
|
|
elif self.isInverse(type, subType):
|
|
type = 'delivery'
|
|
messageHash = 'myTrades'
|
|
if symbol is not None:
|
|
symbol = self.symbol(symbol)
|
|
messageHash += ':' + symbol
|
|
params = self.extend(params, {'type': market['type'], 'symbol': symbol})
|
|
await self.authenticate(self.extend({'type': type, 'subType': subType}, params))
|
|
urlType = type # we don't change type because the listening key is different
|
|
if type == 'margin':
|
|
urlType = 'spot' # spot-margin shares the same stream spot
|
|
isPortfolioMargin = None
|
|
isPortfolioMargin, params = self.handle_option_and_params_2(params, 'watchMyTrades', 'papi', 'portfolioMargin', False)
|
|
url = ''
|
|
if type == 'spot':
|
|
url = self.urls['api']['ws']['ws-api'][type]
|
|
else:
|
|
if isPortfolioMargin:
|
|
urlType = 'papi'
|
|
url = self.urls['api']['ws'][urlType] + '/' + self.options[type]['listenKey']
|
|
client = self.client(url)
|
|
self.set_balance_cache(client, type, isPortfolioMargin)
|
|
self.set_positions_cache(client, type, None, isPortfolioMargin)
|
|
message = None
|
|
trades = await self.watch(url, messageHash, message, type)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_my_trade(self, client: Client, message):
|
|
messageHash = 'myTrades'
|
|
executionType = self.safe_string(message, 'x')
|
|
if executionType == 'TRADE':
|
|
trade = self.parse_ws_trade(message)
|
|
orderId = self.safe_string(trade, 'order')
|
|
tradeFee = self.safe_dict(trade, 'fee', {})
|
|
tradeFee = self.extend({}, tradeFee)
|
|
symbol = self.safe_string(trade, 'symbol')
|
|
if orderId is not None and tradeFee is not None and symbol is not None:
|
|
cachedOrders = self.orders
|
|
if cachedOrders is not None:
|
|
orders = self.safe_value(cachedOrders.hashmap, symbol, {})
|
|
order = self.safe_value(orders, orderId)
|
|
if order is not None:
|
|
# accumulate order fees
|
|
fees = self.safe_value(order, 'fees')
|
|
fee = self.safe_value(order, 'fee')
|
|
if not self.is_empty(fees):
|
|
insertNewFeeCurrency = True
|
|
for i in range(0, len(fees)):
|
|
orderFee = fees[i]
|
|
if orderFee['currency'] == tradeFee['currency']:
|
|
feeCost = self.sum(tradeFee['cost'], orderFee['cost'])
|
|
order['fees'][i]['cost'] = float(self.currency_to_precision(tradeFee['currency'], feeCost))
|
|
insertNewFeeCurrency = False
|
|
break
|
|
if insertNewFeeCurrency:
|
|
order['fees'].append(tradeFee)
|
|
elif fee is not None:
|
|
if fee['currency'] == tradeFee['currency']:
|
|
feeCost = self.sum(fee['cost'], tradeFee['cost'])
|
|
order['fee']['cost'] = float(self.currency_to_precision(tradeFee['currency'], feeCost))
|
|
elif fee['currency'] is None:
|
|
order['fee'] = tradeFee
|
|
else:
|
|
order['fees'] = [fee, tradeFee]
|
|
order['fee'] = None
|
|
else:
|
|
order['fee'] = tradeFee
|
|
# save self trade in the order
|
|
orderTrades = self.safe_list(order, 'trades', [])
|
|
orderTrades.append(trade)
|
|
order['trades'] = orderTrades
|
|
# don't append twice cause it breaks newUpdates mode
|
|
# self order already exists in the cache
|
|
if self.myTrades is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
self.myTrades = ArrayCacheBySymbolById(limit)
|
|
myTrades = self.myTrades
|
|
myTrades.append(trade)
|
|
client.resolve(self.myTrades, messageHash)
|
|
messageHashSymbol = messageHash + ':' + symbol
|
|
client.resolve(self.myTrades, messageHashSymbol)
|
|
|
|
def handle_order(self, client: Client, message):
|
|
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')
|
|
timestamp = self.safe_integer(parsed, 'timestamp')
|
|
if timestamp is None:
|
|
parsed['timestamp'] = self.safe_integer(order, 'timestamp')
|
|
parsed['datetime'] = self.safe_string(order, 'datetime')
|
|
cachedOrders.append(parsed)
|
|
messageHash = 'orders'
|
|
symbolSpecificMessageHash = 'orders:' + symbol
|
|
client.resolve(cachedOrders, messageHash)
|
|
client.resolve(cachedOrders, symbolSpecificMessageHash)
|
|
|
|
def handle_acount_update(self, client, message):
|
|
self.handle_balance(client, message)
|
|
self.handle_positions(client, message)
|
|
|
|
def handle_ws_error(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "error": {
|
|
# "code": 2,
|
|
# "msg": "Invalid request: invalid stream"
|
|
# },
|
|
# "id": 1
|
|
# }
|
|
#
|
|
id = self.safe_string(message, 'id')
|
|
rejected = False
|
|
error = self.safe_dict(message, 'error', {})
|
|
code = self.safe_integer(error, 'code')
|
|
msg = self.safe_string(error, 'msg')
|
|
try:
|
|
self.handle_errors(code, msg, client.url, '', {}, self.json(error), error, {}, {})
|
|
except Exception as e:
|
|
rejected = True
|
|
# private endpoint uses id
|
|
client.reject(e, id)
|
|
# public endpoint stores messageHash in subscriptions
|
|
subscriptionKeys = list(client.subscriptions.keys())
|
|
for i in range(0, len(subscriptionKeys)):
|
|
subscriptionHash = subscriptionKeys[i]
|
|
subscriptionId = self.safe_string(client.subscriptions[subscriptionHash], 'id')
|
|
subscription = self.safe_string(client.subscriptions[subscriptionHash], 'subscription')
|
|
if id == subscriptionId:
|
|
client.reject(e, subscriptionHash)
|
|
if subscription is not None:
|
|
del client.subscriptions[subscription]
|
|
if not rejected:
|
|
client.reject(message, id)
|
|
# reset connection if 5xx error
|
|
codeString = self.safe_string(error, 'code')
|
|
if (codeString is not None) and (codeString[0] == '5'):
|
|
client.reset(message)
|
|
|
|
def handle_event_stream_terminated(self, client: Client, message):
|
|
#
|
|
# {
|
|
# e: 'eventStreamTerminated',
|
|
# E: 1757896885229
|
|
# }
|
|
#
|
|
event = self.safe_string(message, 'e')
|
|
subscriptions = client.subscriptions
|
|
subscriptionsKeys = list(subscriptions.keys())
|
|
accountType = self.get_account_type_from_subscriptions(subscriptionsKeys)
|
|
if event == 'eventStreamTerminated':
|
|
del client.subscriptions[accountType]
|
|
client.reject(message, accountType)
|
|
|
|
def handle_message(self, client: Client, message):
|
|
# handle WebSocketAPI
|
|
eventMsg = self.safe_dict(message, 'event')
|
|
if eventMsg is not None:
|
|
message = eventMsg
|
|
status = self.safe_string(message, 'status')
|
|
error = self.safe_value(message, 'error')
|
|
if (error is not None) or (status is not None and status != '200'):
|
|
self.handle_ws_error(client, message)
|
|
return
|
|
# user subscription wraps message in subscriptionId and event
|
|
id = self.safe_string(message, 'id')
|
|
subscriptions = self.safe_value(client.subscriptions, id)
|
|
method = self.safe_value(subscriptions, 'method')
|
|
if method is not None:
|
|
method(client, message)
|
|
return
|
|
# handle other APIs
|
|
methods: dict = {
|
|
'depthUpdate': self.handle_order_book,
|
|
'trade': self.handle_trade,
|
|
'aggTrade': self.handle_trade,
|
|
'kline': self.handle_ohlcv,
|
|
'markPrice_kline': self.handle_ohlcv,
|
|
'indexPrice_kline': self.handle_ohlcv,
|
|
'1hTicker@arr': self.handle_tickers,
|
|
'4hTicker@arr': self.handle_tickers,
|
|
'1dTicker@arr': self.handle_tickers,
|
|
'24hrTicker@arr': self.handle_tickers,
|
|
'24hrMiniTicker@arr': self.handle_tickers,
|
|
'1hTicker': self.handle_tickers,
|
|
'4hTicker': self.handle_tickers,
|
|
'1dTicker': self.handle_tickers,
|
|
'24hrTicker': self.handle_tickers,
|
|
'24hrMiniTicker': self.handle_tickers,
|
|
'markPriceUpdate': self.handle_mark_prices,
|
|
'markPriceUpdate@arr': self.handle_mark_prices,
|
|
'bookTicker': self.handle_bids_asks, # there is no "bookTicker@arr" endpoint
|
|
'outboundAccountPosition': self.handle_balance,
|
|
'balanceUpdate': self.handle_balance,
|
|
'ACCOUNT_UPDATE': self.handle_acount_update,
|
|
'executionReport': self.handle_order_update,
|
|
'ORDER_TRADE_UPDATE': self.handle_order_update,
|
|
'forceOrder': self.handle_liquidation,
|
|
'eventStreamTerminated': self.handle_event_stream_terminated,
|
|
'externalLockUpdate': self.handle_balance,
|
|
}
|
|
event = self.safe_string(message, 'e')
|
|
if isinstance(message, list):
|
|
data = message[0]
|
|
event = self.safe_string(data, 'e') + '@arr'
|
|
method = self.safe_value(methods, event)
|
|
if method is None:
|
|
requestId = self.safe_string(message, 'id')
|
|
if requestId is not None:
|
|
self.handle_subscription_status(client, message)
|
|
return
|
|
# special case for the real-time bookTicker, since it comes without an event identifier
|
|
#
|
|
# {
|
|
# "u": 7488717758,
|
|
# "s": "BTCUSDT",
|
|
# "b": "28621.74000000",
|
|
# "B": "1.43278800",
|
|
# "a": "28621.75000000",
|
|
# "A": "2.52500800"
|
|
# }
|
|
#
|
|
if event is None and ('a' in message) and ('b' in message):
|
|
self.handle_bids_asks(client, message)
|
|
else:
|
|
method(client, message)
|