1947 lines
84 KiB
Python
1947 lines
84 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.async_support.base.exchange import Exchange
|
|
from ccxt.abstract.coinmetro import ImplicitAPI
|
|
import asyncio
|
|
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"}
|
|
},
|
|
},
|
|
})
|
|
|
|
async 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 = await 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
|
|
|
|
async 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 = await asyncio.gather(*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
|
|
|
|
async 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
|
|
"""
|
|
await 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 = await 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'),
|
|
]
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await 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)
|
|
|
|
async 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
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
}
|
|
response = await 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
|
|
|
|
async 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>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params))
|
|
else:
|
|
response = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
response = await 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
|
|
|
|
async 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>`
|
|
"""
|
|
await 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 = await self.privateGetExchangeOrdersHistorySince(self.extend(request, params))
|
|
return self.parse_orders(response, market, since, limit)
|
|
|
|
async 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>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'orderID': id,
|
|
}
|
|
response = await 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)
|
|
|
|
async 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>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
currencyId = currency['id']
|
|
request: dict = {}
|
|
request[currencyId] = self.currency_to_precision(code, amount)
|
|
response = await 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
|