1743 lines
74 KiB
Python
1743 lines
74 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.cex import ImplicitAPI
|
|
import asyncio
|
|
import hashlib
|
|
from ccxt.base.types import Account, Any, Balances, Currencies, Currency, DepositAddress, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, TradingFees, Transaction, TransferEntry
|
|
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 NullResponse
|
|
from ccxt.base.decimal_to_precision import TICK_SIZE
|
|
from ccxt.base.precise import Precise
|
|
|
|
|
|
class cex(Exchange, ImplicitAPI):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(cex, self).describe(), {
|
|
'id': 'cex',
|
|
'name': 'CEX.IO',
|
|
'countries': ['GB', 'EU', 'CY', 'RU'],
|
|
'rateLimit': 300, # 200 req/min
|
|
'pro': True,
|
|
'has': {
|
|
'CORS': None,
|
|
'spot': True,
|
|
'margin': False, # has, but not through api
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'addMargin': False,
|
|
'borrowCrossMargin': False,
|
|
'borrowIsolatedMargin': False,
|
|
'borrowMargin': False,
|
|
'cancelAllOrders': True,
|
|
'cancelOrder': True,
|
|
'closeAllPositions': False,
|
|
'closePosition': False,
|
|
'createOrder': True,
|
|
'createOrderWithTakeProfitAndStopLoss': False,
|
|
'createOrderWithTakeProfitAndStopLossWs': False,
|
|
'createPostOnlyOrder': False,
|
|
'createReduceOnlyOrder': False,
|
|
'createStopOrder': True,
|
|
'createTriggerOrder': True,
|
|
'fetchAccounts': True,
|
|
'fetchBalance': True,
|
|
'fetchBorrowInterest': False,
|
|
'fetchBorrowRate': False,
|
|
'fetchBorrowRateHistories': False,
|
|
'fetchBorrowRateHistory': False,
|
|
'fetchBorrowRates': False,
|
|
'fetchBorrowRatesPerSymbol': False,
|
|
'fetchClosedOrder': True,
|
|
'fetchClosedOrders': True,
|
|
'fetchCrossBorrowRate': False,
|
|
'fetchCrossBorrowRates': False,
|
|
'fetchCurrencies': True,
|
|
'fetchDepositAddress': True,
|
|
'fetchDepositsWithdrawals': True,
|
|
'fetchFundingHistory': False,
|
|
'fetchFundingInterval': False,
|
|
'fetchFundingIntervals': False,
|
|
'fetchFundingRate': False,
|
|
'fetchFundingRateHistory': False,
|
|
'fetchFundingRates': False,
|
|
'fetchGreeks': False,
|
|
'fetchIndexOHLCV': False,
|
|
'fetchIsolatedBorrowRate': False,
|
|
'fetchIsolatedBorrowRates': False,
|
|
'fetchIsolatedPositions': False,
|
|
'fetchLedger': True,
|
|
'fetchLeverage': False,
|
|
'fetchLeverages': False,
|
|
'fetchLeverageTiers': False,
|
|
'fetchLiquidations': False,
|
|
'fetchLongShortRatio': False,
|
|
'fetchLongShortRatioHistory': False,
|
|
'fetchMarginAdjustmentHistory': False,
|
|
'fetchMarginMode': False,
|
|
'fetchMarginModes': False,
|
|
'fetchMarketLeverageTiers': False,
|
|
'fetchMarkets': True,
|
|
'fetchMarkOHLCV': False,
|
|
'fetchMarkPrices': False,
|
|
'fetchMyLiquidations': False,
|
|
'fetchMySettlementHistory': False,
|
|
'fetchOHLCV': True,
|
|
'fetchOpenInterest': False,
|
|
'fetchOpenInterestHistory': False,
|
|
'fetchOpenInterests': False,
|
|
'fetchOpenOrder': True,
|
|
'fetchOpenOrders': True,
|
|
'fetchOption': False,
|
|
'fetchOptionChain': False,
|
|
'fetchOrderBook': True,
|
|
'fetchPosition': False,
|
|
'fetchPositionHistory': False,
|
|
'fetchPositionMode': False,
|
|
'fetchPositions': False,
|
|
'fetchPositionsForSymbol': False,
|
|
'fetchPositionsHistory': False,
|
|
'fetchPositionsRisk': False,
|
|
'fetchPremiumIndexOHLCV': False,
|
|
'fetchSettlementHistory': False,
|
|
'fetchTicker': True,
|
|
'fetchTickers': True,
|
|
'fetchTime': True,
|
|
'fetchTrades': True,
|
|
'fetchTradingFees': True,
|
|
'fetchVolatilityHistory': False,
|
|
'reduceMargin': False,
|
|
'repayCrossMargin': False,
|
|
'repayIsolatedMargin': False,
|
|
'repayMargin': False,
|
|
'setLeverage': False,
|
|
'setMargin': False,
|
|
'setMarginMode': False,
|
|
'setPositionMode': False,
|
|
'transfer': True,
|
|
},
|
|
'urls': {
|
|
'logo': 'https://user-images.githubusercontent.com/1294454/27766442-8ddc33b0-5ed8-11e7-8b98-f786aef0f3c9.jpg',
|
|
'api': {
|
|
'public': 'https://trade.cex.io/api/spot/rest-public',
|
|
'private': 'https://trade.cex.io/api/spot/rest',
|
|
},
|
|
'www': 'https://cex.io',
|
|
'doc': 'https://trade.cex.io/docs/',
|
|
'fees': [
|
|
'https://cex.io/fee-schedule',
|
|
'https://cex.io/limits-commissions',
|
|
],
|
|
'referral': 'https://cex.io/r/0/up105393824/0/',
|
|
},
|
|
'api': {
|
|
'public': {
|
|
'get': {},
|
|
'post': {
|
|
'get_server_time': 1,
|
|
'get_pairs_info': 1,
|
|
'get_currencies_info': 1,
|
|
'get_processing_info': 10,
|
|
'get_ticker': 1,
|
|
'get_trade_history': 1,
|
|
'get_order_book': 1,
|
|
'get_candles': 1,
|
|
},
|
|
},
|
|
'private': {
|
|
'get': {},
|
|
'post': {
|
|
'get_my_current_fee': 5,
|
|
'get_fee_strategy': 1,
|
|
'get_my_volume': 5,
|
|
'do_create_account': 1,
|
|
'get_my_account_status_v3': 5,
|
|
'get_my_wallet_balance': 5,
|
|
'get_my_orders': 5,
|
|
'do_my_new_order': 1,
|
|
'do_cancel_my_order': 1,
|
|
'do_cancel_all_orders': 5,
|
|
'get_order_book': 1,
|
|
'get_candles': 1,
|
|
'get_trade_history': 1,
|
|
'get_my_transaction_history': 1,
|
|
'get_my_funding_history': 5,
|
|
'do_my_internal_transfer': 1,
|
|
'get_processing_info': 10,
|
|
'get_deposit_address': 5,
|
|
'do_deposit_funds_from_wallet': 1,
|
|
'do_withdrawal_funds_to_wallet': 1,
|
|
},
|
|
},
|
|
},
|
|
'features': {
|
|
'spot': {
|
|
'sandbox': False,
|
|
'createOrder': {
|
|
'marginMode': False,
|
|
'triggerPrice': True,
|
|
'triggerPriceType': None,
|
|
'triggerDirection': False,
|
|
'stopLossPrice': False, # todo
|
|
'takeProfitPrice': False, # todo
|
|
'attachedStopLossTakeProfit': None,
|
|
'timeInForce': {
|
|
'IOC': True,
|
|
'FOK': True,
|
|
'PO': False, # todo check
|
|
'GTD': True,
|
|
},
|
|
'hedged': False,
|
|
'leverage': False,
|
|
'marketBuyRequiresPrice': False,
|
|
'marketBuyByCost': True, # todo check
|
|
'selfTradePrevention': False,
|
|
'trailing': False,
|
|
'iceberg': False,
|
|
},
|
|
'createOrders': None,
|
|
'fetchMyTrades': None,
|
|
'fetchOrder': None,
|
|
'fetchOpenOrders': {
|
|
'marginMode': False,
|
|
'limit': 1000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOrders': None,
|
|
'fetchClosedOrders': {
|
|
'marginMode': False,
|
|
'limit': 1000,
|
|
'daysBack': 100000,
|
|
'daysBackCanceled': 1,
|
|
'untilDays': 100000,
|
|
'trigger': False,
|
|
'trailing': False,
|
|
'symbolRequired': False,
|
|
},
|
|
'fetchOHLCV': {
|
|
'limit': 1000,
|
|
},
|
|
},
|
|
'swap': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
'future': {
|
|
'linear': None,
|
|
'inverse': None,
|
|
},
|
|
},
|
|
'precisionMode': TICK_SIZE,
|
|
'exceptions': {
|
|
'exact': {},
|
|
'broad': {
|
|
'You have negative balance on following accounts': InsufficientFunds,
|
|
'Mandatory parameter side should be one of BUY,SELL': BadRequest,
|
|
'API orders from Main account are not allowed': BadRequest,
|
|
'check failed': BadRequest,
|
|
'Insufficient funds': InsufficientFunds,
|
|
'Get deposit address for main account is not allowed': PermissionDenied,
|
|
'Market Trigger orders are not allowed': BadRequest, # for some reason, triggerPrice does not work for market orders
|
|
'key not passed or incorrect': AuthenticationError,
|
|
},
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'5m': '5m',
|
|
'15m': '15m',
|
|
'30m': '30m',
|
|
'1h': '1h',
|
|
'2h': '2h',
|
|
'4h': '4h',
|
|
'1d': '1d',
|
|
},
|
|
'options': {
|
|
'networks': {
|
|
'BTC': 'bitcoin',
|
|
'ERC20': 'ERC20',
|
|
'BSC20': 'binancesmartchain',
|
|
'DOGE': 'dogecoin',
|
|
'ALGO': 'algorand',
|
|
'XLM': 'stellar',
|
|
'ATOM': 'cosmos',
|
|
'LTC': 'litecoin',
|
|
'XRP': 'ripple',
|
|
'FTM': 'fantom',
|
|
'MINA': 'mina',
|
|
'THETA': 'theta',
|
|
'XTZ': 'tezos',
|
|
'TIA': 'celestia',
|
|
'CRONOS': 'cronos', # CRC20
|
|
'MATIC': 'polygon',
|
|
'TON': 'ton',
|
|
'TRC20': 'tron',
|
|
'SOLANA': 'solana',
|
|
'SGB': 'songbird',
|
|
'DYDX': 'dydx',
|
|
'DASH': 'dash',
|
|
'ZIL': 'zilliqa',
|
|
'EOS': 'eos',
|
|
'AVALANCHEC': 'avalanche',
|
|
'ETHPOW': 'ethereumpow',
|
|
'NEAR': 'near',
|
|
'ARB': 'arbitrum',
|
|
'DOT': 'polkadot',
|
|
'OPT': 'optimism',
|
|
'INJ': 'injective',
|
|
'ADA': 'cardano',
|
|
'ONT': 'ontology',
|
|
'ICP': 'icp',
|
|
'KAVA': 'kava',
|
|
'KSM': 'kusama',
|
|
'SEI': 'sei',
|
|
# 'OSM': 'osmosis',
|
|
'NEO': 'neo',
|
|
'NEO3': 'neo3',
|
|
# 'TERRAOLD': 'terra', # tbd
|
|
# 'TERRA': 'terra2', # tbd
|
|
# 'EVER': 'everscale', # tbd
|
|
'XDC': 'xdc',
|
|
},
|
|
},
|
|
})
|
|
|
|
async def fetch_currencies(self, params={}) -> Currencies:
|
|
"""
|
|
fetches all available currencies on an exchange
|
|
|
|
https://trade.cex.io/docs/#rest-public-api-calls-currencies-info
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an associative dictionary of currencies
|
|
"""
|
|
promises = []
|
|
promises.append(self.publicPostGetCurrenciesInfo(params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": [
|
|
# {
|
|
# "currency": "ZAP",
|
|
# "fiat": False,
|
|
# "precision": "8",
|
|
# "walletPrecision": "6",
|
|
# "walletDeposit": True,
|
|
# "walletWithdrawal": True
|
|
# },
|
|
# ...
|
|
#
|
|
promises.append(self.publicPostGetProcessingInfo(params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "ADA": {
|
|
# "name": "Cardano",
|
|
# "blockchains": {
|
|
# "cardano": {
|
|
# "type": "coin",
|
|
# "deposit": "enabled",
|
|
# "minDeposit": "1",
|
|
# "withdrawal": "enabled",
|
|
# "minWithdrawal": "5",
|
|
# "withdrawalFee": "1",
|
|
# "withdrawalFeePercent": "0",
|
|
# "depositConfirmations": "15"
|
|
# }
|
|
# }
|
|
# },
|
|
# ...
|
|
#
|
|
responses = await asyncio.gather(*promises)
|
|
dataCurrencies = self.safe_list(responses[0], 'data', [])
|
|
dataNetworks = self.safe_dict(responses[1], 'data', {})
|
|
currenciesIndexed = self.index_by(dataCurrencies, 'currency')
|
|
data = self.deep_extend(currenciesIndexed, dataNetworks)
|
|
return self.parse_currencies(self.to_array(data))
|
|
|
|
def parse_currency(self, rawCurrency: dict) -> Currency:
|
|
id = self.safe_string(rawCurrency, 'currency')
|
|
code = self.safe_currency_code(id)
|
|
type = 'fiat' if self.safe_bool(rawCurrency, 'fiat') else 'crypto'
|
|
currencyPrecision = self.parse_number(self.parse_precision(self.safe_string(rawCurrency, 'precision')))
|
|
networks: dict = {}
|
|
rawNetworks = self.safe_dict(rawCurrency, 'blockchains', {})
|
|
keys = list(rawNetworks.keys())
|
|
for j in range(0, len(keys)):
|
|
networkId = keys[j]
|
|
rawNetwork = rawNetworks[networkId]
|
|
networkCode = self.network_id_to_code(networkId)
|
|
deposit = self.safe_string(rawNetwork, 'deposit') == 'enabled'
|
|
withdraw = self.safe_string(rawNetwork, 'withdrawal') == 'enabled'
|
|
networks[networkCode] = {
|
|
'id': networkId,
|
|
'network': networkCode,
|
|
'margin': None,
|
|
'deposit': deposit,
|
|
'withdraw': withdraw,
|
|
'active': None,
|
|
'fee': self.safe_number(rawNetwork, 'withdrawalFee'),
|
|
'precision': currencyPrecision,
|
|
'limits': {
|
|
'deposit': {
|
|
'min': self.safe_number(rawNetwork, 'minDeposit'),
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': self.safe_number(rawNetwork, 'minWithdrawal'),
|
|
'max': None,
|
|
},
|
|
},
|
|
'info': rawNetwork,
|
|
}
|
|
return self.safe_currency_structure({
|
|
'id': id,
|
|
'code': code,
|
|
'name': None,
|
|
'type': type,
|
|
'active': None,
|
|
'deposit': self.safe_bool(rawCurrency, 'walletDeposit'),
|
|
'withdraw': self.safe_bool(rawCurrency, 'walletWithdrawal'),
|
|
'fee': None,
|
|
'precision': currencyPrecision,
|
|
'limits': {
|
|
'amount': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
'withdraw': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'networks': networks,
|
|
'info': rawCurrency,
|
|
})
|
|
|
|
async def fetch_markets(self, params={}) -> List[Market]:
|
|
"""
|
|
retrieves data on all markets for ace
|
|
|
|
https://trade.cex.io/docs/#rest-public-api-calls-pairs-info
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: an array of objects representing market data
|
|
"""
|
|
response = await self.publicPostGetPairsInfo(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": [
|
|
# {
|
|
# "base": "AI",
|
|
# "quote": "USD",
|
|
# "baseMin": "30",
|
|
# "baseMax": "2516000",
|
|
# "baseLotSize": "0.000001",
|
|
# "quoteMin": "10",
|
|
# "quoteMax": "1000000",
|
|
# "quoteLotSize": "0.01000000",
|
|
# "basePrecision": "6",
|
|
# "quotePrecision": "8",
|
|
# "pricePrecision": "4",
|
|
# "minPrice": "0.0377",
|
|
# "maxPrice": "19.5000"
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_markets(data)
|
|
|
|
def parse_market(self, market: dict) -> Market:
|
|
baseId = self.safe_string(market, 'base')
|
|
base = self.safe_currency_code(baseId)
|
|
quoteId = self.safe_string(market, 'quote')
|
|
quote = self.safe_currency_code(quoteId)
|
|
id = base + '-' + quote # not actual id, but for self exchange we can use self abbreviation, because e.g. tickers have hyphen in between
|
|
symbol = base + '/' + quote
|
|
return self.safe_market_structure({
|
|
'id': id,
|
|
'symbol': symbol,
|
|
'base': base,
|
|
'baseId': baseId,
|
|
'quote': quote,
|
|
'quoteId': quoteId,
|
|
'settle': None,
|
|
'settleId': None,
|
|
'type': 'spot',
|
|
'spot': True,
|
|
'margin': False,
|
|
'swap': False,
|
|
'future': False,
|
|
'option': False,
|
|
'contract': False,
|
|
'linear': None,
|
|
'inverse': None,
|
|
'contractSize': None,
|
|
'expiry': None,
|
|
'expiryDatetime': None,
|
|
'strike': None,
|
|
'optionType': None,
|
|
'limits': {
|
|
'amount': {
|
|
'min': self.safe_number(market, 'baseMin'),
|
|
'max': self.safe_number(market, 'baseMax'),
|
|
},
|
|
'price': {
|
|
'min': self.safe_number(market, 'minPrice'),
|
|
'max': self.safe_number(market, 'maxPrice'),
|
|
},
|
|
'cost': {
|
|
'min': self.safe_number(market, 'quoteMin'),
|
|
'max': self.safe_number(market, 'quoteMax'),
|
|
},
|
|
'leverage': {
|
|
'min': None,
|
|
'max': None,
|
|
},
|
|
},
|
|
'precision': {
|
|
'amount': self.safe_string(market, 'baseLotSize'),
|
|
'price': self.parse_number(self.parse_precision(self.safe_string(market, 'pricePrecision'))),
|
|
# 'cost': self.parse_number(self.parse_precision(self.safe_string(market, 'quoteLotSize'))), # buggy, doesn't reflect their documentation
|
|
'base': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))),
|
|
'quote': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))),
|
|
},
|
|
'active': None,
|
|
'created': None,
|
|
'info': market,
|
|
})
|
|
|
|
async def fetch_time(self, params={}) -> Int:
|
|
"""
|
|
fetches the current integer timestamp in milliseconds from the exchange server
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int: the current integer timestamp in milliseconds from the exchange server
|
|
"""
|
|
response = await self.publicPostGetServerTime(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "timestamp": "1728472063472",
|
|
# "ISODate": "2024-10-09T11:07:43.472Z"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'data')
|
|
timestamp = self.safe_integer(data, 'timestamp')
|
|
return timestamp
|
|
|
|
async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
|
|
|
|
https://trade.cex.io/docs/#rest-public-api-calls-ticker
|
|
|
|
:param str symbol:
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.fetch_tickers([symbol], params)
|
|
return self.safe_dict(response, symbol, {})
|
|
|
|
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://trade.cex.io/docs/#rest-public-api-calls-ticker
|
|
|
|
: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()
|
|
request = {}
|
|
if symbols is not None:
|
|
request['pairs'] = self.market_ids(symbols)
|
|
response = await self.publicPostGetTicker(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "AI-USD": {
|
|
# "bestBid": "0.3917",
|
|
# "bestAsk": "0.3949",
|
|
# "bestBidChange": "0.0035",
|
|
# "bestBidChangePercentage": "0.90",
|
|
# "bestAskChange": "0.0038",
|
|
# "bestAskChangePercentage": "0.97",
|
|
# "low": "0.3787",
|
|
# "high": "0.3925",
|
|
# "volume30d": "2945.722277",
|
|
# "lastTradeDateISO": "2024-10-11T06:18:42.077Z",
|
|
# "volume": "120.736000",
|
|
# "quoteVolume": "46.65654070",
|
|
# "lastTradeVolume": "67.914000",
|
|
# "volumeUSD": "46.65",
|
|
# "last": "0.3949",
|
|
# "lastTradePrice": "0.3925",
|
|
# "priceChange": "0.0038",
|
|
# "priceChangePercentage": "0.97"
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
return self.parse_tickers(data, symbols)
|
|
|
|
def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
|
|
marketId = self.safe_string(ticker, 'id')
|
|
symbol = self.safe_symbol(marketId, market)
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'high': self.safe_number(ticker, 'high'),
|
|
'low': self.safe_number(ticker, 'low'),
|
|
'bid': self.safe_number(ticker, 'bestBid'),
|
|
'bidVolume': None,
|
|
'ask': self.safe_number(ticker, 'bestAsk'),
|
|
'askVolume': None,
|
|
'vwap': None,
|
|
'open': None,
|
|
'close': self.safe_string(ticker, 'last'), # last indicative price per api docs(difference also seen here: https://github.com/ccxt/ccxt/actions/runs/14593899575/job/40935513901?pr=25767#step:11:456 )
|
|
'previousClose': None,
|
|
'change': self.safe_number(ticker, 'priceChange'),
|
|
'percentage': self.safe_number(ticker, 'priceChangePercentage'),
|
|
'average': None,
|
|
'baseVolume': self.safe_string(ticker, 'volume'),
|
|
'quoteVolume': self.safe_string(ticker, 'quoteVolume'),
|
|
'info': ticker,
|
|
}, 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://trade.cex.io/docs/#rest-public-api-calls-trade-history
|
|
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest entry
|
|
:returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
}
|
|
if since is not None:
|
|
request['fromDateISO'] = self.iso8601(since)
|
|
until = None
|
|
until, params = self.handle_param_integer_2(params, 'until', 'till')
|
|
if until is not None:
|
|
request['toDateISO'] = self.iso8601(until)
|
|
if limit is not None:
|
|
request['pageSize'] = min(limit, 10000) # has a bug, still returns more trades
|
|
response = await self.publicPostGetTradeHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "pageSize": "10",
|
|
# "trades": [
|
|
# {
|
|
# "tradeId": "1728630559823-0",
|
|
# "dateISO": "2024-10-11T07:09:19.823Z",
|
|
# "side": "SELL",
|
|
# "price": "60879.5",
|
|
# "amount": "0.00165962"
|
|
# },
|
|
# ... followed by older trades
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
trades = self.safe_list(data, 'trades', [])
|
|
return self.parse_trades(trades, market, since, limit)
|
|
|
|
def parse_trade(self, trade: dict, market: Market = None) -> Trade:
|
|
#
|
|
# public fetchTrades
|
|
#
|
|
# {
|
|
# "tradeId": "1728630559823-0",
|
|
# "dateISO": "2024-10-11T07:09:19.823Z",
|
|
# "side": "SELL",
|
|
# "price": "60879.5",
|
|
# "amount": "0.00165962"
|
|
# },
|
|
#
|
|
dateStr = self.safe_string(trade, 'dateISO')
|
|
timestamp = self.parse8601(dateStr)
|
|
market = self.safe_market(None, market)
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'symbol': market['symbol'],
|
|
'id': self.safe_string(trade, 'tradeId'),
|
|
'order': None,
|
|
'type': None,
|
|
'takerOrMaker': None,
|
|
'side': self.safe_string_lower(trade, 'side'),
|
|
'price': self.safe_string(trade, 'price'),
|
|
'amount': self.safe_string(trade, 'amount'),
|
|
'cost': None,
|
|
'fee': None,
|
|
}, market)
|
|
|
|
async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://trade.cex.io/docs/#rest-public-api-calls-order-book
|
|
|
|
: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'],
|
|
}
|
|
response = await self.publicPostGetOrderBook(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "timestamp": "1728636922648",
|
|
# "currency1": "BTC",
|
|
# "currency2": "USDT",
|
|
# "bids": [
|
|
# [
|
|
# "60694.1",
|
|
# "13.12849761"
|
|
# ],
|
|
# [
|
|
# "60694.0",
|
|
# "0.71829244"
|
|
# ],
|
|
# ...
|
|
#
|
|
orderBook = self.safe_dict(response, 'data', {})
|
|
timestamp = self.safe_integer(orderBook, 'timestamp')
|
|
return self.parse_order_book(orderBook, market['symbol'], timestamp)
|
|
|
|
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://trade.cex.io/docs/#rest-public-api-calls-candles
|
|
|
|
: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 entry
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
dataType = None
|
|
dataType, params = self.handle_option_and_params(params, 'fetchOHLCV', 'dataType')
|
|
if dataType is None:
|
|
raise ArgumentsRequired(self.id + ' fetchOHLCV requires a parameter "dataType" to be either "bestBid" or "bestAsk"')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'pair': market['id'],
|
|
'resolution': self.timeframes[timeframe],
|
|
'dataType': dataType,
|
|
}
|
|
if since is not None:
|
|
request['fromISO'] = self.iso8601(since)
|
|
until = None
|
|
until, params = self.handle_param_integer_2(params, 'until', 'till')
|
|
if until is not None:
|
|
request['toISO'] = self.iso8601(until)
|
|
elif since is None:
|
|
# exchange still requires that we provide one of them
|
|
request['toISO'] = self.iso8601(self.milliseconds())
|
|
if since is not None and until is not None and limit is not None:
|
|
raise ArgumentsRequired(self.id + ' fetchOHLCV does not support fetching candles with both a limit and since/until')
|
|
elif (since is not None or until is not None) and limit is None:
|
|
raise ArgumentsRequired(self.id + ' fetchOHLCV requires a limit parameter when fetching candles with since or until')
|
|
if limit is not None:
|
|
request['limit'] = limit
|
|
response = await self.publicPostGetCandles(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": [
|
|
# {
|
|
# "timestamp": "1728643320000",
|
|
# "open": "61061",
|
|
# "high": "61095.1",
|
|
# "low": "61048.5",
|
|
# "close": "61087.8",
|
|
# "volume": "0",
|
|
# "resolution": "1m",
|
|
# "isClosed": True,
|
|
# "timestampISO": "2024-10-11T10:42:00.000Z"
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_ohlcvs(data, market, timeframe, since, limit)
|
|
|
|
def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
|
|
return [
|
|
self.safe_integer(ohlcv, 'timestamp'),
|
|
self.safe_number(ohlcv, 'open'),
|
|
self.safe_number(ohlcv, 'high'),
|
|
self.safe_number(ohlcv, 'low'),
|
|
self.safe_number(ohlcv, 'close'),
|
|
self.safe_number(ohlcv, 'volume'),
|
|
]
|
|
|
|
async def fetch_trading_fees(self, params={}) -> TradingFees:
|
|
"""
|
|
fetch the trading fees for multiple markets
|
|
|
|
https://trade.cex.io/docs/#rest-public-api-calls-candles
|
|
|
|
: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
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.privatePostGetMyCurrentFee(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "tradingFee": {
|
|
# "AI-USD": {
|
|
# "percent": "0.25"
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
fees = self.safe_dict(data, 'tradingFee', {})
|
|
return self.parse_trading_fees(fees, True)
|
|
|
|
def parse_trading_fees(self, response, useKeyAsId=False) -> TradingFees:
|
|
result: dict = {}
|
|
keys = list(response.keys())
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
market = None
|
|
if useKeyAsId:
|
|
market = self.safe_market(key)
|
|
parsed = self.parse_trading_fee(response[key], market)
|
|
result[parsed['symbol']] = parsed
|
|
for i in range(0, len(self.symbols)):
|
|
symbol = self.symbols[i]
|
|
if not (symbol in result):
|
|
market = self.market(symbol)
|
|
result[symbol] = self.parse_trading_fee(response, market)
|
|
return result
|
|
|
|
def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface:
|
|
return {
|
|
'info': fee,
|
|
'symbol': self.safe_string(market, 'symbol'),
|
|
'maker': self.safe_number(fee, 'percent'),
|
|
'taker': self.safe_number(fee, 'percent'),
|
|
'percentage': None,
|
|
'tierBased': None,
|
|
}
|
|
|
|
async def fetch_accounts(self, params={}) -> List[Account]:
|
|
await self.load_markets()
|
|
response = await self.privatePostGetMyAccountStatusV3(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "convertedCurrency": "USD",
|
|
# "balancesPerAccounts": {
|
|
# "": {
|
|
# "AI": {
|
|
# "balance": "0.000000",
|
|
# "balanceOnHold": "0.000000"
|
|
# },
|
|
# "USDT": {
|
|
# "balance": "0.00000000",
|
|
# "balanceOnHold": "0.00000000"
|
|
# }
|
|
# }
|
|
# }
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
balances = self.safe_dict(data, 'balancesPerAccounts', {})
|
|
arrays = self.to_array(balances)
|
|
return self.parse_accounts(arrays, params)
|
|
|
|
def parse_account(self, account: dict) -> Account:
|
|
return {
|
|
'id': None,
|
|
'type': None,
|
|
'code': None,
|
|
'info': account,
|
|
}
|
|
|
|
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://trade.cex.io/docs/#rest-private-api-calls-account-status-v3
|
|
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param dict [params.method]: 'privatePostGetMyWalletBalance' or 'privatePostGetMyAccountStatusV3'
|
|
:param dict [params.account]: in case 'privatePostGetMyAccountStatusV3' is chosen, self can specify the account name(default is empty string)
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
accountName = None
|
|
accountName, params = self.handle_param_string(params, 'account', '') # default is empty string
|
|
method = None
|
|
method, params = self.handle_param_string(params, 'method', 'privatePostGetMyWalletBalance')
|
|
accountBalance = None
|
|
if method == 'privatePostGetMyAccountStatusV3':
|
|
response = await self.privatePostGetMyAccountStatusV3(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "convertedCurrency": "USD",
|
|
# "balancesPerAccounts": {
|
|
# "": {
|
|
# "AI": {
|
|
# "balance": "0.000000",
|
|
# "balanceOnHold": "0.000000"
|
|
# },
|
|
# ....
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
balances = self.safe_dict(data, 'balancesPerAccounts', {})
|
|
accountBalance = self.safe_dict(balances, accountName, {})
|
|
else:
|
|
response = await self.privatePostGetMyWalletBalance(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "AI": {
|
|
# "balance": "25.606429"
|
|
# },
|
|
# "USDT": {
|
|
# "balance": "7.935449"
|
|
# },
|
|
# ...
|
|
#
|
|
accountBalance = self.safe_dict(response, 'data', {})
|
|
return self.parse_balance(accountBalance)
|
|
|
|
def parse_balance(self, response) -> Balances:
|
|
result: dict = {
|
|
'info': response,
|
|
}
|
|
keys = list(response.keys())
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
balance = self.safe_dict(response, key, {})
|
|
code = self.safe_currency_code(key)
|
|
account: dict = {
|
|
'used': self.safe_string(balance, 'balanceOnHold'),
|
|
'total': self.safe_string(balance, 'balance'),
|
|
}
|
|
result[code] = account
|
|
return self.safe_balance(result)
|
|
|
|
async def fetch_orders_by_status(self, status: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
fetches information on multiple orders made by the user
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-orders
|
|
|
|
:param str status: order status to fetch for
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest entry
|
|
:returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {}
|
|
isClosedOrders = (status == 'closed')
|
|
if isClosedOrders:
|
|
request['archived'] = True
|
|
market = None
|
|
if symbol is not None:
|
|
market = self.market(symbol)
|
|
request['pair'] = market['id']
|
|
if limit is not None:
|
|
request['pageSize'] = limit
|
|
if since is not None:
|
|
request['serverCreateTimestampFrom'] = since
|
|
elif isClosedOrders:
|
|
# exchange requires a `since` parameter for closed orders, so set default to allowed 365
|
|
request['serverCreateTimestampFrom'] = self.milliseconds() - 364 * 24 * 60 * 60 * 1000
|
|
until = None
|
|
until, params = self.handle_param_integer_2(params, 'until', 'till')
|
|
if until is not None:
|
|
request['serverCreateTimestampTo'] = until
|
|
response = await self.privatePostGetMyOrders(self.extend(request, params))
|
|
#
|
|
# if called without `pair`
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": [
|
|
# {
|
|
# "orderId": "1313003",
|
|
# "clientOrderId": "037F0AFEB93A",
|
|
# "clientId": "up421412345",
|
|
# "accountId": null,
|
|
# "status": "FILLED",
|
|
# "statusIsFinal": True,
|
|
# "currency1": "AI",
|
|
# "currency2": "USDT",
|
|
# "side": "BUY",
|
|
# "orderType": "Market",
|
|
# "timeInForce": "IOC",
|
|
# "comment": null,
|
|
# "rejectCode": null,
|
|
# "rejectReason": null,
|
|
# "initialOnHoldAmountCcy1": null,
|
|
# "initialOnHoldAmountCcy2": "10.23456700",
|
|
# "executedAmountCcy1": "25.606429",
|
|
# "executedAmountCcy2": "10.20904439",
|
|
# "requestedAmountCcy1": null,
|
|
# "requestedAmountCcy2": "10.20904439",
|
|
# "originalAmountCcy2": "10.23456700",
|
|
# "feeAmount": "0.02552261",
|
|
# "feeCurrency": "USDT",
|
|
# "price": null,
|
|
# "averagePrice": "0.3986",
|
|
# "clientCreateTimestamp": "1728474625320",
|
|
# "serverCreateTimestamp": "1728474624956",
|
|
# "lastUpdateTimestamp": "1728474628015",
|
|
# "expireTime": null,
|
|
# "effectiveTime": null
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_orders(data, market, since, limit)
|
|
|
|
async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-orders
|
|
|
|
fetches information on multiple canceled orders made by the user
|
|
: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
|
|
:returns dict: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
return await self.fetch_orders_by_status('closed', symbol, since, limit, params)
|
|
|
|
async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
|
|
"""
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-orders
|
|
|
|
fetches information on multiple canceled orders made by the user
|
|
: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
|
|
:returns dict: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
return await self.fetch_orders_by_status('open', symbol, since, limit, params)
|
|
|
|
async def fetch_open_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an open order made by the user
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-orders
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'orderId': int(id),
|
|
}
|
|
result = await self.fetch_open_orders(symbol, None, None, self.extend(request, params))
|
|
return result[0]
|
|
|
|
async def fetch_closed_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
fetches information on an closed order made by the user
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-orders
|
|
|
|
:param str id: order id
|
|
:param str [symbol]: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'orderId': int(id),
|
|
}
|
|
result = await self.fetch_closed_orders(symbol, None, None, self.extend(request, params))
|
|
return result[0]
|
|
|
|
def parse_order_status(self, status: Str):
|
|
statuses: dict = {
|
|
'PENDING_NEW': 'open',
|
|
'NEW': 'open',
|
|
'PARTIALLY_FILLED': 'open',
|
|
'FILLED': 'closed',
|
|
'EXPIRED': 'expired',
|
|
'REJECTED': 'rejected',
|
|
'PENDING_CANCEL': 'canceling',
|
|
'CANCELLED': 'canceled',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
def parse_order(self, order: dict, market: Market = None) -> Order:
|
|
#
|
|
# "orderId": "1313003",
|
|
# "clientOrderId": "037F0AFEB93A",
|
|
# "clientId": "up421412345",
|
|
# "accountId": null,
|
|
# "status": "FILLED",
|
|
# "statusIsFinal": True,
|
|
# "currency1": "AI",
|
|
# "currency2": "USDT",
|
|
# "side": "BUY",
|
|
# "orderType": "Market",
|
|
# "timeInForce": "IOC",
|
|
# "comment": null,
|
|
# "rejectCode": null,
|
|
# "rejectReason": null,
|
|
# "initialOnHoldAmountCcy1": null,
|
|
# "initialOnHoldAmountCcy2": "10.23456700",
|
|
# "executedAmountCcy1": "25.606429",
|
|
# "executedAmountCcy2": "10.20904439",
|
|
# "requestedAmountCcy1": null,
|
|
# "requestedAmountCcy2": "10.20904439",
|
|
# "originalAmountCcy2": "10.23456700",
|
|
# "feeAmount": "0.02552261",
|
|
# "feeCurrency": "USDT",
|
|
# "price": null,
|
|
# "averagePrice": "0.3986",
|
|
# "clientCreateTimestamp": "1728474625320",
|
|
# "serverCreateTimestamp": "1728474624956",
|
|
# "lastUpdateTimestamp": "1728474628015",
|
|
# "expireTime": null,
|
|
# "effectiveTime": null
|
|
#
|
|
currency1 = self.safe_string(order, 'currency1')
|
|
currency2 = self.safe_string(order, 'currency2')
|
|
marketId = None
|
|
if currency1 is not None and currency2 is not None:
|
|
marketId = currency1 + '-' + currency2
|
|
market = self.safe_market(marketId, market)
|
|
symbol = market['symbol']
|
|
status = self.parse_order_status(self.safe_string(order, 'status'))
|
|
fee = {}
|
|
feeAmount = self.safe_number(order, 'feeAmount')
|
|
if feeAmount is not None:
|
|
currencyId = self.safe_string(order, 'feeCurrency')
|
|
feeCode = self.safe_currency_code(currencyId)
|
|
fee['currency'] = feeCode
|
|
fee['cost'] = feeAmount
|
|
timestamp = self.safe_integer(order, 'serverCreateTimestamp')
|
|
requestedBase = self.safe_number(order, 'requestedAmountCcy1')
|
|
executedBase = self.safe_number(order, 'executedAmountCcy1')
|
|
# requestedQuote = self.safe_number(order, 'requestedAmountCcy2')
|
|
executedQuote = self.safe_number(order, 'executedAmountCcy2')
|
|
return self.safe_order({
|
|
'id': self.safe_string(order, 'orderId'),
|
|
'clientOrderId': self.safe_string(order, 'clientOrderId'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastUpdateTimestamp': self.safe_integer(order, 'lastUpdateTimestamp'),
|
|
'lastTradeTimestamp': None,
|
|
'symbol': symbol,
|
|
'type': self.safe_string_lower(order, 'orderType'),
|
|
'timeInForce': self.safe_string(order, 'timeInForce'),
|
|
'postOnly': None,
|
|
'side': self.safe_string_lower(order, 'side'),
|
|
'price': self.safe_number(order, 'price'),
|
|
'triggerPrice': self.safe_number(order, 'stopPrice'),
|
|
'amount': requestedBase,
|
|
'cost': executedQuote,
|
|
'average': self.safe_number(order, 'averagePrice'),
|
|
'filled': executedBase,
|
|
'remaining': None,
|
|
'status': status,
|
|
'fee': fee,
|
|
'trades': None,
|
|
'info': order,
|
|
}, market)
|
|
|
|
async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
|
|
"""
|
|
create a trade order
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-new-order
|
|
|
|
: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 str [params.accountId]: account-id to use(default is empty string)
|
|
:param float [params.triggerPrice]: the price at which a trigger order is triggered at
|
|
:returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
accountId = None
|
|
accountId, params = self.handle_option_and_params(params, 'createOrder', 'accountId')
|
|
if accountId is None:
|
|
raise ArgumentsRequired(self.id + ' createOrder() : API trading is now allowed from main account, set params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account')
|
|
await self.load_markets()
|
|
market = self.market(symbol)
|
|
request: dict = {
|
|
'clientOrderId': self.uuid(),
|
|
'currency1': market['baseId'],
|
|
'currency2': market['quoteId'],
|
|
'accountId': accountId,
|
|
'orderType': self.capitalize(type.lower()),
|
|
'side': side.upper(),
|
|
'timestamp': self.milliseconds(),
|
|
'amountCcy1': self.amount_to_precision(symbol, amount),
|
|
}
|
|
timeInForce = None
|
|
timeInForce, params = self.handle_option_and_params(params, 'createOrder', 'timeInForce', 'GTC')
|
|
if type == 'limit':
|
|
request['price'] = self.price_to_precision(symbol, price)
|
|
request['timeInForce'] = timeInForce
|
|
triggerPrice = None
|
|
triggerPrice, params = self.handle_param_string(params, 'triggerPrice')
|
|
if triggerPrice is not None:
|
|
request['type'] = 'Stop Limit'
|
|
request['stopPrice'] = triggerPrice
|
|
response = await self.privatePostDoMyNewOrder(self.extend(request, params))
|
|
#
|
|
# on success
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "messageType": "executionReport",
|
|
# "clientId": "up132245425",
|
|
# "orderId": "1318485",
|
|
# "clientOrderId": "b5b6cd40-154c-4c1c-bd51-4a442f3d50b9",
|
|
# "accountId": "sub1",
|
|
# "status": "FILLED",
|
|
# "currency1": "LTC",
|
|
# "currency2": "USDT",
|
|
# "side": "BUY",
|
|
# "executedAmountCcy1": "0.23000000",
|
|
# "executedAmountCcy2": "15.09030000",
|
|
# "requestedAmountCcy1": "0.23000000",
|
|
# "requestedAmountCcy2": null,
|
|
# "orderType": "Market",
|
|
# "timeInForce": null,
|
|
# "comment": null,
|
|
# "executionType": "Trade",
|
|
# "executionId": "1726747124624_101_41116",
|
|
# "transactTime": "2024-10-15T15:08:12.794Z",
|
|
# "expireTime": null,
|
|
# "effectiveTime": null,
|
|
# "averagePrice": "65.61",
|
|
# "lastQuantity": "0.23000000",
|
|
# "lastAmountCcy1": "0.23000000",
|
|
# "lastAmountCcy2": "15.09030000",
|
|
# "lastPrice": "65.61",
|
|
# "feeAmount": "0.03772575",
|
|
# "feeCurrency": "USDT",
|
|
# "clientCreateTimestamp": "1729004892014",
|
|
# "serverCreateTimestamp": "1729004891628",
|
|
# "lastUpdateTimestamp": "1729004892786"
|
|
# }
|
|
# }
|
|
#
|
|
# on failure, there are extra fields
|
|
#
|
|
# "status": "REJECTED",
|
|
# "requestedAmountCcy1": null,
|
|
# "orderRejectReason": "{\\" code \\ ":405,\\" reason \\ ":\\" Either AmountCcy1(OrderQty)or AmountCcy2(CashOrderQty)should be specified for market order not both \\ "}",
|
|
# "rejectCode": 405,
|
|
# "rejectReason": "Either AmountCcy1(OrderQty) or AmountCcy2(CashOrderQty) should be specified for market order not both",
|
|
#
|
|
data = self.safe_dict(response, 'data')
|
|
return self.parse_order(data, market)
|
|
|
|
async def cancel_order(self, id: str, symbol: Str = None, params={}):
|
|
"""
|
|
cancels an open order
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-cancel-order
|
|
|
|
:param str id: order id
|
|
:param str symbol: unified symbol of the market the order was made in
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
request: dict = {
|
|
'orderId': int(id),
|
|
'cancelRequestId': 'c_' + str((self.milliseconds())),
|
|
'timestamp': self.milliseconds(),
|
|
}
|
|
response = await self.privatePostDoCancelMyOrder(self.extend(request, params))
|
|
#
|
|
# {"ok":"ok","data":{}}
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
return self.parse_order(data)
|
|
|
|
async def cancel_all_orders(self, symbol: Str = None, params={}):
|
|
"""
|
|
cancel all open orders in a market
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-cancel-all-orders
|
|
|
|
:param str symbol: alpaca cancelAllOrders cannot setting symbol, it will cancel all open orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
response = await self.privatePostDoCancelAllOrders(params)
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "clientOrderIds": [
|
|
# "3AF77B67109F"
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
ids = self.safe_list(data, 'clientOrderIds', [])
|
|
orders = []
|
|
for i in range(0, len(ids)):
|
|
id = ids[i]
|
|
orders.append({'clientOrderId': id})
|
|
return self.parse_orders(orders)
|
|
|
|
async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
|
|
"""
|
|
fetch the history of changes, actions done by the user or operations that altered the balance of the user
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-transaction-history
|
|
|
|
:param str [code]: unified currency code
|
|
:param int [since]: timestamp in ms of the earliest ledger entry
|
|
:param int [limit]: max number of ledger entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param int [params.until]: timestamp in ms of the latest ledger entry
|
|
:returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger>`
|
|
"""
|
|
await self.load_markets()
|
|
currency = None
|
|
request: dict = {}
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
request['currency'] = currency['id']
|
|
if since is not None:
|
|
request['dateFrom'] = since
|
|
if limit is not None:
|
|
request['pageSize'] = limit
|
|
until = None
|
|
until, params = self.handle_param_integer_2(params, 'until', 'till')
|
|
if until is not None:
|
|
request['dateTo'] = until
|
|
response = await self.privatePostGetMyTransactionHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": [
|
|
# {
|
|
# "transactionId": "30367722",
|
|
# "timestamp": "2024-10-14T14:08:49.987Z",
|
|
# "accountId": "",
|
|
# "type": "withdraw",
|
|
# "amount": "-12.39060600",
|
|
# "details": "Withdraw fundingId=1235039 clientId=up421412345 walletTxId=76337154166",
|
|
# "currency": "USDT"
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_ledger(data, currency, since, limit)
|
|
|
|
def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
|
|
amount = self.safe_string(item, 'amount')
|
|
direction = None
|
|
if Precise.string_le(amount, '0'):
|
|
direction = 'out'
|
|
amount = Precise.string_mul('-1', amount)
|
|
else:
|
|
direction = 'in'
|
|
currencyId = self.safe_string(item, 'currency')
|
|
currency = self.safe_currency(currencyId, currency)
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
timestampString = self.safe_string(item, 'timestamp')
|
|
timestamp = self.parse8601(timestampString)
|
|
type = self.safe_string(item, 'type')
|
|
return self.safe_ledger_entry({
|
|
'info': item,
|
|
'id': self.safe_string(item, 'transactionId'),
|
|
'direction': direction,
|
|
'account': self.safe_string(item, 'accountId', ''),
|
|
'referenceAccount': None,
|
|
'referenceId': None,
|
|
'type': self.parse_ledger_entry_type(type),
|
|
'currency': code,
|
|
'amount': self.parse_number(amount),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'before': None,
|
|
'after': None,
|
|
'status': None,
|
|
'fee': None,
|
|
}, currency)
|
|
|
|
def parse_ledger_entry_type(self, type):
|
|
ledgerType: dict = {
|
|
'deposit': 'deposit',
|
|
'withdraw': 'withdrawal',
|
|
'commission': 'fee',
|
|
}
|
|
return self.safe_string(ledgerType, type, type)
|
|
|
|
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://trade.cex.io/docs/#rest-private-api-calls-funding-history
|
|
|
|
: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 = {}
|
|
currency = None
|
|
if code is not None:
|
|
currency = self.currency(code)
|
|
if since is not None:
|
|
request['dateFrom'] = since
|
|
if limit is not None:
|
|
request['pageSize'] = limit
|
|
until = None
|
|
until, params = self.handle_param_integer_2(params, 'until', 'till')
|
|
if until is not None:
|
|
request['dateTo'] = until
|
|
response = await self.privatePostGetMyFundingHistory(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": [
|
|
# {
|
|
# "clientId": "up421412345",
|
|
# "accountId": "",
|
|
# "currency": "USDT",
|
|
# "direction": "withdraw",
|
|
# "amount": "12.39060600",
|
|
# "commissionAmount": "0.00000000",
|
|
# "status": "approved",
|
|
# "updatedAt": "2024-10-14T14:08:50.013Z",
|
|
# "txId": "30367718",
|
|
# "details": {}
|
|
# },
|
|
# ...
|
|
#
|
|
data = self.safe_list(response, 'data', [])
|
|
return self.parse_transactions(data, currency, since, limit)
|
|
|
|
def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
|
|
currencyId = self.safe_string(transaction, 'currency')
|
|
direction = self.safe_string(transaction, 'direction')
|
|
type = 'withdrawal' if (direction == 'withdraw') else 'deposit'
|
|
code = self.safe_currency_code(currencyId, currency)
|
|
updatedAt = self.safe_string(transaction, 'updatedAt')
|
|
timestamp = self.parse8601(updatedAt)
|
|
return {
|
|
'info': transaction,
|
|
'id': self.safe_string(transaction, 'txId'),
|
|
'txid': None,
|
|
'type': type,
|
|
'currency': code,
|
|
'network': None,
|
|
'amount': self.safe_number(transaction, 'amount'),
|
|
'status': self.parse_transaction_status(self.safe_string(transaction, 'status')),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'address': None,
|
|
'addressFrom': None,
|
|
'addressTo': None,
|
|
'tag': None,
|
|
'tagFrom': None,
|
|
'tagTo': None,
|
|
'updated': None,
|
|
'comment': None,
|
|
'fee': {
|
|
'currency': code,
|
|
'cost': self.safe_number(transaction, 'commissionAmount'),
|
|
},
|
|
'internal': None,
|
|
}
|
|
|
|
def parse_transaction_status(self, status: Str):
|
|
statuses: dict = {
|
|
'rejected': 'rejected',
|
|
'pending': 'pending',
|
|
'approved': 'ok',
|
|
}
|
|
return self.safe_string(statuses, status, status)
|
|
|
|
async def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
"""
|
|
transfer currency internally between wallets on the same account
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-internal-transfer
|
|
|
|
:param str code: unified currency code
|
|
:param float amount: amount to transfer
|
|
:param str fromAccount: 'SPOT', 'FUND', or 'CONTRACT'
|
|
:param str toAccount: 'SPOT', 'FUND', or 'CONTRACT'
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `transfer structure <https://docs.ccxt.com/#/?id=transfer-structure>`
|
|
"""
|
|
transfer = None
|
|
if toAccount != '' and fromAccount != '':
|
|
transfer = await self.transfer_between_sub_accounts(code, amount, fromAccount, toAccount, params)
|
|
else:
|
|
transfer = await self.transfer_between_main_and_sub_account(code, amount, fromAccount, toAccount, params)
|
|
fillResponseFromRequest = self.handle_option('transfer', 'fillResponseFromRequest', True)
|
|
if fillResponseFromRequest:
|
|
transfer['fromAccount'] = fromAccount
|
|
transfer['toAccount'] = toAccount
|
|
return transfer
|
|
|
|
async def transfer_between_main_and_sub_account(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
fromMain = (fromAccount == '')
|
|
targetAccount = toAccount if fromMain else fromAccount
|
|
guid = self.safe_string(params, 'guid', self.uuid())
|
|
request: dict = {
|
|
'currency': currency['id'],
|
|
'amount': self.currency_to_precision(code, amount),
|
|
'accountId': targetAccount,
|
|
'clientTxId': guid,
|
|
}
|
|
response = None
|
|
if fromMain:
|
|
response = await self.privatePostDoDepositFundsFromWallet(self.extend(request, params))
|
|
else:
|
|
response = await self.privatePostDoWithdrawalFundsToWallet(self.extend(request, params))
|
|
# both endpoints return the same structure, the only difference is that
|
|
# the "accountId" is filled with the "subAccount"
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "accountId": "sub1",
|
|
# "clientTxId": "27ba8284-67cf-4386-9ec7-80b3871abd45",
|
|
# "currency": "USDT",
|
|
# "status": "approved"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
return self.parse_transfer(data, currency)
|
|
|
|
async def transfer_between_sub_accounts(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
|
|
await self.load_markets()
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'currency': currency['id'],
|
|
'amount': self.currency_to_precision(code, amount),
|
|
'fromAccountId': fromAccount,
|
|
'toAccountId': toAccount,
|
|
}
|
|
response = await self.privatePostDoMyInternalTransfer(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "transactionId": "30225415"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
return self.parse_transfer(data, currency)
|
|
|
|
def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
|
|
#
|
|
# transferBetweenSubAccounts
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "transactionId": "30225415"
|
|
# }
|
|
# }
|
|
#
|
|
# transfer between main/sub
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "accountId": "sub1",
|
|
# "clientTxId": "27ba8284-67cf-4386-9ec7-80b3871abd45",
|
|
# "currency": "USDT",
|
|
# "status": "approved"
|
|
# }
|
|
# }
|
|
#
|
|
currencyId = self.safe_string(transfer, 'currency')
|
|
currencyCode = self.safe_currency_code(currencyId, currency)
|
|
return {
|
|
'info': transfer,
|
|
'id': self.safe_string_2(transfer, 'transactionId', 'clientTxId'),
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'currency': currencyCode,
|
|
'amount': None,
|
|
'fromAccount': None,
|
|
'toAccount': None,
|
|
'status': self.parse_transaction_status(self.safe_string(transfer, 'status')),
|
|
}
|
|
|
|
async def fetch_deposit_address(self, code: str, params={}) -> DepositAddress:
|
|
"""
|
|
fetch the deposit address for a currency associated with self account
|
|
|
|
https://trade.cex.io/docs/#rest-private-api-calls-deposit-address
|
|
|
|
:param str code: unified currency code
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.accountId]: account-id(default to empty string) to refer to(at self moment, only sub-accounts allowed by exchange)
|
|
:returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
|
|
"""
|
|
accountId = None
|
|
accountId, params = self.handle_option_and_params(params, 'createOrder', 'accountId')
|
|
if accountId is None:
|
|
raise ArgumentsRequired(self.id + ' fetchDepositAddress() : main account is not allowed to fetch deposit address from api, set params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account')
|
|
await self.load_markets()
|
|
networkCode = None
|
|
networkCode, params = self.handle_network_code_and_params(params)
|
|
currency = self.currency(code)
|
|
request: dict = {
|
|
'accountId': accountId,
|
|
'currency': currency['id'], # documentation is wrong about self param
|
|
'blockchain': self.network_code_to_id(networkCode),
|
|
}
|
|
response = await self.privatePostGetDepositAddress(self.extend(request, params))
|
|
#
|
|
# {
|
|
# "ok": "ok",
|
|
# "data": {
|
|
# "address": "TCr..................1AE",
|
|
# "accountId": "sub1",
|
|
# "currency": "USDT",
|
|
# "blockchain": "tron"
|
|
# }
|
|
# }
|
|
#
|
|
data = self.safe_dict(response, 'data', {})
|
|
return self.parse_deposit_address(data, currency)
|
|
|
|
def parse_deposit_address(self, depositAddress, currency: Currency = None) -> DepositAddress:
|
|
address = self.safe_string(depositAddress, 'address')
|
|
currencyId = self.safe_string(depositAddress, 'currency')
|
|
currency = self.safe_currency(currencyId, currency)
|
|
self.check_address(address)
|
|
return {
|
|
'info': depositAddress,
|
|
'currency': currency['code'],
|
|
'network': self.network_id_to_code(self.safe_string(depositAddress, 'blockchain')),
|
|
'address': address,
|
|
'tag': None,
|
|
}
|
|
|
|
def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
|
|
url = self.urls['api'][api] + '/' + self.implode_params(path, params)
|
|
query = self.omit(params, self.extract_params(path))
|
|
if api == 'public':
|
|
if method == 'GET':
|
|
if query:
|
|
url += '?' + self.urlencode(query)
|
|
else:
|
|
body = self.json(query)
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
else:
|
|
self.check_required_credentials()
|
|
seconds = str(self.seconds())
|
|
body = self.json(query)
|
|
auth = path + seconds + body
|
|
signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64')
|
|
headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-AGGR-KEY': self.apiKey,
|
|
'X-AGGR-TIMESTAMP': seconds,
|
|
'X-AGGR-SIGNATURE': signature,
|
|
}
|
|
return {'url': url, 'method': method, 'body': body, 'headers': headers}
|
|
|
|
def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
|
|
# in some cases, like from createOrder, exchange returns nested escaped JSON string:
|
|
# {"ok":"ok","data":{"messageType":"executionReport", "orderRejectReason":"{\"code\":405}"}}
|
|
# and because of `.parseJson` bug, we need extra fix
|
|
if response is None:
|
|
if body is None:
|
|
raise NullResponse(self.id + ' returned empty response')
|
|
elif body[0] == '{':
|
|
fixed = self.fix_stringified_json_members(body)
|
|
response = self.parse_json(fixed)
|
|
else:
|
|
raise NullResponse(self.id + ' returned unparsed response: ' + body)
|
|
error = self.safe_string(response, 'error')
|
|
if error is not None:
|
|
feedback = self.id + ' ' + body
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback)
|
|
raise ExchangeError(feedback)
|
|
# check errors in order-engine(the responses are not standard, so we parse here)
|
|
if url.find('do_my_new_order') >= 0:
|
|
data = self.safe_dict(response, 'data', {})
|
|
rejectReason = self.safe_string(data, 'rejectReason')
|
|
if rejectReason is not None:
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], rejectReason, rejectReason)
|
|
raise ExchangeError(self.id + ' createOrder() ' + rejectReason)
|
|
return None
|