2942 lines
129 KiB
Python
2942 lines
129 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
|
|
|
|
from ccxt.base.exchange import Exchange
|
|
from ccxt.abstract.bitmex import ImplicitAPI
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Leverage, Leverages, Market, MarketType, Num, Order, OrderBook, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, FundingRate, FundingRates, Trade, Transaction
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import PermissionDenied
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import BadSymbol
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OrderNotFound
|
|
from ccxt.base.errors import DDoSProtection
|
|
from ccxt.base.errors import ExchangeNotAvailable
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class bitmex(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(bitmex, self).describe(), {
|
|
'id': 'bitmex',
|
|
'name': 'BitMEX',
|
|
'countries': ['SC'], # Seychelles
|
|
'version': 'v1',
|
|
'userAgent': None,
|
|
# cheapest endpoints are 10 requests per second(trading)
|
|
# 10 per second => rateLimit = 1000ms / 10 = 100ms
|
|
# 120 per minute => 2 per second => weight = 5(authenticated)
|
|
# 30 per minute => 0.5 per second => weight = 20(unauthenticated)
|
|
'rateLimit': 100,
|
|
'certified': True,
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': True,
|
|
'margin': False,
|
|
'swap': True,
|
|
'future': True,
|
|
'option': False,
|
|
'addMargin': None,
|
|
'cancelAllOrders': True,
|
|
'cancelAllOrdersAfter': True,
|
|
'cancelOrder': True,
|
|
'cancelOrders': True,
|
|
'closeAllPositions': False,
|
|
'closePosition': True,
|
|
'createOrder': True,
|
|
'createReduceOnlyOrder': True,
|
|
'createStopOrder': True,
|
|
'createTrailingAmountOrder': True,
|
|
'createTriggerOrder': True,
|
|
'editOrder': True,
|
|
'fetchBalance': True,
|
|
'fetchClosedOrders': True,
|
|
'fetchCurrencies': True,
|
|
'fetchDepositAddress': True,
|
|
'fetchDepositAddresses': False,
|
|
'fetchDepositAddressesByNetwork': False,
|
|
'fetchDepositsWithdrawals': 'emulated',
|
|
'fetchDepositWithdrawFee': 'emulated',
|
|
'fetchDepositWithdrawFees': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingRate': 'emulated', # emulated in exchange
|
|
'fetchFundingRateHistory': True,
|
|
'fetchFundingRates': True,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchLedger': True,
|
|
'fetchLeverage': 'emulated',
|
|
'fetchLeverages': True,
|
|
'fetchLeverageTiers': False,
|
|
'fetchLiquidations': True,
|
|
'fetchMarginAdjustmentHistory': False,
|
|
'fetchMarketLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMyLiquidations': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': True,
|
|
'fetchOrderBook': True,
|
|
'fetchOrders': True,
|
|
'fetchPosition': False,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositions': True,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTrades': True,
|
|
'fetchTransactions': 'emulated',
|
|
'fetchTransfer': False,
|
|
'fetchTransfers': False,
|
|
'index': True,
|
|
'reduceMargin': None,
|
|
'sandbox': True,
|
|
'setLeverage': True,
|
|
'setMargin': None,
|
|
'setMarginMode': True,
|
|
'setPositionMode': False,
|
|
'transfer': False,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'5m': '5m',
|
|
'1h': '1h',
|
|
'1d': '1d',
|
|
},
|
|
'urls': {
|
|
'test': {
|
|
'public': 'https://testnet.bitmex.com',
|
|
'private': 'https://testnet.bitmex.com',
|
|
},
|
|
'logo': 'https://github.com/user-attachments/assets/c78425ab-78d5-49d6-bd14-db7734798f04',
|
|
'api': {
|
|
'public': 'https://www.bitmex.com',
|
|
'private': 'https://www.bitmex.com',
|
|
},
|
|
'www': 'https://www.bitmex.com',
|
|
'doc': [
|
|
'https://www.bitmex.com/app/apiOverview',
|
|
'https://github.com/BitMEX/api-connectors/tree/master/official-http',
|
|
],
|
|
'fees': 'https://www.bitmex.com/app/fees',
|
|
'referral': {
|
|
'url': 'https://www.bitmex.com/app/register/NZTR1q',
|
|
'discount': 0.1,
|
|
},
|
|
},
|
|
'api': {
|
|
'public': {
|
|
'get': {
|
|
'announcement': 5,
|
|
'announcement/urgent': 5,
|
|
'chat': 5,
|
|
'chat/channels': 5,
|
|
'chat/connected': 5,
|
|
'chat/pinned': 5,
|
|
'funding': 5,
|
|
'guild': 5,
|
|
'instrument': 5,
|
|
'instrument/active': 5,
|
|
'instrument/activeAndIndices': 5,
|
|
'instrument/activeIntervals': 5,
|
|
'instrument/compositeIndex': 5,
|
|
'instrument/indices': 5,
|
|
'instrument/usdVolume': 5,
|
|
'insurance': 5,
|
|
'leaderboard': 5,
|
|
'liquidation': 5,
|
|
'orderBook/L2': 5,
|
|
'porl/nonce': 5,
|
|
'quote': 5,
|
|
'quote/bucketed': 5,
|
|
'schema': 5,
|
|
'schema/websocketHelp': 5,
|
|
'settlement': 5,
|
|
'stats': 5,
|
|
'stats/history': 5,
|
|
'stats/historyUSD': 5,
|
|
'trade': 5,
|
|
'trade/bucketed': 5,
|
|
'wallet/assets': 5,
|
|
'wallet/networks': 5,
|
|
},
|
|
},
|
|
'private': {
|
|
'get': {
|
|
'address': 5,
|
|
'apiKey': 5,
|
|
'execution': 5,
|
|
'execution/tradeHistory': 5,
|
|
'globalNotification': 5,
|
|
'leaderboard/name': 5,
|
|
'order': 5,
|
|
'porl/snapshots': 5,
|
|
'position': 5,
|
|
'user': 5,
|
|
'user/affiliateStatus': 5,
|
|
'user/checkReferralCode': 5,
|
|
'user/commission': 5,
|
|
'user/csa': 5,
|
|
'user/depositAddress': 5,
|
|
'user/executionHistory': 5,
|
|
'user/getWalletTransferAccounts': 5,
|
|
'user/margin': 5,
|
|
'user/quoteFillRatio': 5,
|
|
'user/quoteValueRatio': 5,
|
|
'user/staking': 5,
|
|
'user/staking/instruments': 5,
|
|
'user/staking/tiers': 5,
|
|
'user/tradingVolume': 5,
|
|
'user/unstakingRequests': 5,
|
|
'user/wallet': 5,
|
|
'user/walletHistory': 5,
|
|
'user/walletSummary': 5,
|
|
'userAffiliates': 5,
|
|
'userEvent': 5,
|
|
},
|
|
'post': {
|
|
'address': 5,
|
|
'chat': 5,
|
|
'guild': 5,
|
|
'guild/archive': 5,
|
|
'guild/join': 5,
|
|
'guild/kick': 5,
|
|
'guild/leave': 5,
|
|
'guild/sharesTrades': 5,
|
|
'order': 1,
|
|
'order/cancelAllAfter': 5,
|
|
'order/closePosition': 5,
|
|
'position/isolate': 1,
|
|
'position/leverage': 1,
|
|
'position/riskLimit': 5,
|
|
'position/transferMargin': 1,
|
|
'user/addSubaccount': 5,
|
|
'user/cancelWithdrawal': 5,
|
|
'user/communicationToken': 5,
|
|
'user/confirmEmail': 5,
|
|
'user/confirmWithdrawal': 5,
|
|
'user/logout': 5,
|
|
'user/preferences': 5,
|
|
'user/requestWithdrawal': 5,
|
|
'user/unstakingRequests': 5,
|
|
'user/updateSubaccount': 5,
|
|
'user/walletTransfer': 5,
|
|
},
|
|
'put': {
|
|
'guild': 5,
|
|
'order': 1,
|
|
},
|
|
'delete': {
|
|
'order': 1,
|
|
'order/all': 1,
|
|
'user/unstakingRequests': 5,
|
|
},
|
|
},
|
|
},
|
|
'exceptions': {
|
|
'exact': {
|
|
'Invalid API Key.': AuthenticationError,
|
|
'This key is disabled.': PermissionDenied,
|
|
'Access Denied': PermissionDenied,
|
|
'Duplicate clOrdID': InvalidOrder,
|
|
'orderQty is invalid': InvalidOrder,
|
|
'Invalid price': InvalidOrder,
|
|
'Invalid stopPx for ordType': InvalidOrder,
|
|
'Account is restricted': PermissionDenied, # {"error":{"message":"Account is restricted","name":"HTTPError"}}
|
|
},
|
|
'broad': {
|
|
'Signature not valid': AuthenticationError,
|
|
'overloaded': ExchangeNotAvailable,
|
|
'Account has insufficient Available Balance': InsufficientFunds,
|
|
'Service unavailable': ExchangeNotAvailable, # {"error":{"message":"Service unavailable","name":"HTTPError"}}
|
|
'Server Error': ExchangeError, # {"error":{"message":"Server Error","name":"HTTPError"}}
|
|
'Unable to cancel order due to existing state': InvalidOrder,
|
|
'We require all new traders to verify': PermissionDenied, # {"message":"We require all new traders to verify their identity before their first deposit. Please visit bitmex.com/verify to complete the process.","name":"HTTPError"}
|
|
},
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'options': {
|
|
# https://blog.bitmex.com/api_announcement/deprecation-of-api-nonce-header/
|
|
# https://github.com/ccxt/ccxt/issues/4789
|
|
'api-expires': 5, # in seconds
|
|
'fetchOHLCVOpenTimestamp': True,
|
|
'oldPrecision': False,
|
|
'networks': {
|
|
'BTC': 'btc',
|
|
'ERC20': 'eth',
|
|
'BEP20': 'bsc',
|
|
'TRC20': 'tron',
|
|
'AVAXC': 'avax',
|
|
'NEAR': 'near',
|
|
'XTZ': 'xtz',
|
|
'DOT': 'dot',
|
|
'SOL': 'sol',
|
|
'ADA': 'ada',
|
|
},
|
|
},
|
|
'features': {
|
|
'default': {
|
|
'sandbox': True,
|
|
'createOrder': {
|
|
'marginMode': True,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': {
|
|
'last': True,
|
|
'mark': True,
|
|
},
|
|
'triggerDirection': True,
|
|
'stopLossPrice': False,
|
|
'takeProfitPrice': False,
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': False,
|
|
},
|
|
'hedged': False,
|
|
'trailing': True,
|
|
'marketBuyRequiresPrice': False,
|
|
'marketBuyByCost': False,
|
|
# exchange-supported features
|
|
# 'selfTradePrevention': True,
|
|
# 'twap': False,
|
|
# 'iceberg': False,
|
|
# 'oco': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'daysBack': None,
|
|
'untilDays': 1000000,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'daysBack': None,
|
|
'untilDays': 1000000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 500,
|
|
'daysBack': None,
|
|
'daysBackCanceled': None,
|
|
'untilDays': 1000000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 10000,
|
|
},
|
|
},
|
|
'spot': {
|
|
'extends': 'default',
|
|
'createOrder': {
|
|
'triggerPriceType': {
|
|
'index': False,
|
|
},
|
|
},
|
|
},
|
|
'derivatives': {
|
|
'extends': 'default',
|
|
'createOrder': {
|
|
'triggerPriceType': {
|
|
'index': True,
|
|
},
|
|
},
|
|
},
|
|
'swap': {
|
|
'linear': {
|
|
'extends': 'derivatives',
|
|
},
|
|
'inverse': {
|
|
'extends': 'derivatives',
|
|
},
|
|
},
|
|
'future': {
|
|
'linear': {
|
|
'extends': 'derivatives',
|
|
},
|
|
'inverse': {
|
|
'extends': 'derivatives',
|
|
},
|
|
},
|
|
},
|
|
'commonCurrencies': {
|
|
'USDt': 'USDT',
|
|
'XBt': 'BTC',
|
|
'XBT': 'BTC',
|
|
'Gwei': 'ETH',
|
|
'GWEI': 'ETH',
|
|
'LAMP': 'SOL',
|
|
'LAMp': 'SOL',
|
|
},
|
|
})
|
|
|
|
def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Wallet/Wallet_getAssetsConfig
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
response = self.publicGetWalletAssets(params)
|
|
#
|
|
# {
|
|
# "XBt": {
|
|
# "asset": "XBT",
|
|
# "currency": "XBt",
|
|
# "majorCurrency": "XBT",
|
|
# "name": "Bitcoin",
|
|
# "currencyType": "Crypto",
|
|
# "scale": "8",
|
|
# # "mediumPrecision": "8",
|
|
# # "shorterPrecision": "4",
|
|
# # "symbol": "₿",
|
|
# # "tickLog": "0",
|
|
# # "weight": "1",
|
|
# "enabled": True,
|
|
# "isMarginCurrency": True,
|
|
# "minDepositAmount": "10000",
|
|
# "minWithdrawalAmount": "1000",
|
|
# "maxWithdrawalAmount": "100000000000000",
|
|
# "networks": [
|
|
# {
|
|
# "asset": "btc",
|
|
# "tokenAddress": "",
|
|
# "depositEnabled": True,
|
|
# "withdrawalEnabled": True,
|
|
# "withdrawalFee": "20000",
|
|
# "minFee": "20000",
|
|
# "maxFee": "10000000"
|
|
# }
|
|
# ]
|
|
# },
|
|
# }
|
|
#
|
|
result: dict = {}
|
|
for i in range(0, len(response)):
|
|
currency = response[i]
|
|
asset = self.safe_string(currency, 'asset')
|
|
code = self.safe_currency_code(asset)
|
|
id = self.safe_string(currency, 'currency')
|
|
name = self.safe_string(currency, 'name')
|
|
chains = self.safe_value(currency, 'networks', [])
|
|
depositEnabled = False
|
|
withdrawEnabled = False
|
|
networks: dict = {}
|
|
scale = self.safe_string(currency, 'scale')
|
|
precisionString = self.parse_precision(scale)
|
|
precision = self.parse_number(precisionString)
|
|
for j in range(0, len(chains)):
|
|
chain = chains[j]
|
|
networkId = self.safe_string(chain, 'asset')
|
|
network = self.network_id_to_code(networkId)
|
|
withdrawalFeeRaw = self.safe_string(chain, 'withdrawalFee')
|
|
withdrawalFee = self.parse_number(Precise.string_mul(withdrawalFeeRaw, precisionString))
|
|
isDepositEnabled = self.safe_bool(chain, 'depositEnabled', False)
|
|
isWithdrawEnabled = self.safe_bool(chain, 'withdrawalEnabled', False)
|
|
active = (isDepositEnabled and isWithdrawEnabled)
|
|
if isDepositEnabled:
|
|
depositEnabled = True
|
|
if isWithdrawEnabled:
|
|
withdrawEnabled = True
|
|
networks[network] = {
|
|
'info': chain,
|
|
'id': networkId,
|
|
'network': network,
|
|
'active': active,
|
|
'deposit': isDepositEnabled,
|
|
'withdraw': isWithdrawEnabled,
|
|
'fee': withdrawalFee,
|
|
'precision': None,
|
|
'limits': {
|
|
'withdraw': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'deposit': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
}
|
|
currencyEnabled = self.safe_value(currency, 'enabled')
|
|
currencyActive = currencyEnabled or (depositEnabled or withdrawEnabled)
|
|
minWithdrawalString = self.safe_string(currency, 'minWithdrawalAmount')
|
|
minWithdrawal = self.parse_number(Precise.string_mul(minWithdrawalString, precisionString))
|
|
maxWithdrawalString = self.safe_string(currency, 'maxWithdrawalAmount')
|
|
maxWithdrawal = self.parse_number(Precise.string_mul(maxWithdrawalString, precisionString))
|
|
minDepositString = self.safe_string(currency, 'minDepositAmount')
|
|
minDeposit = self.parse_number(Precise.string_mul(minDepositString, precisionString))
|
|
isCrypto = self.safe_string(currency, 'currencyType') == 'Crypto'
|
|
result[code] = {
|
|
'id': id,
|
|
'code': code,
|
|
'info': currency,
|
|
'name': name,
|
|
'active': currencyActive,
|
|
'deposit': depositEnabled,
|
|
'withdraw': withdrawEnabled,
|
|
'fee': None,
|
|
'precision': precision,
|
|
'limits': {
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': minWithdrawal,
|
|
'max': maxWithdrawal,
|
|
},
|
|
'deposit': {
|
|
'min': minDeposit,
|
|
'max': None,
|
|
},
|
|
},
|
|
'networks': networks,
|
|
'type': 'crypto' if isCrypto else 'other',
|
|
}
|
|
return result
|
|
|
|
def convert_from_real_amount(self, code, amount):
|
|
currency = self.currency(code)
|
|
precision = self.safe_string(currency, 'precision')
|
|
amountString = self.number_to_string(amount)
|
|
finalAmount = Precise.string_div(amountString, precision)
|
|
return self.parse_number(finalAmount)
|
|
|
|
def convert_to_real_amount(self, code: Str, amount: Str):
|
|
if code is None:
|
|
return amount
|
|
elif amount is None:
|
|
return None
|
|
currency = self.currency(code)
|
|
precision = self.safe_string(currency, 'precision')
|
|
return Precise.string_mul(amount, precision)
|
|
|
|
def amount_to_precision(self, symbol, amount):
|
|
symbol = self.safe_symbol(symbol)
|
|
market = self.market(symbol)
|
|
oldPrecision = self.safe_value(self.options, 'oldPrecision')
|
|
if market['spot'] and not oldPrecision:
|
|
amount = self.convert_from_real_amount(market['base'], amount)
|
|
return super(bitmex, self).amount_to_precision(symbol, amount)
|
|
|
|
def convert_from_raw_quantity(self, symbol, rawQuantity, currencySide='base'):
|
|
if self.safe_value(self.options, 'oldPrecision'):
|
|
return self.parse_number(rawQuantity)
|
|
symbol = self.safe_symbol(symbol)
|
|
marketExists = self.in_array(symbol, self.symbols)
|
|
if not marketExists:
|
|
return self.parse_number(rawQuantity)
|
|
market = self.market(symbol)
|
|
if market['spot']:
|
|
return self.parse_number(self.convert_to_real_amount(market[currencySide], rawQuantity))
|
|
return self.parse_number(rawQuantity)
|
|
|
|
def convert_from_raw_cost(self, symbol, rawQuantity):
|
|
return self.convert_from_raw_quantity(symbol, rawQuantity, 'quote')
|
|
|
|
def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for bitmex
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActive
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
response = self.publicGetInstrumentActive(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "symbol": "LTCUSDT",
|
|
# "rootSymbol": "LTC",
|
|
# "state": "Open",
|
|
# "typ": "FFWCSX",
|
|
# "listing": "2021-11-10T04:00:00.000Z",
|
|
# "front": "2021-11-10T04:00:00.000Z",
|
|
# "expiry": null,
|
|
# "settle": null,
|
|
# "listedSettle": null,
|
|
# "relistInterval": null,
|
|
# "inverseLeg": "",
|
|
# "sellLeg": "",
|
|
# "buyLeg": "",
|
|
# "optionStrikePcnt": null,
|
|
# "optionStrikeRound": null,
|
|
# "optionStrikePrice": null,
|
|
# "optionMultiplier": null,
|
|
# "positionCurrency": "LTC", # can be empty for spot markets
|
|
# "underlying": "LTC",
|
|
# "quoteCurrency": "USDT",
|
|
# "underlyingSymbol": "LTCT=", # can be empty for spot markets
|
|
# "reference": "BMEX",
|
|
# "referenceSymbol": ".BLTCT", # can be empty for spot markets
|
|
# "calcInterval": null,
|
|
# "publishInterval": null,
|
|
# "publishTime": null,
|
|
# "maxOrderQty": 1000000000,
|
|
# "maxPrice": 1000000,
|
|
# "lotSize": 1000,
|
|
# "tickSize": 0.01,
|
|
# "multiplier": 100,
|
|
# "settlCurrency": "USDt", # can be empty for spot markets
|
|
# "underlyingToPositionMultiplier": 10000,
|
|
# "underlyingToSettleMultiplier": null,
|
|
# "quoteToSettleMultiplier": 1000000,
|
|
# "isQuanto": False,
|
|
# "isInverse": False,
|
|
# "initMargin": 0.03,
|
|
# "maintMargin": 0.015,
|
|
# "riskLimit": 1000000000000, # can be null for spot markets
|
|
# "riskStep": 1000000000000, # can be null for spot markets
|
|
# "limit": null,
|
|
# "capped": False,
|
|
# "taxed": True,
|
|
# "deleverage": True,
|
|
# "makerFee": -0.0001,
|
|
# "takerFee": 0.0005,
|
|
# "settlementFee": 0,
|
|
# "insuranceFee": 0,
|
|
# "fundingBaseSymbol": ".LTCBON8H", # can be empty for spot markets
|
|
# "fundingQuoteSymbol": ".USDTBON8H", # can be empty for spot markets
|
|
# "fundingPremiumSymbol": ".LTCUSDTPI8H", # can be empty for spot markets
|
|
# "fundingTimestamp": "2022-01-14T20:00:00.000Z",
|
|
# "fundingInterval": "2000-01-01T08:00:00.000Z",
|
|
# "fundingRate": 0.0001,
|
|
# "indicativeFundingRate": 0.0001,
|
|
# "rebalanceTimestamp": null,
|
|
# "rebalanceInterval": null,
|
|
# "openingTimestamp": "2022-01-14T17:00:00.000Z",
|
|
# "closingTimestamp": "2022-01-14T18:00:00.000Z",
|
|
# "sessionInterval": "2000-01-01T01:00:00.000Z",
|
|
# "prevClosePrice": 138.511,
|
|
# "limitDownPrice": null,
|
|
# "limitUpPrice": null,
|
|
# "bankruptLimitDownPrice": null,
|
|
# "bankruptLimitUpPrice": null,
|
|
# "prevTotalVolume": 12699024000,
|
|
# "totalVolume": 12702160000,
|
|
# "volume": 3136000,
|
|
# "volume24h": 114251000,
|
|
# "prevTotalTurnover": 232418052349000,
|
|
# "totalTurnover": 232463353260000,
|
|
# "turnover": 45300911000,
|
|
# "turnover24h": 1604331340000,
|
|
# "homeNotional24h": 11425.1,
|
|
# "foreignNotional24h": 1604331.3400000003,
|
|
# "prevPrice24h": 135.48,
|
|
# "vwap": 140.42165,
|
|
# "highPrice": 146.42,
|
|
# "lowPrice": 135.08,
|
|
# "lastPrice": 144.36,
|
|
# "lastPriceProtected": 144.36,
|
|
# "lastTickDirection": "MinusTick",
|
|
# "lastChangePcnt": 0.0655,
|
|
# "bidPrice": 143.75,
|
|
# "midPrice": 143.855,
|
|
# "askPrice": 143.96,
|
|
# "impactBidPrice": 143.75,
|
|
# "impactMidPrice": 143.855,
|
|
# "impactAskPrice": 143.96,
|
|
# "hasLiquidity": True,
|
|
# "openInterest": 38103000,
|
|
# "openValue": 547963053300,
|
|
# "fairMethod": "FundingRate",
|
|
# "fairBasisRate": 0.1095,
|
|
# "fairBasis": 0.004,
|
|
# "fairPrice": 143.811,
|
|
# "markMethod": "FairPrice",
|
|
# "markPrice": 143.811,
|
|
# "indicativeTaxRate": null,
|
|
# "indicativeSettlePrice": 143.807,
|
|
# "optionUnderlyingPrice": null,
|
|
# "settledPriceAdjustmentRate": null,
|
|
# "settledPrice": null,
|
|
# "timestamp": "2022-01-14T17:49:55.000Z"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_markets(response)
|
|
|
|
def parse_market(self, market: dict) -> Market:
|
|
id = self.safe_string(market, 'symbol')
|
|
baseId = self.safe_string(market, 'underlying')
|
|
quoteId = self.safe_string(market, 'quoteCurrency')
|
|
settleId = self.safe_string(market, 'settlCurrency')
|
|
settle = self.safe_currency_code(settleId)
|
|
# 'positionCurrency' may be empty("", currently returns for ETHUSD)
|
|
# so let's take the settlCurrency first and then adjust if needed
|
|
typ = self.safe_string(market, 'typ') # type definitions at: https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_get
|
|
type: MarketType
|
|
swap = False
|
|
spot = False
|
|
future = False
|
|
if typ == 'FFWCSX':
|
|
type = 'swap'
|
|
swap = True
|
|
elif typ == 'IFXXXP':
|
|
type = 'spot'
|
|
spot = True
|
|
elif typ == 'FFCCSX':
|
|
type = 'future'
|
|
future = True
|
|
elif typ == 'FFICSX':
|
|
# prediction markets(without any volume)
|
|
quoteId = baseId
|
|
baseId = self.safe_string(market, 'rootSymbol')
|
|
type = 'future'
|
|
future = True
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
contract = swap or future
|
|
contractSize = None
|
|
isInverse = self.safe_value(market, 'isInverse') # self is True when BASE and SETTLE are same, i.e. BTC/XXX:BTC
|
|
isQuanto = self.safe_value(market, 'isQuanto') # self is True when BASE and SETTLE are different, i.e. AXS/XXX:BTC
|
|
linear = (not isInverse and not isQuanto) if contract else None
|
|
status = self.safe_string(market, 'state')
|
|
active = status == 'Open' # Open, Settled, Unlisted
|
|
expiry = None
|
|
expiryDatetime = None
|
|
symbol = None
|
|
if spot:
|
|
symbol = base + '/' + quote
|
|
elif contract:
|
|
symbol = base + '/' + quote + ':' + settle
|
|
if linear:
|
|
multiplierString = self.safe_string_2(market, 'underlyingToPositionMultiplier', 'underlyingToSettleMultiplier')
|
|
contractSize = self.parse_number(Precise.string_div('1', multiplierString))
|
|
else:
|
|
multiplierString = Precise.string_abs(self.safe_string(market, 'multiplier'))
|
|
contractSize = self.parse_number(multiplierString)
|
|
expiryDatetime = self.safe_string(market, 'expiry')
|
|
expiry = self.parse8601(expiryDatetime)
|
|
if expiry is not None:
|
|
symbol = symbol + '-' + self.yymmdd(expiry)
|
|
else:
|
|
# for index/exotic markets, default to id
|
|
symbol = id
|
|
positionId = self.safe_string_2(market, 'positionCurrency', 'underlying')
|
|
position = self.safe_currency_code(positionId)
|
|
positionIsQuote = (position == quote)
|
|
maxOrderQty = self.safe_number(market, 'maxOrderQty')
|
|
initMargin = self.safe_string(market, 'initMargin', '1')
|
|
maxLeverage = self.parse_number(Precise.string_div('1', initMargin))
|
|
# subtype should be None for spot markets
|
|
if spot:
|
|
isInverse = None
|
|
isQuanto = None
|
|
linear = None
|
|
return {
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': settle,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': settleId,
|
|
'type': type,
|
|
'spot': spot,
|
|
'margin': False,
|
|
'swap': swap,
|
|
'future': future,
|
|
'option': False,
|
|
'active': active,
|
|
'contract': contract,
|
|
'linear': linear,
|
|
'inverse': isInverse,
|
|
'quanto': isQuanto,
|
|
'taker': self.safe_number(market, 'takerFee'),
|
|
'maker': self.safe_number(market, 'makerFee'),
|
|
'contractSize': contractSize,
|
|
'expiry': expiry,
|
|
'expiryDatetime': expiryDatetime,
|
|
'strike': self.safe_number(market, 'optionStrikePrice'),
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': self.safe_number(market, 'lotSize'),
|
|
'price': self.safe_number(market, 'tickSize'),
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': self.parse_number('1') if contract else None,
|
|
'max': maxLeverage if contract else None,
|
|
},
|
|
'amount': {
|
|
'min': None,
|
|
'max': None if positionIsQuote else maxOrderQty,
|
|
},
|
|
'price': {
|
|
'min': None,
|
|
'max': self.safe_number(market, 'maxPrice'),
|
|
},
|
|
'cost': {
|
|
'min': None,
|
|
'max': maxOrderQty if positionIsQuote else None,
|
|
},
|
|
},
|
|
'created': None, # 'listing' field is buggy, e.g. 2200-02-01T00:00:00.000Z
|
|
'info': market,
|
|
}
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
#
|
|
# [
|
|
# {
|
|
# "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
|
|
# }
|
|
# ]
|
|
#
|
|
result: dict = {'info': response}
|
|
for i in range(0, len(response)):
|
|
balance = response[i]
|
|
currencyId = self.safe_string(balance, 'currency')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
free = self.safe_string(balance, 'availableMargin')
|
|
total = self.safe_string(balance, 'marginBalance')
|
|
account['free'] = self.convert_to_real_amount(code, free)
|
|
account['total'] = self.convert_to_real_amount(code, total)
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
def fetch_balance(self, params={}) -> Balances:
|
|
"""
|
|
query for balance and get the amount of funds available for trading or funds locked in orders
|
|
|
|
https://www.bitmex.com/api/explorer/#not /User/User_getMargin
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
'currency': 'all',
|
|
}
|
|
response = self.privateGetUserMargin(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "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
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_balance(response)
|
|
|
|
def fetch_order_book(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://www.bitmex.com/api/explorer/#not /OrderBook/OrderBook_getL2
|
|
|
|
: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
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['depth'] = limit
|
|
response = self.publicGetOrderBookL2(self.extend(request, params))
|
|
result: dict = {
|
|
'symbol': symbol,
|
|
'bids': [],
|
|
'asks': [],
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'nonce': None,
|
|
}
|
|
for i in range(0, len(response)):
|
|
order = response[i]
|
|
side = 'asks' if (order['side'] == 'Sell') else 'bids'
|
|
amount = self.convert_from_raw_quantity(symbol, self.safe_string(order, 'size'))
|
|
price = self.safe_number(order, 'price')
|
|
# https://github.com/ccxt/ccxt/issues/4926
|
|
# https://github.com/ccxt/ccxt/issues/4927
|
|
# the exchange sometimes returns null price in the orderbook
|
|
if price is not None:
|
|
resultSide = result[side]
|
|
resultSide.append([price, amount])
|
|
result['bids'] = self.sort_by(result['bids'], 0, True)
|
|
result['asks'] = self.sort_by(result['asks'], 0)
|
|
return result
|
|
|
|
def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an order made by the user
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders
|
|
|
|
:param str id: the 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>`
|
|
"""
|
|
filter: dict = {
|
|
'filter': {
|
|
'orderID': id,
|
|
},
|
|
}
|
|
response = self.fetch_orders(symbol, None, None, self.deep_extend(filter, params))
|
|
numResults = len(response)
|
|
if numResults == 1:
|
|
return response[0]
|
|
raise OrderNotFound(self.id + ': The order ' + id + ' not found.')
|
|
|
|
def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders
|
|
|
|
fetches information on multiple orders made by the user
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: the earliest time in ms to fetch orders for
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOrders', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchOrders', symbol, since, limit, params, 100)
|
|
market = None
|
|
request: dict = {}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
if since is not None:
|
|
request['startTime'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['count'] = limit
|
|
until = self.safe_integer_2(params, 'until', 'endTime')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['endTime'] = self.iso8601(until)
|
|
request = self.deep_extend(request, params)
|
|
# why the hassle? urlencode in python is kinda broken for nested dicts.
|
|
# E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}"
|
|
# Bitmex doesn't like that. Hence resorting to self hack.
|
|
if 'filter' in request:
|
|
request['filter'] = self.json(request['filter'])
|
|
response = self.privateGetOrder(request)
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetch all unfilled currently open orders
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders
|
|
|
|
: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 Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
request: dict = {
|
|
'filter': {
|
|
'open': True,
|
|
},
|
|
}
|
|
return self.fetch_orders(symbol, since, limit, self.deep_extend(request, params))
|
|
|
|
def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple closed orders made by the user
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_getOrders
|
|
|
|
: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 Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
# Bitmex barfs if you set 'open': False in the filter...
|
|
orders = self.fetch_orders(symbol, since, limit, params)
|
|
return self.filter_by_array(orders, 'status', ['closed', 'canceled'], False)
|
|
|
|
def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all trades made by the user
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Execution/Execution_getTradeHistory
|
|
|
|
: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
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100)
|
|
market = None
|
|
request: dict = {}
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
if since is not None:
|
|
request['startTime'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['count'] = min(500, limit)
|
|
until = self.safe_integer_2(params, 'until', 'endTime')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['endTime'] = self.iso8601(until)
|
|
request = self.deep_extend(request, params)
|
|
# why the hassle? urlencode in python is kinda broken for nested dicts.
|
|
# E.g. self.urlencode({"filter": {"open": True}}) will return "filter={'open':+True}"
|
|
# Bitmex doesn't like that. Hence resorting to self hack.
|
|
if 'filter' in request:
|
|
request['filter'] = self.json(request['filter'])
|
|
response = self.privateGetExecutionTradeHistory(request)
|
|
#
|
|
# [
|
|
# {
|
|
# "execID": "string",
|
|
# "orderID": "string",
|
|
# "clOrdID": "string",
|
|
# "clOrdLinkID": "string",
|
|
# "account": 0,
|
|
# "symbol": "string",
|
|
# "side": "string",
|
|
# "lastQty": 0,
|
|
# "lastPx": 0,
|
|
# "underlyingLastPx": 0,
|
|
# "lastMkt": "string",
|
|
# "lastLiquidityInd": "string",
|
|
# "simpleOrderQty": 0,
|
|
# "orderQty": 0,
|
|
# "price": 0,
|
|
# "displayQty": 0,
|
|
# "stopPx": 0,
|
|
# "pegOffsetValue": 0,
|
|
# "pegPriceType": "string",
|
|
# "currency": "string",
|
|
# "settlCurrency": "string",
|
|
# "execType": "string",
|
|
# "ordType": "string",
|
|
# "timeInForce": "string",
|
|
# "execInst": "string",
|
|
# "contingencyType": "string",
|
|
# "exDestination": "string",
|
|
# "ordStatus": "string",
|
|
# "triggered": "string",
|
|
# "workingIndicator": True,
|
|
# "ordRejReason": "string",
|
|
# "simpleLeavesQty": 0,
|
|
# "leavesQty": 0,
|
|
# "simpleCumQty": 0,
|
|
# "cumQty": 0,
|
|
# "avgPx": 0,
|
|
# "commission": 0,
|
|
# "tradePublishIndicator": "string",
|
|
# "multiLegReportingType": "string",
|
|
# "text": "string",
|
|
# "trdMatchID": "string",
|
|
# "execCost": 0,
|
|
# "execComm": 0,
|
|
# "homeNotional": 0,
|
|
# "foreignNotional": 0,
|
|
# "transactTime": "2019-03-05T12:47:02.762Z",
|
|
# "timestamp": "2019-03-05T12:47:02.762Z"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
def parse_ledger_entry_type(self, type):
|
|
types: dict = {
|
|
'Withdrawal': 'transaction',
|
|
'RealisedPNL': 'margin',
|
|
'UnrealisedPNL': 'margin',
|
|
'Deposit': 'transaction',
|
|
'Transfer': 'transfer',
|
|
'AffiliatePayout': 'referral',
|
|
'SpotTrade': 'trade',
|
|
}
|
|
return self.safe_string(types, type, type)
|
|
|
|
def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
|
|
#
|
|
# {
|
|
# "transactID": "69573da3-7744-5467-3207-89fd6efe7a47",
|
|
# "account": 24321,
|
|
# "currency": "XBt",
|
|
# "transactType": "Withdrawal", # "AffiliatePayout", "Transfer", "Deposit", "RealisedPNL", ...
|
|
# "amount": -1000000,
|
|
# "fee": 300000,
|
|
# "transactStatus": "Completed", # "Canceled", ...
|
|
# "address": "1Ex4fkF4NhQaQdRWNoYpqiPbDBbq18Kdd9",
|
|
# "tx": "3BMEX91ZhhKoWtsH9QRb5dNXnmnGpiEetA",
|
|
# "text": "",
|
|
# "transactTime": "2017-03-21T20:05:14.388Z",
|
|
# "walletBalance": 0, # balance after
|
|
# "marginBalance": null,
|
|
# "timestamp": "2017-03-22T13:09:23.514Z"
|
|
# }
|
|
#
|
|
# ButMEX returns the unrealized pnl from the wallet history endpoint.
|
|
# The unrealized pnl transaction has an empty timestamp.
|
|
# It is not related to historical pnl it has status set to "Pending".
|
|
# Therefore it's not a part of the history at all.
|
|
# https://github.com/ccxt/ccxt/issues/6047
|
|
#
|
|
# {
|
|
# "transactID":"00000000-0000-0000-0000-000000000000",
|
|
# "account":121210,
|
|
# "currency":"XBt",
|
|
# "transactType":"UnrealisedPNL",
|
|
# "amount":-5508,
|
|
# "fee":0,
|
|
# "transactStatus":"Pending",
|
|
# "address":"XBTUSD",
|
|
# "tx":"",
|
|
# "text":"",
|
|
# "transactTime":null, # ←---------------------------- null
|
|
# "walletBalance":139198767,
|
|
# "marginBalance":139193259,
|
|
# "timestamp":null # ←---------------------------- null
|
|
# }
|
|
#
|
|
id = self.safe_string(item, 'transactID')
|
|
account = self.safe_string(item, 'account')
|
|
referenceId = self.safe_string(item, 'tx')
|
|
referenceAccount = None
|
|
type = self.parse_ledger_entry_type(self.safe_string(item, 'transactType'))
|
|
currencyId = self.safe_string(item, 'currency')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
currency = self.safe_currency(currencyId, currency)
|
|
amountString = self.safe_string(item, 'amount')
|
|
amount = self.convert_to_real_amount(code, amountString)
|
|
timestamp = self.parse8601(self.safe_string(item, 'transactTime'))
|
|
if timestamp is None:
|
|
# https://github.com/ccxt/ccxt/issues/6047
|
|
# set the timestamp to zero, 1970 Jan 1 00:00:00
|
|
# for unrealized pnl and other transactions without a timestamp
|
|
timestamp = 0 # see comments above
|
|
fee = None
|
|
feeCost = self.safe_string(item, 'fee')
|
|
if feeCost is not None:
|
|
feeCost = self.convert_to_real_amount(code, feeCost)
|
|
fee = {
|
|
'cost': self.parse_number(feeCost),
|
|
'currency': code,
|
|
}
|
|
after = self.safe_string(item, 'walletBalance')
|
|
if after is not None:
|
|
after = self.convert_to_real_amount(code, after)
|
|
before = self.parse_number(Precise.string_sub(self.number_to_string(after), self.number_to_string(amount)))
|
|
direction = None
|
|
if Precise.string_lt(amountString, '0'):
|
|
direction = 'out'
|
|
amount = self.convert_to_real_amount(code, Precise.string_abs(amountString))
|
|
else:
|
|
direction = 'in'
|
|
status = self.parse_transaction_status(self.safe_string(item, 'transactStatus'))
|
|
return self.safe_ledger_entry({
|
|
'info': item,
|
|
'id': id,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'direction': direction,
|
|
'account': account,
|
|
'referenceId': referenceId,
|
|
'referenceAccount': referenceAccount,
|
|
'type': type,
|
|
'currency': code,
|
|
'amount': self.parse_number(amount),
|
|
'before': before,
|
|
'after': self.parse_number(after),
|
|
'status': status,
|
|
'fee': fee,
|
|
}, currency)
|
|
|
|
def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
|
|
"""
|
|
fetch the history of changes, actions done by the user or operations that altered the balance of the user
|
|
|
|
https://www.bitmex.com/api/explorer/#not /User/User_getWalletHistory
|
|
|
|
:param str [code]: unified currency code, default is None
|
|
:param int [since]: timestamp in ms of the earliest ledger entry, default is None
|
|
:param int [limit]: max number of ledger entries to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
# 'start': 123,
|
|
}
|
|
#
|
|
# if since is not None:
|
|
# # date-based pagination not supported
|
|
# }
|
|
#
|
|
if limit is not None:
|
|
request['count'] = limit
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
response = self.privateGetUserWalletHistory(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "transactID": "69573da3-7744-5467-3207-89fd6efe7a47",
|
|
# "account": 24321,
|
|
# "currency": "XBt",
|
|
# "transactType": "Withdrawal", # "AffiliatePayout", "Transfer", "Deposit", "RealisedPNL", ...
|
|
# "amount": -1000000,
|
|
# "fee": 300000,
|
|
# "transactStatus": "Completed", # "Canceled", ...
|
|
# "address": "1Ex4fkF4NhQaQdRWNoYpqiPbDBbq18Kdd9",
|
|
# "tx": "3BMEX91ZhhKoWtsH9QRb5dNXnmnGpiEetA",
|
|
# "text": "",
|
|
# "transactTime": "2017-03-21T20:05:14.388Z",
|
|
# "walletBalance": 0, # balance after
|
|
# "marginBalance": null,
|
|
# "timestamp": "2017-03-22T13:09:23.514Z"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_ledger(response, currency, since, limit)
|
|
|
|
def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch history of deposits and withdrawals
|
|
|
|
https://www.bitmex.com/api/explorer/#not /User/User_getWalletHistory
|
|
|
|
:param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
|
|
:param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
|
|
:param int [limit]: max number of deposit/withdrawals to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
'currency': 'all',
|
|
# 'start': 123,
|
|
}
|
|
#
|
|
# if since is not None:
|
|
# # date-based pagination not supported
|
|
# }
|
|
#
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
if limit is not None:
|
|
request['count'] = limit
|
|
response = self.privateGetUserWalletHistory(self.extend(request, params))
|
|
transactions = self.filter_by_array(response, 'transactType', ['Withdrawal', 'Deposit'], False)
|
|
return self.parse_transactions(transactions, currency, since, limit)
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
statuses: dict = {
|
|
'Confirmed': 'pending',
|
|
'Canceled': 'canceled',
|
|
'Completed': 'ok',
|
|
'Pending': 'pending',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# {
|
|
# "transactID": "ffe699c2-95ee-4c13-91f9-0faf41daec25",
|
|
# "account": 123456,
|
|
# "currency": "XBt",
|
|
# "network":'', # "tron" for USDt, etc...
|
|
# "transactType": "Withdrawal",
|
|
# "amount": -100100000,
|
|
# "fee": 100000,
|
|
# "transactStatus": "Completed",
|
|
# "address": "385cR5DM96n1HvBDMzLHPYcw89fZAXULJP",
|
|
# "tx": "3BMEXabcdefghijklmnopqrstuvwxyz123",
|
|
# "text": '',
|
|
# "transactTime": "2019-01-02T01:00:00.000Z",
|
|
# "walletBalance": 99900000, # self field might be inexistent
|
|
# "marginBalance": None, # self field might be inexistent
|
|
# "timestamp": "2019-01-02T13:00:00.000Z"
|
|
# }
|
|
#
|
|
currencyId = self.safe_string(transaction, 'currency')
|
|
currency = self.safe_currency(currencyId, currency)
|
|
# For deposits, transactTime == timestamp
|
|
# For withdrawals, transactTime is submission, timestamp is processed
|
|
transactTime = self.parse8601(self.safe_string(transaction, 'transactTime'))
|
|
timestamp = self.parse8601(self.safe_string(transaction, 'timestamp'))
|
|
type = self.safe_string_lower(transaction, 'transactType')
|
|
# Deposits have no from address or to address, withdrawals have both
|
|
address = None
|
|
addressFrom = None
|
|
addressTo = None
|
|
if type == 'withdrawal':
|
|
address = self.safe_string(transaction, 'address')
|
|
addressFrom = self.safe_string(transaction, 'tx')
|
|
addressTo = address
|
|
elif type == 'deposit':
|
|
addressTo = self.safe_string(transaction, 'address')
|
|
addressFrom = self.safe_string(transaction, 'tx')
|
|
amountString = self.safe_string(transaction, 'amount')
|
|
amountStringAbs = Precise.string_abs(amountString)
|
|
amount = self.convert_to_real_amount(currency['code'], amountStringAbs)
|
|
feeCostString = self.safe_string(transaction, 'fee')
|
|
feeCost = self.convert_to_real_amount(currency['code'], feeCostString)
|
|
status = self.safe_string(transaction, 'transactStatus')
|
|
if status is not None:
|
|
status = self.parse_transaction_status(status)
|
|
return {
|
|
'info': transaction,
|
|
'id': self.safe_string(transaction, 'transactID'),
|
|
'txid': self.safe_string(transaction, 'tx'),
|
|
'type': type,
|
|
'currency': currency['code'],
|
|
'network': self.network_id_to_code(self.safe_string(transaction, 'network'), currency['code']),
|
|
'amount': self.parse_number(amount),
|
|
'status': status,
|
|
'timestamp': transactTime,
|
|
'datetime': self.iso8601(transactTime),
|
|
'address': address,
|
|
'addressFrom': addressFrom,
|
|
'addressTo': addressTo,
|
|
'tag': None,
|
|
'tagFrom': None,
|
|
'tagTo': None,
|
|
'updated': timestamp,
|
|
'internal': None,
|
|
'comment': None,
|
|
'fee': {
|
|
'currency': currency['code'],
|
|
'cost': self.parse_number(feeCost),
|
|
'rate': None,
|
|
},
|
|
}
|
|
|
|
def fetch_ticker(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
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_get
|
|
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
response = self.publicGetInstrument(self.extend(request, params))
|
|
ticker = self.safe_value(response, 0)
|
|
if ticker is None:
|
|
raise BadSymbol(self.id + ' fetchTicker() symbol ' + symbol + ' not found')
|
|
return self.parse_ticker(ticker, market)
|
|
|
|
def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActiveAndIndices
|
|
|
|
:param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
response = self.publicGetInstrumentActiveAndIndices(params)
|
|
# same response "fetchMarkets"
|
|
result: dict = {}
|
|
for i in range(0, len(response)):
|
|
ticker = self.parse_ticker(response[i])
|
|
symbol = self.safe_string(ticker, 'symbol')
|
|
if symbol is not None:
|
|
result[symbol] = ticker
|
|
return self.filter_by_array_tickers(result, 'symbol', symbols)
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
# see response sample under "fetchMarkets" because same endpoint is being used here
|
|
marketId = self.safe_string(ticker, 'symbol')
|
|
symbol = self.safe_symbol(marketId, market)
|
|
timestamp = self.parse8601(self.safe_string(ticker, 'timestamp'))
|
|
open = self.safe_string(ticker, 'prevPrice24h')
|
|
last = self.safe_string(ticker, 'lastPrice')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': self.safe_string(ticker, 'highPrice'),
|
|
'low': self.safe_string(ticker, 'lowPrice'),
|
|
'bid': self.safe_string(ticker, 'bidPrice'),
|
|
'bidVolume': None,
|
|
'ask': self.safe_string(ticker, 'askPrice'),
|
|
'askVolume': None,
|
|
'vwap': self.safe_string(ticker, 'vwap'),
|
|
'open': open,
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': None,
|
|
'baseVolume': self.safe_string(ticker, 'homeNotional24h'),
|
|
'quoteVolume': self.safe_string(ticker, 'foreignNotional24h'),
|
|
'markPrice': self.safe_string(ticker, 'markPrice'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# {
|
|
# "timestamp":"2015-09-25T13:38:00.000Z",
|
|
# "symbol":"XBTUSD",
|
|
# "open":237.45,
|
|
# "high":237.45,
|
|
# "low":237.45,
|
|
# "close":237.45,
|
|
# "trades":0,
|
|
# "volume":0,
|
|
# "vwap":null,
|
|
# "lastSize":null,
|
|
# "turnover":0,
|
|
# "homeNotional":0,
|
|
# "foreignNotional":0
|
|
# }
|
|
#
|
|
marketId = self.safe_string(ohlcv, 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
volume = self.convert_from_raw_quantity(market['symbol'], self.safe_string(ohlcv, 'volume'))
|
|
return [
|
|
self.parse8601(self.safe_string(ohlcv, 'timestamp')),
|
|
self.safe_number(ohlcv, 'open'),
|
|
self.safe_number(ohlcv, 'high'),
|
|
self.safe_number(ohlcv, 'low'),
|
|
self.safe_number(ohlcv, 'close'),
|
|
volume,
|
|
]
|
|
|
|
def fetch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Trade/Trade_getBucketed
|
|
|
|
: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 boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params)
|
|
# send JSON key/value pairs, such as {"key": "value"}
|
|
# filter by individual fields and do advanced queries on timestamps
|
|
# filter: Dict = {'key': 'value'}
|
|
# send a bare series(e.g. XBU) to nearest expiring contract in that series
|
|
# you can also send a timeframe, e.g. XBU:monthly
|
|
# timeframes: daily, weekly, monthly, quarterly, and biquarterly
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'binSize': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
'partial': True, # True == include yet-incomplete current bins
|
|
# 'filter': filter, # filter by individual fields and do advanced queries
|
|
# 'columns': [], # will return all columns if omitted
|
|
# 'start': 0, # starting point for results(wtf?)
|
|
# 'reverse': False, # True == newest first
|
|
# 'endTime': '', # ending date filter for results
|
|
}
|
|
if limit is not None:
|
|
request['count'] = limit # default 100, max 500
|
|
until = self.safe_integer(params, 'until')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['endTime'] = self.iso8601(until)
|
|
duration = self.parse_timeframe(timeframe) * 1000
|
|
fetchOHLCVOpenTimestamp = self.safe_bool(self.options, 'fetchOHLCVOpenTimestamp', True)
|
|
# if since is not set, they will return candles starting from 2017-01-01
|
|
if since is not None:
|
|
timestamp = since
|
|
if fetchOHLCVOpenTimestamp:
|
|
timestamp = self.sum(timestamp, duration)
|
|
startTime = self.iso8601(timestamp)
|
|
request['startTime'] = startTime # starting date filter for results
|
|
else:
|
|
request['reverse'] = True
|
|
response = self.publicGetTradeBucketed(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {"timestamp":"2015-09-25T13:38:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0},
|
|
# {"timestamp":"2015-09-25T13:39:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0},
|
|
# {"timestamp":"2015-09-25T13:40:00.000Z","symbol":"XBTUSD","open":237.45,"high":237.45,"low":237.45,"close":237.45,"trades":0,"volume":0,"vwap":null,"lastSize":null,"turnover":0,"homeNotional":0,"foreignNotional":0}
|
|
# ]
|
|
#
|
|
result = self.parse_ohlcvs(response, market, timeframe, since, limit)
|
|
if fetchOHLCVOpenTimestamp:
|
|
# bitmex returns the candle's close timestamp - https://github.com/ccxt/ccxt/issues/4446
|
|
# we can emulate the open timestamp by shifting all the timestamps one place
|
|
# so the previous close becomes the current open, and we drop the first candle
|
|
for i in range(0, len(result)):
|
|
result[i][0] = result[i][0] - duration
|
|
return result
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# fetchTrades(public)
|
|
#
|
|
# {
|
|
# "timestamp": "2018-08-28T00:00:02.735Z",
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Buy",
|
|
# "size": 2000,
|
|
# "price": 6906.5,
|
|
# "tickDirection": "PlusTick",
|
|
# "trdMatchID": "b9a42432-0a46-6a2f-5ecc-c32e9ca4baf8",
|
|
# "grossValue": 28958000,
|
|
# "homeNotional": 0.28958,
|
|
# "foreignNotional": 2000
|
|
# }
|
|
#
|
|
# fetchMyTrades(private)
|
|
#
|
|
# {
|
|
# "execID": "string",
|
|
# "orderID": "string",
|
|
# "clOrdID": "string",
|
|
# "clOrdLinkID": "string",
|
|
# "account": 0,
|
|
# "symbol": "string",
|
|
# "side": "string",
|
|
# "lastQty": 0,
|
|
# "lastPx": 0,
|
|
# "underlyingLastPx": 0,
|
|
# "lastMkt": "string",
|
|
# "lastLiquidityInd": "string",
|
|
# "simpleOrderQty": 0,
|
|
# "orderQty": 0,
|
|
# "price": 0,
|
|
# "displayQty": 0,
|
|
# "stopPx": 0,
|
|
# "pegOffsetValue": 0,
|
|
# "pegPriceType": "string",
|
|
# "currency": "string",
|
|
# "settlCurrency": "string",
|
|
# "execType": "string",
|
|
# "ordType": "string",
|
|
# "timeInForce": "string",
|
|
# "execInst": "string",
|
|
# "contingencyType": "string",
|
|
# "exDestination": "string",
|
|
# "ordStatus": "string",
|
|
# "triggered": "string",
|
|
# "workingIndicator": True,
|
|
# "ordRejReason": "string",
|
|
# "simpleLeavesQty": 0,
|
|
# "leavesQty": 0,
|
|
# "simpleCumQty": 0,
|
|
# "cumQty": 0,
|
|
# "avgPx": 0,
|
|
# "commission": 0,
|
|
# "tradePublishIndicator": "string",
|
|
# "multiLegReportingType": "string",
|
|
# "text": "string",
|
|
# "trdMatchID": "string",
|
|
# "execCost": 0,
|
|
# "execComm": 0,
|
|
# "homeNotional": 0,
|
|
# "foreignNotional": 0,
|
|
# "transactTime": "2019-03-05T12:47:02.762Z",
|
|
# "timestamp": "2019-03-05T12:47:02.762Z"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(trade, 'symbol')
|
|
symbol = self.safe_symbol(marketId, market)
|
|
timestamp = self.parse8601(self.safe_string(trade, 'timestamp'))
|
|
priceString = self.safe_string_2(trade, 'avgPx', 'price')
|
|
amountString = self.convert_from_raw_quantity(symbol, self.safe_string_2(trade, 'size', 'lastQty'))
|
|
execCost = self.number_to_string(self.convert_from_raw_cost(symbol, self.safe_string(trade, 'execCost')))
|
|
id = self.safe_string(trade, 'trdMatchID')
|
|
order = self.safe_string(trade, 'orderID')
|
|
side = self.safe_string_lower(trade, 'side')
|
|
# price * amount doesn't work for all symbols(e.g. XBT, ETH)
|
|
fee = None
|
|
feeCostString = self.number_to_string(self.convert_from_raw_cost(symbol, self.safe_string(trade, 'execComm')))
|
|
if feeCostString is not None:
|
|
currencyId = self.safe_string_2(trade, 'settlCurrency', 'currency')
|
|
fee = {
|
|
'cost': feeCostString,
|
|
'currency': self.safe_currency_code(currencyId),
|
|
'rate': self.safe_string(trade, 'commission'),
|
|
}
|
|
# Trade or Funding
|
|
execType = self.safe_string(trade, 'execType')
|
|
takerOrMaker = None
|
|
if feeCostString is not None and execType == 'Trade':
|
|
takerOrMaker = 'maker' if Precise.string_lt(feeCostString, '0') else 'taker'
|
|
type = self.safe_string_lower(trade, 'ordType')
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'order': order,
|
|
'type': type,
|
|
'takerOrMaker': takerOrMaker,
|
|
'side': side,
|
|
'price': priceString,
|
|
'cost': Precise.string_abs(execCost),
|
|
'amount': amountString,
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'New': 'open',
|
|
'PartiallyFilled': 'open',
|
|
'Filled': 'closed',
|
|
'DoneForDay': 'open',
|
|
'Canceled': 'canceled',
|
|
'PendingCancel': 'open',
|
|
'PendingNew': 'open',
|
|
'Rejected': 'rejected',
|
|
'Expired': 'expired',
|
|
'Stopped': 'open',
|
|
'Untriggered': 'open',
|
|
'Triggered': 'open',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_time_in_force(self, timeInForce: Str):
|
|
timeInForces: dict = {
|
|
'Day': 'Day',
|
|
'GoodTillCancel': 'GTC',
|
|
'ImmediateOrCancel': 'IOC',
|
|
'FillOrKill': 'FOK',
|
|
}
|
|
return self.safe_string(timeInForces, timeInForce, timeInForce)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# {
|
|
# "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"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(order, 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
symbol = market['symbol']
|
|
qty = self.safe_string(order, 'orderQty')
|
|
cost = None
|
|
amount = None
|
|
isInverse = False
|
|
if marketId is None:
|
|
defaultSubType = self.safe_string(self.options, 'defaultSubType', 'linear')
|
|
isInverse = (defaultSubType == 'inverse')
|
|
else:
|
|
isInverse = self.safe_bool(market, 'inverse', False)
|
|
if isInverse:
|
|
cost = self.convert_from_raw_quantity(symbol, qty)
|
|
else:
|
|
amount = self.convert_from_raw_quantity(symbol, qty)
|
|
average = self.safe_string(order, 'avgPx')
|
|
filled = None
|
|
cumQty = self.number_to_string(self.convert_from_raw_quantity(symbol, self.safe_string(order, 'cumQty')))
|
|
if isInverse:
|
|
filled = Precise.string_div(cumQty, average)
|
|
else:
|
|
filled = cumQty
|
|
execInst = self.safe_string(order, 'execInst')
|
|
postOnly = None
|
|
if execInst is not None:
|
|
postOnly = (execInst == 'ParticipateDoNotInitiate')
|
|
timestamp = self.parse8601(self.safe_string(order, 'timestamp'))
|
|
triggerPrice = self.safe_number(order, 'stopPx')
|
|
remaining = self.safe_string(order, 'leavesQty')
|
|
return self.safe_order({
|
|
'info': order,
|
|
'id': self.safe_string(order, 'orderID'),
|
|
'clientOrderId': self.safe_string(order, 'clOrdID'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': self.parse8601(self.safe_string(order, 'transactTime')),
|
|
'symbol': symbol,
|
|
'type': self.safe_string_lower(order, 'ordType'),
|
|
'timeInForce': self.parse_time_in_force(self.safe_string(order, 'timeInForce')),
|
|
'postOnly': postOnly,
|
|
'side': self.safe_string_lower(order, 'side'),
|
|
'price': self.safe_string(order, 'price'),
|
|
'triggerPrice': triggerPrice,
|
|
'amount': amount,
|
|
'cost': cost,
|
|
'average': average,
|
|
'filled': filled,
|
|
'remaining': self.convert_from_raw_quantity(symbol, remaining),
|
|
'status': self.parse_order_status(self.safe_string(order, 'ordStatus')),
|
|
'fee': None,
|
|
'trades': None,
|
|
}, market)
|
|
|
|
def fetch_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/api/explorer/#not /Trade/Trade_get
|
|
|
|
: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 boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchTrades', symbol, since, limit, params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
if since is not None:
|
|
request['startTime'] = self.iso8601(since)
|
|
else:
|
|
# by default reverse=false, i.e. trades are fetched since the time of market inception(year 2015 for XBTUSD)
|
|
request['reverse'] = True
|
|
if limit is not None:
|
|
request['count'] = min(limit, 1000) # api maximum 1000
|
|
until = self.safe_integer_2(params, 'until', 'endTime')
|
|
if until is not None:
|
|
params = self.omit(params, ['until'])
|
|
request['endTime'] = self.iso8601(until)
|
|
response = self.publicGetTrade(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "timestamp": "2018-08-28T00:00:02.735Z",
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Buy",
|
|
# "size": 2000,
|
|
# "price": 6906.5,
|
|
# "tickDirection": "PlusTick",
|
|
# "trdMatchID": "b9a42432-0a46-6a2f-5ecc-c32e9ca4baf8",
|
|
# "grossValue": 28958000,
|
|
# "homeNotional": 0.28958,
|
|
# "foreignNotional": 2000
|
|
# },
|
|
# {
|
|
# "timestamp": "2018-08-28T00:00:03.778Z",
|
|
# "symbol": "XBTUSD",
|
|
# "side": "Sell",
|
|
# "size": 1000,
|
|
# "price": 6906,
|
|
# "tickDirection": "MinusTick",
|
|
# "trdMatchID": "0d4f1682-5270-a800-569b-4a0eb92db97c",
|
|
# "grossValue": 14480000,
|
|
# "homeNotional": 0.1448,
|
|
# "foreignNotional": 1000
|
|
# },
|
|
# ]
|
|
#
|
|
return self.parse_trades(response, market, since, limit)
|
|
|
|
def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
create a trade order
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_new
|
|
|
|
: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 [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 dict [params.triggerPrice]: the price at which a trigger order is triggered at
|
|
:param dict [params.triggerDirection]: the direction whenever the trigger happens with relation to price - 'ascending' or 'descending'
|
|
:param float [params.trailingAmount]: the quote amount to trail away from the current market price
|
|
:returns dict: an `order structure <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
orderType = self.capitalize(type)
|
|
reduceOnly = self.safe_value(params, 'reduceOnly')
|
|
if reduceOnly is not None:
|
|
if (not market['swap']) and (not market['future']):
|
|
raise InvalidOrder(self.id + ' createOrder() does not support reduceOnly for ' + market['type'] + ' orders, reduceOnly orders are supported for swap and future markets only')
|
|
brokerId = self.safe_string(self.options, 'brokerId', 'CCXT')
|
|
qty = self.parse_to_int(self.amount_to_precision(symbol, amount))
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'side': self.capitalize(side),
|
|
'orderQty': qty, # lot size multiplied by the number of contracts
|
|
'ordType': orderType,
|
|
'text': brokerId,
|
|
}
|
|
# support for unified trigger format
|
|
triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPx', 'stopPrice'])
|
|
trailingAmount = self.safe_string_2(params, 'trailingAmount', 'pegOffsetValue')
|
|
isTriggerOrder = triggerPrice is not None
|
|
isTrailingAmountOrder = trailingAmount is not None
|
|
if isTriggerOrder or isTrailingAmountOrder:
|
|
triggerDirection = self.safe_string(params, 'triggerDirection')
|
|
triggerAbove = ((triggerDirection == 'ascending') or (triggerDirection == 'above'))
|
|
if (type == 'limit') or (type == 'market'):
|
|
self.check_required_argument('createOrder', triggerDirection, 'triggerDirection', ['above', 'below'])
|
|
if type == 'limit':
|
|
if side == 'buy':
|
|
orderType = 'StopLimit' if triggerAbove else 'LimitIfTouched'
|
|
else:
|
|
orderType = 'LimitIfTouched' if triggerAbove else 'StopLimit'
|
|
elif type == 'market':
|
|
if side == 'buy':
|
|
orderType = 'Stop' if triggerAbove else 'MarketIfTouched'
|
|
else:
|
|
orderType = 'MarketIfTouched' if triggerAbove else 'Stop'
|
|
if isTrailingAmountOrder:
|
|
isStopSellOrder = (side == 'sell') and ((orderType == 'Stop') or (orderType == 'StopLimit'))
|
|
isBuyIfTouchedOrder = (side == 'buy') and ((orderType == 'MarketIfTouched') or (orderType == 'LimitIfTouched'))
|
|
if isStopSellOrder or isBuyIfTouchedOrder:
|
|
trailingAmount = '-' + trailingAmount
|
|
request['pegOffsetValue'] = self.parse_to_numeric(trailingAmount)
|
|
request['pegPriceType'] = 'TrailingStopPeg'
|
|
else:
|
|
if triggerPrice is None:
|
|
# if exchange specific trigger types were provided
|
|
raise ArgumentsRequired(self.id + ' createOrder() requires a triggerPrice parameter for the ' + orderType + ' order type')
|
|
request['stopPx'] = self.parse_to_numeric(self.price_to_precision(symbol, triggerPrice))
|
|
request['ordType'] = orderType
|
|
params = self.omit(params, ['triggerPrice', 'stopPrice', 'stopPx', 'triggerDirection', 'trailingAmount'])
|
|
if (orderType == 'Limit') or (orderType == 'StopLimit') or (orderType == 'LimitIfTouched'):
|
|
request['price'] = self.parse_to_numeric(self.price_to_precision(symbol, price))
|
|
clientOrderId = self.safe_string_2(params, 'clOrdID', 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
request['clOrdID'] = clientOrderId
|
|
params = self.omit(params, ['clOrdID', 'clientOrderId'])
|
|
response = self.privatePostOrder(self.extend(request, params))
|
|
return self.parse_order(response, market)
|
|
|
|
def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
|
|
self.load_markets()
|
|
request: dict = {}
|
|
trailingAmount = self.safe_string_2(params, 'trailingAmount', 'pegOffsetValue')
|
|
isTrailingAmountOrder = trailingAmount is not None
|
|
if isTrailingAmountOrder:
|
|
triggerDirection = self.safe_string(params, 'triggerDirection')
|
|
triggerAbove = ((triggerDirection == 'ascending') or (triggerDirection == 'above'))
|
|
if (type == 'limit') or (type == 'market'):
|
|
self.check_required_argument('createOrder', triggerDirection, 'triggerDirection', ['above', 'below'])
|
|
orderType = None
|
|
if type == 'limit':
|
|
if side == 'buy':
|
|
orderType = 'StopLimit' if triggerAbove else 'LimitIfTouched'
|
|
else:
|
|
orderType = 'LimitIfTouched' if triggerAbove else 'StopLimit'
|
|
elif type == 'market':
|
|
if side == 'buy':
|
|
orderType = 'Stop' if triggerAbove else 'MarketIfTouched'
|
|
else:
|
|
orderType = 'MarketIfTouched' if triggerAbove else 'Stop'
|
|
isStopSellOrder = (side == 'sell') and ((orderType == 'Stop') or (orderType == 'StopLimit'))
|
|
isBuyIfTouchedOrder = (side == 'buy') and ((orderType == 'MarketIfTouched') or (orderType == 'LimitIfTouched'))
|
|
if isStopSellOrder or isBuyIfTouchedOrder:
|
|
trailingAmount = '-' + trailingAmount
|
|
request['pegOffsetValue'] = self.parse_to_numeric(trailingAmount)
|
|
params = self.omit(params, ['triggerDirection', 'trailingAmount'])
|
|
origClOrdID = self.safe_string_2(params, 'origClOrdID', 'clientOrderId')
|
|
if origClOrdID is not None:
|
|
request['origClOrdID'] = origClOrdID
|
|
clientOrderId = self.safe_string(params, 'clOrdID', 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
request['clOrdID'] = clientOrderId
|
|
params = self.omit(params, ['origClOrdID', 'clOrdID', 'clientOrderId'])
|
|
else:
|
|
request['orderID'] = id
|
|
if amount is not None:
|
|
qty = self.parse_to_int(self.amount_to_precision(symbol, amount))
|
|
request['orderQty'] = qty
|
|
if price is not None:
|
|
request['price'] = price
|
|
brokerId = self.safe_string(self.options, 'brokerId', 'CCXT')
|
|
request['text'] = brokerId
|
|
response = self.privatePutOrder(self.extend(request, params))
|
|
return self.parse_order(response)
|
|
|
|
def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_cancel
|
|
|
|
:param str id: order id
|
|
:param str symbol: not used by bitmex cancelOrder()
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
self.load_markets()
|
|
# https://github.com/ccxt/ccxt/issues/6507
|
|
clientOrderId = self.safe_value_2(params, 'clOrdID', 'clientOrderId')
|
|
request: dict = {}
|
|
if clientOrderId is None:
|
|
request['orderID'] = id
|
|
else:
|
|
request['clOrdID'] = clientOrderId
|
|
params = self.omit(params, ['clOrdID', 'clientOrderId'])
|
|
response = self.privateDeleteOrder(self.extend(request, params))
|
|
order = self.safe_value(response, 0, {})
|
|
error = self.safe_string(order, 'error')
|
|
if error is not None:
|
|
if error.find('Unable to cancel order due to existing state') >= 0:
|
|
raise OrderNotFound(self.id + ' cancelOrder() failed: ' + error)
|
|
return self.parse_order(order)
|
|
|
|
def cancel_orders(self, ids: List[str], symbol: Str = None, params={}):
|
|
"""
|
|
cancel multiple orders
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_cancel
|
|
|
|
:param str[] ids: order ids
|
|
:param str symbol: not used by bitmex cancelOrders()
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
# return self.cancel_order(ids, symbol, params)
|
|
self.load_markets()
|
|
# https://github.com/ccxt/ccxt/issues/6507
|
|
clientOrderId = self.safe_value_2(params, 'clOrdID', 'clientOrderId')
|
|
request: dict = {}
|
|
if clientOrderId is None:
|
|
request['orderID'] = ids
|
|
else:
|
|
request['clOrdID'] = clientOrderId
|
|
params = self.omit(params, ['clOrdID', 'clientOrderId'])
|
|
response = self.privateDeleteOrder(self.extend(request, params))
|
|
return self.parse_orders(response)
|
|
|
|
def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_cancelAll
|
|
|
|
:param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
|
|
: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>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {}
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
response = self.privateDeleteOrderAll(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "orderID": "string",
|
|
# "clOrdID": "string",
|
|
# "clOrdLinkID": "string",
|
|
# "account": 0,
|
|
# "symbol": "string",
|
|
# "side": "string",
|
|
# "simpleOrderQty": 0,
|
|
# "orderQty": 0,
|
|
# "price": 0,
|
|
# "displayQty": 0,
|
|
# "stopPx": 0,
|
|
# "pegOffsetValue": 0,
|
|
# "pegPriceType": "string",
|
|
# "currency": "string",
|
|
# "settlCurrency": "string",
|
|
# "ordType": "string",
|
|
# "timeInForce": "string",
|
|
# "execInst": "string",
|
|
# "contingencyType": "string",
|
|
# "exDestination": "string",
|
|
# "ordStatus": "string",
|
|
# "triggered": "string",
|
|
# "workingIndicator": True,
|
|
# "ordRejReason": "string",
|
|
# "simpleLeavesQty": 0,
|
|
# "leavesQty": 0,
|
|
# "simpleCumQty": 0,
|
|
# "cumQty": 0,
|
|
# "avgPx": 0,
|
|
# "multiLegReportingType": "string",
|
|
# "text": "string",
|
|
# "transactTime": "2020-06-01T09:36:35.290Z",
|
|
# "timestamp": "2020-06-01T09:36:35.290Z"
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_orders(response, market)
|
|
|
|
def cancel_all_orders_after(self, timeout: Int, params={}):
|
|
"""
|
|
dead man's switch, cancel all orders after the given timeout
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Order/Order_cancelAllAfter
|
|
|
|
:param number timeout: time in milliseconds, 0 represents cancel the timer
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: the api result
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {
|
|
'timeout': self.parse_to_int(timeout / 1000) if (timeout > 0) else 0,
|
|
}
|
|
response = self.privatePostOrderCancelAllAfter(self.extend(request, params))
|
|
#
|
|
# {
|
|
# now: '2024-04-09T09:01:56.560Z',
|
|
# cancelTime: '2024-04-09T09:01:56.660Z'
|
|
# }
|
|
#
|
|
return response
|
|
|
|
def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages:
|
|
"""
|
|
fetch the set leverage for all contract markets
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Position/Position_get
|
|
|
|
:param str[] [symbols]: a list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `leverage structures <https://docs.ccxt.com/#/?id=leverage-structure>`
|
|
"""
|
|
self.load_markets()
|
|
leverages = self.fetch_positions(symbols, params)
|
|
return self.parse_leverages(leverages, symbols, 'symbol')
|
|
|
|
def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage:
|
|
marketId = self.safe_string(leverage, 'symbol')
|
|
return {
|
|
'info': leverage,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'marginMode': self.safe_string_lower(leverage, 'marginMode'),
|
|
'longLeverage': self.safe_integer(leverage, 'leverage'),
|
|
'shortLeverage': self.safe_integer(leverage, 'leverage'),
|
|
}
|
|
|
|
def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
|
|
"""
|
|
fetch all open positions
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Position/Position_get
|
|
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/#/?id=position-structure>`
|
|
"""
|
|
self.load_markets()
|
|
response = self.privateGetPosition(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "account": 0,
|
|
# "symbol": "string",
|
|
# "currency": "string",
|
|
# "underlying": "string",
|
|
# "quoteCurrency": "string",
|
|
# "commission": 0,
|
|
# "initMarginReq": 0,
|
|
# "maintMarginReq": 0,
|
|
# "riskLimit": 0,
|
|
# "leverage": 0,
|
|
# "crossMargin": True,
|
|
# "deleveragePercentile": 0,
|
|
# "rebalancedPnl": 0,
|
|
# "prevRealisedPnl": 0,
|
|
# "prevUnrealisedPnl": 0,
|
|
# "prevClosePrice": 0,
|
|
# "openingTimestamp": "2020-11-09T06:53:59.892Z",
|
|
# "openingQty": 0,
|
|
# "openingCost": 0,
|
|
# "openingComm": 0,
|
|
# "openOrderBuyQty": 0,
|
|
# "openOrderBuyCost": 0,
|
|
# "openOrderBuyPremium": 0,
|
|
# "openOrderSellQty": 0,
|
|
# "openOrderSellCost": 0,
|
|
# "openOrderSellPremium": 0,
|
|
# "execBuyQty": 0,
|
|
# "execBuyCost": 0,
|
|
# "execSellQty": 0,
|
|
# "execSellCost": 0,
|
|
# "execQty": 0,
|
|
# "execCost": 0,
|
|
# "execComm": 0,
|
|
# "currentTimestamp": "2020-11-09T06:53:59.893Z",
|
|
# "currentQty": 0,
|
|
# "currentCost": 0,
|
|
# "currentComm": 0,
|
|
# "realisedCost": 0,
|
|
# "unrealisedCost": 0,
|
|
# "grossOpenCost": 0,
|
|
# "grossOpenPremium": 0,
|
|
# "grossExecCost": 0,
|
|
# "isOpen": True,
|
|
# "markPrice": 0,
|
|
# "markValue": 0,
|
|
# "riskValue": 0,
|
|
# "homeNotional": 0,
|
|
# "foreignNotional": 0,
|
|
# "posState": "string",
|
|
# "posCost": 0,
|
|
# "posCost2": 0,
|
|
# "posCross": 0,
|
|
# "posInit": 0,
|
|
# "posComm": 0,
|
|
# "posLoss": 0,
|
|
# "posMargin": 0,
|
|
# "posMaint": 0,
|
|
# "posAllowance": 0,
|
|
# "taxableMargin": 0,
|
|
# "initMargin": 0,
|
|
# "maintMargin": 0,
|
|
# "sessionMargin": 0,
|
|
# "targetExcessMargin": 0,
|
|
# "varMargin": 0,
|
|
# "realisedGrossPnl": 0,
|
|
# "realisedTax": 0,
|
|
# "realisedPnl": 0,
|
|
# "unrealisedGrossPnl": 0,
|
|
# "longBankrupt": 0,
|
|
# "shortBankrupt": 0,
|
|
# "taxBase": 0,
|
|
# "indicativeTaxRate": 0,
|
|
# "indicativeTax": 0,
|
|
# "unrealisedTax": 0,
|
|
# "unrealisedPnl": 0,
|
|
# "unrealisedPnlPcnt": 0,
|
|
# "unrealisedRoePcnt": 0,
|
|
# "simpleQty": 0,
|
|
# "simpleCost": 0,
|
|
# "simpleValue": 0,
|
|
# "simplePnl": 0,
|
|
# "simplePnlPcnt": 0,
|
|
# "avgCostPrice": 0,
|
|
# "avgEntryPrice": 0,
|
|
# "breakEvenPrice": 0,
|
|
# "marginCallPrice": 0,
|
|
# "liquidationPrice": 0,
|
|
# "bankruptPrice": 0,
|
|
# "timestamp": "2020-11-09T06:53:59.894Z",
|
|
# "lastPrice": 0,
|
|
# "lastValue": 0
|
|
# }
|
|
# ]
|
|
#
|
|
results = self.parse_positions(response, symbols)
|
|
return self.filter_by_array_positions(results, 'symbol', symbols, False)
|
|
|
|
def parse_position(self, position: dict, market: Market = None):
|
|
#
|
|
# {
|
|
# "account": 9371654,
|
|
# "symbol": "ETHUSDT",
|
|
# "currency": "USDt",
|
|
# "underlying": "ETH",
|
|
# "quoteCurrency": "USDT",
|
|
# "commission": 0.00075,
|
|
# "initMarginReq": 0.3333333333333333,
|
|
# "maintMarginReq": 0.01,
|
|
# "riskLimit": 1000000000000,
|
|
# "leverage": 3,
|
|
# "crossMargin": False,
|
|
# "deleveragePercentile": 1,
|
|
# "rebalancedPnl": 0,
|
|
# "prevRealisedPnl": 0,
|
|
# "prevUnrealisedPnl": 0,
|
|
# "prevClosePrice": 2053.738,
|
|
# "openingTimestamp": "2022-05-21T04:00:00.000Z",
|
|
# "openingQty": 0,
|
|
# "openingCost": 0,
|
|
# "openingComm": 0,
|
|
# "openOrderBuyQty": 0,
|
|
# "openOrderBuyCost": 0,
|
|
# "openOrderBuyPremium": 0,
|
|
# "openOrderSellQty": 0,
|
|
# "openOrderSellCost": 0,
|
|
# "openOrderSellPremium": 0,
|
|
# "execBuyQty": 2000,
|
|
# "execBuyCost": 39260000,
|
|
# "execSellQty": 0,
|
|
# "execSellCost": 0,
|
|
# "execQty": 2000,
|
|
# "execCost": 39260000,
|
|
# "execComm": 26500,
|
|
# "currentTimestamp": "2022-05-21T04:35:16.397Z",
|
|
# "currentQty": 2000,
|
|
# "currentCost": 39260000,
|
|
# "currentComm": 26500,
|
|
# "realisedCost": 0,
|
|
# "unrealisedCost": 39260000,
|
|
# "grossOpenCost": 0,
|
|
# "grossOpenPremium": 0,
|
|
# "grossExecCost": 39260000,
|
|
# "isOpen": True,
|
|
# "markPrice": 1964.195,
|
|
# "markValue": 39283900,
|
|
# "riskValue": 39283900,
|
|
# "homeNotional": 0.02,
|
|
# "foreignNotional": -39.2839,
|
|
# "posState": "",
|
|
# "posCost": 39260000,
|
|
# "posCost2": 39260000,
|
|
# "posCross": 0,
|
|
# "posInit": 13086667,
|
|
# "posComm": 39261,
|
|
# "posLoss": 0,
|
|
# "posMargin": 13125928,
|
|
# "posMaint": 435787,
|
|
# "posAllowance": 0,
|
|
# "taxableMargin": 0,
|
|
# "initMargin": 0,
|
|
# "maintMargin": 13149828,
|
|
# "sessionMargin": 0,
|
|
# "targetExcessMargin": 0,
|
|
# "varMargin": 0,
|
|
# "realisedGrossPnl": 0,
|
|
# "realisedTax": 0,
|
|
# "realisedPnl": -26500,
|
|
# "unrealisedGrossPnl": 23900,
|
|
# "longBankrupt": 0,
|
|
# "shortBankrupt": 0,
|
|
# "taxBase": 0,
|
|
# "indicativeTaxRate": null,
|
|
# "indicativeTax": 0,
|
|
# "unrealisedTax": 0,
|
|
# "unrealisedPnl": 23900,
|
|
# "unrealisedPnlPcnt": 0.0006,
|
|
# "unrealisedRoePcnt": 0.0018,
|
|
# "simpleQty": null,
|
|
# "simpleCost": null,
|
|
# "simpleValue": null,
|
|
# "simplePnl": null,
|
|
# "simplePnlPcnt": null,
|
|
# "avgCostPrice": 1963,
|
|
# "avgEntryPrice": 1963,
|
|
# "breakEvenPrice": 1964.35,
|
|
# "marginCallPrice": 1328.5,
|
|
# "liquidationPrice": 1328.5,
|
|
# "bankruptPrice": 1308.7,
|
|
# "timestamp": "2022-05-21T04:35:16.397Z",
|
|
# "lastPrice": 1964.195,
|
|
# "lastValue": 39283900
|
|
# }
|
|
#
|
|
market = self.safe_market(self.safe_string(position, 'symbol'), market)
|
|
symbol = market['symbol']
|
|
datetime = self.safe_string(position, 'timestamp')
|
|
crossMargin = self.safe_value(position, 'crossMargin')
|
|
marginMode = 'cross' if (crossMargin is True) else 'isolated'
|
|
notionalString = Precise.string_abs(self.safe_string_2(position, 'foreignNotional', 'homeNotional'))
|
|
settleCurrencyCode = self.safe_string(market, 'settle')
|
|
maintenanceMargin = self.convert_to_real_amount(settleCurrencyCode, self.safe_string(position, 'maintMargin'))
|
|
unrealisedPnl = self.convert_to_real_amount(settleCurrencyCode, self.safe_string(position, 'unrealisedPnl'))
|
|
contracts = self.parse_number(Precise.string_abs(self.safe_string(position, 'currentQty')))
|
|
contractSize = self.safe_number(market, 'contractSize')
|
|
side = None
|
|
homeNotional = self.safe_string(position, 'homeNotional')
|
|
if homeNotional is not None:
|
|
if homeNotional[0] == '-':
|
|
side = 'short'
|
|
else:
|
|
side = 'long'
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': self.safe_string(position, 'account'),
|
|
'symbol': symbol,
|
|
'timestamp': self.parse8601(datetime),
|
|
'datetime': datetime,
|
|
'lastUpdateTimestamp': None,
|
|
'hedged': None,
|
|
'side': side,
|
|
'contracts': contracts,
|
|
'contractSize': contractSize,
|
|
'entryPrice': self.safe_number(position, 'avgEntryPrice'),
|
|
'markPrice': self.safe_number(position, 'markPrice'),
|
|
'lastPrice': None,
|
|
'notional': self.parse_number(notionalString),
|
|
'leverage': self.safe_number(position, 'leverage'),
|
|
'collateral': None,
|
|
'initialMargin': self.safe_number(position, 'initMargin'),
|
|
'initialMarginPercentage': self.safe_number(position, 'initMarginReq'),
|
|
'maintenanceMargin': maintenanceMargin,
|
|
'maintenanceMarginPercentage': self.safe_number(position, 'maintMarginReq'),
|
|
'unrealizedPnl': unrealisedPnl,
|
|
'liquidationPrice': self.safe_number(position, 'liquidationPrice'),
|
|
'marginMode': marginMode,
|
|
'marginRatio': None,
|
|
'percentage': self.safe_number(position, 'unrealisedPnlPcnt'),
|
|
'stopLossPrice': None,
|
|
'takeProfitPrice': None,
|
|
})
|
|
|
|
def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
|
|
https://www.bitmex.com/api/explorer/#not /User/User_requestWithdrawal
|
|
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to
|
|
:param str tag:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
tag, params = self.handle_withdraw_tag_and_params(tag, params)
|
|
self.check_address(address)
|
|
self.load_markets()
|
|
currency = self.currency(code)
|
|
qty = self.convert_from_real_amount(code, amount)
|
|
networkCode = None
|
|
networkCode, params = self.handle_network_code_and_params(params)
|
|
request: dict = {
|
|
'currency': currency['id'],
|
|
'amount': qty,
|
|
'address': address,
|
|
'network': self.network_code_to_id(networkCode, currency['code']),
|
|
# 'otpToken': '123456', # requires if two-factor auth(OTP) is enabled
|
|
# 'fee': 0.001, # bitcoin network fee
|
|
}
|
|
if self.twofa is not None:
|
|
request['otpToken'] = self.totp(self.twofa)
|
|
response = self.privatePostUserRequestWithdrawal(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "transactID": "3aece414-bb29-76c8-6c6d-16a477a51a1e",
|
|
# "account": 1403035,
|
|
# "currency": "USDt",
|
|
# "network": "tron",
|
|
# "transactType": "Withdrawal",
|
|
# "amount": -11000000,
|
|
# "fee": 1000000,
|
|
# "transactStatus": "Pending",
|
|
# "address": "TAf5JxcAQQsC2Nm2zu21XE2iDtnisxPo1x",
|
|
# "tx": "",
|
|
# "text": "",
|
|
# "transactTime": "2022-12-16T07:37:06.500Z",
|
|
# "timestamp": "2022-12-16T07:37:06.500Z",
|
|
# }
|
|
#
|
|
return self.parse_transaction(response, currency)
|
|
|
|
def fetch_funding_rates(self, symbols: Strings = None, params={}) -> FundingRates:
|
|
"""
|
|
fetch the funding rate for multiple markets
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Instrument/Instrument_getActiveAndIndices
|
|
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/#/?id=funding-rates-structure>`, indexed by market symbols
|
|
"""
|
|
self.load_markets()
|
|
response = self.publicGetInstrumentActiveAndIndices(params)
|
|
# same response "fetchMarkets"
|
|
filteredResponse = []
|
|
for i in range(0, len(response)):
|
|
item = response[i]
|
|
marketId = self.safe_string(item, 'symbol')
|
|
market = self.safe_market(marketId)
|
|
swap = self.safe_bool(market, 'swap', False)
|
|
if swap:
|
|
filteredResponse.append(item)
|
|
symbols = self.market_symbols(symbols)
|
|
result = self.parse_funding_rates(filteredResponse)
|
|
return self.filter_by_array(result, 'symbol', symbols)
|
|
|
|
def parse_funding_rate(self, contract, market: Market = None) -> FundingRate:
|
|
# see response sample under "fetchMarkets" because same endpoint is being used here
|
|
datetime = self.safe_string(contract, 'timestamp')
|
|
marketId = self.safe_string(contract, 'symbol')
|
|
fundingDatetime = self.safe_string(contract, 'fundingTimestamp')
|
|
return {
|
|
'info': contract,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'markPrice': self.safe_number(contract, 'markPrice'),
|
|
'indexPrice': None,
|
|
'interestRate': None,
|
|
'estimatedSettlePrice': self.safe_number(contract, 'indicativeSettlePrice'),
|
|
'timestamp': self.parse8601(datetime),
|
|
'datetime': datetime,
|
|
'fundingRate': self.safe_number(contract, 'fundingRate'),
|
|
'fundingTimestamp': self.parse8601(fundingDatetime),
|
|
'fundingDatetime': fundingDatetime,
|
|
'nextFundingRate': self.safe_number(contract, 'indicativeFundingRate'),
|
|
'nextFundingTimestamp': None,
|
|
'nextFundingDatetime': None,
|
|
'previousFundingRate': None,
|
|
'previousFundingTimestamp': None,
|
|
'previousFundingDatetime': None,
|
|
'interval': None,
|
|
}
|
|
|
|
def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
Fetches the history of funding rates
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Funding/Funding_get
|
|
|
|
:param str symbol: unified symbol of the market to fetch the funding rate history for
|
|
:param int [since]: timestamp in ms of the earliest funding rate to fetch
|
|
:param int [limit]: the maximum amount of `funding rate structures <https://docs.ccxt.com/#/?id=funding-rate-history-structure>` to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms for ending date filter
|
|
:param bool [params.reverse]: if True, will sort results newest first
|
|
:param int [params.start]: starting point for results
|
|
:param str [params.columns]: array of column names to fetch in info, if omitted, will return all columns
|
|
:param str [params.filter]: generic table filter, send json key/value pairs, such as {"key": "value"}, you can key on individual fields, and do more advanced querying on timestamps, see the `timestamp docs <https://www.bitmex.com/app/restAPI#Timestamp-Filters>` for more details
|
|
:returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/#/?id=funding-rate-history-structure>`
|
|
"""
|
|
self.load_markets()
|
|
request: dict = {}
|
|
market = None
|
|
if symbol in self.currencies:
|
|
code = self.currency(symbol)
|
|
request['symbol'] = code['id']
|
|
elif symbol is not None:
|
|
splitSymbol = symbol.split(':')
|
|
splitSymbolLength = len(splitSymbol)
|
|
timeframes = ['nearest', 'daily', 'weekly', 'monthly', 'quarterly', 'biquarterly', 'perpetual']
|
|
if (splitSymbolLength > 1) and self.in_array(splitSymbol[1], timeframes):
|
|
code = self.currency(splitSymbol[0])
|
|
symbol = code['id'] + ':' + splitSymbol[1]
|
|
request['symbol'] = symbol
|
|
else:
|
|
market = self.market(symbol)
|
|
request['symbol'] = market['id']
|
|
if since is not None:
|
|
request['startTime'] = self.iso8601(since)
|
|
if limit is not None:
|
|
request['count'] = limit
|
|
until = self.safe_integer(params, 'until')
|
|
params = self.omit(params, ['until'])
|
|
if until is not None:
|
|
request['endTime'] = self.iso8601(until)
|
|
if (since is None) and (until is None):
|
|
request['reverse'] = True
|
|
response = self.publicGetFunding(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "timestamp": "2016-05-07T12:00:00.000Z",
|
|
# "symbol": "ETHXBT",
|
|
# "fundingInterval": "2000-01-02T00:00:00.000Z",
|
|
# "fundingRate": 0.0010890000000000001,
|
|
# "fundingRateDaily": 0.0010890000000000001
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_funding_rate_histories(response, market, since, limit)
|
|
|
|
def parse_funding_rate_history(self, info, market: Market = None):
|
|
#
|
|
# {
|
|
# "timestamp": "2016-05-07T12:00:00.000Z",
|
|
# "symbol": "ETHXBT",
|
|
# "fundingInterval": "2000-01-02T00:00:00.000Z",
|
|
# "fundingRate": 0.0010890000000000001,
|
|
# "fundingRateDaily": 0.0010890000000000001
|
|
# }
|
|
#
|
|
marketId = self.safe_string(info, 'symbol')
|
|
datetime = self.safe_string(info, 'timestamp')
|
|
return {
|
|
'info': info,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'fundingRate': self.safe_number(info, 'fundingRate'),
|
|
'timestamp': self.parse8601(datetime),
|
|
'datetime': datetime,
|
|
}
|
|
|
|
def set_leverage(self, leverage: int, symbol: Str = None, params={}):
|
|
"""
|
|
set the level of leverage for a market
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Position/Position_updateLeverage
|
|
|
|
:param float leverage: the rate of leverage
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: response from the exchange
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument')
|
|
if (leverage < 0.01) or (leverage > 100):
|
|
raise BadRequest(self.id + ' leverage should be between 0.01 and 100')
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
if market['type'] != 'swap' and market['type'] != 'future':
|
|
raise BadSymbol(self.id + ' setLeverage() supports future and swap contracts only')
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'leverage': leverage,
|
|
}
|
|
return self.privatePostPositionLeverage(self.extend(request, params))
|
|
|
|
def set_margin_mode(self, marginMode: str, symbol: Str = None, params={}):
|
|
"""
|
|
set margin mode to 'cross' or 'isolated'
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Position/Position_isolateMargin
|
|
|
|
:param str marginMode: 'cross' or 'isolated'
|
|
:param str symbol: unified market symbol
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: response from the exchange
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' setMarginMode() requires a symbol argument')
|
|
marginMode = marginMode.lower()
|
|
if marginMode != 'isolated' and marginMode != 'cross':
|
|
raise BadRequest(self.id + ' setMarginMode() marginMode argument should be isolated or cross')
|
|
self.load_markets()
|
|
market = self.market(symbol)
|
|
if (market['type'] != 'swap') and (market['type'] != 'future'):
|
|
raise BadSymbol(self.id + ' setMarginMode() supports swap and future contracts only')
|
|
enabled = False if (marginMode == 'cross') else True
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'enabled': enabled,
|
|
}
|
|
return self.privatePostPositionIsolate(self.extend(request, params))
|
|
|
|
def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
fetch the deposit address for a currency associated with self account
|
|
|
|
https://www.bitmex.com/api/explorer/#not /User/User_getDepositAddress
|
|
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.network]: deposit chain, can view all chains via self.publicGetWalletAssets, default is eth, unless the currency has a default chain within self.options['networks']
|
|
:returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
|
|
"""
|
|
self.load_markets()
|
|
networkCode = None
|
|
networkCode, params = self.handle_network_code_and_params(params)
|
|
if networkCode is None:
|
|
raise ArgumentsRequired(self.id + ' fetchDepositAddress requires params["network"]')
|
|
currency = self.currency(code)
|
|
params = self.omit(params, 'network')
|
|
request: dict = {
|
|
'currency': currency['id'],
|
|
'network': self.network_code_to_id(networkCode, currency['code']),
|
|
}
|
|
response = self.privateGetUserDepositAddress(self.extend(request, params))
|
|
#
|
|
# '"bc1qmex3puyrzn2gduqcnlu70c2uscpyaa9nm2l2j9le2lt2wkgmw33sy7ndjg"'
|
|
#
|
|
return {
|
|
'info': response,
|
|
'currency': code,
|
|
'network': networkCode,
|
|
'address': response.replace('"', '').replace('"', ''), # Done twice because some languages only replace the first instance
|
|
'tag': None,
|
|
}
|
|
|
|
def parse_deposit_withdraw_fee(self, fee, currency: Currency = None):
|
|
#
|
|
# {
|
|
# "asset": "XBT",
|
|
# "currency": "XBt",
|
|
# "majorCurrency": "XBT",
|
|
# "name": "Bitcoin",
|
|
# "currencyType": "Crypto",
|
|
# "scale": "8",
|
|
# "enabled": True,
|
|
# "isMarginCurrency": True,
|
|
# "minDepositAmount": "10000",
|
|
# "minWithdrawalAmount": "1000",
|
|
# "maxWithdrawalAmount": "100000000000000",
|
|
# "networks": [
|
|
# {
|
|
# "asset": "btc",
|
|
# "tokenAddress": '',
|
|
# "depositEnabled": True,
|
|
# "withdrawalEnabled": True,
|
|
# "withdrawalFee": "20000",
|
|
# "minFee": "20000",
|
|
# "maxFee": "10000000"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
networks = self.safe_value(fee, 'networks', [])
|
|
networksLength = len(networks)
|
|
result: dict = {
|
|
'info': fee,
|
|
'withdraw': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
'deposit': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
'networks': {},
|
|
}
|
|
if networksLength != 0:
|
|
scale = self.safe_string(fee, 'scale')
|
|
precision = self.parse_precision(scale)
|
|
for i in range(0, networksLength):
|
|
network = networks[i]
|
|
networkId = self.safe_string(network, 'asset')
|
|
currencyCode = self.safe_string(currency, 'code')
|
|
networkCode = self.network_id_to_code(networkId, currencyCode)
|
|
withdrawalFeeId = self.safe_string(network, 'withdrawalFee')
|
|
withdrawalFee = self.parse_number(Precise.string_mul(withdrawalFeeId, precision))
|
|
result['networks'][networkCode] = {
|
|
'deposit': {'fee': None, 'percentage': None},
|
|
'withdraw': {'fee': withdrawalFee, 'percentage': False},
|
|
}
|
|
if networksLength == 1:
|
|
result['withdraw']['fee'] = withdrawalFee
|
|
result['withdraw']['percentage'] = False
|
|
return result
|
|
|
|
def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}):
|
|
"""
|
|
fetch deposit and withdraw fees
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Wallet/Wallet_getAssetsConfig
|
|
|
|
:param str[]|None codes: list of unified currency codes
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `fee structures <https://docs.ccxt.com/#/?id=fee-structure>`
|
|
"""
|
|
self.load_markets()
|
|
assets = self.publicGetWalletAssets(params)
|
|
#
|
|
# [
|
|
# {
|
|
# "asset": "XBT",
|
|
# "currency": "XBt",
|
|
# "majorCurrency": "XBT",
|
|
# "name": "Bitcoin",
|
|
# "currencyType": "Crypto",
|
|
# "scale": "8",
|
|
# "enabled": True,
|
|
# "isMarginCurrency": True,
|
|
# "minDepositAmount": "10000",
|
|
# "minWithdrawalAmount": "1000",
|
|
# "maxWithdrawalAmount": "100000000000000",
|
|
# "networks": [
|
|
# {
|
|
# "asset": "btc",
|
|
# "tokenAddress": '',
|
|
# "depositEnabled": True,
|
|
# "withdrawalEnabled": True,
|
|
# "withdrawalFee": "20000",
|
|
# "minFee": "20000",
|
|
# "maxFee": "10000000"
|
|
# }
|
|
# ]
|
|
# },
|
|
# ...
|
|
# ]
|
|
#
|
|
return self.parse_deposit_withdraw_fees(assets, codes, 'asset')
|
|
|
|
def calculate_rate_limiter_cost(self, api, method, path, params, config={}):
|
|
isAuthenticated = self.check_required_credentials(False)
|
|
cost = self.safe_value(config, 'cost', 1)
|
|
if cost != 1: # trading endpoints
|
|
if isAuthenticated:
|
|
return cost
|
|
else:
|
|
return 20
|
|
return cost
|
|
|
|
def fetch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
retrieves the public liquidations of a trading pair
|
|
|
|
https://www.bitmex.com/api/explorer/#not /Liquidation/Liquidation_get
|
|
|
|
: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
|
|
:param int [params.until]: timestamp in ms of the latest liquidation
|
|
:param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
|
|
:returns dict: an array of `liquidation structures <https://docs.ccxt.com/#/?id=liquidation-structure>`
|
|
"""
|
|
self.load_markets()
|
|
paginate = False
|
|
paginate, params = self.handle_option_and_params(params, 'fetchLiquidations', 'paginate')
|
|
if paginate:
|
|
return self.fetch_paginated_call_dynamic('fetchLiquidations', symbol, since, limit, params)
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
}
|
|
if since is not None:
|
|
request['startTime'] = since
|
|
if limit is not None:
|
|
request['count'] = limit
|
|
request, params = self.handle_until_option('endTime', request, params)
|
|
response = self.publicGetLiquidation(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "orderID": "string",
|
|
# "symbol": "string",
|
|
# "side": "string",
|
|
# "price": 0,
|
|
# "leavesQty": 0
|
|
# }
|
|
# ]
|
|
#
|
|
return self.parse_liquidations(response, market, since, limit)
|
|
|
|
def parse_liquidation(self, liquidation, market: Market = None):
|
|
#
|
|
# {
|
|
# "orderID": "string",
|
|
# "symbol": "string",
|
|
# "side": "string",
|
|
# "price": 0,
|
|
# "leavesQty": 0
|
|
# }
|
|
#
|
|
marketId = self.safe_string(liquidation, 'symbol')
|
|
return self.safe_liquidation({
|
|
'info': liquidation,
|
|
'symbol': self.safe_symbol(marketId, market),
|
|
'contracts': None,
|
|
'contractSize': self.safe_number(market, 'contractSize'),
|
|
'price': self.safe_number(liquidation, 'price'),
|
|
'side': self.safe_string_lower(liquidation, 'side'),
|
|
'baseValue': None,
|
|
'quoteValue': None,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
})
|
|
|
|
def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if response is None:
|
|
return None
|
|
if code == 429:
|
|
raise DDoSProtection(self.id + ' ' + body)
|
|
if code >= 400:
|
|
error = self.safe_value(response, 'error', {})
|
|
message = self.safe_string(error, 'message')
|
|
feedback = self.id + ' ' + body
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
if code == 400:
|
|
raise BadRequest(feedback)
|
|
raise ExchangeError(feedback) # unknown message
|
|
return None
|
|
|
|
def nonce(self):
|
|
return self.milliseconds()
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
query = '/api/' + self.version + '/' + path
|
|
if method == 'GET':
|
|
if params:
|
|
query += '?' + self.urlencode(params)
|
|
else:
|
|
format = self.safe_string(params, '_format')
|
|
if format is not None:
|
|
query += '?' + self.urlencode({'_format': format})
|
|
params = self.omit(params, '_format')
|
|
url = self.urls['api'][api] + query
|
|
isAuthenticated = self.check_required_credentials(False)
|
|
if api == 'private' or (api == 'public' and isAuthenticated):
|
|
self.check_required_credentials()
|
|
auth = method + query
|
|
expires = self.safe_integer(self.options, 'api-expires')
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'api-key': self.apiKey,
|
|
}
|
|
expires = self.sum(self.seconds(), expires)
|
|
stringExpires = str(expires)
|
|
auth += stringExpires
|
|
headers['api-expires'] = stringExpires
|
|
if method == 'POST' or method == 'PUT' or method == 'DELETE':
|
|
if params:
|
|
body = self.json(params)
|
|
auth += body
|
|
headers['api-signature'] = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|