# -*- 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.coinmetro import ImplicitAPI from ccxt.base.types import Any, Balances, Currencies, Currency, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade from typing import List from ccxt.base.errors import ExchangeError 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 RateLimitExceeded from ccxt.base.decimal_to_precision import TICK_SIZE from ccxt.base.precise import Precise class coinmetro(Exchange, ImplicitAPI): def describe(self) -> Any: return self.deep_extend(super(coinmetro, self).describe(), { 'id': 'coinmetro', 'name': 'Coinmetro', 'countries': ['EE'], # Republic of Estonia 'version': 'v1', 'rateLimit': 200, # 1 request per 200 ms, 20 per minute, 300 per hour, 1k per day 'certified': False, 'pro': False, 'has': { 'CORS': None, 'spot': True, 'margin': True, 'swap': False, 'future': False, 'option': False, 'addMargin': False, 'borrowCrossMargin': True, 'borrowIsolatedMargin': False, 'cancelAllOrders': False, 'cancelOrder': True, 'cancelOrders': False, 'closeAllPositions': False, 'closePosition': True, 'createDepositAddress': False, 'createOrder': True, 'createPostOnlyOrder': False, 'createReduceOnlyOrder': False, 'createStopLimitOrder': True, 'createStopMarketOrder': True, 'createStopOrder': True, 'deposit': False, 'editOrder': False, 'fetchAccounts': False, 'fetchBalance': True, 'fetchBidsAsks': True, 'fetchBorrowInterest': False, 'fetchBorrowRateHistories': False, 'fetchBorrowRateHistory': False, 'fetchCanceledAndClosedOrders': True, 'fetchCanceledOrders': False, 'fetchClosedOrder': False, 'fetchClosedOrders': False, 'fetchCrossBorrowRate': False, 'fetchCrossBorrowRates': False, 'fetchCurrencies': True, 'fetchDeposit': False, 'fetchDepositAddress': False, 'fetchDepositAddresses': False, 'fetchDepositAddressesByNetwork': False, 'fetchDeposits': False, 'fetchDepositsWithdrawals': False, 'fetchDepositWithdrawFee': False, 'fetchDepositWithdrawFees': False, 'fetchFundingHistory': False, 'fetchFundingRate': False, 'fetchFundingRateHistory': False, 'fetchFundingRates': False, 'fetchIndexOHLCV': False, 'fetchIsolatedBorrowRate': False, 'fetchIsolatedBorrowRates': False, 'fetchL3OrderBook': False, 'fetchLedger': True, 'fetchLeverage': False, 'fetchLeverageTiers': False, 'fetchMarketLeverageTiers': False, 'fetchMarkets': True, 'fetchMarkOHLCV': False, 'fetchMyTrades': True, 'fetchOHLCV': True, 'fetchOpenInterestHistory': False, 'fetchOpenOrder': False, 'fetchOpenOrders': True, 'fetchOrder': True, 'fetchOrderBook': True, 'fetchOrderBooks': False, 'fetchOrders': False, 'fetchOrderTrades': False, 'fetchPosition': False, 'fetchPositions': False, 'fetchPositionsRisk': False, 'fetchPremiumIndexOHLCV': False, 'fetchStatus': False, 'fetchTicker': False, 'fetchTickers': True, 'fetchTime': False, 'fetchTrades': True, 'fetchTradingFee': False, 'fetchTradingFees': False, 'fetchTradingLimits': False, 'fetchTransactionFee': False, 'fetchTransactionFees': False, 'fetchTransactions': False, 'fetchTransfers': False, 'fetchWithdrawal': False, 'fetchWithdrawals': False, 'fetchWithdrawalWhitelist': False, 'reduceMargin': False, 'repayCrossMargin': False, 'repayIsolatedMargin': False, 'sandbox': True, 'setLeverage': False, 'setMargin': False, 'setMarginMode': False, 'setPositionMode': False, 'signIn': False, 'transfer': False, 'withdraw': False, 'ws': False, }, 'timeframes': { '1m': '60000', '5m': '300000', '30m': '1800000', '4h': '14400000', '1d': '86400000', }, 'urls': { 'logo': 'https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539', 'api': { 'public': 'https://api.coinmetro.com', 'private': 'https://api.coinmetro.com', }, 'test': { 'public': 'https://api.coinmetro.com/open', 'private': 'https://api.coinmetro.com/open', }, 'www': 'https://coinmetro.com/', 'doc': [ 'https://documenter.getpostman.com/view/3653795/SVfWN6KS', ], 'fees': 'https://help.coinmetro.com/hc/en-gb/articles/6844007317789-What-are-the-fees-on-Coinmetro-', 'referral': 'https://go.coinmetro.com/?ref=crypto24', }, 'api': { 'public': { 'get': { 'demo/temp': 1, 'exchange/candles/{pair}/{timeframe}/{from}/{to}': 3, 'exchange/prices': 1, 'exchange/ticks/{pair}/{from}': 3, 'assets': 1, 'markets': 1, 'exchange/book/{pair}': 3, 'exchange/bookUpdates/{pair}/{from}': 1, # not unified }, }, 'private': { 'get': { 'users/balances': 1, 'users/wallets': 1, 'users/wallets/history/{since}': 1.67, 'exchange/orders/status/{orderID}': 1, 'exchange/orders/active': 1, 'exchange/orders/history/{since}': 1.67, 'exchange/fills/{since}': 1.67, 'exchange/margin': 1, # not unified }, 'post': { 'jwt': 1, # not unified 'jwtDevice': 1, # not unified 'devices': 1, # not unified 'jwt-read-only': 1, # not unified 'exchange/orders/create': 1, 'exchange/orders/modify/{orderID}': 1, # not unified 'exchange/swap': 1, # not unified 'exchange/swap/confirm/{swapId}': 1, # not unified 'exchange/orders/close/{orderID}': 1, 'exchange/orders/hedge': 1, # not unified }, 'put': { 'jwt': 1, # not unified 'exchange/orders/cancel/{orderID}': 1, 'users/margin/collateral': 1, 'users/margin/primary/{currency}': 1, # not unified }, }, }, 'requiredCredentials': { 'apiKey': False, 'secret': False, 'uid': True, 'token': True, }, 'fees': { 'trading': { 'feeSide': 'get', 'tierBased': False, 'percentage': True, 'taker': self.parse_number('0.001'), 'maker': self.parse_number('0'), }, }, 'precisionMode': TICK_SIZE, # exchange-specific options 'options': { 'currenciesByIdForParseMarket': None, 'currencyIdsListForParseMarket': ['QRDO'], }, 'features': { 'spot': { 'sandbox': True, 'createOrder': { 'marginMode': True, # todo implement 'triggerPrice': True, 'triggerPriceType': None, 'triggerDirection': False, 'stopLossPrice': False, # todo 'takeProfitPrice': False, # todo 'attachedStopLossTakeProfit': { 'triggerPriceType': None, 'price': False, }, 'timeInForce': { 'IOC': True, 'FOK': True, 'PO': False, 'GTD': True, }, 'hedged': False, 'trailing': False, 'leverage': False, 'marketBuyByCost': True, 'marketBuyRequiresPrice': False, 'selfTradePrevention': False, 'iceberg': True, }, 'createOrders': None, 'fetchMyTrades': { 'marginMode': False, 'limit': None, 'daysBack': 100000, 'untilDays': None, 'symbolRequired': False, }, 'fetchOrder': { 'marginMode': False, 'trigger': False, 'trailing': False, 'symbolRequired': False, }, 'fetchOpenOrders': { 'marginMode': False, 'limit': None, 'trigger': False, 'trailing': False, 'symbolRequired': False, }, 'fetchOrders': { 'marginMode': False, 'limit': None, 'daysBack': 100000, 'untilDays': None, 'trigger': False, 'trailing': False, 'symbolRequired': False, }, 'fetchClosedOrders': None, 'fetchOHLCV': { 'limit': 1000, }, }, 'swap': { 'linear': None, 'inverse': None, }, 'future': { 'linear': None, 'inverse': None, }, }, 'exceptions': { # https://trade-docs.coinmetro.co/?javascript--nodejs#message-codes 'exact': { 'Both buyingCurrency and sellingCurrency are required': InvalidOrder, # 422 - "Both buyingCurrency and sellingCurrency are required" 'One and only one of buyingQty and sellingQty is required': InvalidOrder, # 422 - "One and only one of buyingQty and sellingQty is required" 'Invalid buyingCurrency': InvalidOrder, # 422 - "Invalid buyingCurrency" 'Invalid \'from\'': BadRequest, # 422 Unprocessable Entity {"message":"Invalid 'from'"} 'Invalid sellingCurrency': InvalidOrder, # 422 - "Invalid sellingCurrency" 'Invalid buyingQty': InvalidOrder, # 422 - "Invalid buyingQty" 'Invalid sellingQty': InvalidOrder, # 422 - "Invalid sellingQty" 'Insufficient balance': InsufficientFunds, # 422 - "Insufficient balance" 'Expiration date is in the past or too near in the future': InvalidOrder, # 422 Unprocessable Entity {"message":"Expiration date is in the past or too near in the future"} 'Forbidden': PermissionDenied, # 403 Forbidden {"message":"Forbidden"} 'Order Not Found': OrderNotFound, # 404 Not Found {"message":"Order Not Found"} 'since must be a millisecond timestamp': BadRequest, # 422 Unprocessable Entity {"message":"since must be a millisecond timestamp"} 'This pair is disabled on margin': BadSymbol, # 422 Unprocessable Entity {"message":"This pair is disabled on margin"} }, 'broad': { 'accessing from a new IP': PermissionDenied, # 403 Forbidden {"message":"You're accessing from a new IP. Please check your email."} 'available to allocate': InsufficientFunds, # 403 Forbidden {"message":"Insufficient EUR available to allocate"} 'At least': BadRequest, # 422 Unprocessable Entity {"message":"At least 5 EUR per operation"} 'collateral is not allowed': BadRequest, # 422 Unprocessable Entity {"message":"DOGE collateral is not allowed"} 'Insufficient liquidity': InvalidOrder, # 503 Service Unavailable {"message":"Insufficient liquidity to fill the FOK order completely."} 'Insufficient order size': InvalidOrder, # 422 Unprocessable Entity {"message":"Insufficient order size - min 0.002 ETH"} 'Invalid quantity': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid quantity!"} 'Invalid Stop Loss': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid Stop Loss!"} 'Invalid stop price!': InvalidOrder, # 422 Unprocessable Entity {"message":"Invalid stop price!"} 'Not enough balance': InsufficientFunds, # 422 Unprocessable Entity {"message":"Not enough balance!"} 'Not enough margin': InsufficientFunds, # Unprocessable Entity {"message":"Not enough margin!"} 'orderType missing': BadRequest, # 422 Unprocessable Entity {"message":"orderType missing!"} 'Server Timeout': ExchangeError, # 503 Service Unavailable {"message":"Server Timeout!"} 'Time in force has to be IOC or FOK for market orders': InvalidOrder, # 422 Unprocessable Entity {"message":"Time in force has to be IOC or FOK for market orders!"} 'Too many attempts': RateLimitExceeded, # 429 Too Many Requests {"message":"Too many attempts. Try again in 3 seconds"} }, }, }) def fetch_currencies(self, params={}) -> Currencies: """ fetches all available currencies on an exchange https://documenter.getpostman.com/view/3653795/SVfWN6KS#d5876d43-a3fe-4479-8c58-24d0f044edfb :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: an associative dictionary of currencies """ response = self.publicGetAssets(params) # # [ # { # "symbol": "BTC", # "name": "Bitcoin", # "color": "#FFA500", # "type": "coin", # "canDeposit": True, # "canWithdraw": True, # "canTrade": True, # "notabeneDecimals": 8, # "canMarket": True, # "maxSwap": 10000, # "digits": 6, # "multiplier": 1000000, # "bookDigits": 8, # "bookMultiplier": 100000000, # "sentimentData": { # "sentiment": 51.59555555555555, # "interest": 1.127511216044664 # }, # "minQty": 0.0001 # }, # { # "symbol": "EUR", # "name": "Euro", # "color": "#1246FF", # "type": "fiat", # "canDeposit": True, # "canWithdraw": True, # "canTrade": True, # "canMarket": True, # "maxSwap": 10000, # "digits": 2, # "multiplier": 100, # "bookDigits": 3, # "bookMultiplier": 1000, # "minQty": 5 # } # ... # ] # result: dict = {} for i in range(0, len(response)): currency = response[i] id = self.safe_string(currency, 'symbol') code = self.safe_currency_code(id) typeRaw = self.safe_string(currency, 'type') type = None if typeRaw == 'coin' or typeRaw == 'token' or typeRaw == 'erc20': type = 'crypto' elif typeRaw == 'fiat': type = 'fiat' precisionDigits = self.safe_string_2(currency, 'digits', 'notabeneDecimals') if code == 'RENDER': # RENDER is an exception(with broken info) precisionDigits = '4' result[code] = self.safe_currency_structure({ 'id': id, 'code': code, 'name': code, 'type': type, 'info': currency, 'active': self.safe_bool(currency, 'canTrade'), 'deposit': self.safe_bool(currency, 'canDeposit'), 'withdraw': self.safe_bool(currency, 'canWithdraw'), 'fee': None, 'precision': self.parse_number(self.parse_precision(precisionDigits)), 'limits': { 'amount': { 'min': self.safe_number(currency, 'minQty'), 'max': None, }, 'withdraw': { 'min': None, 'max': None, }, }, 'networks': {}, }) if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None: currenciesById = self.index_by(result, 'id') self.options['currenciesByIdForParseMarket'] = currenciesById currentCurrencyIdsList = self.safe_list(self.options, 'currencyIdsListForParseMarket', []) currencyIdsList = list(currenciesById.keys()) for i in range(0, len(currencyIdsList)): currentCurrencyIdsList.append(currencyIdsList[i]) self.options['currencyIdsListForParseMarket'] = currentCurrencyIdsList return result def fetch_markets(self, params={}) -> List[Market]: """ retrieves data on all markets for coinmetro https://documenter.getpostman.com/view/3653795/SVfWN6KS#9fd18008-338e-4863-b07d-722878a46832 :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict[]: an array of objects representing market data """ promises = [] promises.append(self.publicGetMarkets(params)) if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None: promises.append(self.fetch_currencies()) responses = promises response = responses[0] # # [ # { # "pair": "YFIEUR", # "precision": 5, # "margin": False # }, # { # "pair": "BTCEUR", # "precision": 2, # "margin": True # }, # ... # ] # result = [] for i in range(0, len(response)): market = self.parse_market(response[i]) # there are several broken(unavailable info) markets if market['base'] is None or market['quote'] is None: continue result.append(market) return result def parse_market(self, market: dict) -> Market: id = self.safe_string(market, 'pair') parsedMarketId = self.parse_market_id(id) baseId = self.safe_string(parsedMarketId, 'baseId') quoteId = self.safe_string(parsedMarketId, 'quoteId') base = self.safe_currency_code(baseId) quote = self.safe_currency_code(quoteId) basePrecisionAndLimits = self.parse_market_precision_and_limits(baseId) quotePrecisionAndLimits = self.parse_market_precision_and_limits(quoteId) margin = self.safe_bool(market, 'margin', False) tradingFees = self.safe_value(self.fees, 'trading', {}) return self.safe_market_structure({ 'id': id, 'symbol': base + '/' + quote, 'base': base, 'quote': quote, 'settle': None, 'baseId': baseId, 'quoteId': quoteId, 'settleId': None, 'type': 'spot', 'spot': True, 'margin': margin, 'swap': False, 'future': False, 'option': False, 'active': True, 'contract': False, 'linear': None, 'inverse': None, 'taker': self.safe_number(tradingFees, 'taker'), 'maker': self.safe_number(tradingFees, 'maker'), 'contractSize': None, 'expiry': None, 'expiryDatetime': None, 'strike': None, 'optionType': None, 'precision': { 'amount': basePrecisionAndLimits['precision'], 'price': self.parse_number(self.parse_precision(self.safe_string(market, 'precision'))), }, 'limits': { 'leverage': { 'min': None, 'max': None, }, 'amount': { 'min': basePrecisionAndLimits['minLimit'], 'max': None, }, 'price': { 'min': None, 'max': None, }, 'cost': { 'min': quotePrecisionAndLimits['minLimit'], 'max': None, }, }, 'created': None, 'info': market, }) def parse_market_id(self, marketId): baseId = None quoteId = None currencyIds = self.safe_value(self.options, 'currencyIdsListForParseMarket', []) # Bubble sort by length(longest first) currencyIdsLength = len(currencyIds) for i in range(0, currencyIdsLength): for j in range(0, currencyIdsLength - i - 1): a = currencyIds[j] b = currencyIds[j + 1] if len(a) < len(b): currencyIds[j] = b currencyIds[j + 1] = a for i in range(0, len(currencyIds)): currencyId = currencyIds[i] entryIndex = marketId.find(currencyId) if entryIndex == 0: restId = marketId.replace(currencyId, '') if self.in_array(restId, currencyIds): if entryIndex == 0: baseId = currencyId quoteId = restId else: baseId = restId quoteId = currencyId break if baseId is None or quoteId is None: # https://github.com/ccxt/ccxt/issues/26820 if marketId.endswith('USDT'): baseId = marketId.replace('USDT', '') quoteId = 'USDT' if marketId.endswith('USD'): baseId = marketId.replace('USD', '') quoteId = 'USD' result: dict = { 'baseId': baseId, 'quoteId': quoteId, } return result def parse_market_precision_and_limits(self, currencyId): currencies = self.safe_value(self.options, 'currenciesByIdForParseMarket', {}) currency = self.safe_value(currencies, currencyId, {}) limits = self.safe_value(currency, 'limits', {}) amountLimits = self.safe_value(limits, 'amount', {}) minLimit = self.safe_number(amountLimits, 'min') result: dict = { 'precision': self.safe_number(currency, 'precision'), 'minLimit': minLimit, } return result 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://documenter.getpostman.com/view/3653795/SVfWN6KS#13cfb5bc-7bfb-4847-85e1-e0f35dfb3573 :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 int [params.until]: the latest time in ms to fetch entries for :returns int[][]: A list of candles ordered, open, high, low, close, volume """ self.load_markets() market = self.market(symbol) request: dict = { 'pair': market['id'], 'timeframe': self.safe_string(self.timeframes, timeframe, timeframe), } until = None if since is not None: request['from'] = since if limit is not None: duration = self.parse_timeframe(timeframe) * 1000 until = self.sum(since, duration * (limit)) else: request['from'] = ':from' # self endpoint doesn't accept empty from and to params(setting them into the value described in the documentation) until = self.safe_integer(params, 'until', until) if until is not None: params = self.omit(params, ['until']) request['to'] = until else: request['to'] = ':to' response = self.publicGetExchangeCandlesPairTimeframeFromTo(self.extend(request, params)) # # { # "candleHistory": [ # { # "pair": "ETHUSDT", # "timeframe": 86400000, # "timestamp": 1697673600000, # "c": 1567.4409353098604, # "h": 1566.7514068472303, # "l": 1549.4563666936847, # "o": 1563.4490341395904, # "v": 0 # }, # { # "pair": "ETHUSDT", # "timeframe": 86400000, # "timestamp": 1697760000000, # "c": 1603.7831363339324, # "h": 1625.0356823666407, # "l": 1565.4629390011505, # "o": 1566.8387619426028, # "v": 0 # }, # ... # ] # } # candleHistory = self.safe_list(response, 'candleHistory', []) return self.parse_ohlcvs(candleHistory, market, timeframe, since, limit) def parse_ohlcv(self, ohlcv, market: Market = None) -> list: return [ self.safe_integer(ohlcv, 'timestamp'), self.safe_number(ohlcv, 'o'), self.safe_number(ohlcv, 'h'), self.safe_number(ohlcv, 'l'), self.safe_number(ohlcv, 'c'), self.safe_number(ohlcv, 'v'), ] 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://documenter.getpostman.com/view/3653795/SVfWN6KS#6ee5d698-06da-4570-8c84-914185e05065 :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(default 200, max 500) :param dict [params]: extra parameters specific to the exchange API endpoint :returns Trade[]: a list of `trade structures ` """ self.load_markets() market = self.market(symbol) request: dict = { 'pair': market['id'], } if since is not None: request['from'] = since else: # self endpoint accepts empty from param request['from'] = '' response = self.publicGetExchangeTicksPairFrom(self.extend(request, params)) # # { # "tickHistory": [ # { # "pair": "ETHUSDT", # "price": 2077.5623, # "qty": 0.002888, # "timestamp": 1700684689420, # "seqNum": 10644554718 # }, # { # "pair": "ETHUSDT", # "price": 2078.3848, # "qty": 0.003368, # "timestamp": 1700684738410, # "seqNum": 10644559561 # }, # { # "pair": "ETHUSDT", # "price": 2077.1513, # "qty": 0.00337, # "timestamp": 1700684816853, # "seqNum": 10644567113 # }, # ... # ] # } # tickHistory = self.safe_list(response, 'tickHistory', []) return self.parse_trades(tickHistory, market, since, limit) def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}): """ fetch all trades made by the user https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b :param str symbol: unified market symbol :param int [since]: the earliest time in ms to fetch trades for :param int [limit]: the maximum number of trades structures to retrieve(default 500, max 1000) :param dict [params]: extra parameters specific to the exchange API endpoint :returns Trade[]: a list of `trade structures ` """ self.load_markets() market = None if symbol is not None: market = self.market(symbol) request: dict = {} if since is not None: request['since'] = since else: # the exchange requires a value for the since param request['since'] = 0 response = self.privateGetExchangeFillsSince(self.extend(request, params)) # # [ # { # "pair": "ETHUSDC", # "seqNumber": 10873722343, # "timestamp": 1702570610747, # "qty": 0.002, # "price": 2282, # "side": "buy", # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c" # }, # ... # ] # return self.parse_trades(response, market, since, limit) def parse_trade(self, trade: dict, market: Market = None) -> Trade: # # fetchTrades # { # "pair": "ETHUSDT", # "price": 2077.1513, # "qty": 0.00337, # "timestamp": 1700684816853, # "seqNum": 10644567113 # }, # # fetchMyTrades # { # "pair": "ETHUSDC", # "seqNumber": 10873722343, # "timestamp": 1702570610747, # "qty": 0.002, # "price": 2282, # "side": "buy", # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c" # } # # fetchOrders # { # "_id": "657b31d360a9542449381bdc", # "seqNumber": 10873722343, # "timestamp": 1702570610747, # "qty": 0.002, # "price": 2282, # "side": "buy" # } # # { # "pair":"ETHUSDC", # "seqNumber":"10873722343", # "timestamp":"1702570610747", # "qty":"0.002", # "price":"2282", # "side":"buy", # "orderID":"65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c", # "userID":"65671262d93d9525ac009e36" # } # marketId = self.safe_string_2(trade, 'symbol', 'pair') market = self.safe_market(marketId, market) symbol = market['symbol'] id = self.safe_string_n(trade, ['_id', 'seqNum', 'seqNumber']) timestamp = self.safe_integer(trade, 'timestamp') priceString = self.safe_string(trade, 'price') amountString = self.safe_string(trade, 'qty') order = self.safe_string(trade, 'orderID') side = self.safe_string(trade, 'side') return self.safe_trade({ 'id': id, 'order': order, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'symbol': symbol, 'type': None, 'side': side, 'takerOrMaker': None, 'price': priceString, 'amount': amountString, 'cost': None, 'fee': None, 'info': trade, }, market) 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://documenter.getpostman.com/view/3653795/SVfWN6KS#26ad80d7-8c46-41b5-9208-386f439a8b87 :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(default 100, max 200) :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: A dictionary of `order book structures ` indexed by market symbols """ self.load_markets() market = self.market(symbol) request: dict = { 'pair': market['id'], } response = self.publicGetExchangeBookPair(self.extend(request, params)) # # { # "book": { # "pair": "ETHUSDT", # "seqNumber": 10800409239, # "ask": { # "2354.2861": 3.75, # "2354.3138": 19, # "2354.7538": 80, # "2355.5430": 260, # "2356.4611": 950, # "2361.7150": 1500, # "206194.0000": 0.01 # }, # "bid": { # "2352.6339": 3.75, # "2352.6002": 19, # "2352.2402": 80, # "2351.4582": 260, # "2349.3111": 950, # "2343.8601": 1500, # "1.0000": 5 # }, # "checksum": 2108177337 # } # } # book = self.safe_value(response, 'book', {}) rawBids = self.safe_value(book, 'bid', {}) rawAsks = self.safe_value(book, 'ask', {}) rawOrderbook: dict = { 'bids': rawBids, 'asks': rawAsks, } orderbook = self.parse_order_book(rawOrderbook, symbol) orderbook['nonce'] = self.safe_integer(book, 'seqNumber') return orderbook def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2): prices = list(bidasks.keys()) result = [] for i in range(0, len(prices)): priceString = self.safe_string(prices, i) price = self.safe_number(prices, i) volume = self.safe_number(bidasks, priceString) result.append([price, volume]) return result 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://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485 :param str[] [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 ` """ self.load_markets() response = self.publicGetExchangePrices(params) # # { # "latestPrices": [ # { # "pair": "PERPEUR", # "timestamp": 1702549840393, # "price": 0.7899997816001223, # "qty": 1e-12, # "ask": 0.8, # "bid": 0.7799995632002446 # }, # { # "pair": "PERPUSD", # "timestamp": 1702549841973, # "price": 0.8615317721366659, # "qty": 1e-12, # "ask": 0.8742333599999257, # "bid": 0.8490376365388491 # }, # ... # ], # "24hInfo": [ # { # "delta": 0.25396444229149906, # "h": 0.78999978160012, # "l": 0.630001740844, # "v": 54.910000002833996, # "pair": "PERPEUR", # "sentimentData": { # "sentiment": 36.71333333333333, # "interest": 0.47430830039525695 # } # }, # { # "delta": 0.26915154078134096, # "h": 0.86220315458898, # "l": 0.67866757035154, # "v": 2.835000000000001e-9, # "pair": "PERPUSD", # "sentimentData": { # "sentiment": 36.71333333333333, # "interest": 0.47430830039525695 # } # }, # ... # ] # } # latestPrices = self.safe_value(response, 'latestPrices', []) twentyFourHInfos = self.safe_value(response, '24hInfo', []) tickersObject: dict = {} # merging info from two lists into one for i in range(0, len(latestPrices)): latestPrice = latestPrices[i] marketId = self.safe_string(latestPrice, 'pair') if marketId is not None: tickersObject[marketId] = latestPrice for i in range(0, len(twentyFourHInfos)): twentyFourHInfo = twentyFourHInfos[i] marketId = self.safe_string(twentyFourHInfo, 'pair') if marketId is not None: latestPrice = self.safe_value(tickersObject, marketId, {}) tickersObject[marketId] = self.extend(twentyFourHInfo, latestPrice) tickers = list(tickersObject.values()) return self.parse_tickers(tickers, symbols) def fetch_bids_asks(self, symbols: Strings = None, params={}): """ fetches the bid and ask price and volume for multiple markets https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485 :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a dictionary of `ticker structures ` """ self.load_markets() response = self.publicGetExchangePrices(params) latestPrices = self.safe_list(response, 'latestPrices', []) return self.parse_tickers(latestPrices, symbols) def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker: # # { # "pair": "PERPUSD", # "timestamp": 1702549841973, # "price": 0.8615317721366659, # "qty": 1e-12, # "ask": 0.8742333599999257, # "bid": 0.8490376365388491 # "delta": 0.26915154078134096, # "h": 0.86220315458898, # "l": 0.67866757035154, # "v": 2.835000000000001e-9, # "sentimentData": { # "sentiment": 36.71333333333333, # "interest": 0.47430830039525695 # } # } # marketId = self.safe_string(ticker, 'pair') market = self.safe_market(marketId, market) timestamp = self.safe_integer(ticker, 'timestamp') bid = self.safe_string(ticker, 'bid') ask = self.safe_string(ticker, 'ask') high = self.safe_string(ticker, 'h') low = self.safe_string(ticker, 'l') last = self.safe_string(ticker, 'price') baseVolume = self.safe_string(ticker, 'v') delta = self.safe_string(ticker, 'delta') percentage = Precise.string_mul(delta, '100') return self.safe_ticker({ 'symbol': market['symbol'], 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'open': None, 'high': high, 'low': low, 'close': None, 'last': last, 'bid': bid, 'bidVolume': None, 'ask': ask, 'askVolume': None, 'vwap': None, 'previousClose': None, 'change': None, 'percentage': percentage, 'average': None, 'baseVolume': baseVolume, 'quoteVolume': None, 'info': ticker, }, market) def fetch_balance(self, params={}) -> Balances: """ query for balance and get the amount of funds available for trading or funds locked in orders https://documenter.getpostman.com/view/3653795/SVfWN6KS#741a1dcc-7307-40d0-acca-28d003d1506a :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `balance structure ` """ self.load_markets() response = self.privateGetUsersWallets(params) list = self.safe_list(response, 'list', []) return self.parse_balance(list) def parse_balance(self, balances) -> Balances: # # [ # { # "xcmLocks": [], # "xcmLockAmounts": [], # "refList": [], # "balanceHistory": [], # "_id": "5fecd3c998e75c2e4d63f7c3", # "currency": "BTC", # "label": "BTC", # "userId": "5fecd3c97fbfed1521db23bd", # "__v": 0, # "balance": 0.5, # "createdAt": "2020-12-30T19:23:53.646Z", # "disabled": False, # "updatedAt": "2020-12-30T19:23:53.653Z", # "reserved": 0, # "id": "5fecd3c998e75c2e4d63f7c3" # }, # ... # ] # result: dict = { 'info': balances, } for i in range(0, len(balances)): balanceEntry = self.safe_dict(balances, i, {}) currencyId = self.safe_string(balanceEntry, 'currency') code = self.safe_currency_code(currencyId) account = self.account() account['total'] = self.safe_string(balanceEntry, 'balance') account['used'] = self.safe_string(balanceEntry, 'reserved') result[code] = account return self.safe_balance(result) 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://documenter.getpostman.com/view/3653795/SVfWN6KS#4e7831f7-a0e7-4c3e-9336-1d0e5dcb15cf :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 200, max 500) :param dict [params]: extra parameters specific to the exchange API endpoint :param int [params.until]: the latest time in ms to fetch entries for :returns dict: a `ledger structure ` """ self.load_markets() request: dict = {} if since is not None: request['since'] = since else: # self endpoint accepts empty since param request['since'] = '' currency = None if code is not None: currency = self.currency(code) response = self.privateGetUsersWalletsHistorySince(self.extend(request, params)) # # { # "list": [ # { # "currency": "USDC", # "label": "USDC", # "userId": "65671262d93d9525ac009e36", # "balance": 0, # "disabled": False, # "balanceHistory": [ # { # "description": "Deposit - 657973a9b6eadf0f33d70100", # "JSONdata": { # "fees": 0, # "notes": "Via Crypto", # "txHash": "0x2e4875185b0f312d8e24b2d26d46bf9877db798b608ad2ff97b2b8bc7d8134e5", # "last4Digits": null, # "IBAN": null, # "alternativeChain": "polygon", # "referenceId": "657973a9b6eadf0f33d70100", # "status": "completed", # "tracked": True # }, # "amount": 99, # "timestamp": "2023-12-13T09:04:51.270Z", # "amountEUR": 91.79310117335974 # }, # { # "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342", # "JSONdata": { # "price": "2282.00 ETH/USDC", # "fees": 0, # "notes": "Order 3a8c5b4d6c" # }, # "amount": -4.564, # "timestamp": "2023-12-14T16:16:50.760Z", # "amountEUR": -4.150043849187587 # }, # ... # ] # }, # { # "currency": "ETH", # "label": "ETH", # "userId": "65671262d93d9525ac009e36", # "balance": 0, # "disabled": False, # "balanceHistory": [ # { # "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342", # "JSONdata": { # "price": "2282.00 ETH/USDC", # "fees": 0.000002, # "notes": "Order 3a8c5b4d6c" # }, # "amount": 0.001998, # "timestamp": "2023-12-14T16:16:50.761Z", # "amountEUR": 4.144849415806856 # }, # ... # ] # }, # { # "currency": "DOGE", # "label": "DOGE", # "userId": "65671262d93d9525ac009e36", # "balance": 0, # "disabled": False, # "balanceHistory": [ # { # "description": "Order 65671262d93d9525ac009e361702905785319b5d9016dc20736034d13ca6a - Swap", # "JSONdata": { # "swap": True, # "subtype": "swap", # "fees": 0, # "price": "0.0905469 DOGE/USDC", # "notes": "Swap 034d13ca6a" # }, # "amount": 70, # "timestamp": "2023-12-18T13:23:05.836Z", # "amountEUR": 5.643627624549227 # } # ] # }, # ... # ] # } # ledgerByCurrencies = self.safe_value(response, 'list', []) ledger = [] for i in range(0, len(ledgerByCurrencies)): currencyLedger = ledgerByCurrencies[i] currencyId = self.safe_string(currencyLedger, 'currency') balanceHistory = self.safe_value(currencyLedger, 'balanceHistory', []) for j in range(0, len(balanceHistory)): rawLedgerEntry = balanceHistory[j] rawLedgerEntry['currencyId'] = currencyId ledger.append(rawLedgerEntry) return self.parse_ledger(ledger, currency, since, limit) def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry: datetime = self.safe_string(item, 'timestamp') currencyId = self.safe_string(item, 'currencyId') item = self.omit(item, 'currencyId') currency = self.safe_currency(currencyId, currency) description = self.safe_string(item, 'description', '') type, referenceId = self.parse_ledger_entry_description(description) JSONdata = self.safe_value(item, 'JSONdata', {}) feeCost = self.safe_string(JSONdata, 'fees') fee = { 'cost': feeCost, 'currency': None, } amount = self.safe_string(item, 'amount') direction = None if amount is not None: if Precise.string_lt(amount, '0'): direction = 'out' amount = Precise.string_abs(amount) elif Precise.string_gt(amount, '0'): direction = 'in' return self.safe_ledger_entry({ 'info': item, 'id': None, 'timestamp': self.parse8601(datetime), 'datetime': datetime, 'direction': direction, 'account': None, 'referenceId': referenceId, 'referenceAccount': None, 'type': type, 'currency': currency, 'amount': amount, 'before': None, 'after': None, 'status': None, 'fee': fee, }, currency) def parse_ledger_entry_description(self, description): descriptionArray = [] if description is not None: descriptionArray = description.split(' ') type = None referenceId = None length = len(descriptionArray) if length > 1: type = self.parse_ledger_entry_type(descriptionArray[0]) if descriptionArray[1] != '-': referenceId = descriptionArray[1] else: referenceId = self.safe_string(descriptionArray, 2) return [type, referenceId] def parse_ledger_entry_type(self, type): types: dict = { 'Deposit': 'transaction', 'Withdraw': 'transaction', 'Order': 'trade', } return self.safe_string(types, type, type) def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}): """ create a trade order https://documenter.getpostman.com/view/3653795/SVfWN6KS#a4895a1d-3f50-40ae-8231-6962ef06c771 :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 float [params.cost]: the quote quantity that can be used alternative for the amount in market orders :param str [params.timeInForce]: "GTC", "IOC", "FOK", "GTD" :param number [params.expirationTime]: timestamp in millisecond, for GTD orders only :param float [params.triggerPrice]: the price at which a trigger order is triggered at :param float [params.stopLossPrice]: *margin only* The price at which a stop loss order is triggered at :param float [params.takeProfitPrice]: *margin only* The price at which a take profit order is triggered at :param bool [params.margin]: True for creating a margin order :param str [params.fillStyle]: fill style of the limit order: "sell" fulfills selling quantity "buy" fulfills buying quantity "base" fulfills base currency quantity "quote" fulfills quote currency quantity :param str [params.clientOrderId]: client's comment :returns dict: an `order structure ` """ self.load_markets() market = self.market(symbol) request: dict = { } request['orderType'] = type formattedAmount = None if amount is not None: formattedAmount = self.amount_to_precision(symbol, amount) cost = self.safe_value(params, 'cost') params = self.omit(params, 'cost') if type == 'limit': if (price is None) and (cost is None): raise ArgumentsRequired(self.id + ' createOrder() requires a price or params.cost argument for a ' + type + ' order') elif (price is not None) and (amount is not None): costString = Precise.string_mul(self.number_to_string(price), self.number_to_string(formattedAmount)) cost = self.parse_to_numeric(costString) precisedCost = None if cost is not None: precisedCost = self.cost_to_precision(symbol, cost) if side == 'sell': request = self.handle_create_order_side(market['baseId'], market['quoteId'], formattedAmount, precisedCost, request) elif side == 'buy': request = self.handle_create_order_side(market['quoteId'], market['baseId'], precisedCost, formattedAmount, request) timeInForce = self.safe_value(params, 'timeInForce') if timeInForce is not None: params = self.omit(params, 'timeInForce') request['timeInForce'] = self.encode_order_time_in_force(timeInForce) triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice') if triggerPrice is not None: params = self.omit(params, ['triggerPrice']) request['stopPrice'] = self.price_to_precision(symbol, triggerPrice) userData = self.safe_value(params, 'userData', {}) comment = self.safe_string_2(params, 'clientOrderId', 'comment') if comment is not None: params = self.omit(params, ['clientOrderId']) userData['comment'] = comment stopLossPrice = self.safe_string(params, 'stopLossPrice') if stopLossPrice is not None: params = self.omit(params, 'stopLossPrice') userData['stopLoss'] = self.price_to_precision(symbol, stopLossPrice) takeProfitPrice = self.safe_string(params, 'takeProfitPrice') if takeProfitPrice is not None: params = self.omit(params, 'takeProfitPrice') userData['takeProfit'] = self.price_to_precision(symbol, takeProfitPrice) if not self.is_empty(userData): request['userData'] = userData response = self.privatePostExchangeOrdersCreate(self.extend(request, params)) # # { # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e36170257448481749b7ee2893bafec2", # "orderType": "market", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "buyingQty": 0.002, # "timeInForce": 4, # "boughtQty": 0.002, # "soldQty": 4.587, # "creationTime": 1702574484829, # "seqNumber": 10874285330, # "firstFillTime": 1702574484831, # "lastFillTime": 1702574484831, # "fills": [ # { # "seqNumber": 10874285329, # "timestamp": 1702574484831, # "qty": 0.002, # "price": 2293.5, # "side": "buy" # } # ], # "completionTime": 1702574484831, # "takerQty": 0.002 # } # return self.parse_order(response, market) def handle_create_order_side(self, sellingCurrency, buyingCurrency, sellingQty, buyingQty, request={}): request['sellingCurrency'] = sellingCurrency request['buyingCurrency'] = buyingCurrency if sellingQty is not None: request['sellingQty'] = sellingQty if buyingQty is not None: request['buyingQty'] = buyingQty return request def encode_order_time_in_force(self, timeInForce): timeInForceTypes: dict = { 'GTC': 1, 'IOC': 2, 'GTD': 3, 'FOK': 4, } return self.safe_value(timeInForceTypes, timeInForce, timeInForce) def cancel_order(self, id: str, symbol: Str = None, params={}): """ cancels an open order https://documenter.getpostman.com/view/3653795/SVfWN6KS#eaea86da-16ca-4c56-9f00-5b1cb2ad89f8 https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316 :param str id: order id :param str symbol: not used by coinmetro cancelOrder() :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.margin]: True for cancelling a margin order :returns dict: An `order structure ` """ self.load_markets() request: dict = { 'orderID': id, } marginMode = None params, params = self.handle_margin_mode_and_params('cancelOrder', params) isMargin = self.safe_bool(params, 'margin', False) params = self.omit(params, 'margin') response = None if isMargin or (marginMode is not None): response = self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params)) else: response = self.privatePutExchangeOrdersCancelOrderID(self.extend(request, params)) # # { # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4", # "orderType": "limit", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "fillStyle": "sell", # "orderPlatform": "trade-v3", # "timeInForce": 1, # "buyingQty": 0.005655, # "sellingQty": 11.31, # "boughtQty": 0, # "soldQty": 0, # "creationTime": 1702663525713, # "seqNumber": 10915220048, # "completionTime": 1702928369053 # } # return self.parse_order(response) def close_position(self, symbol: str, side: OrderSide = None, params={}): """ closes an open position https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316 :param str symbol: not used by coinmetro closePosition() :param str [side]: not used by coinmetro closePosition() :param dict [params]: extra parameters specific to the exchange API endpoint :param str [params.orderID]: order id :param number [params.fraction]: fraction of order to close, between 0 and 1(defaults to 1) :returns dict: An `order structure ` """ self.load_markets() orderId = self.safe_string(params, 'orderId') if orderId is None: raise ArgumentsRequired(self.id + ' closePosition() requires a orderId parameter') request: dict = { 'orderID': orderId, } response = self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params)) # # { # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8_CL", # "orderType": "market", # "buyingCurrency": "ETH", # "sellingCurrency": "EUR", # "margin": True, # "buyingQty": 0.03, # "timeInForce": 4, # "boughtQty": 0.03, # "soldQty": 59.375, # "creationTime": 1703015488482, # "seqNumber": 10925321179, # "firstFillTime": 1703015488483, # "lastFillTime": 1703015488483, # "fills": [ # { # "seqNumber": 10925321178, # "timestamp": 1703015488483, # "qty": 0.03, # "price": 1979.1666666666667, # "side": "buy" # } # ], # "completionTime": 1703015488483, # "takerQty": 0.03 # } # return self.parse_order(response) def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetch all unfilled currently open orders https://documenter.getpostman.com/view/3653795/SVfWN6KS#518afd7a-4338-439c-a651-d4fdaa964138 :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 order structures to retrieve :param dict [params]: extra parameters specific to the exchange API endpoint :returns Order[]: a list of `order structures ` """ self.load_markets() market = None if symbol is not None: market = self.market(symbol) response = self.privateGetExchangeOrdersActive(params) orders = self.parse_orders(response, market, since, limit) for i in range(0, len(orders)): order = orders[i] order['status'] = 'open' return orders def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]: """ fetches information on multiple canceled and closed orders made by the user https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b :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 ` """ self.load_markets() market = None if symbol is not None: market = self.market(symbol) request: dict = {} if since is not None: request['since'] = since response = self.privateGetExchangeOrdersHistorySince(self.extend(request, params)) return self.parse_orders(response, market, since, limit) def fetch_order(self, id: str, symbol: Str = None, params={}): """ fetches information on an order made by the user https://documenter.getpostman.com/view/3653795/SVfWN6KS#95bbed87-db1c-47a7-a03e-aa247e91d5a6 :param int|str id: order id :param str symbol: not used by coinmetro fetchOrder() :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: An `order structure ` """ self.load_markets() request: dict = { 'orderID': id, } response = self.privateGetExchangeOrdersStatusOrderID(self.extend(request, params)) # # { # "_id": "657b4e6d60a954244939ac6f", # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e361702576531985b78465468b9cc544", # "orderType": "market", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "buyingQty": 0.004, # "timeInForce": 4, # "boughtQty": 0.004, # "soldQty": 9.236, # "creationTime": 1702576531995, # "seqNumber": 10874644062, # "firstFillTime": 1702576531995, # "lastFillTime": 1702576531995, # "fills": [ # { # "_id": "657b4e6d60a954244939ac70", # "seqNumber": 10874644061, # "timestamp": 1702576531995, # "qty": 0.004, # "price": 2309, # "side": "buy" # } # ], # "completionTime": 1702576531995, # "takerQty": 0.004, # "fees": 0.000004, # "isAncillary": False, # "margin": False, # "trade": False, # "canceled": False # } # return self.parse_order(response) def parse_order(self, order: dict, market: Market = None) -> Order: # # createOrder market # { # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e36170257448481749b7ee2893bafec2", # "orderType": "market", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "buyingQty": 0.002, # "timeInForce": 4, # "boughtQty": 0.002, # "soldQty": 4.587, # "creationTime": 1702574484829, # "seqNumber": 10874285330, # "firstFillTime": 1702574484831, # "lastFillTime": 1702574484831, # "fills": [ # { # "seqNumber": 10874285329, # "timestamp": 1702574484831, # "qty": 0.002, # "price": 2293.5, # "side": "buy" # } # ], # "completionTime": 1702574484831, # "takerQty": 0.002 # } # # createOrder limit # { # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4", # "orderType": "limit", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "fillStyle": "sell", # "orderPlatform": "trade-v3", # "timeInForce": 1, # "buyingQty": 0.005655, # "sellingQty": 11.31, # "boughtQty": 0, # "soldQty": 0, # "creationTime": 1702663525713, # "seqNumber": 10885528683, # "fees": 0, # "fills": [], # "isAncillary": False, # "margin": False, # "trade": False # } # # fetchOrders market # { # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c", # "orderType": "market", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "buyingQty": 0.002, # "timeInForce": 4, # "boughtQty": 0.002, # "soldQty": 4.564, # "creationTime": 1702570610746, # "seqNumber": 10873722344, # "firstFillTime": 1702570610747, # "lastFillTime": 1702570610747, # "fills": [ # { # "_id": "657b31d360a9542449381bdc", # "seqNumber": 10873722343, # "timestamp": 1702570610747, # "qty": 0.002, # "price": 2282, # "side": "buy" # } # ], # "completionTime": 1702570610747, # "takerQty": 0.002, # "fees": 0.000002, # "isAncillary": False, # "margin": False, # "trade": False, # "canceled": False, # "__v": 0 # } # # fetchOrders margin # { # "userData": { # "takeProfit": 1700, # "stopLoss": 2100 # }, # "_id": "658201d060a95424499394a2", # "seqNumber": 10925300213, # "orderType": "limit", # "buyingCurrency": "EUR", # "sellingCurrency": "ETH", # "userID": "65671262d93d9525ac009e36", # "closedQty": 0.03, # "sellingQty": 0.03, # "buyingQty": 58.8, # "creationTime": 1703015281205, # "margin": True, # "timeInForce": 1, # "boughtQty": 59.31, # "orderID": "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8", # "lastFillTime": 1703015281206, # "soldQty": 0.03, # "closedTime": 1703015488488, # "closedVal": 59.375, # "trade": True, # "takerQty": 59.31, # "firstFillTime": 1703015281206, # "completionTime": 1703015281206, # "fills": [ # { # "_id": "658201d060a95424499394a3", # "seqNumber": 10925300212, # "side": "sell", # "price": 1977, # "qty": 0.03, # "timestamp": 1703015281206 # }, # { # "_id": "658201d060a95424499394a4", # "seqNumber": 10925321178, # "timestamp": 1703015488483, # "qty": 0.03, # "price": 1979.1666666666667, # "side": "buy" # } # ], # "fees": 0.11875000200000001, # "settledQtys": { # "ETH": -0.000092842104710025 # }, # "isAncillary": False, # "canceled": False # } # # fetchOrder # { # "_id": "657b4e6d60a954244939ac6f", # "userID": "65671262d93d9525ac009e36", # "orderID": "65671262d93d9525ac009e361702576531985b78465468b9cc544", # "orderType": "market", # "buyingCurrency": "ETH", # "sellingCurrency": "USDC", # "buyingQty": 0.004, # "timeInForce": 4, # "boughtQty": 0.004, # "soldQty": 9.236, # "creationTime": 1702576531995, # "seqNumber": 10874644062, # "firstFillTime": 1702576531995, # "lastFillTime": 1702576531995, # "fills": [ # { # "_id": "657b4e6d60a954244939ac70", # "seqNumber": 10874644061, # "timestamp": 1702576531995, # "qty": 0.004, # "price": 2309, # "side": "buy" # } # ], # "completionTime": 1702576531995, # "takerQty": 0.004, # "fees": 0.000004, # "isAncillary": False, # "margin": False, # "trade": False, # "canceled": False # } # timestamp = self.safe_integer(order, 'creationTime') isCanceled = self.safe_value(order, 'canceled') status = None if isCanceled is True: if timestamp is None: timestamp = self.safe_integer(order, 'completionTime') # market orders with bad price gain IOC - we mark them as 'rejected'? status = 'rejected' # these orders don't have the 'creationTime` param and have 'canceled': True else: status = 'canceled' else: status = self.safe_string(order, 'status') order = self.omit(order, 'status') # we mark orders from fetchOpenOrders with param 'status': 'open' type = self.safe_string(order, 'orderType') buyingQty = self.safe_string(order, 'buyingQty') sellingQty = self.safe_string(order, 'sellingQty') boughtQty = self.safe_string(order, 'boughtQty') soldQty = self.safe_string(order, 'soldQty') if type == 'market': if (buyingQty is None) and (boughtQty is not None) and (boughtQty != '0'): buyingQty = boughtQty if (sellingQty is None) and (soldQty is not None) and (soldQty != '0'): sellingQty = soldQty buyingCurrencyId = self.safe_string(order, 'buyingCurrency', '') sellingCurrencyId = self.safe_string(order, 'sellingCurrency', '') byuingIdPlusSellingId = buyingCurrencyId + sellingCurrencyId sellingIdPlusBuyingId = sellingCurrencyId + buyingCurrencyId side = None marketId = None baseAmount = buyingQty quoteAmount = buyingQty filled = None cost = None feeInBaseOrQuote = None marketsById = self.index_by(self.markets, 'id') if self.safe_value(marketsById, byuingIdPlusSellingId) is not None: side = 'buy' marketId = byuingIdPlusSellingId quoteAmount = sellingQty filled = boughtQty cost = soldQty feeInBaseOrQuote = 'base' elif self.safe_value(marketsById, sellingIdPlusBuyingId) is not None: side = 'sell' marketId = sellingIdPlusBuyingId baseAmount = sellingQty filled = soldQty cost = boughtQty feeInBaseOrQuote = 'quote' price = None if (baseAmount is not None) and (quoteAmount is not None): price = Precise.string_div(quoteAmount, baseAmount) market = self.safe_market(marketId, market) fee = None feeCost = self.safe_string(order, 'fees') if (feeCost is not None) and (feeInBaseOrQuote is not None): fee = { 'currency': market[feeInBaseOrQuote], 'cost': feeCost, 'rate': None, } trades = self.safe_value(order, 'fills', []) userData = self.safe_value(order, 'userData', {}) clientOrderId = self.safe_string(userData, 'comment') takeProfitPrice = self.safe_string(userData, 'takeProfit') stopLossPrice = self.safe_string(userData, 'stopLoss') return self.safe_order({ 'id': self.safe_string(order, 'orderID'), 'clientOrderId': clientOrderId, 'timestamp': timestamp, 'datetime': self.iso8601(timestamp), 'lastTradeTimestamp': self.safe_integer(order, 'lastFillTime'), 'status': status, 'symbol': market['symbol'], 'type': type, 'timeInForce': self.parse_order_time_in_force(self.safe_integer(order, 'timeInForce')), 'side': side, 'price': price, 'triggerPrice': self.safe_string(order, 'stopPrice'), 'takeProfitPrice': takeProfitPrice, 'stopLossPrice': stopLossPrice, 'average': None, 'amount': baseAmount, 'cost': cost, 'filled': filled, 'remaining': None, 'fee': fee, 'fees': None, 'trades': trades, 'info': order, }, market) def parse_order_time_in_force(self, timeInForce): timeInForceTypes = [ None, 'GTC', 'IOC', 'GTD', 'FOK', ] return self.safe_value(timeInForceTypes, timeInForce, timeInForce) def borrow_cross_margin(self, code: str, amount: float, params={}): """ create a loan to borrow margin https://documenter.getpostman.com/view/3653795/SVfWN6KS#5b90b3b9-e5db-4d07-ac9d-d680a06fd110 :param str code: unified currency code of the currency to borrow :param float amount: the amount to borrow :param dict [params]: extra parameters specific to the exchange API endpoint :returns dict: a `margin loan structure ` """ self.load_markets() currency = self.currency(code) currencyId = currency['id'] request: dict = {} request[currencyId] = self.currency_to_precision(code, amount) response = self.privatePutUsersMarginCollateral(self.extend(request, params)) # # {"message": "OK"} # result = self.safe_value(response, 'result', {}) transaction = self.parse_margin_loan(result, currency) return self.extend(transaction, { 'amount': amount, }) def parse_margin_loan(self, info, currency: Currency = None): currencyId = self.safe_string(info, 'coin') return { 'id': None, 'currency': self.safe_currency_code(currencyId, currency), 'amount': None, 'symbol': None, 'timestamp': None, 'datetime': None, 'info': info, } def sign(self, path, api='public', method='GET', params={}, headers=None, body=None): request = self.omit(params, self.extract_params(path)) endpoint = '/' + self.implode_params(path, params) url = self.urls['api'][api] + endpoint query = self.urlencode(request) if headers is None: headers = {} headers['CCXT'] = 'true' if api == 'private': if (self.uid is None) and (self.apiKey is not None): self.uid = self.apiKey if (self.token is None) and (self.secret is not None): self.token = self.secret if url == 'https://api.coinmetro.com/jwt': # handle with headers for login endpoint headers['X-Device-Id'] = 'bypass' if self.twofa is not None: headers['X-OTP'] = self.twofa elif url == 'https://api.coinmetro.com/jwtDevice': # handle with headers for long lived token login endpoint headers['X-Device-Id'] = self.uid if self.twofa is not None: headers['X-OTP'] = self.twofa else: headers['Authorization'] = 'Bearer ' + self.token if not url.startswith('https://api.coinmetro.com/open'): # if not sandbox endpoint self.check_required_credentials() headers['X-Device-Id'] = self.uid if (method == 'POST') or (method == 'PUT'): headers['Content-Type'] = 'application/x-www-form-urlencoded' body = self.urlencode(request) elif len(query) != 0: url += '?' + query while(url.endswith('/')): url = url[0:len(url) - 1] return {'url': url, 'method': method, 'body': body, 'headers': headers} 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 != 200) and (code != 201) and (code != 202): feedback = self.id + ' ' + body message = self.safe_string(response, 'message') self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback) self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback) raise ExchangeError(feedback) return None