Files
ccxt_with_mt5/ccxt/coinmetro.py
lz_db 0fab423a18 add
2025-11-16 12:31:03 +08:00

1946 lines
83 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.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 <https://docs.ccxt.com/#/?id=public-trades>`
"""
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 <https://docs.ccxt.com/#/?id=trade-structure>`
"""
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 <https://docs.ccxt.com/#/?id=order-book-structure>` 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 <https://docs.ccxt.com/#/?id=ticker-structure>`
"""
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 <https://docs.ccxt.com/#/?id=ticker-structure>`
"""
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=ledger>`
"""
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=order-structure>`
"""
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 <https://docs.ccxt.com/#/?id=order-structure>`
"""
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 <https://docs.ccxt.com/#/?id=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 <https://docs.ccxt.com/#/?id=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