2676 lines
120 KiB
Python
2676 lines
120 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.exmo import ImplicitAPI
|
|
import asyncio
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Currencies, Currency, DepositAddress, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, OrderBooks, Trade, TradingFees, Transaction
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import PermissionDenied
|
|
from ccxt.base.errors import ArgumentsRequired
|
|
from ccxt.base.errors import BadRequest
|
|
from ccxt.base.errors import InsufficientFunds
|
|
from ccxt.base.errors import InvalidOrder
|
|
from ccxt.base.errors import OrderNotFound
|
|
from ccxt.base.errors import RateLimitExceeded
|
|
from ccxt.base.errors import OnMaintenance
|
|
from ccxt.base.errors import InvalidNonce
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class exmo(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(exmo, self).describe(), {
|
|
'id': 'exmo',
|
|
'name': 'EXMO',
|
|
'countries': ['LT'], # Lithuania
|
|
'rateLimit': 100, # 10 requests per 1 second
|
|
'version': 'v1.1',
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': True,
|
|
'margin': True,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': True,
|
|
'cancelOrder': True,
|
|
'cancelOrders': False,
|
|
'createDepositAddress': False,
|
|
'createMarketBuyOrder': True,
|
|
'createMarketBuyOrderWithCost': True,
|
|
'createMarketOrderWithCost': True,
|
|
'createOrder': True,
|
|
'createStopLimitOrder': True,
|
|
'createStopMarketOrder': True,
|
|
'createStopOrder': True,
|
|
'editOrder': True, # margin only
|
|
'fetchAccounts': False,
|
|
'fetchBalance': True,
|
|
'fetchCanceledOrders': True,
|
|
'fetchCurrencies': True,
|
|
'fetchDeposit': True,
|
|
'fetchDepositAddress': True,
|
|
'fetchDepositAddresses': False,
|
|
'fetchDepositAddressesByNetwork': False,
|
|
'fetchDeposits': True,
|
|
'fetchDepositsWithdrawals': True,
|
|
'fetchDepositWithdrawFee': 'emulated',
|
|
'fetchDepositWithdrawFees': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': False,
|
|
'fetchFundingRates': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchMarginMode': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMyTrades': True,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenOrders': True,
|
|
'fetchOrder': 'emulated',
|
|
'fetchOrderBook': True,
|
|
'fetchOrderBooks': True,
|
|
'fetchOrderTrades': True,
|
|
'fetchPosition': False,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': False,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFee': False,
|
|
'fetchTradingFees': True,
|
|
'fetchTransactionFees': True,
|
|
'fetchTransactions': 'emulated',
|
|
'fetchTransfer': False,
|
|
'fetchTransfers': False,
|
|
'fetchWithdrawal': True,
|
|
'fetchWithdrawals': True,
|
|
'reduceMargin': True,
|
|
'setMargin': False,
|
|
'transfer': False,
|
|
'withdraw': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1',
|
|
'5m': '5',
|
|
'15m': '15',
|
|
'30m': '30',
|
|
'45m': '45',
|
|
'1h': '60',
|
|
'2h': '120',
|
|
'3h': '180',
|
|
'4h': '240',
|
|
'1d': 'D',
|
|
'1w': 'W',
|
|
'1M': 'M',
|
|
},
|
|
'urls': {
|
|
'logo': 'https://user-images.githubusercontent.com/1294454/27766491-1b0ea956-5eda-11e7-9225-40d67b481b8d.jpg',
|
|
'api': {
|
|
'public': 'https://api.exmo.com',
|
|
'private': 'https://api.exmo.com',
|
|
'web': 'https://exmo.me',
|
|
},
|
|
'www': 'https://exmo.me',
|
|
'referral': 'https://exmo.me/?ref=131685',
|
|
'doc': [
|
|
'https://exmo.me/en/api_doc?ref=131685',
|
|
],
|
|
'fees': 'https://exmo.com/en/docs/fees',
|
|
},
|
|
'api': {
|
|
'web': {
|
|
'get': [
|
|
'ctrl/feesAndLimits',
|
|
'en/docs/fees',
|
|
],
|
|
},
|
|
'public': {
|
|
'get': [
|
|
'currency',
|
|
'currency/list/extended',
|
|
'order_book',
|
|
'pair_settings',
|
|
'ticker',
|
|
'trades',
|
|
'candles_history',
|
|
'required_amount',
|
|
'payments/providers/crypto/list',
|
|
],
|
|
},
|
|
'private': {
|
|
'post': [
|
|
'user_info',
|
|
'order_create',
|
|
'order_cancel',
|
|
'stop_market_order_create',
|
|
'stop_market_order_cancel',
|
|
'user_open_orders',
|
|
'user_trades',
|
|
'user_cancelled_orders',
|
|
'order_trades',
|
|
'deposit_address',
|
|
'withdraw_crypt',
|
|
'withdraw_get_txid',
|
|
'excode_create',
|
|
'excode_load',
|
|
'code_check',
|
|
'wallet_history',
|
|
'wallet_operations',
|
|
'margin/user/order/create',
|
|
'margin/user/order/update',
|
|
'margin/user/order/cancel',
|
|
'margin/user/position/close',
|
|
'margin/user/position/margin_add',
|
|
'margin/user/position/margin_remove',
|
|
'margin/currency/list',
|
|
'margin/pair/list',
|
|
'margin/settings',
|
|
'margin/funding/list',
|
|
'margin/user/info',
|
|
'margin/user/order/list',
|
|
'margin/user/order/history',
|
|
'margin/user/order/trades',
|
|
'margin/user/order/max_quantity',
|
|
'margin/user/position/list',
|
|
'margin/user/position/margin_remove_info',
|
|
'margin/user/position/margin_add_info',
|
|
'margin/user/wallet/list',
|
|
'margin/user/wallet/history',
|
|
'margin/user/trade/list',
|
|
'margin/trades',
|
|
'margin/liquidation/feed',
|
|
],
|
|
},
|
|
},
|
|
'fees': {
|
|
'trading': {
|
|
'feeSide': 'get',
|
|
'tierBased': True,
|
|
'percentage': True,
|
|
'maker': self.parse_number('0.004'),
|
|
'taker': self.parse_number('0.004'),
|
|
},
|
|
'transaction': {
|
|
'tierBased': False,
|
|
'percentage': False, # fixed transaction fees for crypto, see fetchDepositWithdrawFees below
|
|
},
|
|
},
|
|
'options': {
|
|
'networks': {
|
|
'ETH': 'ERC20',
|
|
'TRX': 'TRC20',
|
|
},
|
|
'fetchTradingFees': {
|
|
'method': 'fetchPrivateTradingFees', # or 'fetchPublicTradingFees'
|
|
},
|
|
'margin': {
|
|
'fillResponseFromRequest': True,
|
|
},
|
|
},
|
|
'features': {
|
|
'spot': {
|
|
'sandbox': False,
|
|
'createOrder': {
|
|
'marginMode': True, # todo revise
|
|
'triggerPrice': True, # todo: endpoint lacks other features
|
|
'triggerPriceType': None,
|
|
'triggerDirection': False,
|
|
'stopLossPrice': False,
|
|
'takeProfitPrice': False,
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': True,
|
|
'GTD': True,
|
|
},
|
|
'hedged': False,
|
|
'selfTradePrevention': False,
|
|
'trailing': False,
|
|
'leverage': True,
|
|
'marketBuyByCost': True,
|
|
'marketBuyRequiresPrice': False,
|
|
'iceberg': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': {
|
|
'marginMode': True,
|
|
'limit': 100,
|
|
'daysBack': None,
|
|
'untilDays': None,
|
|
'symbolRequired': True,
|
|
},
|
|
'fetchOrder': {
|
|
'marginMode': False,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': None,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': None,
|
|
'fetchClosedOrders': None,
|
|
'fetchOHLCV': {
|
|
'limit': 1000, # todo, not in request
|
|
},
|
|
},
|
|
'swap': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'commonCurrencies': {
|
|
'GMT': 'GMT Token',
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'exceptions': {
|
|
'exact': {
|
|
'140333': InvalidOrder, # {"error":{"code":140333,"msg":"The number of characters after the point in the price exceeds the maximum number '8\u003e6'"}}
|
|
'140434': BadRequest,
|
|
'40005': AuthenticationError, # Authorization error, incorrect signature
|
|
'40009': InvalidNonce, #
|
|
'40015': ExchangeError, # API function do not exist
|
|
'40016': OnMaintenance, # {"result":false,"error":"Error 40016: Maintenance work in progress"}
|
|
'40017': AuthenticationError, # Wrong API Key
|
|
'40032': PermissionDenied, # {"result":false,"error":"Error 40032: Access is denied for self API key"}
|
|
'40033': PermissionDenied, # {"result":false,"error":"Error 40033: Access is denied, self resources are temporarily blocked to user"}
|
|
'40034': RateLimitExceeded, # {"result":false,"error":"Error 40034: Access is denied, rate limit is exceeded"}
|
|
'50052': InsufficientFunds,
|
|
'50054': InsufficientFunds,
|
|
'50304': OrderNotFound, # "Order was not found '123456789'"(fetching order trades for an order that does not have trades yet)
|
|
'50173': OrderNotFound, # "Order with id X was not found."(cancelling non-existent, closed and cancelled order)
|
|
'50277': InvalidOrder,
|
|
'50319': InvalidOrder, # Price by order is less than permissible minimum for self pair
|
|
'50321': InvalidOrder, # Price by order is more than permissible maximum for self pair
|
|
'50381': InvalidOrder, # {"result":false,"error":"Error 50381: More than 2 decimal places are not permitted for pair BTC_USD"}
|
|
},
|
|
'broad': {
|
|
'range period is too long': BadRequest,
|
|
'invalid syntax': BadRequest,
|
|
'API rate limit exceeded': RateLimitExceeded, # {"result":false,"error":"API rate limit exceeded for x.x.x.x. Retry after 60 sec.","history":[],"begin":1579392000,"end":1579478400}
|
|
},
|
|
},
|
|
})
|
|
|
|
async def modify_margin_helper(self, symbol: str, amount, type, params={}):
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'position_id': market['id'],
|
|
'quantity': amount,
|
|
}
|
|
response = None
|
|
if type == 'add':
|
|
response = await self.privatePostMarginUserPositionMarginAdd(self.extend(request, params))
|
|
elif type == 'reduce':
|
|
response = await self.privatePostMarginUserPositionMarginRemove(self.extend(request, params))
|
|
#
|
|
# {}
|
|
#
|
|
margin = self.parse_margin_modification(response, market)
|
|
options = self.safe_value(self.options, 'margin', {})
|
|
fillResponseFromRequest = self.safe_bool(options, 'fillResponseFromRequest', True)
|
|
if fillResponseFromRequest:
|
|
margin['type'] = type
|
|
margin['amount'] = amount
|
|
return margin
|
|
|
|
def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification:
|
|
#
|
|
# {}
|
|
#
|
|
return {
|
|
'info': data,
|
|
'symbol': self.safe_symbol(None, market),
|
|
'type': None,
|
|
'marginMode': 'isolated',
|
|
'amount': None,
|
|
'total': None,
|
|
'code': self.safe_value(market, 'quote'),
|
|
'status': 'ok',
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
}
|
|
|
|
async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
|
|
"""
|
|
remove margin from a position
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#eebf9f25-0289-4946-9482-89872c738449
|
|
|
|
:param str symbol: unified market symbol
|
|
:param float amount: the amount of margin to remove
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `margin structure <https://docs.ccxt.com/#/?id=reduce-margin-structure>`
|
|
"""
|
|
return await self.modify_margin_helper(symbol, amount, 'reduce', params)
|
|
|
|
async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
|
|
"""
|
|
add margin
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#143ef808-79ca-4e49-9e79-a60ea4d8c0e3
|
|
|
|
:param str symbol: unified market symbol
|
|
:param float amount: amount of margin to add
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `margin structure <https://docs.ccxt.com/#/?id=add-margin-structure>`
|
|
"""
|
|
return await self.modify_margin_helper(symbol, amount, 'add', params)
|
|
|
|
async def fetch_trading_fees(self, params={}) -> TradingFees:
|
|
"""
|
|
fetch the trading fees for multiple markets
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#90927062-256c-4b03-900f-2b99131f9a54
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `fee structures <https://docs.ccxt.com/#/?id=fee-structure>` indexed by market symbols
|
|
"""
|
|
options = self.safe_value(self.options, 'fetchTradingFees', {})
|
|
defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTradingFees')
|
|
method = self.safe_string(params, 'method', defaultMethod)
|
|
params = self.omit(params, 'method')
|
|
if method == 'fetchPrivateTradingFees':
|
|
return await self.fetch_private_trading_fees(params)
|
|
else:
|
|
return await self.fetch_public_trading_fees(params)
|
|
|
|
async def fetch_private_trading_fees(self, params={}):
|
|
await self.load_markets()
|
|
response = await self.privatePostMarginPairList(params)
|
|
#
|
|
# {
|
|
# "pairs": [{
|
|
# "name": "EXM_USD",
|
|
# "buy_price": "0.02728391",
|
|
# "sell_price": "0.0276",
|
|
# "last_trade_price": "0.0276",
|
|
# "ticker_updated": "1646956050056696046",
|
|
# "is_fair_price": True,
|
|
# "max_price_precision": "8",
|
|
# "min_order_quantity": "1",
|
|
# "max_order_quantity": "50000",
|
|
# "min_order_price": "0.00000001",
|
|
# "max_order_price": "1000",
|
|
# "max_position_quantity": "50000",
|
|
# "trade_taker_fee": "0.05",
|
|
# "trade_maker_fee": "0",
|
|
# "liquidation_fee": "0.5",
|
|
# "max_leverage": "3",
|
|
# "default_leverage": "3",
|
|
# "liquidation_level": "5",
|
|
# "margin_call_level": "7.5",
|
|
# "position": "1",
|
|
# "updated": "1638976144797807397"
|
|
# }
|
|
# ...
|
|
# ]
|
|
# }
|
|
#
|
|
pairs = self.safe_value(response, 'pairs', [])
|
|
result: dict = {}
|
|
for i in range(0, len(pairs)):
|
|
pair = pairs[i]
|
|
marketId = self.safe_string(pair, 'name')
|
|
symbol = self.safe_symbol(marketId, None, '_')
|
|
makerString = self.safe_string(pair, 'trade_maker_fee')
|
|
takerString = self.safe_string(pair, 'trade_taker_fee')
|
|
maker = self.parse_number(Precise.string_div(makerString, '100'))
|
|
taker = self.parse_number(Precise.string_div(takerString, '100'))
|
|
result[symbol] = {
|
|
'info': pair,
|
|
'symbol': symbol,
|
|
'maker': maker,
|
|
'taker': taker,
|
|
'percentage': True,
|
|
'tierBased': True,
|
|
}
|
|
return result
|
|
|
|
async def fetch_public_trading_fees(self, params={}):
|
|
await self.load_markets()
|
|
response = await self.publicGetPairSettings(params)
|
|
#
|
|
# {
|
|
# "BTC_USD": {
|
|
# "min_quantity": "0.00002",
|
|
# "max_quantity": "1000",
|
|
# "min_price": "1",
|
|
# "max_price": "150000",
|
|
# "max_amount": "500000",
|
|
# "min_amount": "1",
|
|
# "price_precision": "2",
|
|
# "commission_taker_percent": "0.3",
|
|
# "commission_maker_percent": "0.3"
|
|
# },
|
|
# }
|
|
#
|
|
result: dict = {}
|
|
for i in range(0, len(self.symbols)):
|
|
symbol = self.symbols[i]
|
|
market = self.market(symbol)
|
|
fee = self.safe_value(response, market['id'], {})
|
|
makerString = self.safe_string(fee, 'commission_maker_percent')
|
|
takerString = self.safe_string(fee, 'commission_taker_percent')
|
|
maker = self.parse_number(Precise.string_div(makerString, '100'))
|
|
taker = self.parse_number(Precise.string_div(takerString, '100'))
|
|
result[symbol] = {
|
|
'info': fee,
|
|
'symbol': symbol,
|
|
'maker': maker,
|
|
'taker': taker,
|
|
'percentage': True,
|
|
'tierBased': True,
|
|
}
|
|
return result
|
|
|
|
def parse_fixed_float_value(self, input):
|
|
if (input is None) or (input == '-'):
|
|
return None
|
|
if input == '':
|
|
return 0
|
|
isPercentage = (input.find('%') >= 0)
|
|
parts = input.split(' ')
|
|
value = parts[0].replace('%', '')
|
|
result = float(value)
|
|
if (result > 0) and isPercentage:
|
|
raise ExchangeError(self.id + ' parseFixedFloatValue() detected an unsupported non-zero percentage-based fee ' + input)
|
|
return result
|
|
|
|
async def fetch_transaction_fees(self, codes: Strings = None, params={}):
|
|
"""
|
|
@deprecated
|
|
please use fetchDepositWithdrawFees instead
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2
|
|
|
|
:param str[]|None codes: list of unified currency codes
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `transaction fees structures <https://docs.ccxt.com/#/?id=fees-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
cryptoList = await self.publicGetPaymentsProvidersCryptoList(params)
|
|
#
|
|
# {
|
|
# "BTC":[
|
|
# {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
|
|
# {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6}
|
|
# ],
|
|
# "ETH":[
|
|
# {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4},
|
|
# {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}
|
|
# ],
|
|
# "USDT":[
|
|
# {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
|
|
# {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6},
|
|
# {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
|
|
# {
|
|
# "type":"withdraw",
|
|
# "name":"USDT(ERC20)",
|
|
# "currency_name":"USDT",
|
|
# "min":"55",
|
|
# "max":"200000",
|
|
# "enabled":true,
|
|
# "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.",
|
|
# "commission_desc":"10 USDT",
|
|
# "currency_confirmations":6
|
|
# },
|
|
# {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2},
|
|
# {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6}
|
|
# ],
|
|
# "XLM":[
|
|
# {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
|
|
# {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1}
|
|
# ],
|
|
# }
|
|
#
|
|
result: dict = {}
|
|
cryptoListKeys = list(cryptoList.keys())
|
|
for i in range(0, len(cryptoListKeys)):
|
|
code = cryptoListKeys[i]
|
|
if codes is not None and not self.in_array(code, codes):
|
|
continue
|
|
result[code] = {
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
}
|
|
currency = self.currency(code)
|
|
currencyId = self.safe_string(currency, 'id')
|
|
providers = self.safe_value(cryptoList, currencyId, [])
|
|
for j in range(0, len(providers)):
|
|
provider = providers[j]
|
|
typeInner = self.safe_string(provider, 'type')
|
|
commissionDesc = self.safe_string(provider, 'commission_desc')
|
|
fee = self.parse_fixed_float_value(commissionDesc)
|
|
result[code][typeInner] = fee
|
|
result[code]['info'] = providers
|
|
# cache them for later use
|
|
self.options['transactionFees'] = result
|
|
return result
|
|
|
|
async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}):
|
|
"""
|
|
fetch deposit and withdraw fees
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2
|
|
|
|
:param str[]|None codes: list of unified currency codes
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `transaction fees structures <https://docs.ccxt.com/#/?id=fees-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.publicGetPaymentsProvidersCryptoList(params)
|
|
#
|
|
# {
|
|
# "USDT": [
|
|
# {
|
|
# "type": "deposit", # or "withdraw"
|
|
# "name": "USDT(ERC20)",
|
|
# "currency_name": "USDT",
|
|
# "min": "10",
|
|
# "max": "0",
|
|
# "enabled": True,
|
|
# "comment": "Minimum deposit amount is 10 USDT",
|
|
# "commission_desc": "0%",
|
|
# "currency_confirmations": 2
|
|
# },
|
|
# ...
|
|
# ],
|
|
# ...
|
|
# }
|
|
#
|
|
result = self.parse_deposit_withdraw_fees(response, codes)
|
|
# cache them for later use
|
|
self.options['transactionFees'] = result
|
|
return result
|
|
|
|
def parse_deposit_withdraw_fee(self, fee, currency: Currency = None):
|
|
#
|
|
# [
|
|
# {
|
|
# "type": "deposit", # or "withdraw"
|
|
# "name": "BTC",
|
|
# "currency_name": "BTC",
|
|
# "min": "0.001",
|
|
# "max": "0",
|
|
# "enabled": True,
|
|
# "comment": "Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds",
|
|
# "commission_desc": "0%",
|
|
# "currency_confirmations": 1
|
|
# },
|
|
# ...
|
|
# ]
|
|
#
|
|
result = self.deposit_withdraw_fee(fee)
|
|
for i in range(0, len(fee)):
|
|
provider = fee[i]
|
|
type = self.safe_string(provider, 'type')
|
|
networkId = self.safe_string(provider, 'name')
|
|
networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code'))
|
|
commissionDesc = self.safe_string(provider, 'commission_desc')
|
|
splitCommissionDesc = []
|
|
percentage = None
|
|
if commissionDesc is not None:
|
|
splitCommissionDesc = commissionDesc.split('%')
|
|
splitCommissionDescLength = len(splitCommissionDesc)
|
|
percentage = splitCommissionDescLength >= 2
|
|
network = self.safe_value(result['networks'], networkCode)
|
|
if network is None:
|
|
result['networks'][networkCode] = {
|
|
'withdraw': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
'deposit': {
|
|
'fee': None,
|
|
'percentage': None,
|
|
},
|
|
}
|
|
result['networks'][networkCode][type] = {
|
|
'fee': self.parse_fixed_float_value(self.safe_string(splitCommissionDesc, 0)),
|
|
'percentage': percentage,
|
|
}
|
|
return self.assign_default_deposit_withdraw_fees(result)
|
|
|
|
async def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#7cdf0ca8-9ff6-4cf3-aa33-bcec83155c49
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
promises = []
|
|
#
|
|
promises.append(self.publicGetCurrencyListExtended(params))
|
|
#
|
|
# [
|
|
# {"name":"VLX","description":"Velas"},
|
|
# {"name":"RUB","description":"Russian Ruble"},
|
|
# {"name":"BTC","description":"Bitcoin"},
|
|
# {"name":"USD","description":"US Dollar"}
|
|
# ]
|
|
#
|
|
promises.append(self.publicGetPaymentsProvidersCryptoList(params))
|
|
#
|
|
# {
|
|
# "BTC":[
|
|
# {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
|
|
# {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6}
|
|
# ],
|
|
# "ETH":[
|
|
# {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4},
|
|
# {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}
|
|
# ],
|
|
# "USDT":[
|
|
# {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
|
|
# {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6},
|
|
# {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
|
|
# {"type":"withdraw", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"55", "max":"200000", "enabled":true, "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.", "commission_desc":"10 USDT", "currency_confirmations":6},
|
|
# {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2},
|
|
# {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6}
|
|
# ],
|
|
# "XLM":[
|
|
# {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
|
|
# {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1}
|
|
# ],
|
|
# }
|
|
#
|
|
responses = await asyncio.gather(*promises)
|
|
currencyList = responses[0]
|
|
cryptoList = responses[1]
|
|
result: dict = {}
|
|
for i in range(0, len(currencyList)):
|
|
currency = currencyList[i]
|
|
currencyId = self.safe_string(currency, 'name')
|
|
code = self.safe_currency_code(currencyId)
|
|
type = 'crypto'
|
|
networks = {}
|
|
providers = self.safe_list(cryptoList, currencyId)
|
|
if providers is None:
|
|
type = 'fiat'
|
|
else:
|
|
for j in range(0, len(providers)):
|
|
provider = providers[j]
|
|
name = self.safe_string(provider, 'name')
|
|
# get network-id by removing extra things
|
|
networkId = name.replace(currencyId + ' ', '')
|
|
networkId = networkId.replace('(', '')
|
|
replaceChar = ')' # transpiler trick
|
|
networkId = networkId.replace(replaceChar, '')
|
|
networkCode = self.network_id_to_code(networkId)
|
|
if not (networkCode in networks):
|
|
networks[networkCode] = {
|
|
'id': networkId,
|
|
'network': networkCode,
|
|
'active': None,
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
'fee': None,
|
|
'limits': {
|
|
'withdraw': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'deposit': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'info': [], # set, because of multiple network sub-entries
|
|
}
|
|
typeInner = self.safe_string(provider, 'type')
|
|
minValue = self.safe_string(provider, 'min')
|
|
maxValue = self.safe_string(provider, 'max')
|
|
activeProvider = self.safe_bool(provider, 'enabled')
|
|
networkEntry = networks[networkCode]
|
|
if typeInner == 'deposit':
|
|
networkEntry['deposit'] = activeProvider
|
|
networkEntry['limits']['deposit']['min'] = minValue
|
|
networkEntry['limits']['deposit']['max'] = maxValue
|
|
elif typeInner == 'withdraw':
|
|
networkEntry['withdraw'] = activeProvider
|
|
networkEntry['limits']['withdraw']['min'] = minValue
|
|
networkEntry['limits']['withdraw']['max'] = maxValue
|
|
info = self.safe_list(networkEntry, 'info')
|
|
info.append(provider)
|
|
networkEntry['info'] = info
|
|
networks[networkCode] = networkEntry
|
|
result[code] = self.safe_currency_structure({
|
|
'id': currencyId,
|
|
'code': code,
|
|
'name': self.safe_string(currency, 'description'),
|
|
'type': type,
|
|
'active': None,
|
|
'deposit': None,
|
|
'withdraw': None,
|
|
'fee': None,
|
|
'precision': self.parse_number('1e-8'),
|
|
'limits': {
|
|
'withdraw': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'deposit': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'info': {
|
|
'currency': currency,
|
|
'providers': providers,
|
|
},
|
|
'networks': networks,
|
|
})
|
|
return result
|
|
|
|
async def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for exmo
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
promises = []
|
|
promises.append(self.publicGetPairSettings(params))
|
|
#
|
|
# {
|
|
# "BTC_USD":{
|
|
# "min_quantity":"0.0001",
|
|
# "max_quantity":"1000",
|
|
# "min_price":"1",
|
|
# "max_price":"30000",
|
|
# "max_amount":"500000",
|
|
# "min_amount":"1",
|
|
# "price_precision":8,
|
|
# "commission_taker_percent":"0.4",
|
|
# "commission_maker_percent":"0.4"
|
|
# },
|
|
# }
|
|
#
|
|
marginPairsDict: dict = {}
|
|
fetchMargin = self.check_required_credentials(False)
|
|
if fetchMargin:
|
|
promises.append(self.privatePostMarginPairList(params))
|
|
#
|
|
# {
|
|
# "pairs": [
|
|
# {
|
|
# "buy_price": "55978.85",
|
|
# "default_leverage": "3",
|
|
# "is_fair_price": True,
|
|
# "last_trade_price": "55999.23",
|
|
# "liquidation_fee": "2",
|
|
# "liquidation_level": "10",
|
|
# "margin_call_level": "15",
|
|
# "max_leverage": "3",
|
|
# "max_order_price": "150000",
|
|
# "max_order_quantity": "1",
|
|
# "max_position_quantity": "1",
|
|
# "max_price_precision": 2,
|
|
# "min_order_price": "1",
|
|
# "min_order_quantity": "0.00002",
|
|
# "name": "BTC_USD",
|
|
# "position": 1,
|
|
# "sell_price": "55985.51",
|
|
# "ticker_updated": "1619019818936107989",
|
|
# "trade_maker_fee": "0",
|
|
# "trade_taker_fee": "0.05",
|
|
# "updated": "1619008608955599013"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
responses = await asyncio.gather(*promises)
|
|
spotResponse = responses[0]
|
|
if fetchMargin:
|
|
marginPairs = responses[1]
|
|
pairs = self.safe_list(marginPairs, 'pairs')
|
|
marginPairsDict = self.index_by(pairs, 'name')
|
|
keys = list(spotResponse.keys())
|
|
result = []
|
|
for i in range(0, len(keys)):
|
|
id = keys[i]
|
|
market = spotResponse[id]
|
|
marginMarket = self.safe_dict(marginPairsDict, id)
|
|
symbol = id.replace('_', '/')
|
|
baseId, quoteId = symbol.split('/')
|
|
base = self.safe_currency_code(baseId)
|
|
quote = self.safe_currency_code(quoteId)
|
|
takerString = self.safe_string(market, 'commission_taker_percent')
|
|
makerString = self.safe_string(market, 'commission_maker_percent')
|
|
maxQuantity = self.safe_string(market, 'max_quantity')
|
|
marginMaxQuantity = self.safe_string(marginMarket, 'max_order_quantity')
|
|
result.append({
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'quote': quote,
|
|
'settle': None,
|
|
'baseId': baseId,
|
|
'quoteId': quoteId,
|
|
'settleId': None,
|
|
'type': 'spot',
|
|
'spot': True,
|
|
'margin': marginMarket is not None,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'active': None,
|
|
'contract': False,
|
|
'linear': None,
|
|
'inverse': None,
|
|
'taker': self.parse_number(Precise.string_div(takerString, '100')),
|
|
'maker': self.parse_number(Precise.string_div(makerString, '100')),
|
|
'contractSize': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'precision': {
|
|
'amount': self.parse_number('1e-8'),
|
|
'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))),
|
|
},
|
|
'limits': {
|
|
'leverage': {
|
|
'min': None,
|
|
'max': self.safe_number(market, 'leverage'),
|
|
},
|
|
'amount': {
|
|
'min': self.safe_number(market, 'min_quantity'),
|
|
'max': self.parse_number(Precise.string_max(maxQuantity, marginMaxQuantity)),
|
|
},
|
|
'price': {
|
|
'min': self.safe_number(market, 'min_price'),
|
|
'max': self.safe_number(market, 'max_price'),
|
|
},
|
|
'cost': {
|
|
'min': self.safe_number(market, 'min_amount'),
|
|
'max': self.safe_number(market, 'max_amount'),
|
|
},
|
|
},
|
|
'created': None,
|
|
'info': market,
|
|
})
|
|
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/10287440/SzYXWKPi#65eeb949-74e5-4631-9184-c38387fe53e8
|
|
|
|
: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]: timestamp in ms of the latest candle to fetch
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
until = self.safe_integer_product(params, 'until', 0.001)
|
|
untilIsDefined = (until is not None)
|
|
request: dict = {
|
|
'symbol': market['id'],
|
|
'resolution': self.safe_string(self.timeframes, timeframe, timeframe),
|
|
}
|
|
maxLimit = 3000
|
|
duration = self.parse_timeframe(timeframe)
|
|
now = self.parse_to_int(self.milliseconds() / 1000)
|
|
if since is None:
|
|
to = min(until, now) if untilIsDefined else now
|
|
if limit is None:
|
|
limit = 1000 # cap default at generous amount
|
|
else:
|
|
limit = min(limit, maxLimit)
|
|
request['from'] = to - (limit * duration) - 1
|
|
request['to'] = to
|
|
else:
|
|
request['from'] = self.parse_to_int(since / 1000)
|
|
if untilIsDefined:
|
|
request['to'] = min(until, now)
|
|
else:
|
|
if limit is None:
|
|
limit = maxLimit
|
|
else:
|
|
limit = min(limit, maxLimit)
|
|
to = self.sum(since, limit * duration)
|
|
request['to'] = min(to, now)
|
|
params = self.omit(params, 'until')
|
|
response = await self.publicGetCandlesHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "candles":[
|
|
# {"t":1584057600000,"o":0.02235144,"c":0.02400233,"h":0.025171,"l":0.02221,"v":5988.34031761},
|
|
# {"t":1584144000000,"o":0.0240373,"c":0.02367413,"h":0.024399,"l":0.0235,"v":2027.82522329},
|
|
# {"t":1584230400000,"o":0.02363458,"c":0.02319242,"h":0.0237948,"l":0.02223196,"v":1707.96944997},
|
|
# ]
|
|
# }
|
|
#
|
|
candles = self.safe_list(response, 'candles', [])
|
|
return self.parse_ohlcvs(candles, market, timeframe, since, limit)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
#
|
|
# {
|
|
# "t":1584057600000,
|
|
# "o":0.02235144,
|
|
# "c":0.02400233,
|
|
# "h":0.025171,
|
|
# "l":0.02221,
|
|
# "v":5988.34031761
|
|
# }
|
|
#
|
|
return [
|
|
self.safe_integer(ohlcv, 't'),
|
|
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 parse_balance(self, response) -> Balances:
|
|
result: dict = {'info': response}
|
|
wallets = self.safe_value(response, 'wallets')
|
|
if wallets is not None:
|
|
currencyIds = list(wallets.keys())
|
|
for i in range(0, len(currencyIds)):
|
|
currencyId = currencyIds[i]
|
|
item = wallets[currencyId]
|
|
currency = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
account['used'] = self.safe_string(item, 'used')
|
|
account['free'] = self.safe_string(item, 'free')
|
|
account['total'] = self.safe_string(item, 'balance')
|
|
result[currency] = account
|
|
else:
|
|
free = self.safe_value(response, 'balances', {})
|
|
used = self.safe_value(response, 'reserved', {})
|
|
currencyIds = list(free.keys())
|
|
for i in range(0, len(currencyIds)):
|
|
currencyId = currencyIds[i]
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
if currencyId in free:
|
|
account['free'] = self.safe_string(free, currencyId)
|
|
if currencyId in used:
|
|
account['used'] = self.safe_string(used, currencyId)
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
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/10287440/SzYXWKPi#59c5160f-27a1-4d9a-8cfb-7979c7ffaac6
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8388df7-1f9f-4d41-81c4-5a387d171dc6
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.marginMode]: *isolated* fetches the isolated margin balance
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params)
|
|
if marginMode == 'cross':
|
|
raise BadRequest(self.id + ' does not support cross margin')
|
|
response = None
|
|
if marginMode == 'isolated':
|
|
response = await self.privatePostMarginUserWalletList(params)
|
|
#
|
|
# {
|
|
# "wallets": {
|
|
# "USD": {
|
|
# "balance": "1000",
|
|
# "free": "600",
|
|
# "used": "400"
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
else:
|
|
response = await self.privatePostUserInfo(params)
|
|
#
|
|
# {
|
|
# "uid":131685,
|
|
# "server_date":1628999600,
|
|
# "balances":{
|
|
# "EXM":"0",
|
|
# "USD":"0",
|
|
# "EUR":"0",
|
|
# "GBP":"0",
|
|
# },
|
|
# }
|
|
#
|
|
return self.parse_balance(response)
|
|
|
|
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/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871
|
|
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
response = await self.publicGetOrderBook(self.extend(request, params))
|
|
result = self.safe_dict(response, market['id'])
|
|
return self.parse_order_book(result, market['symbol'], None, 'bid', 'ask')
|
|
|
|
async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}) -> OrderBooks:
|
|
"""
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871
|
|
|
|
:param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None
|
|
:param int [limit]: max number of entries per orderbook to return, default is None
|
|
: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 symbol
|
|
"""
|
|
await self.load_markets()
|
|
ids = None
|
|
if symbols is None:
|
|
ids = ','.join(self.ids)
|
|
# max URL length is 2083 symbols, including http schema, hostname, tld, etc...
|
|
if len(ids) > 2048:
|
|
numIds = len(self.ids)
|
|
raise ExchangeError(self.id + ' fetchOrderBooks() has ' + str(numIds) + ' symbols exceeding max URL length, you are required to specify a list of symbols in the first argument to fetchOrderBooks')
|
|
else:
|
|
ids = self.market_ids(symbols)
|
|
ids = ','.join(ids)
|
|
request: dict = {
|
|
'pair': ids,
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
response = await self.publicGetOrderBook(self.extend(request, params))
|
|
result: dict = {}
|
|
marketIds = list(response.keys())
|
|
for i in range(0, len(marketIds)):
|
|
marketId = marketIds[i]
|
|
symbol = self.safe_symbol(marketId)
|
|
result[symbol] = self.parse_order_book(response[marketId], symbol, None, 'bid', 'ask')
|
|
return result
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
#
|
|
# {
|
|
# "buy_price":"0.00002996",
|
|
# "sell_price":"0.00003002",
|
|
# "last_trade":"0.00002992",
|
|
# "high":"0.00003028",
|
|
# "low":"0.00002935",
|
|
# "avg":"0.00002963",
|
|
# "vol":"1196546.3163222",
|
|
# "vol_curr":"35.80066578",
|
|
# "updated":1642291733
|
|
# }
|
|
#
|
|
timestamp = self.safe_timestamp(ticker, 'updated')
|
|
market = self.safe_market(None, market)
|
|
last = self.safe_string(ticker, 'last_trade')
|
|
return self.safe_ticker({
|
|
'symbol': market['symbol'],
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'high': self.safe_string(ticker, 'high'),
|
|
'low': self.safe_string(ticker, 'low'),
|
|
'bid': self.safe_string(ticker, 'buy_price'),
|
|
'bidVolume': None,
|
|
'ask': self.safe_string(ticker, 'sell_price'),
|
|
'askVolume': None,
|
|
'vwap': None,
|
|
'open': None,
|
|
'close': last,
|
|
'last': last,
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': self.safe_string(ticker, 'avg'),
|
|
'baseVolume': self.safe_string(ticker, 'vol'),
|
|
'quoteVolume': self.safe_string(ticker, 'vol_curr'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
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/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385
|
|
|
|
:param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols)
|
|
response = await self.publicGetTicker(params)
|
|
#
|
|
# {
|
|
# "ADA_BTC":{
|
|
# "buy_price":"0.00002996",
|
|
# "sell_price":"0.00003002",
|
|
# "last_trade":"0.00002992",
|
|
# "high":"0.00003028",
|
|
# "low":"0.00002935",
|
|
# "avg":"0.00002963",
|
|
# "vol":"1196546.3163222",
|
|
# "vol_curr":"35.80066578",
|
|
# "updated":1642291733
|
|
# }
|
|
# }
|
|
#
|
|
result: dict = {}
|
|
marketIds = list(response.keys())
|
|
for i in range(0, len(marketIds)):
|
|
marketId = marketIds[i]
|
|
market = self.safe_market(marketId, None, '_')
|
|
symbol = market['symbol']
|
|
ticker = self.safe_value(response, marketId)
|
|
result[symbol] = self.parse_ticker(ticker, market)
|
|
return self.filter_by_array_tickers(result, 'symbol', symbols)
|
|
|
|
async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385
|
|
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.publicGetTicker(params)
|
|
market = self.market(symbol)
|
|
return self.parse_ticker(response[market['id']], market)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# fetchTrades(public)
|
|
#
|
|
# {
|
|
# "trade_id":165087520,
|
|
# "date":1587470005,
|
|
# "type":"buy",
|
|
# "quantity":"1.004",
|
|
# "price":"0.02491461",
|
|
# "amount":"0.02501426"
|
|
# },
|
|
#
|
|
# fetchMyTrades, fetchOrderTrades
|
|
#
|
|
# {
|
|
# "trade_id": 3,
|
|
# "date": 1435488248,
|
|
# "type": "buy",
|
|
# "pair": "BTC_USD",
|
|
# "order_id": 12345,
|
|
# "quantity": 1,
|
|
# "price": 100,
|
|
# "amount": 100,
|
|
# "exec_type": "taker",
|
|
# "commission_amount": "0.02",
|
|
# "commission_currency": "BTC",
|
|
# "commission_percent": "0.2"
|
|
# }
|
|
#
|
|
# fetchMyTrades(margin)
|
|
#
|
|
# {
|
|
# "trade_id": "692861757015952517",
|
|
# "trade_dt": "1693951853197811824",
|
|
# "trade_type": "buy",
|
|
# "pair": "ADA_USDT",
|
|
# "quantity": "1.96607879",
|
|
# "price": "0.2568",
|
|
# "amount": "0.50488903"
|
|
# }
|
|
#
|
|
timestamp = self.safe_timestamp(trade, 'date')
|
|
id = self.safe_string(trade, 'trade_id')
|
|
orderId = self.safe_string(trade, 'order_id')
|
|
priceString = self.safe_string(trade, 'price')
|
|
amountString = self.safe_string(trade, 'quantity')
|
|
costString = self.safe_string(trade, 'amount')
|
|
side = self.safe_string_2(trade, 'type', 'trade_type')
|
|
type = None
|
|
marketId = self.safe_string(trade, 'pair')
|
|
market = self.safe_market(marketId, market, '_')
|
|
symbol = market['symbol']
|
|
isMaker = self.safe_value(trade, 'is_maker')
|
|
takerOrMakerDefault = None
|
|
if isMaker is not None:
|
|
takerOrMakerDefault = 'maker' if isMaker else 'taker'
|
|
takerOrMaker = self.safe_string(trade, 'exec_type', takerOrMakerDefault)
|
|
fee = None
|
|
feeCostString = self.safe_string(trade, 'commission_amount')
|
|
if feeCostString is not None:
|
|
feeCurrencyId = self.safe_string(trade, 'commission_currency')
|
|
feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
|
|
feeRateString = self.safe_string(trade, 'commission_percent')
|
|
if feeRateString is not None:
|
|
feeRateString = Precise.string_div(feeRateString, '1000', 18)
|
|
fee = {
|
|
'cost': feeCostString,
|
|
'currency': feeCurrencyCode,
|
|
'rate': feeRateString,
|
|
}
|
|
return self.safe_trade({
|
|
'id': id,
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': symbol,
|
|
'order': orderId,
|
|
'type': type,
|
|
'side': side,
|
|
'takerOrMaker': takerOrMaker,
|
|
'price': priceString,
|
|
'amount': amountString,
|
|
'cost': costString,
|
|
'fee': fee,
|
|
}, market)
|
|
|
|
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/10287440/SzYXWKPi#5a5a9c0d-cf17-47f6-9d62-6d4404ebd5ac
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns 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'],
|
|
}
|
|
response = await self.publicGetTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ETH_BTC":[
|
|
# {
|
|
# "trade_id":165087520,
|
|
# "date":1587470005,
|
|
# "type":"buy",
|
|
# "quantity":"1.004",
|
|
# "price":"0.02491461",
|
|
# "amount":"0.02501426"
|
|
# },
|
|
# {
|
|
# "trade_id":165087369,
|
|
# "date":1587469938,
|
|
# "type":"buy",
|
|
# "quantity":"0.94",
|
|
# "price":"0.02492348",
|
|
# "amount":"0.02342807"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_list(response, market['id'], [])
|
|
return self.parse_trades(data, 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/10287440/SzYXWKPi#b8d8d9af-4f46-46a1-939b-ad261d79f452 # spot
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#f4b1aaf8-399f-403b-ab5e-4926d967a106 # margin
|
|
|
|
:param str symbol: a symbol is required but it can be a single string, or a non-empty array
|
|
:param int [since]: the earliest time in ms to fetch trades for
|
|
:param int [limit]: *required for margin orders* the maximum number of trades structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
|
|
EXCHANGE SPECIFIC PARAMETERS
|
|
:param int [params.offset]: last deal offset, default = 0
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
if symbol is None:
|
|
raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument')
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params)
|
|
if marginMode == 'cross':
|
|
raise BadRequest(self.id + ' only isolated margin is supported')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
pair = market['id']
|
|
isSpot = marginMode != 'isolated'
|
|
if limit is None:
|
|
limit = 100
|
|
request: dict = {}
|
|
if isSpot:
|
|
request['pair'] = pair
|
|
else:
|
|
request['pair_name'] = pair
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
offset = self.safe_integer(params, 'offset', 0)
|
|
request['offset'] = offset
|
|
response = None
|
|
if isSpot:
|
|
response = await self.privatePostUserTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "BTC_USD": [
|
|
# {
|
|
# "trade_id": 20056872,
|
|
# "client_id": 100500,
|
|
# "date": 1435488248,
|
|
# "type": "buy",
|
|
# "pair": "BTC_USD",
|
|
# "quantity": "1",
|
|
# "price": "100",
|
|
# "amount": "100",
|
|
# "order_id": 7,
|
|
# "parent_order_id": 117684023830293,
|
|
# "exec_type": "taker",
|
|
# "commission_amount": "0.02",
|
|
# "commission_currency": "BTC",
|
|
# "commission_percent": "0.2"
|
|
# }
|
|
# ],
|
|
# ...
|
|
# }
|
|
#
|
|
else:
|
|
responseFromExchange = await self.privatePostMarginTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "trades": {
|
|
# "ADA_USDT": [
|
|
# {
|
|
# "trade_id": "692861757015952517",
|
|
# "trade_dt": "1693951853197811824",
|
|
# "trade_type": "buy",
|
|
# "pair": "ADA_USDT",
|
|
# "quantity": "1.96607879",
|
|
# "price": "0.2568",
|
|
# "amount": "0.50488903"
|
|
# },
|
|
# ]
|
|
# ...
|
|
# }
|
|
# }
|
|
#
|
|
response = self.safe_value(responseFromExchange, 'trades')
|
|
result = []
|
|
marketIdsInner = list(response.keys())
|
|
for i in range(0, len(marketIdsInner)):
|
|
marketId = marketIdsInner[i]
|
|
resultMarket = self.safe_market(marketId, None, '_')
|
|
items = response[marketId]
|
|
trades = self.parse_trades(items, resultMarket, since, limit)
|
|
result = self.array_concat(result, trades)
|
|
return self.filter_by_since_limit(result, since, limit)
|
|
|
|
async def create_market_order_with_cost(self, symbol: str, side: OrderSide, cost: float, params={}):
|
|
"""
|
|
create a market order by providing the symbol, side and cost
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param str side: 'buy' or 'sell'
|
|
:param float cost: how much you want to trade in units of the quote currency
|
|
: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()
|
|
params = self.extend(params, {'cost': cost})
|
|
return await self.create_order(symbol, 'market', side, cost, None, params)
|
|
|
|
async def create_market_buy_order_with_cost(self, symbol: str, cost: float, params={}):
|
|
"""
|
|
create a market buy order by providing the symbol and cost
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param float cost: how much you want to trade in units of the quote currency
|
|
: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()
|
|
params = self.extend(params, {'cost': cost})
|
|
return await self.create_order(symbol, 'market', 'buy', cost, None, params)
|
|
|
|
async def create_market_sell_order_with_cost(self, symbol: str, cost: float, params={}):
|
|
"""
|
|
create a market sell order by providing the symbol and cost
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd
|
|
|
|
:param str symbol: unified symbol of the market to create an order in
|
|
:param float cost: how much you want to trade in units of the quote currency
|
|
: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()
|
|
params = self.extend(params, {'cost': cost})
|
|
return await self.create_order(symbol, 'market', 'sell', cost, None, params)
|
|
|
|
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/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#de6f4321-eeac-468c-87f7-c4ad7062e265 # stop market
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#3561b86c-9ff1-436e-8e68-ac926b7eb523 # margin
|
|
|
|
: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.triggerPrice]: the price at which a trigger order is triggered at
|
|
:param str [params.timeInForce]: *spot only* 'fok', 'ioc' or 'post_only'
|
|
:param boolean [params.postOnly]: *spot only* True for post only orders
|
|
:param float [params.cost]: *spot only* *market orders only* the cost of the order in the quote currency for market orders
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
isMarket = (type == 'market') and (price is None)
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('createOrder', params)
|
|
if marginMode == 'cross':
|
|
raise BadRequest(self.id + ' only supports isolated margin')
|
|
isSpot = (marginMode != 'isolated')
|
|
triggerPrice = self.safe_string_n(params, ['triggerPrice', 'stopPrice', 'stop_price'])
|
|
cost = self.safe_string(params, 'cost')
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
# 'leverage': 2,
|
|
# 'quantity': self.amount_to_precision(market['symbol'], amount),
|
|
# spot - buy, sell, market_buy, market_sell, market_buy_total, market_sell_total
|
|
# margin - limit_buy, limit_sell, market_buy, market_sell, stop_buy, stop_sell, stop_limit_buy, stop_limit_sell, trailing_stop_buy, trailing_stop_sell
|
|
# 'stop_price': self.price_to_precision(symbol, stopPrice),
|
|
# 'distance': 0, # distance for trailing stop orders
|
|
# 'expire': 0, # expiration timestamp in UTC timezone for the order, unless expire is 0
|
|
# 'client_id': 123, # optional, must be a positive integer
|
|
# 'comment': '', # up to 50 latin symbols, whitespaces, underscores
|
|
}
|
|
if cost is None:
|
|
request['quantity'] = self.amount_to_precision(market['symbol'], amount)
|
|
else:
|
|
request['quantity'] = self.cost_to_precision(market['symbol'], cost)
|
|
clientOrderId = self.safe_value_2(params, 'client_id', 'clientOrderId')
|
|
if clientOrderId is not None:
|
|
clientOrderId = self.safe_integer_2(params, 'client_id', 'clientOrderId')
|
|
if clientOrderId is None:
|
|
raise BadRequest(self.id + ' createOrder() client order id must be an integer / numeric literal')
|
|
else:
|
|
request['client_id'] = clientOrderId
|
|
leverage = self.safe_number(params, 'leverage')
|
|
if not isSpot and (leverage is None):
|
|
raise ArgumentsRequired(self.id + ' createOrder requires an extra param params["leverage"] for margin orders')
|
|
params = self.omit(params, ['stopPrice', 'stop_price', 'triggerPrice', 'timeInForce', 'client_id', 'clientOrderId', 'cost'])
|
|
if price is not None:
|
|
request['price'] = self.price_to_precision(market['symbol'], price)
|
|
response = None
|
|
if isSpot:
|
|
if triggerPrice is not None:
|
|
if type == 'limit':
|
|
raise BadRequest(self.id + ' createOrder() cannot create stop limit orders for spot, only stop market')
|
|
else:
|
|
request['type'] = side
|
|
request['trigger_price'] = self.price_to_precision(symbol, triggerPrice)
|
|
response = await self.privatePostStopMarketOrderCreate(self.extend(request, params))
|
|
else:
|
|
execType = self.safe_string(params, 'exec_type')
|
|
isPostOnly = None
|
|
isPostOnly, params = self.handle_post_only(type == 'market', execType == 'post_only', params)
|
|
timeInForce = self.safe_string(params, 'timeInForce')
|
|
request['price'] = 0 if isMarket else self.price_to_precision(market['symbol'], price)
|
|
if type == 'limit':
|
|
request['type'] = side
|
|
elif type == 'market':
|
|
marketSuffix = '_total' if (cost is not None) else ''
|
|
request['type'] = 'market_' + side + marketSuffix
|
|
if isPostOnly:
|
|
request['exec_type'] = 'post_only'
|
|
elif timeInForce is not None:
|
|
request['exec_type'] = timeInForce
|
|
response = await self.privatePostOrderCreate(self.extend(request, params))
|
|
else:
|
|
if triggerPrice is not None:
|
|
request['stop_price'] = self.price_to_precision(symbol, triggerPrice)
|
|
if type == 'limit':
|
|
request['type'] = 'stop_limit_' + side
|
|
elif type == 'market':
|
|
request['type'] = 'stop_' + side
|
|
else:
|
|
request['type'] = type
|
|
else:
|
|
if type == 'limit' or type == 'market':
|
|
request['type'] = type + '_' + side
|
|
else:
|
|
request['type'] = type
|
|
response = await self.privatePostMarginUserOrderCreate(self.extend(request, params))
|
|
return self.parse_order(response, market)
|
|
|
|
async def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#1f710d4b-75bc-4b65-ad68-006f863a3f26
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#a4d0aae8-28f7-41ac-94fd-c4030130453d # stop market
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#705dfec5-2b35-4667-862b-faf54eca6209 # margin
|
|
|
|
:param str id: order id
|
|
:param str symbol: not used by exmo cancelOrder()
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param boolean [params.trigger]: True to cancel a trigger order
|
|
:param str [params.marginMode]: set to 'cross' or 'isolated' to cancel a margin order
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {}
|
|
trigger = self.safe_value_2(params, 'trigger', 'stop')
|
|
params = self.omit(params, ['trigger', 'stop'])
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params)
|
|
if marginMode == 'cross':
|
|
raise BadRequest(self.id + ' only supports isolated margin')
|
|
response = None
|
|
if (marginMode == 'isolated'):
|
|
request['order_id'] = id
|
|
response = await self.privatePostMarginUserOrderCancel(self.extend(request, params))
|
|
#
|
|
# {}
|
|
#
|
|
else:
|
|
if trigger:
|
|
request['parent_order_id'] = id
|
|
response = await self.privatePostStopMarketOrderCancel(self.extend(request, params))
|
|
#
|
|
# {}
|
|
#
|
|
else:
|
|
request['order_id'] = id
|
|
response = await self.privatePostOrderCancel(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "error": '',
|
|
# "result": True
|
|
# }
|
|
#
|
|
return self.parse_order(response)
|
|
|
|
async def fetch_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
*spot only* fetches information on an order made by the user
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459 # spot
|
|
|
|
:param str id: order id
|
|
:param str symbol: not used by exmo 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 = {
|
|
'order_id': str(id),
|
|
}
|
|
response = await self.privatePostOrderTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "type": "buy",
|
|
# "in_currency": "BTC",
|
|
# "in_amount": "1",
|
|
# "out_currency": "USD",
|
|
# "out_amount": "100",
|
|
# "trades": [
|
|
# {
|
|
# "trade_id": 3,
|
|
# "date": 1435488248,
|
|
# "type": "buy",
|
|
# "pair": "BTC_USD",
|
|
# "order_id": 12345,
|
|
# "quantity": 1,
|
|
# "price": 100,
|
|
# "amount": 100
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
order = self.parse_order(response)
|
|
order['id'] = str(id)
|
|
return order
|
|
|
|
async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetch all the trades made from a single order
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459 # spot
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#00810661-9119-46c5-aec5-55abe9cb42c7 # margin
|
|
|
|
:param str id: order id
|
|
: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 to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.marginMode]: set to "isolated" to fetch trades for a margin order
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
|
|
"""
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('fetchOrderTrades', params)
|
|
if marginMode == 'cross':
|
|
raise BadRequest(self.id + ' only supports isolated margin')
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'order_id': str(id),
|
|
}
|
|
response = None
|
|
if marginMode == 'isolated':
|
|
response = await self.privatePostMarginUserOrderTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "trades": [
|
|
# {
|
|
# "is_maker": False,
|
|
# "order_id": "123",
|
|
# "pair": "BTC_USD",
|
|
# "price": "54122.25",
|
|
# "quantity": "0.00069994",
|
|
# "trade_dt": "1619069561718824428",
|
|
# "trade_id": "692842802860135010",
|
|
# "type": "sell"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
else:
|
|
response = await self.privatePostOrderTrades(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "type": "buy",
|
|
# "in_currency": "BTC",
|
|
# "in_amount": "1",
|
|
# "out_currency": "USD",
|
|
# "out_amount": "100",
|
|
# "trades": [
|
|
# {
|
|
# "trade_id": 3,
|
|
# "date": 1435488248,
|
|
# "type": "buy",
|
|
# "pair": "BTC_USD",
|
|
# "order_id": 12345,
|
|
# "quantity": 1,
|
|
# "price": 100,
|
|
# "amount": 100,
|
|
# "exec_type": "taker",
|
|
# "commission_amount": "0.02",
|
|
# "commission_currency": "BTC",
|
|
# "commission_percent": "0.2"
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
trades = self.safe_list(response, 'trades')
|
|
return self.parse_trades(trades, market, since, limit)
|
|
|
|
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/10287440/SzYXWKPi#0e135370-daa4-4689-8acd-b6876dee9ba1 # spot open orders
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#a7cfd4f0-476e-4675-b33f-22a46902f245 # margin
|
|
|
|
:param str symbol: unified market symbol
|
|
:param int [since]: the earliest time in ms to fetch open orders for
|
|
:param int [limit]: the maximum number of open orders structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.marginMode]: set to "isolated" for margin orders
|
|
: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)
|
|
symbol = market['symbol']
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params)
|
|
isMargin = ((marginMode == 'cross') or (marginMode == 'isolated'))
|
|
response = None
|
|
orders = []
|
|
if isMargin:
|
|
response = await self.privatePostMarginUserOrderList(params)
|
|
#
|
|
# {
|
|
# "orders": [
|
|
# {
|
|
# "client_id": "0",
|
|
# "comment": "",
|
|
# "created": "1619068707985325495",
|
|
# "distance": "0",
|
|
# "expire": 0,
|
|
# "funding_currency": "BTC",
|
|
# "funding_quantity": "0.01",
|
|
# "funding_rate": "0.02",
|
|
# "leverage": "2",
|
|
# "order_id": "123",
|
|
# "pair": "BTC_USD",
|
|
# "previous_type": "limit_sell",
|
|
# "price": "58000",
|
|
# "quantity": "0.01",
|
|
# "src": 0,
|
|
# "stop_price": "0",
|
|
# "trigger_price": "58000",
|
|
# "type": "limit_sell",
|
|
# "updated": 1619068707989411800
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
params = self.extend(params, {
|
|
'status': 'open',
|
|
})
|
|
responseOrders = self.safe_value(response, 'orders')
|
|
orders = self.parse_orders(responseOrders, market, since, limit, params)
|
|
else:
|
|
response = await self.privatePostUserOpenOrders(params)
|
|
#
|
|
# {
|
|
# "USDT_USD": [
|
|
# {
|
|
# "parent_order_id": "507061384740151010",
|
|
# "client_id": "100500",
|
|
# "created": "1589547391",
|
|
# "type": "stop_market_buy",
|
|
# "pair": "USDT_USD",
|
|
# "quantity": "1",
|
|
# "trigger_price": "5",
|
|
# "amount": "5"
|
|
# }
|
|
# ],
|
|
# ...
|
|
# }
|
|
#
|
|
marketIds = list(response.keys())
|
|
for i in range(0, len(marketIds)):
|
|
marketId = marketIds[i]
|
|
marketInner = self.safe_market(marketId)
|
|
params = self.extend(params, {
|
|
'status': 'open',
|
|
})
|
|
parsedOrders = self.parse_orders(response[marketId], marketInner, since, limit, params)
|
|
orders = self.array_concat(orders, parsedOrders)
|
|
return orders
|
|
|
|
def parse_status(self, status):
|
|
if status is None:
|
|
return None
|
|
statuses: dict = {
|
|
'cancel_started': 'canceled',
|
|
}
|
|
if status.find('cancel') >= 0:
|
|
status = 'canceled'
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_side(self, orderType):
|
|
side: dict = {
|
|
'limit_buy': 'buy',
|
|
'limit_sell': 'sell',
|
|
'market_buy': 'buy',
|
|
'market_sell': 'sell',
|
|
'stop_buy': 'buy',
|
|
'stop_sell': 'sell',
|
|
'stop_limit_buy': 'buy',
|
|
'stop_limit_sell': 'sell',
|
|
'trailing_stop_buy': 'buy',
|
|
'trailing_stop_sell': 'sell',
|
|
'stop_market_sell': 'sell',
|
|
'stop_market_buy': 'buy',
|
|
'buy': 'buy',
|
|
'sell': 'sell',
|
|
}
|
|
return self.safe_string(side, orderType, orderType)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders
|
|
#
|
|
# {
|
|
# "order_id": "14",
|
|
# "created": "1435517311",
|
|
# "type": "buy",
|
|
# "pair": "BTC_USD",
|
|
# "price": "100",
|
|
# "quantity": "1",
|
|
# "amount": "100"
|
|
# }
|
|
#
|
|
# fetchOrder
|
|
#
|
|
# {
|
|
# "type": "buy",
|
|
# "in_currency": "BTC",
|
|
# "in_amount": "1",
|
|
# "out_currency": "USD",
|
|
# "out_amount": "100",
|
|
# "trades": [
|
|
# {
|
|
# "trade_id": 3,
|
|
# "date": 1435488248,
|
|
# "type": "buy",
|
|
# "pair": "BTC_USD",
|
|
# "order_id": 12345,
|
|
# "quantity": 1,
|
|
# "price": 100,
|
|
# "amount": 100
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# Margin fetchOpenOrders
|
|
#
|
|
# {
|
|
# "client_id": "0",
|
|
# "comment": "",
|
|
# "created": "1619068707985325495",
|
|
# "distance": "0",
|
|
# "expire": 0,
|
|
# "funding_currency": "BTC",
|
|
# "funding_quantity": "0.01",
|
|
# "funding_rate": "0.02",
|
|
# "leverage": "2",
|
|
# "order_id": "123",
|
|
# "pair": "BTC_USD",
|
|
# "previous_type": "limit_sell",
|
|
# "price": "58000",
|
|
# "quantity": "0.01",
|
|
# "src": 0,
|
|
# "stop_price": "0",
|
|
# "trigger_price": "58000",
|
|
# "type": "limit_sell",
|
|
# "updated": 1619068707989411800
|
|
# }
|
|
#
|
|
# Margin fetchClosedOrders
|
|
#
|
|
# {
|
|
# "distance": "0",
|
|
# "event_id": "692842802860022508",
|
|
# "event_time": "1619069531190173720",
|
|
# "event_type": "OrderCancelStarted",
|
|
# "order_id": "123",
|
|
# "order_status": "cancel_started",
|
|
# "order_type": "limit_sell",
|
|
# "pair": "BTC_USD",
|
|
# "price": "54115",
|
|
# "quantity": "0.001",
|
|
# "stop_price": "0",
|
|
# "trade_id": "0",
|
|
# "trade_price": "0",
|
|
# "trade_quantity": "0",
|
|
# "trade_type": ""
|
|
# },
|
|
#
|
|
id = self.safe_string_2(order, 'order_id', 'parent_order_id')
|
|
eventTime = self.safe_integer_product_2(order, 'event_time', 'created', 0.000001)
|
|
timestamp = self.safe_timestamp(order, 'created', eventTime)
|
|
orderType = self.safe_string_2(order, 'type', 'order_type')
|
|
side = self.parse_side(orderType)
|
|
marketId = None
|
|
if 'pair' in order:
|
|
marketId = order['pair']
|
|
elif ('in_currency' in order) and ('out_currency' in order):
|
|
if side == 'buy':
|
|
marketId = order['in_currency'] + '_' + order['out_currency']
|
|
else:
|
|
marketId = order['out_currency'] + '_' + order['in_currency']
|
|
market = self.safe_market(marketId, market)
|
|
symbol = market['symbol']
|
|
amount = self.safe_string(order, 'quantity')
|
|
if amount is None:
|
|
amountField = 'in_amount' if (side == 'buy') else 'out_amount'
|
|
amount = self.safe_string(order, amountField)
|
|
price = self.safe_string(order, 'price')
|
|
cost = self.safe_string(order, 'amount')
|
|
transactions = self.safe_value(order, 'trades', [])
|
|
clientOrderId = self.safe_integer(order, 'client_id')
|
|
triggerPrice = self.safe_string(order, 'stop_price')
|
|
if triggerPrice == '0':
|
|
triggerPrice = None
|
|
type = None
|
|
if (orderType != 'buy') and (orderType != 'sell'):
|
|
type = orderType
|
|
return self.safe_order({
|
|
'id': id,
|
|
'clientOrderId': clientOrderId,
|
|
'datetime': self.iso8601(timestamp),
|
|
'timestamp': timestamp,
|
|
'lastTradeTimestamp': self.safe_integer_product(order, 'updated', 0.000001),
|
|
'status': self.parse_status(self.safe_string(order, 'order_status')),
|
|
'symbol': symbol,
|
|
'type': type,
|
|
'timeInForce': None,
|
|
'postOnly': None,
|
|
'side': side,
|
|
'price': price,
|
|
'triggerPrice': triggerPrice,
|
|
'cost': cost,
|
|
'amount': amount,
|
|
'filled': None,
|
|
'remaining': None,
|
|
'average': None,
|
|
'trades': transactions,
|
|
'fee': None,
|
|
'info': order,
|
|
}, market)
|
|
|
|
async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
fetches information on multiple canceled orders made by the user
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#1d2524dd-ae6d-403a-a067-77b50d13fbe5 # margin
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#a51be1d0-af5f-44e4-99d7-f7b04c6067d0 # spot canceled orders
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: timestamp in ms of the earliest order, default is None
|
|
:param int [limit]: max number of orders to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.marginMode]: set to "isolated" for margin orders
|
|
:returns dict: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params)
|
|
if marginMode == 'cross':
|
|
raise BadRequest(self.id + ' only supports isolated margin')
|
|
if limit is None:
|
|
limit = 100
|
|
isSpot = (marginMode != 'isolated')
|
|
if symbol is not None:
|
|
marketInner = self.market(symbol)
|
|
symbol = marketInner['symbol']
|
|
request: dict = {
|
|
'limit': limit,
|
|
}
|
|
request['offset'] = limit if (since is not None) else 0
|
|
request['limit'] = limit
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
response = None
|
|
if isSpot:
|
|
response = await self.privatePostUserCancelledOrders(self.extend(request, params))
|
|
#
|
|
# [
|
|
# {
|
|
# "order_id": "27056153840",
|
|
# "client_id": "0",
|
|
# "created": "1653428646",
|
|
# "type": "buy",
|
|
# "pair": "BTC_USDT",
|
|
# "quantity": "0.1",
|
|
# "price": "10",
|
|
# "amount": "1"
|
|
# }
|
|
# ]
|
|
#
|
|
params = self.extend(params, {
|
|
'status': 'canceled',
|
|
})
|
|
return self.parse_orders(response, market, since, limit, params)
|
|
else:
|
|
responseSwap = await self.privatePostMarginUserOrderHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "items": [
|
|
# {
|
|
# "event_id": "692862104574106858",
|
|
# "event_time": "1694116400173489405",
|
|
# "event_type": "OrderCancelStarted",
|
|
# "order_id": "692862104561289319",
|
|
# "order_type": "stop_limit_sell",
|
|
# "order_status": "cancel_started",
|
|
# "trade_id": "0",
|
|
# "trade_type":"",
|
|
# "trade_quantity": "0",
|
|
# "trade_price": "0",
|
|
# "pair": "ADA_USDT",
|
|
# "quantity": "12",
|
|
# "price": "0.23",
|
|
# "stop_price": "0.22",
|
|
# "distance": "0"
|
|
# }
|
|
# ...
|
|
# ]
|
|
# }
|
|
#
|
|
items = self.safe_value(responseSwap, 'items')
|
|
orders = self.parse_orders(items, market, since, limit, params)
|
|
result = []
|
|
for i in range(0, len(orders)):
|
|
order = orders[i]
|
|
if order['status'] == 'canceled':
|
|
result.append(order)
|
|
return result
|
|
|
|
async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
|
|
"""
|
|
*margin only* edit a trade order
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#f27ee040-c75f-4b59-b608-d05bd45b7899 # margin
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified CCXT market symbol
|
|
:param str type: not used by exmo editOrder
|
|
:param str side: not used by exmo editOrder
|
|
:param float [amount]: how much of the currency you want to trade in units of the 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.triggerPrice]: stop price for stop-market and stop-limit orders
|
|
:param str params['marginMode']: must be set to isolated
|
|
|
|
EXCHANGE SPECIFIC PARAMETERS
|
|
:param int [params.distance]: distance for trailing stop orders
|
|
:param int [params.expire]: expiration timestamp in UTC timezone for the order. order will not be expired if expire is 0
|
|
:param str [params.comment]: optional comment for order. up to 50 latin symbols, whitespaces, underscores
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
marginMode = None
|
|
marginMode, params = self.handle_margin_mode_and_params('editOrder', params)
|
|
if marginMode != 'isolated':
|
|
raise BadRequest(self.id + ' editOrder() can only be used for isolated margin orders')
|
|
triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price'])
|
|
params = self.omit(params, ['triggerPrice', 'stopPrice'])
|
|
request: dict = {
|
|
'order_id': id, # id of the open order
|
|
}
|
|
if amount is not None:
|
|
request['quantity'] = amount
|
|
if price is not None:
|
|
request['price'] = self.price_to_precision(market['symbol'], price)
|
|
if triggerPrice is not None:
|
|
request['stop_price'] = self.price_to_precision(market['symbol'], triggerPrice)
|
|
response = await self.privatePostMarginUserOrderUpdate(self.extend(request, params))
|
|
return self.parse_order(response)
|
|
|
|
async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
fetch the deposit address for a currency associated with self account
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8f9ced9-7ab6-4383-a6a4-bc54469ba60e
|
|
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.privatePostDepositAddress(params)
|
|
#
|
|
# {
|
|
# "TRX":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9",
|
|
# "USDTTRC20":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9"
|
|
# }
|
|
#
|
|
depositAddress = self.safe_string(response, code)
|
|
address = None
|
|
tag = None
|
|
if depositAddress:
|
|
addressAndTag = depositAddress.split(',')
|
|
address = addressAndTag[0]
|
|
numParts = len(addressAndTag)
|
|
if numParts > 1:
|
|
tag = addressAndTag[1]
|
|
self.check_address(address)
|
|
return {
|
|
'info': response,
|
|
'currency': code,
|
|
'network': None,
|
|
'address': address,
|
|
'tag': tag,
|
|
}
|
|
|
|
def get_market_from_trades(self, trades):
|
|
tradesBySymbol = self.index_by(trades, 'pair')
|
|
symbols = list(tradesBySymbol.keys())
|
|
numSymbols = len(symbols)
|
|
if numSymbols == 1:
|
|
return self.markets[symbols[0]]
|
|
return None
|
|
|
|
async def withdraw(self, code: str, amount: float, address: str, tag: Str = None, params={}) -> Transaction:
|
|
"""
|
|
make a withdrawal
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#3ab9c34d-ad58-4f87-9c57-2e2ea88a8325
|
|
|
|
:param str code: unified currency code
|
|
:param float amount: the amount to withdraw
|
|
:param str address: the address to withdraw to
|
|
:param str tag:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
tag, params = self.handle_withdraw_tag_and_params(tag, params)
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'amount': amount,
|
|
'currency': currency['id'],
|
|
'address': address,
|
|
}
|
|
if tag is not None:
|
|
request['invoice'] = tag
|
|
networks = self.safe_value(self.options, 'networks', {})
|
|
network = self.safe_string_upper(params, 'network') # self line allows the user to specify either ERC20 or ETH
|
|
network = self.safe_string(networks, network, network) # handle ERC20>ETH alias
|
|
if network is not None:
|
|
request['transport'] = network
|
|
params = self.omit(params, 'network')
|
|
response = await self.privatePostWithdrawCrypt(self.extend(request, params))
|
|
return self.parse_transaction(response, currency)
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
statuses: dict = {
|
|
'transferred': 'ok',
|
|
'paid': 'ok',
|
|
'pending': 'pending',
|
|
'processing': 'pending',
|
|
'verifying': 'pending',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
#
|
|
# fetchDepositsWithdrawals
|
|
#
|
|
# {
|
|
# "dt": 1461841192,
|
|
# "type": "deposit",
|
|
# "curr": "RUB",
|
|
# "status": "processing",
|
|
# "provider": "Qiwi(LA) [12345]",
|
|
# "amount": "1",
|
|
# "account": "",
|
|
# "txid": "ec46f784ad976fd7f7539089d1a129fe46...",
|
|
# }
|
|
#
|
|
# fetchWithdrawals
|
|
#
|
|
# {
|
|
# "operation_id": 47412538520634344,
|
|
# "created": 1573760013,
|
|
# "updated": 1573760013,
|
|
# "type": "withdraw",
|
|
# "currency": "DOGE",
|
|
# "status": "Paid",
|
|
# "amount": "300",
|
|
# "provider": "DOGE",
|
|
# "commission": "0",
|
|
# "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
|
|
# "order_id": 69670170,
|
|
# "provider_type": "crypto",
|
|
# "crypto_address": "DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
|
|
# "card_number": "",
|
|
# "wallet_address": "",
|
|
# "email": "",
|
|
# "phone": "",
|
|
# "extra": {
|
|
# "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
|
|
# "confirmations": null,
|
|
# "excode": "",
|
|
# "invoice": ""
|
|
# },
|
|
# "error": ""
|
|
# }
|
|
#
|
|
# withdraw
|
|
#
|
|
# {
|
|
# "result": True,
|
|
# "error": "",
|
|
# "task_id": 11775077
|
|
# }
|
|
#
|
|
timestamp = self.safe_timestamp_2(transaction, 'dt', 'created')
|
|
amountString = self.safe_string(transaction, 'amount')
|
|
if amountString is not None:
|
|
amountString = Precise.string_abs(amountString)
|
|
txid = self.safe_string(transaction, 'txid')
|
|
if txid is None:
|
|
extra = self.safe_value(transaction, 'extra', {})
|
|
extraTxid = self.safe_string(extra, 'txid')
|
|
if extraTxid != '':
|
|
txid = extraTxid
|
|
type = self.safe_string(transaction, 'type')
|
|
currencyId = self.safe_string_2(transaction, 'curr', 'currency')
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
address = None
|
|
comment = None
|
|
account = self.safe_string(transaction, 'account')
|
|
if type == 'deposit':
|
|
comment = account
|
|
elif type == 'withdrawal':
|
|
address = account
|
|
if address is not None:
|
|
parts = address.split(':')
|
|
numParts = len(parts)
|
|
if numParts == 2:
|
|
address = self.safe_string(parts, 1)
|
|
address = address.replace(' ', '')
|
|
fee = {
|
|
'currency': None,
|
|
'cost': None,
|
|
'rate': None,
|
|
}
|
|
# fixed funding fees only(for now)
|
|
if not self.fees['transaction']['percentage']:
|
|
key = 'withdraw' if (type == 'withdrawal') else 'deposit'
|
|
feeCost = self.safe_string(transaction, 'commission')
|
|
if feeCost is None:
|
|
transactionFees = self.safe_value(self.options, 'transactionFees', {})
|
|
codeFees = self.safe_value(transactionFees, code, {})
|
|
feeCost = self.safe_string(codeFees, key)
|
|
# users don't pay for cashbacks, no fees for that
|
|
provider = self.safe_string(transaction, 'provider')
|
|
if provider == 'cashback':
|
|
feeCost = '0'
|
|
if feeCost is not None:
|
|
# withdrawal amount includes the fee
|
|
if type == 'withdrawal':
|
|
amountString = Precise.string_sub(amountString, feeCost)
|
|
fee['cost'] = self.parse_number(feeCost)
|
|
fee['currency'] = code
|
|
return {
|
|
'info': transaction,
|
|
'id': self.safe_string_2(transaction, 'order_id', 'task_id'),
|
|
'txid': txid,
|
|
'type': type,
|
|
'currency': code,
|
|
'network': self.safe_string(transaction, 'provider'),
|
|
'amount': self.parse_number(amountString),
|
|
'status': self.parse_transaction_status(self.safe_string_lower(transaction, 'status')),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'address': address,
|
|
'addressFrom': None,
|
|
'addressTo': address,
|
|
'tag': None,
|
|
'tagFrom': None,
|
|
'tagTo': None,
|
|
'updated': self.safe_timestamp(transaction, 'updated'),
|
|
'comment': comment,
|
|
'internal': None,
|
|
'fee': fee,
|
|
}
|
|
|
|
async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch history of deposits and withdrawals
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#31e69a33-4849-4e6a-b4b4-6d574238f6a7
|
|
|
|
:param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
|
|
:param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
|
|
:param int [limit]: max number of deposit/withdrawals to return, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a list of `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {}
|
|
if since is not None:
|
|
request['date'] = self.parse_to_int(since / 1000)
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
response = await self.privatePostWalletHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "result": True,
|
|
# "error": "",
|
|
# "begin": "1493942400",
|
|
# "end": "1494028800",
|
|
# "history": [
|
|
# {
|
|
# "dt": 1461841192,
|
|
# "type": "deposit",
|
|
# "curr": "RUB",
|
|
# "status": "processing",
|
|
# "provider": "Qiwi(LA) [12345]",
|
|
# "amount": "1",
|
|
# "account": "",
|
|
# "txid": "ec46f784ad976fd7f7539089d1a129fe46...",
|
|
# },
|
|
# {
|
|
# "dt": 1463414785,
|
|
# "type": "withdrawal",
|
|
# "curr": "USD",
|
|
# "status": "paid",
|
|
# "provider": "EXCODE",
|
|
# "amount": "-1",
|
|
# "account": "EX-CODE_19371_USDda...",
|
|
# "txid": "",
|
|
# },
|
|
# ],
|
|
# }
|
|
#
|
|
return self.parse_transactions(response['history'], currency, since, limit)
|
|
|
|
async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all withdrawals made from an account
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
|
|
|
|
:param str code: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch withdrawals for
|
|
:param int [limit]: the maximum number of withdrawals structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = None
|
|
request: dict = {
|
|
'type': 'withdraw',
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit # default: 100, maximum: 100
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
response = await self.privatePostWalletOperations(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "items": [
|
|
# {
|
|
# "operation_id": 47412538520634344,
|
|
# "created": 1573760013,
|
|
# "updated": 1573760013,
|
|
# "type": "withdraw",
|
|
# "currency": "DOGE",
|
|
# "status": "Paid",
|
|
# "amount": "300",
|
|
# "provider": "DOGE",
|
|
# "commission": "0",
|
|
# "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
|
|
# "order_id": 69670170,
|
|
# "extra": {
|
|
# "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
|
|
# "excode": "",
|
|
# "invoice": ""
|
|
# },
|
|
# "error": ""
|
|
# },
|
|
# ],
|
|
# "count": 23
|
|
# }
|
|
#
|
|
items = self.safe_list(response, 'items', [])
|
|
return self.parse_transactions(items, currency, since, limit)
|
|
|
|
async def fetch_withdrawal(self, id: str, code: Str = None, params={}):
|
|
"""
|
|
fetch data on a currency withdrawal via the withdrawal id
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
|
|
|
|
:param str id: withdrawal id
|
|
:param str code: unified currency code of the currency withdrawn, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = None
|
|
request: dict = {
|
|
'order_id': id,
|
|
'type': 'withdraw',
|
|
}
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
response = await self.privatePostWalletOperations(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "items": [
|
|
# {
|
|
# "operation_id": 47412538520634344,
|
|
# "created": 1573760013,
|
|
# "updated": 1573760013,
|
|
# "type": "deposit",
|
|
# "currency": "DOGE",
|
|
# "status": "Paid",
|
|
# "amount": "300",
|
|
# "provider": "DOGE",
|
|
# "commission": "0",
|
|
# "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
|
|
# "order_id": 69670170,
|
|
# "extra": {
|
|
# "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
|
|
# "excode": "",
|
|
# "invoice": ""
|
|
# },
|
|
# "error": ""
|
|
# },
|
|
# ],
|
|
# "count": 23
|
|
# }
|
|
#
|
|
items = self.safe_value(response, 'items', [])
|
|
first = self.safe_dict(items, 0, {})
|
|
return self.parse_transaction(first, currency)
|
|
|
|
async def fetch_deposit(self, id: str, code: Str = None, params={}):
|
|
"""
|
|
fetch information on a deposit
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
|
|
|
|
:param str id: deposit id
|
|
:param str code: unified currency code, default is None
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = None
|
|
request: dict = {
|
|
'order_id': id,
|
|
'type': 'deposit',
|
|
}
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
response = await self.privatePostWalletOperations(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "items": [
|
|
# {
|
|
# "operation_id": 47412538520634344,
|
|
# "created": 1573760013,
|
|
# "updated": 1573760013,
|
|
# "type": "deposit",
|
|
# "currency": "DOGE",
|
|
# "status": "Paid",
|
|
# "amount": "300",
|
|
# "provider": "DOGE",
|
|
# "commission": "0",
|
|
# "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
|
|
# "order_id": 69670170,
|
|
# "extra": {
|
|
# "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
|
|
# "excode": "",
|
|
# "invoice": ""
|
|
# },
|
|
# "error": ""
|
|
# },
|
|
# ],
|
|
# "count": 23
|
|
# }
|
|
#
|
|
items = self.safe_value(response, 'items', [])
|
|
first = self.safe_dict(items, 0, {})
|
|
return self.parse_transaction(first, currency)
|
|
|
|
async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
|
|
"""
|
|
fetch all deposits made to an account
|
|
|
|
https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
|
|
|
|
:param str code: unified currency code
|
|
:param int [since]: the earliest time in ms to fetch deposits for
|
|
:param int [limit]: the maximum number of deposits structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = None
|
|
request: dict = {
|
|
'type': 'deposit',
|
|
}
|
|
if limit is not None:
|
|
request['limit'] = limit # default: 100, maximum: 100
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
response = await self.privatePostWalletOperations(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "items": [
|
|
# {
|
|
# "operation_id": 47412538520634344,
|
|
# "created": 1573760013,
|
|
# "updated": 1573760013,
|
|
# "type": "deposit",
|
|
# "currency": "DOGE",
|
|
# "status": "Paid",
|
|
# "amount": "300",
|
|
# "provider": "DOGE",
|
|
# "commission": "0",
|
|
# "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
|
|
# "order_id": 69670170,
|
|
# "extra": {
|
|
# "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
|
|
# "excode": "",
|
|
# "invoice": ""
|
|
# },
|
|
# "error": ""
|
|
# },
|
|
# ],
|
|
# "count": 23
|
|
# }
|
|
#
|
|
items = self.safe_list(response, 'items', [])
|
|
return self.parse_transactions(items, currency, since, limit)
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
url = self.urls['api'][api] + '/'
|
|
if api != 'web':
|
|
url += self.version + '/'
|
|
url += path
|
|
if (api == 'public') or (api == 'web'):
|
|
if params:
|
|
url += '?' + self.urlencode(params)
|
|
elif api == 'private':
|
|
self.check_required_credentials()
|
|
nonce = self.nonce()
|
|
body = self.urlencode(self.extend({'nonce': nonce}, params))
|
|
headers = {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'Key': self.apiKey,
|
|
'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512),
|
|
}
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def nonce(self):
|
|
return self.milliseconds()
|
|
|
|
def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
if response is None:
|
|
return None # fallback to default error handler
|
|
if ('error' in response) and not ('result' in response):
|
|
# error: {
|
|
# "code": "140434",
|
|
# "msg": "Your margin balance is not sufficient to place the order for '5 TON'. Please top up your margin wallet by "2.5 USDT"."
|
|
# }
|
|
#
|
|
errorCode = self.safe_value(response, 'error', {})
|
|
messageError = self.safe_string(errorCode, 'msg')
|
|
code = self.safe_string(errorCode, 'code')
|
|
feedback = self.id + ' ' + body
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], messageError, feedback)
|
|
raise ExchangeError(feedback)
|
|
if ('result' in response) or ('errmsg' in response):
|
|
#
|
|
# {"result":false,"error":"Error 50052: Insufficient funds"}
|
|
# {"s":"error","errmsg":"strconv.ParseInt: parsing \"\": invalid syntax"}
|
|
#
|
|
success = self.safe_bool(response, 'result', False)
|
|
if isinstance(success, str):
|
|
if (success == 'true') or (success == '1'):
|
|
success = True
|
|
else:
|
|
success = False
|
|
if not success:
|
|
code = None
|
|
message = self.safe_string_2(response, 'error', 'errmsg')
|
|
errorParts = message.split(':')
|
|
numParts = len(errorParts)
|
|
if numParts > 1:
|
|
errorSubParts = errorParts[0].split(' ')
|
|
numSubParts = len(errorSubParts)
|
|
code = errorSubParts[1] if (numSubParts > 1) else errorSubParts[0]
|
|
feedback = self.id + ' ' + body
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
|
|
raise ExchangeError(feedback)
|
|
return None
|