1695 lines
73 KiB
Python
1695 lines
73 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
import ccxt.async_support
|
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Bool, Int, Liquidation, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
|
|
|
|
class bitmex(ccxt.async_support.bitmex):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(bitmex, self).describe(), {
|
|
'has': {
|
|
'ws': True,
|
|
'watchBalance': True,
|
|
'watchLiquidations': True,
|
|
'watchLiquidationsForSymbols': True,
|
|
'watchMyLiquidations': None,
|
|
'watchMyLiquidationsForSymbols': None,
|
|
'watchMyTrades': True,
|
|
'watchOHLCV': True,
|
|
'watchOrderBook': True,
|
|
'watchOrderBookForSymbols': True,
|
|
'watchOrders': True,
|
|
'watchPostions': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
},
|
|
'urls': {
|
|
'test': {
|
|
'ws': 'wss://ws.testnet.bitmex.com/realtime',
|
|
},
|
|
'api': {
|
|
'ws': 'wss://ws.bitmex.com/realtime',
|
|
},
|
|
},
|
|
# 'versions': {
|
|
# 'ws': '0.2.0',
|
|
# },
|
|
'options': {
|
|
'watchOrderBookLevel': 'orderBookL2', # 'orderBookL2' = L2 full order book, 'orderBookL2_25' = L2 top 25, 'orderBook10' L3 top 10
|
|
'tradesLimit': 1000,
|
|
'OHLCVLimit': 1000,
|
|
},
|
|
'exceptions': {
|
|
'ws': {
|
|
'exact': {
|
|
},
|
|
'broad': {
|
|
'Rate limit exceeded': RateLimitExceeded,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
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://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
tickers = await self.watch_tickers([symbol], params)
|
|
return tickers[symbol]
|
|
|
|
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://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
: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)
|
|
name = 'instrument'
|
|
url = self.urls['api']['ws']
|
|
messageHashes = []
|
|
rawSubscriptions = []
|
|
if symbols is not None:
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
subscription = name + ':' + market['id']
|
|
rawSubscriptions.append(subscription)
|
|
messageHash = 'ticker:' + symbol
|
|
messageHashes.append(messageHash)
|
|
else:
|
|
rawSubscriptions.append(name)
|
|
messageHashes.append('alltickers')
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': rawSubscriptions,
|
|
}
|
|
ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), rawSubscriptions)
|
|
if self.newUpdates:
|
|
result: dict = {}
|
|
result[ticker['symbol']] = ticker
|
|
return result
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "table": "instrument",
|
|
# "action": "partial",
|
|
# "keys": ["symbol"],
|
|
# "types": {
|
|
# "symbol": "symbol",
|
|
# "rootSymbol": "symbol",
|
|
# "state": "symbol",
|
|
# "typ": "symbol",
|
|
# "listing": "timestamp",
|
|
# "front": "timestamp",
|
|
# "expiry": "timestamp",
|
|
# "settle": "timestamp",
|
|
# "relistInterval": "timespan",
|
|
# "inverseLeg": "symbol",
|
|
# "sellLeg": "symbol",
|
|
# "buyLeg": "symbol",
|
|
# "optionStrikePcnt": "float",
|
|
# "optionStrikeRound": "float",
|
|
# "optionStrikePrice": "float",
|
|
# "optionMultiplier": "float",
|
|
# "positionCurrency": "symbol",
|
|
# "underlying": "symbol",
|
|
# "quoteCurrency": "symbol",
|
|
# "underlyingSymbol": "symbol",
|
|
# "reference": "symbol",
|
|
# "referenceSymbol": "symbol",
|
|
# "calcInterval": "timespan",
|
|
# "publishInterval": "timespan",
|
|
# "publishTime": "timespan",
|
|
# "maxOrderQty": "long",
|
|
# "maxPrice": "float",
|
|
# "lotSize": "long",
|
|
# "tickSize": "float",
|
|
# "multiplier": "long",
|
|
# "settlCurrency": "symbol",
|
|
# "underlyingToPositionMultiplier": "long",
|
|
# "underlyingToSettleMultiplier": "long",
|
|
# "quoteToSettleMultiplier": "long",
|
|
# "isQuanto": "boolean",
|
|
# "isInverse": "boolean",
|
|
# "initMargin": "float",
|
|
# "maintMargin": "float",
|
|
# "riskLimit": "long",
|
|
# "riskStep": "long",
|
|
# "limit": "float",
|
|
# "capped": "boolean",
|
|
# "taxed": "boolean",
|
|
# "deleverage": "boolean",
|
|
# "makerFee": "float",
|
|
# "takerFee": "float",
|
|
# "settlementFee": "float",
|
|
# "insuranceFee": "float",
|
|
# "fundingBaseSymbol": "symbol",
|
|
# "fundingQuoteSymbol": "symbol",
|
|
# "fundingPremiumSymbol": "symbol",
|
|
# "fundingTimestamp": "timestamp",
|
|
# "fundingInterval": "timespan",
|
|
# "fundingRate": "float",
|
|
# "indicativeFundingRate": "float",
|
|
# "rebalanceTimestamp": "timestamp",
|
|
# "rebalanceInterval": "timespan",
|
|
# "openingTimestamp": "timestamp",
|
|
# "closingTimestamp": "timestamp",
|
|
# "sessionInterval": "timespan",
|
|
# "prevClosePrice": "float",
|
|
# "limitDownPrice": "float",
|
|
# "limitUpPrice": "float",
|
|
# "bankruptLimitDownPrice": "float",
|
|
# "bankruptLimitUpPrice": "float",
|
|
# "prevTotalVolume": "long",
|
|
# "totalVolume": "long",
|
|
# "volume": "long",
|
|
# "volume24h": "long",
|
|
# "prevTotalTurnover": "long",
|
|
# "totalTurnover": "long",
|
|
# "turnover": "long",
|
|
# "turnover24h": "long",
|
|
# "homeNotional24h": "float",
|
|
# "foreignNotional24h": "float",
|
|
# "prevPrice24h": "float",
|
|
# "vwap": "float",
|
|
# "highPrice": "float",
|
|
# "lowPrice": "float",
|
|
# "lastPrice": "float",
|
|
# "lastPriceProtected": "float",
|
|
# "lastTickDirection": "symbol",
|
|
# "lastChangePcnt": "float",
|
|
# "bidPrice": "float",
|
|
# "midPrice": "float",
|
|
# "askPrice": "float",
|
|
# "impactBidPrice": "float",
|
|
# "impactMidPrice": "float",
|
|
# "impactAskPrice": "float",
|
|
# "hasLiquidity": "boolean",
|
|
# "openInterest": "long",
|
|
# "openValue": "long",
|
|
# "fairMethod": "symbol",
|
|
# "fairBasisRate": "float",
|
|
# "fairBasis": "float",
|
|
# "fairPrice": "float",
|
|
# "markMethod": "symbol",
|
|
# "markPrice": "float",
|
|
# "indicativeTaxRate": "float",
|
|
# "indicativeSettlePrice": "float",
|
|
# "optionUnderlyingPrice": "float",
|
|
# "settledPrice": "float",
|
|
# "timestamp": "timestamp"
|
|
# },
|
|
# "foreignKeys": {
|
|
# "inverseLeg": "instrument",
|
|
# "sellLeg": "instrument",
|
|
# "buyLeg": "instrument"
|
|
# },
|
|
# "attributes": {symbol: "unique"},
|
|
# "filter": {symbol: "XBTUSD"},
|
|
# "data": [
|
|
# {
|
|
# "symbol": "XBTUSD",
|
|
# "rootSymbol": "XBT",
|
|
# "state": "Open",
|
|
# "typ": "FFWCSX",
|
|
# "listing": "2016-05-13T12:00:00.000Z",
|
|
# "front": "2016-05-13T12:00:00.000Z",
|
|
# "expiry": null,
|
|
# "settle": null,
|
|
# "relistInterval": null,
|
|
# "inverseLeg": '',
|
|
# "sellLeg": '',
|
|
# "buyLeg": '',
|
|
# "optionStrikePcnt": null,
|
|
# "optionStrikeRound": null,
|
|
# "optionStrikePrice": null,
|
|
# "optionMultiplier": null,
|
|
# "positionCurrency": "USD",
|
|
# "underlying": "XBT",
|
|
# "quoteCurrency": "USD",
|
|
# "underlyingSymbol": "XBT=",
|
|
# "reference": "BMEX",
|
|
# "referenceSymbol": ".BXBT",
|
|
# "calcInterval": null,
|
|
# "publishInterval": null,
|
|
# "publishTime": null,
|
|
# "maxOrderQty": 10000000,
|
|
# "maxPrice": 1000000,
|
|
# "lotSize": 1,
|
|
# "tickSize": 0.5,
|
|
# "multiplier": -100000000,
|
|
# "settlCurrency": "XBt",
|
|
# "underlyingToPositionMultiplier": null,
|
|
# "underlyingToSettleMultiplier": -100000000,
|
|
# "quoteToSettleMultiplier": null,
|
|
# "isQuanto": False,
|
|
# "isInverse": True,
|
|
# "initMargin": 0.01,
|
|
# "maintMargin": 0.005,
|
|
# "riskLimit": 20000000000,
|
|
# "riskStep": 10000000000,
|
|
# "limit": null,
|
|
# "capped": False,
|
|
# "taxed": True,
|
|
# "deleverage": True,
|
|
# "makerFee": -0.00025,
|
|
# "takerFee": 0.00075,
|
|
# "settlementFee": 0,
|
|
# "insuranceFee": 0,
|
|
# "fundingBaseSymbol": ".XBTBON8H",
|
|
# "fundingQuoteSymbol": ".USDBON8H",
|
|
# "fundingPremiumSymbol": ".XBTUSDPI8H",
|
|
# "fundingTimestamp": "2020-01-29T12:00:00.000Z",
|
|
# "fundingInterval": "2000-01-01T08:00:00.000Z",
|
|
# "fundingRate": 0.000597,
|
|
# "indicativeFundingRate": 0.000652,
|
|
# "rebalanceTimestamp": null,
|
|
# "rebalanceInterval": null,
|
|
# "openingTimestamp": "2020-01-29T11:00:00.000Z",
|
|
# "closingTimestamp": "2020-01-29T12:00:00.000Z",
|
|
# "sessionInterval": "2000-01-01T01:00:00.000Z",
|
|
# "prevClosePrice": 9063.96,
|
|
# "limitDownPrice": null,
|
|
# "limitUpPrice": null,
|
|
# "bankruptLimitDownPrice": null,
|
|
# "bankruptLimitUpPrice": null,
|
|
# "prevTotalVolume": 1989881049026,
|
|
# "totalVolume": 1990196740950,
|
|
# "volume": 315691924,
|
|
# "volume24h": 4491824765,
|
|
# "prevTotalTurnover": 27865497128425564,
|
|
# "totalTurnover": 27868891594857150,
|
|
# "turnover": 3394466431587,
|
|
# "turnover24h": 48863390064843,
|
|
# "homeNotional24h": 488633.9006484273,
|
|
# "foreignNotional24h": 4491824765,
|
|
# "prevPrice24h": 9091,
|
|
# "vwap": 9192.8663,
|
|
# "highPrice": 9440,
|
|
# "lowPrice": 8886,
|
|
# "lastPrice": 9287,
|
|
# "lastPriceProtected": 9287,
|
|
# "lastTickDirection": "PlusTick",
|
|
# "lastChangePcnt": 0.0216,
|
|
# "bidPrice": 9286,
|
|
# "midPrice": 9286.25,
|
|
# "askPrice": 9286.5,
|
|
# "impactBidPrice": 9285.9133,
|
|
# "impactMidPrice": 9286.75,
|
|
# "impactAskPrice": 9287.6382,
|
|
# "hasLiquidity": True,
|
|
# "openInterest": 967826984,
|
|
# "openValue": 10432207060536,
|
|
# "fairMethod": "FundingRate",
|
|
# "fairBasisRate": 0.6537149999999999,
|
|
# "fairBasis": 0.33,
|
|
# "fairPrice": 9277.2,
|
|
# "markMethod": "FairPrice",
|
|
# "markPrice": 9277.2,
|
|
# "indicativeTaxRate": 0,
|
|
# "indicativeSettlePrice": 9276.87,
|
|
# "optionUnderlyingPrice": null,
|
|
# "settledPrice": null,
|
|
# "timestamp": "2020-01-29T11:31:37.114Z"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(message, 'data', [])
|
|
tickers: dict = {}
|
|
for i in range(0, len(data)):
|
|
update = data[i]
|
|
marketId = self.safe_string(update, 'symbol')
|
|
symbol = self.safe_symbol(marketId)
|
|
if not (symbol in self.tickers):
|
|
self.tickers[symbol] = self.parse_ticker({})
|
|
updatedTicker = self.parse_ticker(update)
|
|
fullParsedTicker = self.deep_extend(self.tickers[symbol], updatedTicker)
|
|
tickers[symbol] = fullParsedTicker
|
|
self.tickers[symbol] = fullParsedTicker
|
|
messageHash = 'ticker:' + symbol
|
|
client.resolve(fullParsedTicker, messageHash)
|
|
client.resolve(fullParsedTicker, 'alltickers')
|
|
return message
|
|
|
|
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://www.bitmex.com/app/wsAPI#Liquidation
|
|
|
|
: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_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://www.bitmex.com/app/wsAPI#Liquidation
|
|
|
|
:param str[] 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)
|
|
messageHashes = []
|
|
subscriptionHashes = []
|
|
if self.is_empty(symbols):
|
|
subscriptionHashes.append('liquidation')
|
|
messageHashes.append('liquidations')
|
|
else:
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
subscriptionHashes.append('liquidation:' + market['id'])
|
|
messageHashes.append('liquidations::' + symbol)
|
|
url = self.urls['api']['ws']
|
|
request = {
|
|
'op': 'subscribe',
|
|
'args': subscriptionHashes,
|
|
}
|
|
newLiquidations = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), subscriptionHashes)
|
|
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):
|
|
#
|
|
# {
|
|
# "table":"liquidation",
|
|
# "action":"partial",
|
|
# "keys":[
|
|
# "orderID"
|
|
# ],
|
|
# "types":{
|
|
# "orderID":"guid",
|
|
# "symbol":"symbol",
|
|
# "side":"symbol",
|
|
# "price":"float",
|
|
# "leavesQty":"long"
|
|
# },
|
|
# "filter":{},
|
|
# "data":[
|
|
# {
|
|
# "orderID":"e0a568ee-7830-4428-92c3-73e82b9576ce",
|
|
# "symbol":"XPLAUSDT",
|
|
# "side":"Sell",
|
|
# "price":0.206,
|
|
# "leavesQty":340
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
rawLiquidations = self.safe_value(message, 'data', [])
|
|
newLiquidations = []
|
|
for i in range(0, len(rawLiquidations)):
|
|
rawLiquidation = rawLiquidations[i]
|
|
liquidation = self.parse_liquidation(rawLiquidation)
|
|
symbol = liquidation['symbol']
|
|
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
|
|
newLiquidations.append(liquidation)
|
|
client.resolve(newLiquidations, 'liquidations')
|
|
liquidationsBySymbol = self.index_by(newLiquidations, 'symbol')
|
|
symbols = list(liquidationsBySymbol.keys())
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
client.resolve(liquidationsBySymbol[symbol], 'liquidations::' + symbol)
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
watch balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.authenticate()
|
|
messageHash = 'margin'
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [
|
|
messageHash,
|
|
],
|
|
}
|
|
return await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "table": "margin",
|
|
# "action": "partial",
|
|
# "keys": ["account"],
|
|
# "types": {
|
|
# "account": "long",
|
|
# "currency": "symbol",
|
|
# "riskLimit": "long",
|
|
# "prevState": "symbol",
|
|
# "state": "symbol",
|
|
# "action": "symbol",
|
|
# "amount": "long",
|
|
# "pendingCredit": "long",
|
|
# "pendingDebit": "long",
|
|
# "confirmedDebit": "long",
|
|
# "prevRealisedPnl": "long",
|
|
# "prevUnrealisedPnl": "long",
|
|
# "grossComm": "long",
|
|
# "grossOpenCost": "long",
|
|
# "grossOpenPremium": "long",
|
|
# "grossExecCost": "long",
|
|
# "grossMarkValue": "long",
|
|
# "riskValue": "long",
|
|
# "taxableMargin": "long",
|
|
# "initMargin": "long",
|
|
# "maintMargin": "long",
|
|
# "sessionMargin": "long",
|
|
# "targetExcessMargin": "long",
|
|
# "varMargin": "long",
|
|
# "realisedPnl": "long",
|
|
# "unrealisedPnl": "long",
|
|
# "indicativeTax": "long",
|
|
# "unrealisedProfit": "long",
|
|
# "syntheticMargin": "long",
|
|
# "walletBalance": "long",
|
|
# "marginBalance": "long",
|
|
# "marginBalancePcnt": "float",
|
|
# "marginLeverage": "float",
|
|
# "marginUsedPcnt": "float",
|
|
# "excessMargin": "long",
|
|
# "excessMarginPcnt": "float",
|
|
# "availableMargin": "long",
|
|
# "withdrawableMargin": "long",
|
|
# "timestamp": "timestamp",
|
|
# "grossLastValue": "long",
|
|
# "commission": "float"
|
|
# },
|
|
# "foreignKeys": {},
|
|
# "attributes": {account: "sorted"},
|
|
# "filter": {account: 1455728},
|
|
# "data": [
|
|
# {
|
|
# "account": 1455728,
|
|
# "currency": "XBt",
|
|
# "riskLimit": 1000000000000,
|
|
# "prevState": '',
|
|
# "state": '',
|
|
# "action": '',
|
|
# "amount": 263542,
|
|
# "pendingCredit": 0,
|
|
# "pendingDebit": 0,
|
|
# "confirmedDebit": 0,
|
|
# "prevRealisedPnl": 0,
|
|
# "prevUnrealisedPnl": 0,
|
|
# "grossComm": 0,
|
|
# "grossOpenCost": 0,
|
|
# "grossOpenPremium": 0,
|
|
# "grossExecCost": 0,
|
|
# "grossMarkValue": 0,
|
|
# "riskValue": 0,
|
|
# "taxableMargin": 0,
|
|
# "initMargin": 0,
|
|
# "maintMargin": 0,
|
|
# "sessionMargin": 0,
|
|
# "targetExcessMargin": 0,
|
|
# "varMargin": 0,
|
|
# "realisedPnl": 0,
|
|
# "unrealisedPnl": 0,
|
|
# "indicativeTax": 0,
|
|
# "unrealisedProfit": 0,
|
|
# "syntheticMargin": null,
|
|
# "walletBalance": 263542,
|
|
# "marginBalance": 263542,
|
|
# "marginBalancePcnt": 1,
|
|
# "marginLeverage": 0,
|
|
# "marginUsedPcnt": 0,
|
|
# "excessMargin": 263542,
|
|
# "excessMarginPcnt": 1,
|
|
# "availableMargin": 263542,
|
|
# "withdrawableMargin": 263542,
|
|
# "timestamp": "2020-08-03T12:01:01.246Z",
|
|
# "grossLastValue": 0,
|
|
# "commission": null
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_value(message, 'data')
|
|
balance = self.parse_balance(data)
|
|
self.balance = self.extend(self.balance, balance)
|
|
messageHash = self.safe_string(message, 'table')
|
|
client.resolve(self.balance, messageHash)
|
|
|
|
def handle_trades(self, client: Client, message):
|
|
#
|
|
# initial snapshot
|
|
#
|
|
# {
|
|
# "table": "trade",
|
|
# "action": "partial",
|
|
# "keys": [],
|
|
# "types": {
|
|
# "timestamp": "timestamp",
|
|
# "symbol": "symbol",
|
|
# "side": "symbol",
|
|
# "size": "long",
|
|
# "price": "float",
|
|
# "tickDirection": "symbol",
|
|
# "trdMatchID": "guid",
|
|
# "grossValue": "long",
|
|
# "homeNotional": "float",
|
|
# "foreignNotional": "float"
|
|
# },
|
|
# "foreignKeys": {symbol: "instrument", side: "side"},
|
|
# "attributes": {timestamp: "sorted", symbol: "grouped"},
|
|
# "filter": {symbol: "XBTUSD"},
|
|
# "data": [
|
|
# {
|
|
# "timestamp": "2020-01-30T17:03:07.854Z",
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Buy",
|
|
# "size": 15000,
|
|
# "price": 9378,
|
|
# "tickDirection": "ZeroPlusTick",
|
|
# "trdMatchID": "5b426e7f-83d1-2c80-295d-ee995b8ceb4a",
|
|
# "grossValue": 159945000,
|
|
# "homeNotional": 1.59945,
|
|
# "foreignNotional": 15000
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# updates
|
|
#
|
|
# {
|
|
# "table": "trade",
|
|
# "action": "insert",
|
|
# "data": [
|
|
# {
|
|
# "timestamp": "2020-01-30T17:31:40.160Z",
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Sell",
|
|
# "size": 37412,
|
|
# "price": 9521.5,
|
|
# "tickDirection": "ZeroMinusTick",
|
|
# "trdMatchID": "a4bfc6bc-6cf1-1a11-622e-270eef8ca5c7",
|
|
# "grossValue": 392938236,
|
|
# "homeNotional": 3.92938236,
|
|
# "foreignNotional": 37412
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
table = 'trade'
|
|
data = self.safe_value(message, 'data', [])
|
|
dataByMarketIds = self.group_by(data, 'symbol')
|
|
marketIds = list(dataByMarketIds.keys())
|
|
for i in range(0, len(marketIds)):
|
|
marketId = marketIds[i]
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
messageHash = table + ':' + symbol
|
|
trades = self.parse_trades(dataByMarketIds[marketId], market)
|
|
stored = self.safe_value(self.trades, symbol)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
stored = ArrayCache(limit)
|
|
self.trades[symbol] = stored
|
|
for j in range(0, len(trades)):
|
|
stored.append(trades[j])
|
|
client.resolve(stored, messageHash)
|
|
|
|
async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
get the list of most recent trades for a particular symbol
|
|
|
|
https://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
return await self.watch_trades_for_symbols([symbol], since, limit, params)
|
|
|
|
async def authenticate(self, params={}):
|
|
url = self.urls['api']['ws']
|
|
client = self.client(url)
|
|
messageHash = 'authenticated'
|
|
future = client.reusableFuture(messageHash)
|
|
authenticated = self.safe_value(client.subscriptions, messageHash)
|
|
if authenticated is None:
|
|
self.check_required_credentials()
|
|
timestamp = self.milliseconds()
|
|
payload = 'GET' + '/realtime' + str(timestamp)
|
|
signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256)
|
|
request: dict = {
|
|
'op': 'authKeyExpires',
|
|
'args': [
|
|
self.apiKey,
|
|
timestamp,
|
|
signature,
|
|
],
|
|
}
|
|
message = self.extend(request, params)
|
|
self.watch(url, messageHash, message, messageHash)
|
|
return await future
|
|
|
|
def handle_authentication_message(self, client: Client, message):
|
|
authenticated = self.safe_bool(message, 'success', False)
|
|
messageHash = 'authenticated'
|
|
if authenticated:
|
|
# we resolve the future here permanently so authentication only happens once
|
|
future = self.safe_value(client.futures, messageHash)
|
|
future.resolve(True)
|
|
else:
|
|
error = AuthenticationError(self.json(message))
|
|
client.reject(error, messageHash)
|
|
if messageHash in client.subscriptions:
|
|
del client.subscriptions[messageHash]
|
|
|
|
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
|
|
"""
|
|
watch all open positions
|
|
|
|
https://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param int [since]: the earliest time in ms to watch positions for
|
|
:param int [limit]: the maximum number of positions to retrieve
|
|
:param dict params: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.authenticate()
|
|
subscriptionHash = 'position'
|
|
messageHash = 'positions'
|
|
if not self.is_empty(symbols):
|
|
messageHash = '::' + ','.join(symbols)
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [
|
|
subscriptionHash,
|
|
],
|
|
}
|
|
newPositions = await self.watch(url, messageHash, request, subscriptionHash)
|
|
if self.newUpdates:
|
|
return newPositions
|
|
return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True)
|
|
|
|
def handle_positions(self, client, message):
|
|
#
|
|
# partial
|
|
# {
|
|
# table: 'position',
|
|
# action: 'partial',
|
|
# keys: ['account', 'symbol'],
|
|
# types: {
|
|
# account: 'long',
|
|
# symbol: 'symbol',
|
|
# currency: 'symbol',
|
|
# underlying: 'symbol',
|
|
# quoteCurrency: 'symbol',
|
|
# commission: 'float',
|
|
# initMarginReq: 'float',
|
|
# maintMarginReq: 'float',
|
|
# riskLimit: 'long',
|
|
# leverage: 'float',
|
|
# crossMargin: 'boolean',
|
|
# deleveragePercentile: 'float',
|
|
# rebalancedPnl: 'long',
|
|
# prevRealisedPnl: 'long',
|
|
# prevUnrealisedPnl: 'long',
|
|
# openingQty: 'long',
|
|
# openOrderBuyQty: 'long',
|
|
# openOrderBuyCost: 'long',
|
|
# openOrderBuyPremium: 'long',
|
|
# openOrderSellQty: 'long',
|
|
# openOrderSellCost: 'long',
|
|
# openOrderSellPremium: 'long',
|
|
# currentQty: 'long',
|
|
# currentCost: 'long',
|
|
# currentComm: 'long',
|
|
# realisedCost: 'long',
|
|
# unrealisedCost: 'long',
|
|
# grossOpenPremium: 'long',
|
|
# isOpen: 'boolean',
|
|
# markPrice: 'float',
|
|
# markValue: 'long',
|
|
# riskValue: 'long',
|
|
# homeNotional: 'float',
|
|
# foreignNotional: 'float',
|
|
# posState: 'symbol',
|
|
# posCost: 'long',
|
|
# posCross: 'long',
|
|
# posComm: 'long',
|
|
# posLoss: 'long',
|
|
# posMargin: 'long',
|
|
# posMaint: 'long',
|
|
# initMargin: 'long',
|
|
# maintMargin: 'long',
|
|
# realisedPnl: 'long',
|
|
# unrealisedPnl: 'long',
|
|
# unrealisedPnlPcnt: 'float',
|
|
# unrealisedRoePcnt: 'float',
|
|
# avgCostPrice: 'float',
|
|
# avgEntryPrice: 'float',
|
|
# breakEvenPrice: 'float',
|
|
# marginCallPrice: 'float',
|
|
# liquidationPrice: 'float',
|
|
# bankruptPrice: 'float',
|
|
# timestamp: 'timestamp'
|
|
# },
|
|
# filter: {account: 412475},
|
|
# data: [
|
|
# {
|
|
# account: 412475,
|
|
# symbol: 'XBTUSD',
|
|
# currency: 'XBt',
|
|
# underlying: 'XBT',
|
|
# quoteCurrency: 'USD',
|
|
# commission: 0.00075,
|
|
# initMarginReq: 0.01,
|
|
# maintMarginReq: 0.0035,
|
|
# riskLimit: 20000000000,
|
|
# leverage: 100,
|
|
# crossMargin: True,
|
|
# deleveragePercentile: 1,
|
|
# rebalancedPnl: 0,
|
|
# prevRealisedPnl: 0,
|
|
# prevUnrealisedPnl: 0,
|
|
# openingQty: 400,
|
|
# openOrderBuyQty: 0,
|
|
# openOrderBuyCost: 0,
|
|
# openOrderBuyPremium: 0,
|
|
# openOrderSellQty: 0,
|
|
# openOrderSellCost: 0,
|
|
# openOrderSellPremium: 0,
|
|
# currentQty: 400,
|
|
# currentCost: -912269,
|
|
# currentComm: 684,
|
|
# realisedCost: 0,
|
|
# unrealisedCost: -912269,
|
|
# grossOpenPremium: 0,
|
|
# isOpen: True,
|
|
# markPrice: 43772,
|
|
# markValue: -913828,
|
|
# riskValue: 913828,
|
|
# homeNotional: 0.00913828,
|
|
# foreignNotional: -400,
|
|
# posCost: -912269,
|
|
# posCross: 1559,
|
|
# posComm: 694,
|
|
# posLoss: 0,
|
|
# posMargin: 11376,
|
|
# posMaint: 3887,
|
|
# initMargin: 0,
|
|
# maintMargin: 9817,
|
|
# realisedPnl: -684,
|
|
# unrealisedPnl: -1559,
|
|
# unrealisedPnlPcnt: -0.0017,
|
|
# unrealisedRoePcnt: -0.1709,
|
|
# avgCostPrice: 43846.7643,
|
|
# avgEntryPrice: 43846.7643,
|
|
# breakEvenPrice: 43880,
|
|
# marginCallPrice: 20976,
|
|
# liquidationPrice: 20976,
|
|
# bankruptPrice: 20941,
|
|
# timestamp: '2023-12-07T00:09:00.709Z'
|
|
# }
|
|
# ]
|
|
# }
|
|
# update
|
|
# {
|
|
# table: 'position',
|
|
# action: 'update',
|
|
# data: [
|
|
# {
|
|
# account: 412475,
|
|
# symbol: 'XBTUSD',
|
|
# currency: 'XBt',
|
|
# currentQty: 400,
|
|
# markPrice: 43772.75,
|
|
# markValue: -913812,
|
|
# riskValue: 913812,
|
|
# homeNotional: 0.00913812,
|
|
# posCross: 1543,
|
|
# posComm: 693,
|
|
# posMargin: 11359,
|
|
# posMaint: 3886,
|
|
# maintMargin: 9816,
|
|
# unrealisedPnl: -1543,
|
|
# unrealisedRoePcnt: -0.1691,
|
|
# liquidationPrice: 20976,
|
|
# timestamp: '2023-12-07T00:09:10.760Z'
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
if self.positions is None:
|
|
self.positions = ArrayCacheBySymbolBySide()
|
|
cache = self.positions
|
|
rawPositions = self.safe_value(message, 'data', [])
|
|
newPositions = []
|
|
for i in range(0, len(rawPositions)):
|
|
rawPosition = rawPositions[i]
|
|
position = self.parse_position(rawPosition)
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
messageHashes = self.find_message_hashes(client, '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, 'positions')
|
|
|
|
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://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.authenticate()
|
|
name = 'order'
|
|
subscriptionHash = name
|
|
messageHash = name
|
|
if symbol is not None:
|
|
symbol = self.symbol(symbol)
|
|
messageHash += ':' + symbol
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [
|
|
subscriptionHash,
|
|
],
|
|
}
|
|
orders = await self.watch(url, messageHash, request, subscriptionHash)
|
|
if self.newUpdates:
|
|
limit = orders.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
|
|
|
|
def handle_orders(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "table": "order",
|
|
# "action": "partial",
|
|
# "keys": ["orderID"],
|
|
# "types": {
|
|
# "orderID": "guid",
|
|
# "clOrdID": "string",
|
|
# "clOrdLinkID": "symbol",
|
|
# "account": "long",
|
|
# "symbol": "symbol",
|
|
# "side": "symbol",
|
|
# "simpleOrderQty": "float",
|
|
# "orderQty": "long",
|
|
# "price": "float",
|
|
# "displayQty": "long",
|
|
# "stopPx": "float",
|
|
# "pegOffsetValue": "float",
|
|
# "pegPriceType": "symbol",
|
|
# "currency": "symbol",
|
|
# "settlCurrency": "symbol",
|
|
# "ordType": "symbol",
|
|
# "timeInForce": "symbol",
|
|
# "execInst": "symbol",
|
|
# "contingencyType": "symbol",
|
|
# "exDestination": "symbol",
|
|
# "ordStatus": "symbol",
|
|
# "triggered": "symbol",
|
|
# "workingIndicator": "boolean",
|
|
# "ordRejReason": "symbol",
|
|
# "simpleLeavesQty": "float",
|
|
# "leavesQty": "long",
|
|
# "simpleCumQty": "float",
|
|
# "cumQty": "long",
|
|
# "avgPx": "float",
|
|
# "multiLegReportingType": "symbol",
|
|
# "text": "string",
|
|
# "transactTime": "timestamp",
|
|
# "timestamp": "timestamp"
|
|
# },
|
|
# "foreignKeys": {symbol: 'instrument', side: "side", ordStatus: "ordStatus"},
|
|
# "attributes": {
|
|
# "orderID": "grouped",
|
|
# "account": "grouped",
|
|
# "ordStatus": "grouped",
|
|
# "workingIndicator": "grouped"
|
|
# },
|
|
# "filter": {account: 1455728},
|
|
# "data": [
|
|
# {
|
|
# "orderID": "56222c7a-9956-413a-82cf-99f4812c214b",
|
|
# "clOrdID": '',
|
|
# "clOrdLinkID": '',
|
|
# "account": 1455728,
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Sell",
|
|
# "simpleOrderQty": null,
|
|
# "orderQty": 1,
|
|
# "price": 40000,
|
|
# "displayQty": null,
|
|
# "stopPx": null,
|
|
# "pegOffsetValue": null,
|
|
# "pegPriceType": '',
|
|
# "currency": "USD",
|
|
# "settlCurrency": "XBt",
|
|
# "ordType": "Limit",
|
|
# "timeInForce": "GoodTillCancel",
|
|
# "execInst": '',
|
|
# "contingencyType": '',
|
|
# "exDestination": "XBME",
|
|
# "ordStatus": "New",
|
|
# "triggered": '',
|
|
# "workingIndicator": True,
|
|
# "ordRejReason": '',
|
|
# "simpleLeavesQty": null,
|
|
# "leavesQty": 1,
|
|
# "simpleCumQty": null,
|
|
# "cumQty": 0,
|
|
# "avgPx": null,
|
|
# "multiLegReportingType": "SingleSecurity",
|
|
# "text": "Submitted via API.",
|
|
# "transactTime": "2021-01-02T21:38:49.246Z",
|
|
# "timestamp": "2021-01-02T21:38:49.246Z"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# {
|
|
# "table": "order",
|
|
# "action": "insert",
|
|
# "data": [
|
|
# {
|
|
# "orderID": "fa993d8e-f7e4-46ed-8097-04f8e9393585",
|
|
# "clOrdID": '',
|
|
# "clOrdLinkID": '',
|
|
# "account": 1455728,
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Sell",
|
|
# "simpleOrderQty": null,
|
|
# "orderQty": 1,
|
|
# "price": 40000,
|
|
# "displayQty": null,
|
|
# "stopPx": null,
|
|
# "pegOffsetValue": null,
|
|
# "pegPriceType": '',
|
|
# "currency": "USD",
|
|
# "settlCurrency": "XBt",
|
|
# "ordType": "Limit",
|
|
# "timeInForce": "GoodTillCancel",
|
|
# "execInst": '',
|
|
# "contingencyType": '',
|
|
# "exDestination": "XBME",
|
|
# "ordStatus": "New",
|
|
# "triggered": '',
|
|
# "workingIndicator": True,
|
|
# "ordRejReason": '',
|
|
# "simpleLeavesQty": null,
|
|
# "leavesQty": 1,
|
|
# "simpleCumQty": null,
|
|
# "cumQty": 0,
|
|
# "avgPx": null,
|
|
# "multiLegReportingType": "SingleSecurity",
|
|
# "text": "Submitted via API.",
|
|
# "transactTime": "2021-01-02T23:49:02.286Z",
|
|
# "timestamp": "2021-01-02T23:49:02.286Z"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
#
|
|
#
|
|
# {
|
|
# "table": "order",
|
|
# "action": "update",
|
|
# "data": [
|
|
# {
|
|
# "orderID": "fa993d8e-f7e4-46ed-8097-04f8e9393585",
|
|
# "ordStatus": "Canceled",
|
|
# "workingIndicator": False,
|
|
# "leavesQty": 0,
|
|
# "text": "Canceled: Canceled via API.\nSubmitted via API.",
|
|
# "timestamp": "2021-01-02T23:50:51.272Z",
|
|
# "clOrdID": '',
|
|
# "account": 1455728,
|
|
# "symbol": "XBTUSD"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_value(message, 'data', [])
|
|
messageHash = 'order'
|
|
# initial subscription response with multiple orders
|
|
dataLength = len(data)
|
|
if dataLength > 0:
|
|
if self.orders is None:
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
self.orders = ArrayCacheBySymbolById(limit)
|
|
stored = self.orders
|
|
symbols: dict = {}
|
|
for i in range(0, dataLength):
|
|
currentOrder = data[i]
|
|
orderId = self.safe_string(currentOrder, 'orderID')
|
|
previousOrder = self.safe_value(stored.hashmap, orderId)
|
|
rawOrder = currentOrder
|
|
if previousOrder is not None:
|
|
rawOrder = self.extend(previousOrder['info'], currentOrder)
|
|
order = self.parse_order(rawOrder)
|
|
stored.append(order)
|
|
symbol = order['symbol']
|
|
symbols[symbol] = True
|
|
client.resolve(self.orders, messageHash)
|
|
keys = list(symbols.keys())
|
|
for i in range(0, len(keys)):
|
|
symbol = keys[i]
|
|
client.resolve(self.orders, messageHash + ':' + symbol)
|
|
|
|
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
|
|
|
|
https://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str symbol: unified market symbol of the market trades were made in
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: the maximum number of trade structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
await self.authenticate()
|
|
name = 'execution'
|
|
subscriptionHash = name
|
|
messageHash = name
|
|
if symbol is not None:
|
|
symbol = self.symbol(symbol)
|
|
messageHash += ':' + symbol
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [
|
|
subscriptionHash,
|
|
],
|
|
}
|
|
trades = await self.watch(url, messageHash, request, subscriptionHash)
|
|
if self.newUpdates:
|
|
limit = trades.getLimit(symbol, limit)
|
|
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
|
|
|
def handle_my_trades(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "table":"execution",
|
|
# "action":"insert",
|
|
# "data":[
|
|
# {
|
|
# "execID":"0193e879-cb6f-2891-d099-2c4eb40fee21",
|
|
# "orderID":"00000000-0000-0000-0000-000000000000",
|
|
# "clOrdID":"",
|
|
# "clOrdLinkID":"",
|
|
# "account":2,
|
|
# "symbol":"XBTUSD",
|
|
# "side":"Sell",
|
|
# "lastQty":1,
|
|
# "lastPx":1134.37,
|
|
# "underlyingLastPx":null,
|
|
# "lastMkt":"XBME",
|
|
# "lastLiquidityInd":"RemovedLiquidity",
|
|
# "simpleOrderQty":null,
|
|
# "orderQty":1,
|
|
# "price":1134.37,
|
|
# "displayQty":null,
|
|
# "stopPx":null,
|
|
# "pegOffsetValue":null,
|
|
# "pegPriceType":"",
|
|
# "currency":"USD",
|
|
# "settlCurrency":"XBt",
|
|
# "execType":"Trade",
|
|
# "ordType":"Limit",
|
|
# "timeInForce":"ImmediateOrCancel",
|
|
# "execInst":"",
|
|
# "contingencyType":"",
|
|
# "exDestination":"XBME",
|
|
# "ordStatus":"Filled",
|
|
# "triggered":"",
|
|
# "workingIndicator":false,
|
|
# "ordRejReason":"",
|
|
# "simpleLeavesQty":0,
|
|
# "leavesQty":0,
|
|
# "simpleCumQty":0.001,
|
|
# "cumQty":1,
|
|
# "avgPx":1134.37,
|
|
# "commission":0.00075,
|
|
# "tradePublishIndicator":"DoNotPublishTrade",
|
|
# "multiLegReportingType":"SingleSecurity",
|
|
# "text":"Liquidation",
|
|
# "trdMatchID":"7f4ab7f6-0006-3234-76f4-ae1385aad00f",
|
|
# "execCost":88155,
|
|
# "execComm":66,
|
|
# "homeNotional":-0.00088155,
|
|
# "foreignNotional":1,
|
|
# "transactTime":"2017-04-04T22:07:46.035Z",
|
|
# "timestamp":"2017-04-04T22:07:46.035Z"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
messageHash = self.safe_string(message, 'table')
|
|
data = self.safe_value(message, 'data', [])
|
|
dataByExecType = self.group_by(data, 'execType')
|
|
rawTrades = self.safe_value(dataByExecType, 'Trade', [])
|
|
trades = self.parse_trades(rawTrades)
|
|
if self.myTrades is None:
|
|
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
self.myTrades = ArrayCacheBySymbolById(limit)
|
|
stored = self.myTrades
|
|
symbols: dict = {}
|
|
for j in range(0, len(trades)):
|
|
trade = trades[j]
|
|
symbol = trade['symbol']
|
|
stored.append(trade)
|
|
symbols[symbol] = trade
|
|
numTrades = len(trades)
|
|
if numTrades > 0:
|
|
client.resolve(stored, messageHash)
|
|
keys = list(symbols.keys())
|
|
for i in range(0, len(keys)):
|
|
client.resolve(stored, messageHash + ':' + keys[i])
|
|
|
|
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://www.bitmex.com/app/wsAPI#OrderBookL2
|
|
|
|
: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
|
|
"""
|
|
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://www.bitmex.com/app/wsAPI#OrderBookL2
|
|
|
|
: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
|
|
"""
|
|
table = None
|
|
if limit is None:
|
|
table = self.safe_string(self.options, 'watchOrderBookLevel', 'orderBookL2')
|
|
elif limit == 25:
|
|
table = 'orderBookL2_25'
|
|
elif limit == 10:
|
|
table = 'orderBookL10'
|
|
else:
|
|
raise ExchangeError(self.id + ' watchOrderBookForSymbols limit argument must be None(L2), 25(L2) or 10(L3)')
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
topics = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
topic = table + ':' + market['id']
|
|
topics.append(topic)
|
|
messageHash = table + ':' + symbol
|
|
messageHashes.append(messageHash)
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': topics,
|
|
}
|
|
orderbook = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), topics)
|
|
return orderbook.limit()
|
|
|
|
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://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
table = 'trade'
|
|
topics = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
symbol = symbols[i]
|
|
market = self.market(symbol)
|
|
topic = table + ':' + market['id']
|
|
topics.append(topic)
|
|
messageHash = table + ':' + symbol
|
|
messageHashes.append(messageHash)
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': topics,
|
|
}
|
|
trades = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), topics)
|
|
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 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://www.bitmex.com/app/wsAPI#Subscriptions
|
|
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
symbol = market['symbol']
|
|
table = 'tradeBin' + self.safe_string(self.timeframes, timeframe, timeframe)
|
|
messageHash = table + ':' + market['id']
|
|
url = self.urls['api']['ws']
|
|
request: dict = {
|
|
'op': 'subscribe',
|
|
'args': [
|
|
messageHash,
|
|
],
|
|
}
|
|
ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash)
|
|
if self.newUpdates:
|
|
limit = ohlcv.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "table": "tradeBin1m",
|
|
# "action": "partial",
|
|
# "keys": [],
|
|
# "types": {
|
|
# "timestamp": "timestamp",
|
|
# "symbol": "symbol",
|
|
# "open": "float",
|
|
# "high": "float",
|
|
# "low": "float",
|
|
# "close": "float",
|
|
# "trades": "long",
|
|
# "volume": "long",
|
|
# "vwap": "float",
|
|
# "lastSize": "long",
|
|
# "turnover": "long",
|
|
# "homeNotional": "float",
|
|
# "foreignNotional": "float"
|
|
# },
|
|
# "foreignKeys": {symbol: "instrument"},
|
|
# "attributes": {timestamp: "sorted", symbol: "grouped"},
|
|
# "filter": {symbol: "XBTUSD"},
|
|
# "data": [
|
|
# {
|
|
# "timestamp": "2020-02-03T01:13:00.000Z",
|
|
# "symbol": "XBTUSD",
|
|
# "open": 9395,
|
|
# "high": 9395.5,
|
|
# "low": 9394.5,
|
|
# "close": 9395,
|
|
# "trades": 221,
|
|
# "volume": 839204,
|
|
# "vwap": 9394.9643,
|
|
# "lastSize": 1874,
|
|
# "turnover": 8932641535,
|
|
# "homeNotional": 89.32641534999999,
|
|
# "foreignNotional": 839204
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
#
|
|
# {
|
|
# "table": "tradeBin1m",
|
|
# "action": "insert",
|
|
# "data": [
|
|
# {
|
|
# "timestamp": "2020-02-03T18:28:00.000Z",
|
|
# "symbol": "XBTUSD",
|
|
# "open": 9256,
|
|
# "high": 9256.5,
|
|
# "low": 9256,
|
|
# "close": 9256,
|
|
# "trades": 29,
|
|
# "volume": 79057,
|
|
# "vwap": 9256.688,
|
|
# "lastSize": 100,
|
|
# "turnover": 854077082,
|
|
# "homeNotional": 8.540770820000002,
|
|
# "foreignNotional": 79057
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
table = self.safe_string(message, 'table')
|
|
interval = table.replace('tradeBin', '')
|
|
timeframe = self.find_timeframe(interval)
|
|
duration = self.parse_timeframe(timeframe)
|
|
candles = self.safe_value(message, 'data', [])
|
|
results: dict = {}
|
|
for i in range(0, len(candles)):
|
|
candle = candles[i]
|
|
marketId = self.safe_string(candle, 'symbol')
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
messageHash = table + ':' + market['id']
|
|
result = [
|
|
self.parse8601(self.safe_string(candle, 'timestamp')) - duration * 1000,
|
|
None, # set open price to None, see: https://github.com/ccxt/ccxt/pull/21356#issuecomment-1969565862
|
|
self.safe_float(candle, 'high'),
|
|
self.safe_float(candle, 'low'),
|
|
self.safe_float(candle, 'close'),
|
|
self.safe_float(candle, 'volume'),
|
|
]
|
|
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
|
stored = self.safe_value(self.ohlcvs[symbol], timeframe)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
|
stored = ArrayCacheByTimestamp(limit)
|
|
self.ohlcvs[symbol][timeframe] = stored
|
|
stored.append(result)
|
|
results[messageHash] = stored
|
|
messageHashes = list(results.keys())
|
|
for i in range(0, len(messageHashes)):
|
|
messageHash = messageHashes[i]
|
|
client.resolve(results[messageHash], messageHash)
|
|
|
|
async def watch_heartbeat(self, params={}):
|
|
await self.load_markets()
|
|
event = 'heartbeat'
|
|
url = self.urls['api']['ws']
|
|
return await self.watch(url, event)
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# first snapshot
|
|
#
|
|
# {
|
|
# "table": "orderBookL2",
|
|
# "action": "partial",
|
|
# "keys": ['symbol', "id", "side"],
|
|
# "types": {
|
|
# "symbol": "symbol",
|
|
# "id": "long",
|
|
# "side": "symbol",
|
|
# "size": "long",
|
|
# "price": "float"
|
|
# },
|
|
# "foreignKeys": {symbol: "instrument", side: "side"},
|
|
# "attributes": {symbol: "parted", id: "sorted"},
|
|
# "filter": {symbol: "XBTUSD"},
|
|
# "data": [
|
|
# {symbol: "XBTUSD", id: 8700000100, side: "Sell", size: 1, price: 999999},
|
|
# {symbol: "XBTUSD", id: 8700000200, side: "Sell", size: 3, price: 999998},
|
|
# {symbol: "XBTUSD", id: 8716991250, side: "Sell", size: 26, price: 830087.5},
|
|
# {symbol: "XBTUSD", id: 8728701950, side: "Sell", size: 1720, price: 712980.5},
|
|
# ]
|
|
# }
|
|
#
|
|
# subsequent updates
|
|
#
|
|
# {
|
|
# "table": "orderBookL2",
|
|
# "action": "update",
|
|
# "data": [
|
|
# {
|
|
# "table": "orderBookL2",
|
|
# "action": "insert",
|
|
# "data": [
|
|
# {
|
|
# "symbol": "ETH_USDT",
|
|
# "id": 85499965912,
|
|
# "side": "Buy",
|
|
# "size": 83000000,
|
|
# "price": 1704.4,
|
|
# "timestamp": "2023-03-26T22:29:00.299Z"
|
|
# }
|
|
# ]
|
|
# }
|
|
# ...
|
|
# ]
|
|
# }
|
|
#
|
|
action = self.safe_string(message, 'action')
|
|
table = self.safe_string(message, 'table')
|
|
if table is None:
|
|
return # protecting from weird updates
|
|
data = self.safe_value(message, 'data', [])
|
|
# if it's an initial snapshot
|
|
if action == 'partial':
|
|
filter = self.safe_dict(message, 'filter', {})
|
|
marketId = self.safe_value(filter, 'symbol')
|
|
if marketId is None:
|
|
return # protecting from weird update
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
if table == 'orderBookL2':
|
|
self.orderbooks[symbol] = self.indexed_order_book()
|
|
elif table == 'orderBookL2_25':
|
|
self.orderbooks[symbol] = self.indexed_order_book({}, 25)
|
|
elif table == 'orderBook10':
|
|
self.orderbooks[symbol] = self.indexed_order_book({}, 10)
|
|
orderbook = self.orderbooks[symbol]
|
|
orderbook['symbol'] = symbol
|
|
for i in range(0, len(data)):
|
|
price = self.safe_float(data[i], 'price')
|
|
size = self.convertFromRawQuantity(symbol, self.safe_string(data[i], 'size'))
|
|
id = self.safe_string(data[i], 'id')
|
|
side = self.safe_string(data[i], 'side')
|
|
side = 'bids' if (side == 'Buy') else 'asks'
|
|
bookside = orderbook[side]
|
|
bookside.storeArray([price, size, id])
|
|
datetime = self.safe_string(data[i], 'timestamp')
|
|
orderbook['timestamp'] = self.parse8601(datetime)
|
|
orderbook['datetime'] = datetime
|
|
messageHash = table + ':' + symbol
|
|
client.resolve(orderbook, messageHash)
|
|
else:
|
|
numUpdatesByMarketId: dict = {}
|
|
for i in range(0, len(data)):
|
|
marketId = self.safe_value(data[i], 'symbol')
|
|
if marketId is None:
|
|
return # protecting from weird update
|
|
if not (marketId in numUpdatesByMarketId):
|
|
numUpdatesByMarketId[marketId] = 0
|
|
numUpdatesByMarketId[marketId] = self.sum(numUpdatesByMarketId, 1)
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
orderbook = self.orderbooks[symbol]
|
|
price = self.safe_number(data[i], 'price')
|
|
size = 0 if (action == 'delete') else self.convertFromRawQuantity(symbol, self.safe_string(data[i], 'size', '0'))
|
|
id = self.safe_string(data[i], 'id')
|
|
side = self.safe_string(data[i], 'side')
|
|
side = 'bids' if (side == 'Buy') else 'asks'
|
|
bookside = orderbook[side]
|
|
bookside.storeArray([price, size, id])
|
|
datetime = self.safe_string(data[i], 'timestamp')
|
|
orderbook['timestamp'] = self.parse8601(datetime)
|
|
orderbook['datetime'] = datetime
|
|
marketIds = list(numUpdatesByMarketId.keys())
|
|
for i in range(0, len(marketIds)):
|
|
marketId = marketIds[i]
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
messageHash = table + ':' + symbol
|
|
orderbook = self.orderbooks[symbol]
|
|
client.resolve(orderbook, messageHash)
|
|
|
|
def handle_system_status(self, client: Client, message):
|
|
#
|
|
# todo answer the question whether handleSystemStatus should be renamed
|
|
# and unified for any usage pattern that
|
|
# involves system status and maintenance updates
|
|
#
|
|
# {
|
|
# "info": "Welcome to the BitMEX Realtime API.",
|
|
# "version": "2019-11-22T00:24:37.000Z",
|
|
# "timestamp": "2019-11-23T09:02:27.771Z",
|
|
# "docs": "https://www.bitmex.com/app/wsAPI",
|
|
# "limit": {remaining: 39}
|
|
# }
|
|
#
|
|
return message
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "success": True,
|
|
# "subscribe": "orderBookL2:XBTUSD",
|
|
# "request": {op: "subscribe", args: ["orderBookL2:XBTUSD"]}
|
|
# }
|
|
#
|
|
return message
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
#
|
|
# generic error format
|
|
#
|
|
# {"error": errorMessage}
|
|
#
|
|
# examples
|
|
#
|
|
# {
|
|
# "status": 429,
|
|
# "error": "Rate limit exceeded, retry in 1 seconds.",
|
|
# "meta": {"retryAfter": 1},
|
|
# "request": {"op": "subscribe", "args": "orderBook"},
|
|
# }
|
|
#
|
|
# {"error": "Rate limit exceeded, retry in 29 seconds."}
|
|
#
|
|
error = self.safe_string(message, 'error')
|
|
if error is not None:
|
|
request = self.safe_value(message, 'request', {})
|
|
args = self.safe_value(request, 'args', [])
|
|
numArgs = len(args)
|
|
if numArgs > 0:
|
|
messageHash = args[0]
|
|
broad = self.exceptions['ws']['broad']
|
|
broadKey = self.find_broadly_matched_key(broad, error)
|
|
exception = None
|
|
if broadKey is None:
|
|
exception = ExchangeError(error) # c# requirement for now
|
|
else:
|
|
exception = broad[broadKey](error)
|
|
client.reject(exception, messageHash)
|
|
return False
|
|
return True
|
|
|
|
def handle_message(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "info": "Welcome to the BitMEX Realtime API.",
|
|
# "version": "2019-11-22T00:24:37.000Z",
|
|
# "timestamp": "2019-11-23T09:04:42.569Z",
|
|
# "docs": "https://www.bitmex.com/app/wsAPI",
|
|
# "limit": {remaining: 38}
|
|
# }
|
|
#
|
|
# {
|
|
# "success": True,
|
|
# "subscribe": "orderBookL2:XBTUSD",
|
|
# "request": {op: "subscribe", args: ["orderBookL2:XBTUSD"]}
|
|
# }
|
|
#
|
|
# {
|
|
# "table": "orderBookL2",
|
|
# "action": "update",
|
|
# "data": [
|
|
# {symbol: "XBTUSD", id: 8799284800, side: "Sell", size: 721000},
|
|
# {symbol: "XBTUSD", id: 8799285100, side: "Sell", size: 70590},
|
|
# {symbol: "XBTUSD", id: 8799285550, side: "Sell", size: 217652},
|
|
# {symbol: "XBTUSD", id: 8799285850, side: "Sell", size: 105578},
|
|
# {symbol: "XBTUSD", id: 8799286350, side: "Sell", size: 172093},
|
|
# {symbol: "XBTUSD", id: 8799286650, side: "Sell", size: 201125},
|
|
# {symbol: "XBTUSD", id: 8799288950, side: "Buy", size: 47552},
|
|
# {symbol: "XBTUSD", id: 8799289250, side: "Buy", size: 78217},
|
|
# {symbol: "XBTUSD", id: 8799289700, side: "Buy", size: 193677},
|
|
# {symbol: "XBTUSD", id: 8799290000, side: "Buy", size: 818161},
|
|
# {symbol: "XBTUSD", id: 8799290500, side: "Buy", size: 218806},
|
|
# {symbol: "XBTUSD", id: 8799290800, side: "Buy", size: 102946}
|
|
# ]
|
|
# }
|
|
#
|
|
if self.handle_error_message(client, message):
|
|
table = self.safe_string(message, 'table')
|
|
methods: dict = {
|
|
'orderBookL2': self.handle_order_book,
|
|
'orderBookL2_25': self.handle_order_book,
|
|
'orderBook10': self.handle_order_book,
|
|
'instrument': self.handle_ticker,
|
|
'trade': self.handle_trades,
|
|
'tradeBin1m': self.handle_ohlcv,
|
|
'tradeBin5m': self.handle_ohlcv,
|
|
'tradeBin1h': self.handle_ohlcv,
|
|
'tradeBin1d': self.handle_ohlcv,
|
|
'order': self.handle_orders,
|
|
'execution': self.handle_my_trades,
|
|
'margin': self.handle_balance,
|
|
'liquidation': self.handle_liquidation,
|
|
'position': self.handle_positions,
|
|
}
|
|
method = self.safe_value(methods, table)
|
|
if method is None:
|
|
request = self.safe_value(message, 'request', {})
|
|
op = self.safe_value(request, 'op')
|
|
if op == 'authKeyExpires':
|
|
self.handle_authentication_message(client, message)
|
|
else:
|
|
method(client, message)
|