/python/ccxt/kucoin.py

https://github.com/kroitor/ccxt · Python · 1729 lines · 1046 code · 43 blank · 640 comment · 130 complexity · 8baba9fe9caddce84265d1f560f020f6 MD5 · raw file

  1. # -*- coding: utf-8 -*-
  2. # PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
  3. # https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
  4. from ccxt.base.exchange import Exchange
  5. import hashlib
  6. import math
  7. from ccxt.base.errors import ExchangeError
  8. from ccxt.base.errors import AuthenticationError
  9. from ccxt.base.errors import PermissionDenied
  10. from ccxt.base.errors import AccountSuspended
  11. from ccxt.base.errors import ArgumentsRequired
  12. from ccxt.base.errors import BadRequest
  13. from ccxt.base.errors import BadSymbol
  14. from ccxt.base.errors import InsufficientFunds
  15. from ccxt.base.errors import InvalidOrder
  16. from ccxt.base.errors import OrderNotFound
  17. from ccxt.base.errors import NotSupported
  18. from ccxt.base.errors import RateLimitExceeded
  19. from ccxt.base.errors import ExchangeNotAvailable
  20. from ccxt.base.errors import InvalidNonce
  21. class kucoin(Exchange):
  22. def describe(self):
  23. return self.deep_extend(super(kucoin, self).describe(), {
  24. 'id': 'kucoin',
  25. 'name': 'KuCoin',
  26. 'countries': ['SC'],
  27. 'rateLimit': 334,
  28. 'version': 'v2',
  29. 'certified': False,
  30. 'pro': True,
  31. 'comment': 'Platform 2.0',
  32. 'has': {
  33. 'CORS': False,
  34. 'fetchStatus': True,
  35. 'fetchTime': True,
  36. 'fetchMarkets': True,
  37. 'fetchCurrencies': True,
  38. 'fetchTicker': True,
  39. 'fetchTickers': True,
  40. 'fetchOrderBook': True,
  41. 'fetchOrder': True,
  42. 'fetchClosedOrders': True,
  43. 'fetchOpenOrders': True,
  44. 'fetchDepositAddress': True,
  45. 'createDepositAddress': True,
  46. 'withdraw': True,
  47. 'fetchDeposits': True,
  48. 'fetchWithdrawals': True,
  49. 'fetchBalance': True,
  50. 'fetchTrades': True,
  51. 'fetchMyTrades': True,
  52. 'createOrder': True,
  53. 'cancelOrder': True,
  54. 'fetchAccounts': True,
  55. 'fetchFundingFee': True,
  56. 'fetchOHLCV': True,
  57. 'fetchLedger': True,
  58. },
  59. 'urls': {
  60. 'logo': 'https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg',
  61. 'referral': 'https://www.kucoin.com/?rcode=E5wkqe',
  62. 'api': {
  63. 'public': 'https://openapi-v2.kucoin.com',
  64. 'private': 'https://openapi-v2.kucoin.com',
  65. },
  66. 'test': {
  67. 'public': 'https://openapi-sandbox.kucoin.com',
  68. 'private': 'https://openapi-sandbox.kucoin.com',
  69. },
  70. 'www': 'https://www.kucoin.com',
  71. 'doc': [
  72. 'https://docs.kucoin.com',
  73. ],
  74. },
  75. 'requiredCredentials': {
  76. 'apiKey': True,
  77. 'secret': True,
  78. 'password': True,
  79. },
  80. 'api': {
  81. 'public': {
  82. 'get': [
  83. 'timestamp',
  84. 'status',
  85. 'symbols',
  86. 'markets',
  87. 'market/allTickers',
  88. 'market/orderbook/level{level}',
  89. 'market/orderbook/level2',
  90. 'market/orderbook/level2_20',
  91. 'market/orderbook/level2_100',
  92. 'market/orderbook/level3',
  93. 'market/histories',
  94. 'market/candles',
  95. 'market/stats',
  96. 'currencies',
  97. 'currencies/{currency}',
  98. 'prices',
  99. 'mark-price/{symbol}/current',
  100. 'margin/config',
  101. ],
  102. 'post': [
  103. 'bullet-public',
  104. ],
  105. },
  106. 'private': {
  107. 'get': [
  108. 'accounts',
  109. 'accounts/{accountId}',
  110. 'accounts/{accountId}/ledgers',
  111. 'accounts/{accountId}/holds',
  112. 'accounts/transferable',
  113. 'sub/user',
  114. 'sub-accounts',
  115. 'sub-accounts/{subUserId}',
  116. 'deposit-addresses',
  117. 'deposits',
  118. 'hist-deposits',
  119. 'hist-orders',
  120. 'hist-withdrawals',
  121. 'withdrawals',
  122. 'withdrawals/quotas',
  123. 'orders',
  124. 'orders/{orderId}',
  125. 'limit/orders',
  126. 'fills',
  127. 'limit/fills',
  128. 'margin/account',
  129. 'margin/borrow',
  130. 'margin/borrow/outstanding',
  131. 'margin/borrow/borrow/repaid',
  132. 'margin/lend/active',
  133. 'margin/lend/done',
  134. 'margin/lend/trade/unsettled',
  135. 'margin/lend/trade/settled',
  136. 'margin/lend/assets',
  137. 'margin/market',
  138. 'margin/margin/trade/last',
  139. ],
  140. 'post': [
  141. 'accounts',
  142. 'accounts/inner-transfer',
  143. 'accounts/sub-transfer',
  144. 'deposit-addresses',
  145. 'withdrawals',
  146. 'orders',
  147. 'orders/multi',
  148. 'margin/borrow',
  149. 'margin/repay/all',
  150. 'margin/repay/single',
  151. 'margin/lend',
  152. 'margin/toggle-auto-lend',
  153. 'bullet-private',
  154. ],
  155. 'delete': [
  156. 'withdrawals/{withdrawalId}',
  157. 'orders',
  158. 'orders/{orderId}',
  159. 'margin/lend/{orderId}',
  160. ],
  161. },
  162. },
  163. 'timeframes': {
  164. '1m': '1min',
  165. '3m': '3min',
  166. '5m': '5min',
  167. '15m': '15min',
  168. '30m': '30min',
  169. '1h': '1hour',
  170. '2h': '2hour',
  171. '4h': '4hour',
  172. '6h': '6hour',
  173. '8h': '8hour',
  174. '12h': '12hour',
  175. '1d': '1day',
  176. '1w': '1week',
  177. },
  178. 'exceptions': {
  179. 'exact': {
  180. 'order not exist': OrderNotFound,
  181. 'order not exist.': OrderNotFound, # duplicated error temporarily
  182. 'order_not_exist': OrderNotFound, # {"code":"order_not_exist","msg":"order_not_exist"} ¯\_()_/¯
  183. 'order_not_exist_or_not_allow_to_cancel': InvalidOrder, # {"code":"400100","msg":"order_not_exist_or_not_allow_to_cancel"}
  184. 'Order size below the minimum requirement.': InvalidOrder, # {"code":"400100","msg":"Order size below the minimum requirement."}
  185. 'The withdrawal amount is below the minimum requirement.': ExchangeError, # {"code":"400100","msg":"The withdrawal amount is below the minimum requirement."}
  186. '400': BadRequest,
  187. '401': AuthenticationError,
  188. '403': NotSupported,
  189. '404': NotSupported,
  190. '405': NotSupported,
  191. '429': RateLimitExceeded,
  192. '500': ExchangeNotAvailable, # Internal Server Error -- We had a problem with our server. Try again later.
  193. '503': ExchangeNotAvailable,
  194. '200004': InsufficientFunds,
  195. '230003': InsufficientFunds, # {"code":"230003","msg":"Balance insufficient!"}
  196. '260100': InsufficientFunds, # {"code":"260100","msg":"account.noBalance"}
  197. '300000': InvalidOrder,
  198. '400000': BadSymbol,
  199. '400001': AuthenticationError,
  200. '400002': InvalidNonce,
  201. '400003': AuthenticationError,
  202. '400004': AuthenticationError,
  203. '400005': AuthenticationError,
  204. '400006': AuthenticationError,
  205. '400007': AuthenticationError,
  206. '400008': NotSupported,
  207. '400100': BadRequest,
  208. '411100': AccountSuspended,
  209. '415000': BadRequest, # {"code":"415000","msg":"Unsupported Media Type"}
  210. '500000': ExchangeError,
  211. },
  212. 'broad': {
  213. 'Exceeded the access frequency': RateLimitExceeded,
  214. 'require more permission': PermissionDenied,
  215. },
  216. },
  217. 'fees': {
  218. 'trading': {
  219. 'tierBased': False,
  220. 'percentage': True,
  221. 'taker': 0.001,
  222. 'maker': 0.001,
  223. },
  224. 'funding': {
  225. 'tierBased': False,
  226. 'percentage': False,
  227. 'withdraw': {},
  228. 'deposit': {},
  229. },
  230. },
  231. 'commonCurrencies': {
  232. 'HOT': 'HOTNOW',
  233. 'EDGE': 'DADI', # https://github.com/ccxt/ccxt/issues/5756
  234. 'WAX': 'WAXP',
  235. 'TRY': 'Trias',
  236. },
  237. 'options': {
  238. 'version': 'v1',
  239. 'symbolSeparator': '-',
  240. 'fetchMyTradesMethod': 'private_get_fills',
  241. 'fetchBalance': {
  242. 'type': 'trade', # or 'main'
  243. },
  244. # endpoint versions
  245. 'versions': {
  246. 'public': {
  247. 'GET': {
  248. 'status': 'v1',
  249. 'market/orderbook/level{level}': 'v1',
  250. 'market/orderbook/level2': 'v2',
  251. 'market/orderbook/level2_20': 'v1',
  252. 'market/orderbook/level2_100': 'v1',
  253. },
  254. },
  255. 'private': {
  256. 'POST': {
  257. 'accounts/inner-transfer': 'v2',
  258. 'accounts/sub-transfer': 'v2',
  259. },
  260. },
  261. },
  262. },
  263. })
  264. def nonce(self):
  265. return self.milliseconds()
  266. def load_time_difference(self, params={}):
  267. response = self.publicGetTimestamp(params)
  268. after = self.milliseconds()
  269. kucoinTime = self.safe_integer(response, 'data')
  270. self.options['timeDifference'] = int(after - kucoinTime)
  271. return self.options['timeDifference']
  272. def fetch_time(self, params={}):
  273. response = self.publicGetTimestamp(params)
  274. #
  275. # {
  276. # "code":"200000",
  277. # "msg":"success",
  278. # "data":1546837113087
  279. # }
  280. #
  281. return self.safe_integer(response, 'data')
  282. def fetch_status(self, params={}):
  283. response = self.publicGetStatus(params)
  284. #
  285. # {
  286. # "code":"200000",
  287. # "data":{
  288. # "msg":"",
  289. # "status":"open"
  290. # }
  291. # }
  292. #
  293. data = self.safe_value(response, 'data', {})
  294. status = self.safe_value(data, 'status')
  295. if status is not None:
  296. status = 'ok' if (status == 'open') else 'maintenance'
  297. self.status = self.extend(self.status, {
  298. 'status': status,
  299. 'updated': self.milliseconds(),
  300. })
  301. return self.status
  302. def fetch_markets(self, params={}):
  303. response = self.publicGetSymbols(params)
  304. #
  305. # {
  306. # quoteCurrency: 'BTC',
  307. # symbol: 'KCS-BTC',
  308. # quoteMaxSize: '9999999',
  309. # quoteIncrement: '0.000001',
  310. # baseMinSize: '0.01',
  311. # quoteMinSize: '0.00001',
  312. # enableTrading: True,
  313. # priceIncrement: '0.00000001',
  314. # name: 'KCS-BTC',
  315. # baseIncrement: '0.01',
  316. # baseMaxSize: '9999999',
  317. # baseCurrency: 'KCS'
  318. # }
  319. #
  320. data = response['data']
  321. result = []
  322. for i in range(0, len(data)):
  323. market = data[i]
  324. id = self.safe_string(market, 'symbol')
  325. baseId, quoteId = id.split('-')
  326. base = self.safe_currency_code(baseId)
  327. quote = self.safe_currency_code(quoteId)
  328. symbol = base + '/' + quote
  329. active = self.safe_value(market, 'enableTrading')
  330. baseMaxSize = self.safe_float(market, 'baseMaxSize')
  331. baseMinSize = self.safe_float(market, 'baseMinSize')
  332. quoteMaxSize = self.safe_float(market, 'quoteMaxSize')
  333. quoteMinSize = self.safe_float(market, 'quoteMinSize')
  334. # quoteIncrement = self.safe_float(market, 'quoteIncrement')
  335. precision = {
  336. 'amount': self.precision_from_string(self.safe_string(market, 'baseIncrement')),
  337. 'price': self.precision_from_string(self.safe_string(market, 'priceIncrement')),
  338. }
  339. limits = {
  340. 'amount': {
  341. 'min': baseMinSize,
  342. 'max': baseMaxSize,
  343. },
  344. 'price': {
  345. 'min': self.safe_float(market, 'priceIncrement'),
  346. 'max': quoteMaxSize / baseMinSize,
  347. },
  348. 'cost': {
  349. 'min': quoteMinSize,
  350. 'max': quoteMaxSize,
  351. },
  352. }
  353. result.append({
  354. 'id': id,
  355. 'symbol': symbol,
  356. 'baseId': baseId,
  357. 'quoteId': quoteId,
  358. 'base': base,
  359. 'quote': quote,
  360. 'active': active,
  361. 'precision': precision,
  362. 'limits': limits,
  363. 'info': market,
  364. })
  365. return result
  366. def fetch_currencies(self, params={}):
  367. response = self.publicGetCurrencies(params)
  368. #
  369. # {
  370. # precision: 10,
  371. # name: 'KCS',
  372. # fullName: 'KCS shares',
  373. # currency: 'KCS'
  374. # }
  375. #
  376. responseData = response['data']
  377. result = {}
  378. for i in range(0, len(responseData)):
  379. entry = responseData[i]
  380. id = self.safe_string(entry, 'currency')
  381. name = self.safe_string(entry, 'fullName')
  382. code = self.safe_currency_code(id)
  383. precision = self.safe_integer(entry, 'precision')
  384. result[code] = {
  385. 'id': id,
  386. 'name': name,
  387. 'code': code,
  388. 'precision': precision,
  389. 'info': entry,
  390. 'active': None,
  391. 'fee': None,
  392. 'limits': self.limits,
  393. }
  394. return result
  395. def fetch_accounts(self, params={}):
  396. response = self.privateGetAccounts(params)
  397. #
  398. # {
  399. # code: "200000",
  400. # data: [
  401. # {
  402. # balance: "0.00009788",
  403. # available: "0.00009788",
  404. # holds: "0",
  405. # currency: "BTC",
  406. # id: "5c6a4fd399a1d81c4f9cc4d0",
  407. # type: "trade"
  408. # },
  409. # {
  410. # balance: "0.00000001",
  411. # available: "0.00000001",
  412. # holds: "0",
  413. # currency: "ETH",
  414. # id: "5c6a49ec99a1d819392e8e9f",
  415. # type: "trade"
  416. # }
  417. # ]
  418. # }
  419. #
  420. data = self.safe_value(response, 'data')
  421. result = []
  422. for i in range(0, len(data)):
  423. account = data[i]
  424. accountId = self.safe_string(account, 'id')
  425. currencyId = self.safe_string(account, 'currency')
  426. code = self.safe_currency_code(currencyId)
  427. type = self.safe_string(account, 'type') # main or trade
  428. result.append({
  429. 'id': accountId,
  430. 'type': type,
  431. 'currency': code,
  432. 'info': account,
  433. })
  434. return result
  435. def fetch_funding_fee(self, code, params={}):
  436. currencyId = self.currency_id(code)
  437. request = {
  438. 'currency': currencyId,
  439. }
  440. response = self.privateGetWithdrawalsQuotas(self.extend(request, params))
  441. data = response['data']
  442. withdrawFees = {}
  443. withdrawFees[code] = self.safe_float(data, 'withdrawMinFee')
  444. return {
  445. 'info': response,
  446. 'withdraw': withdrawFees,
  447. 'deposit': {},
  448. }
  449. def parse_ticker(self, ticker, market=None):
  450. #
  451. # {
  452. # symbol: "ETH-BTC",
  453. # high: "0.019518",
  454. # vol: "7997.82836194",
  455. # last: "0.019329",
  456. # low: "0.019",
  457. # buy: "0.019329",
  458. # sell: "0.01933",
  459. # changePrice: "-0.000139",
  460. # time: 1580553706304,
  461. # averagePrice: "0.01926386",
  462. # changeRate: "-0.0071",
  463. # volValue: "154.40791568183474"
  464. # }
  465. #
  466. # {
  467. # "trading": True,
  468. # "symbol": "KCS-BTC",
  469. # "buy": 0.00011,
  470. # "sell": 0.00012,
  471. # "sort": 100,
  472. # "volValue": 3.13851792584, #total
  473. # "baseCurrency": "KCS",
  474. # "market": "BTC",
  475. # "quoteCurrency": "BTC",
  476. # "symbolCode": "KCS-BTC",
  477. # "datetime": 1548388122031,
  478. # "high": 0.00013,
  479. # "vol": 27514.34842,
  480. # "low": 0.0001,
  481. # "changePrice": -1.0e-5,
  482. # "changeRate": -0.0769,
  483. # "lastTradedPrice": 0.00012,
  484. # "board": 0,
  485. # "mark": 0
  486. # }
  487. #
  488. percentage = self.safe_float(ticker, 'changeRate')
  489. if percentage is not None:
  490. percentage = percentage * 100
  491. last = self.safe_float_2(ticker, 'last', 'lastTradedPrice')
  492. symbol = None
  493. marketId = self.safe_string(ticker, 'symbol')
  494. if marketId is not None:
  495. if marketId in self.markets_by_id:
  496. market = self.markets_by_id[marketId]
  497. symbol = market['symbol']
  498. else:
  499. baseId, quoteId = marketId.split('-')
  500. base = self.safe_currency_code(baseId)
  501. quote = self.safe_currency_code(quoteId)
  502. symbol = base + '/' + quote
  503. if symbol is None:
  504. if market is not None:
  505. symbol = market['symbol']
  506. baseVolume = self.safe_float(ticker, 'vol')
  507. quoteVolume = self.safe_float(ticker, 'volValue')
  508. vwap = self.vwap(baseVolume, quoteVolume)
  509. timestamp = self.safe_integer_2(ticker, 'time', 'datetime')
  510. return {
  511. 'symbol': symbol,
  512. 'timestamp': timestamp,
  513. 'datetime': self.iso8601(timestamp),
  514. 'high': self.safe_float(ticker, 'high'),
  515. 'low': self.safe_float(ticker, 'low'),
  516. 'bid': self.safe_float(ticker, 'buy'),
  517. 'bidVolume': None,
  518. 'ask': self.safe_float(ticker, 'sell'),
  519. 'askVolume': None,
  520. 'vwap': vwap,
  521. 'open': self.safe_float(ticker, 'open'),
  522. 'close': last,
  523. 'last': last,
  524. 'previousClose': None,
  525. 'change': self.safe_float(ticker, 'changePrice'),
  526. 'percentage': percentage,
  527. 'average': self.safe_float(ticker, 'averagePrice'),
  528. 'baseVolume': baseVolume,
  529. 'quoteVolume': quoteVolume,
  530. 'info': ticker,
  531. }
  532. def fetch_tickers(self, symbols=None, params={}):
  533. self.load_markets()
  534. response = self.publicGetMarketAllTickers(params)
  535. #
  536. # {
  537. # "code": "200000",
  538. # "data": {
  539. # "date": 1550661940645,
  540. # "ticker": [
  541. # 'buy': '0.00001168',
  542. # 'changePrice': '-0.00000018',
  543. # 'changeRate': '-0.0151',
  544. # 'datetime': 1550661146316,
  545. # 'high': '0.0000123',
  546. # 'last': '0.00001169',
  547. # 'low': '0.00001159',
  548. # 'sell': '0.00001182',
  549. # 'symbol': 'LOOM-BTC',
  550. # 'vol': '44399.5669'
  551. # },
  552. # ]
  553. # }
  554. #
  555. data = self.safe_value(response, 'data', {})
  556. tickers = self.safe_value(data, 'ticker', [])
  557. result = {}
  558. for i in range(0, len(tickers)):
  559. ticker = self.parse_ticker(tickers[i])
  560. symbol = self.safe_string(ticker, 'symbol')
  561. if symbol is not None:
  562. result[symbol] = ticker
  563. return self.filter_by_array(result, 'symbol', symbols)
  564. def fetch_ticker(self, symbol, params={}):
  565. self.load_markets()
  566. market = self.market(symbol)
  567. request = {
  568. 'symbol': market['id'],
  569. }
  570. response = self.publicGetMarketStats(self.extend(request, params))
  571. #
  572. # {
  573. # "code": "200000",
  574. # "data": {
  575. # 'buy': '0.00001168',
  576. # 'changePrice': '-0.00000018',
  577. # 'changeRate': '-0.0151',
  578. # 'datetime': 1550661146316,
  579. # 'high': '0.0000123',
  580. # 'last': '0.00001169',
  581. # 'low': '0.00001159',
  582. # 'sell': '0.00001182',
  583. # 'symbol': 'LOOM-BTC',
  584. # 'vol': '44399.5669'
  585. # },
  586. # }
  587. #
  588. return self.parse_ticker(response['data'], market)
  589. def parse_ohlcv(self, ohlcv, market=None):
  590. #
  591. # [
  592. # "1545904980", # Start time of the candle cycle
  593. # "0.058", # opening price
  594. # "0.049", # closing price
  595. # "0.058", # highest price
  596. # "0.049", # lowest price
  597. # "0.018", # base volume
  598. # "0.000945", # quote volume
  599. # ]
  600. #
  601. return [
  602. self.safe_timestamp(ohlcv, 0),
  603. self.safe_float(ohlcv, 1),
  604. self.safe_float(ohlcv, 3),
  605. self.safe_float(ohlcv, 4),
  606. self.safe_float(ohlcv, 2),
  607. self.safe_float(ohlcv, 5),
  608. ]
  609. def fetch_ohlcv(self, symbol, timeframe='15m', since=None, limit=None, params={}):
  610. self.load_markets()
  611. market = self.market(symbol)
  612. marketId = market['id']
  613. request = {
  614. 'symbol': marketId,
  615. 'type': self.timeframes[timeframe],
  616. }
  617. duration = self.parse_timeframe(timeframe) * 1000
  618. endAt = self.milliseconds() # required param
  619. if since is not None:
  620. request['startAt'] = int(int(math.floor(since / 1000)))
  621. if limit is None:
  622. # https://docs.kucoin.com/#get-klines
  623. # https://docs.kucoin.com/#details
  624. # For each query, the system would return at most 1500 pieces of data.
  625. # To obtain more data, please page the data by time.
  626. limit = self.safe_integer(self.options, 'fetchOHLCVLimit', 1500)
  627. endAt = self.sum(since, limit * duration)
  628. elif limit is not None:
  629. since = endAt - limit * duration
  630. request['startAt'] = int(int(math.floor(since / 1000)))
  631. request['endAt'] = int(int(math.floor(endAt / 1000)))
  632. response = self.publicGetMarketCandles(self.extend(request, params))
  633. #
  634. # {
  635. # "code":"200000",
  636. # "data":[
  637. # ["1591517700","0.025078","0.025069","0.025084","0.025064","18.9883256","0.4761861079404"],
  638. # ["1591516800","0.025089","0.025079","0.025089","0.02506","99.4716622","2.494143499081"],
  639. # ["1591515900","0.025079","0.02509","0.025091","0.025068","59.83701271","1.50060885172798"],
  640. # ]
  641. # }
  642. #
  643. data = self.safe_value(response, 'data', [])
  644. return self.parse_ohlcvs(data, market, timeframe, since, limit)
  645. def create_deposit_address(self, code, params={}):
  646. self.load_markets()
  647. currencyId = self.currency_id(code)
  648. request = {'currency': currencyId}
  649. response = self.privatePostDepositAddresses(self.extend(request, params))
  650. # BCH {"code":"200000","data":{"address":"bitcoincash:qza3m4nj9rx7l9r0cdadfqxts6f92shvhvr5ls4q7z","memo":""}}
  651. # BTC {"code":"200000","data":{"address":"36SjucKqQpQSvsak9A7h6qzFjrVXpRNZhE","memo":""}}
  652. data = self.safe_value(response, 'data', {})
  653. address = self.safe_string(data, 'address')
  654. # BCH/BSV is returned with a "bitcoincash:" prefix, which we cut off here and only keep the address
  655. if address is not None:
  656. address = address.replace('bitcoincash:', '')
  657. tag = self.safe_string(data, 'memo')
  658. self.check_address(address)
  659. return {
  660. 'info': response,
  661. 'currency': code,
  662. 'address': address,
  663. 'tag': tag,
  664. }
  665. def fetch_deposit_address(self, code, params={}):
  666. self.load_markets()
  667. currencyId = self.currency_id(code)
  668. request = {'currency': currencyId}
  669. response = self.privateGetDepositAddresses(self.extend(request, params))
  670. # BCH {"code":"200000","data":{"address":"bitcoincash:qza3m4nj9rx7l9r0cdadfqxts6f92shvhvr5ls4q7z","memo":""}}
  671. # BTC {"code":"200000","data":{"address":"36SjucKqQpQSvsak9A7h6qzFjrVXpRNZhE","memo":""}}
  672. data = self.safe_value(response, 'data', {})
  673. address = self.safe_string(data, 'address')
  674. # BCH/BSV is returned with a "bitcoincash:" prefix, which we cut off here and only keep the address
  675. if address is not None:
  676. address = address.replace('bitcoincash:', '')
  677. tag = self.safe_string(data, 'memo')
  678. self.check_address(address)
  679. return {
  680. 'info': response,
  681. 'currency': code,
  682. 'address': address,
  683. 'tag': tag,
  684. }
  685. def fetch_l3_order_book(self, symbol, limit=None, params={}):
  686. return self.fetch_order_book(symbol, limit, {'level': 3})
  687. def fetch_order_book(self, symbol, limit=None, params={}):
  688. level = self.safe_integer(params, 'level', 2)
  689. levelLimit = str(level)
  690. if levelLimit == '2':
  691. if limit is not None:
  692. if (limit != 20) and (limit != 100):
  693. raise ExchangeError(self.id + ' fetchOrderBook limit argument must be None, 20 or 100')
  694. levelLimit += '_' + str(limit)
  695. self.load_markets()
  696. marketId = self.market_id(symbol)
  697. request = {'symbol': marketId, 'level': levelLimit}
  698. response = self.publicGetMarketOrderbookLevelLevel(self.extend(request, params))
  699. #
  700. # 'market/orderbook/level2'
  701. # 'market/orderbook/level2_20'
  702. # 'market/orderbook/level2_100'
  703. #
  704. # {
  705. # "code":"200000",
  706. # "data":{
  707. # "sequence":"1583235112106",
  708. # "asks":[
  709. # # ...
  710. # ["0.023197","12.5067468"],
  711. # ["0.023194","1.8"],
  712. # ["0.023191","8.1069672"]
  713. # ],
  714. # "bids":[
  715. # ["0.02319","1.6000002"],
  716. # ["0.023189","2.2842325"],
  717. # ],
  718. # "time":1586584067274
  719. # }
  720. # }
  721. #
  722. # 'market/orderbook/level3'
  723. #
  724. # {
  725. # "code":"200000",
  726. # "data":{
  727. # "sequence":"1583731857120",
  728. # "asks":[
  729. # # id, price, size, timestamp in nanoseconds
  730. # ["5e915f8acd26670009675300","6925.7","0.2","1586585482194286069"],
  731. # ["5e915f8ace35a200090bba48","6925.7","0.001","1586585482229569826"],
  732. # ["5e915f8a8857740009ca7d33","6926","0.00001819","1586585482149148621"],
  733. # ],
  734. # "bids":[
  735. # ["5e915f8acca406000ac88194","6925.6","0.05","1586585482384384842"],
  736. # ["5e915f93cd26670009676075","6925.6","0.08","1586585491334914600"],
  737. # ["5e915f906aa6e200099b49f6","6925.4","0.2","1586585488941126340"],
  738. # ],
  739. # "time":1586585492487
  740. # }
  741. # }
  742. #
  743. data = self.safe_value(response, 'data', {})
  744. timestamp = self.safe_integer(data, 'time')
  745. orderbook = self.parse_order_book(data, timestamp, 'bids', 'asks', level - 2, level - 1)
  746. orderbook['nonce'] = self.safe_integer(data, 'sequence')
  747. return orderbook
  748. def create_order(self, symbol, type, side, amount, price=None, params={}):
  749. self.load_markets()
  750. marketId = self.market_id(symbol)
  751. # required param, cannot be used twice
  752. clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId', self.uuid())
  753. params = self.omit(params, ['clientOid', 'clientOrderId'])
  754. request = {
  755. 'clientOid': clientOrderId,
  756. 'side': side,
  757. 'symbol': marketId,
  758. 'type': type,
  759. }
  760. if type != 'market':
  761. request['price'] = self.price_to_precision(symbol, price)
  762. request['size'] = self.amount_to_precision(symbol, amount)
  763. else:
  764. if self.safe_value(params, 'quoteAmount'):
  765. # used to create market order by quote amount - https://github.com/ccxt/ccxt/issues/4876
  766. request['funds'] = self.amount_to_precision(symbol, amount)
  767. else:
  768. request['size'] = self.amount_to_precision(symbol, amount)
  769. response = self.privatePostOrders(self.extend(request, params))
  770. #
  771. # {
  772. # code: '200000',
  773. # data: {
  774. # "orderId": "5bd6e9286d99522a52e458de"
  775. # }
  776. # }
  777. #
  778. data = self.safe_value(response, 'data', {})
  779. timestamp = self.milliseconds()
  780. id = self.safe_string(data, 'orderId')
  781. order = {
  782. 'id': id,
  783. 'clientOrderId': clientOrderId,
  784. 'info': data,
  785. 'timestamp': timestamp,
  786. 'datetime': self.iso8601(timestamp),
  787. 'lastTradeTimestamp': None,
  788. 'symbol': symbol,
  789. 'type': type,
  790. 'side': side,
  791. 'price': price,
  792. 'amount': None,
  793. 'cost': None,
  794. 'average': None,
  795. 'filled': None,
  796. 'remaining': None,
  797. 'status': None,
  798. 'fee': None,
  799. 'trades': None,
  800. }
  801. if not self.safe_value(params, 'quoteAmount'):
  802. order['amount'] = amount
  803. return order
  804. def cancel_order(self, id, symbol=None, params={}):
  805. request = {'orderId': id}
  806. response = self.privateDeleteOrdersOrderId(self.extend(request, params))
  807. return response
  808. def fetch_orders_by_status(self, status, symbol=None, since=None, limit=None, params={}):
  809. self.load_markets()
  810. request = {
  811. 'status': status,
  812. }
  813. market = None
  814. if symbol is not None:
  815. market = self.market(symbol)
  816. request['symbol'] = market['id']
  817. if since is not None:
  818. request['startAt'] = since
  819. if limit is not None:
  820. request['pageSize'] = limit
  821. response = self.privateGetOrders(self.extend(request, params))
  822. #
  823. # {
  824. # code: '200000',
  825. # data: {
  826. # "currentPage": 1,
  827. # "pageSize": 1,
  828. # "totalNum": 153408,
  829. # "totalPage": 153408,
  830. # "items": [
  831. # {
  832. # "id": "5c35c02703aa673ceec2a168", #orderid
  833. # "symbol": "BTC-USDT", #symbol
  834. # "opType": "DEAL", # operation type,deal is pending order,cancel is cancel order
  835. # "type": "limit", # order type,e.g. limit,markrt,stop_limit.
  836. # "side": "buy", # transaction direction,include buy and sell
  837. # "price": "10", # order price
  838. # "size": "2", # order quantity
  839. # "funds": "0", # order funds
  840. # "dealFunds": "0.166", # deal funds
  841. # "dealSize": "2", # deal quantity
  842. # "fee": "0", # fee
  843. # "feeCurrency": "USDT", # charge fee currency
  844. # "stp": "", # self trade prevention,include CN,CO,DC,CB
  845. # "stop": "", # stop type
  846. # "stopTriggered": False, # stop order is triggered
  847. # "stopPrice": "0", # stop price
  848. # "timeInForce": "GTC", # time InForce,include GTC,GTT,IOC,FOK
  849. # "postOnly": False, # postOnly
  850. # "hidden": False, # hidden order
  851. # "iceberg": False, # iceberg order
  852. # "visibleSize": "0", # display quantity for iceberg order
  853. # "cancelAfter": 0, # cancel orders timerequires timeInForce to be GTT
  854. # "channel": "IOS", # order source
  855. # "clientOid": "", # user-entered order unique mark
  856. # "remark": "", # remark
  857. # "tags": "", # tag order source
  858. # "isActive": False, # status before unfilled or uncancelled
  859. # "cancelExist": False, # order cancellation transaction record
  860. # "createdAt": 1547026471000 # time
  861. # },
  862. # ]
  863. # }
  864. # }
  865. responseData = self.safe_value(response, 'data', {})
  866. orders = self.safe_value(responseData, 'items', [])
  867. return self.parse_orders(orders, market, since, limit)
  868. def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
  869. return self.fetch_orders_by_status('done', symbol, since, limit, params)
  870. def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
  871. return self.fetch_orders_by_status('active', symbol, since, limit, params)
  872. def fetch_order(self, id, symbol=None, params={}):
  873. self.load_markets()
  874. # a special case for None ids
  875. # otherwise a wrong endpoint for all orders will be triggered
  876. # https://github.com/ccxt/ccxt/issues/7234
  877. if id is None:
  878. raise InvalidOrder(self.id + ' fetchOrder requires an order id')
  879. request = {
  880. 'orderId': id,
  881. }
  882. market = None
  883. if symbol is not None:
  884. market = self.market(symbol)
  885. response = self.privateGetOrdersOrderId(self.extend(request, params))
  886. responseData = self.safe_value(response, 'data')
  887. return self.parse_order(responseData, market)
  888. def parse_order(self, order, market=None):
  889. #
  890. # fetchOpenOrders, fetchClosedOrders
  891. #
  892. # {
  893. # "id": "5c35c02703aa673ceec2a168", #orderid
  894. # "symbol": "BTC-USDT", #symbol
  895. # "opType": "DEAL", # operation type,deal is pending order,cancel is cancel order
  896. # "type": "limit", # order type,e.g. limit,markrt,stop_limit.
  897. # "side": "buy", # transaction direction,include buy and sell
  898. # "price": "10", # order price
  899. # "size": "2", # order quantity
  900. # "funds": "0", # order funds
  901. # "dealFunds": "0.166", # deal funds
  902. # "dealSize": "2", # deal quantity
  903. # "fee": "0", # fee
  904. # "feeCurrency": "USDT", # charge fee currency
  905. # "stp": "", # self trade prevention,include CN,CO,DC,CB
  906. # "stop": "", # stop type
  907. # "stopTriggered": False, # stop order is triggered
  908. # "stopPrice": "0", # stop price
  909. # "timeInForce": "GTC", # time InForce,include GTC,GTT,IOC,FOK
  910. # "postOnly": False, # postOnly
  911. # "hidden": False, # hidden order
  912. # "iceberg": False, # iceberg order
  913. # "visibleSize": "0", # display quantity for iceberg order
  914. # "cancelAfter": 0, # cancel orders timerequires timeInForce to be GTT
  915. # "channel": "IOS", # order source
  916. # "clientOid": "", # user-entered order unique mark
  917. # "remark": "", # remark
  918. # "tags": "", # tag order source
  919. # "isActive": False, # status before unfilled or uncancelled
  920. # "cancelExist": False, # order cancellation transaction record
  921. # "createdAt": 1547026471000 # time
  922. # }
  923. #
  924. symbol = None
  925. marketId = self.safe_string(order, 'symbol')
  926. if marketId is not None:
  927. if marketId in self.markets_by_id:
  928. market = self.markets_by_id[marketId]
  929. symbol = market['symbol']
  930. else:
  931. baseId, quoteId = marketId.split('-')
  932. base = self.safe_currency_code(baseId)
  933. quote = self.safe_currency_code(quoteId)
  934. symbol = base + '/' + quote
  935. market = self.safe_value(self.markets_by_id, marketId)
  936. if symbol is None:
  937. if market is not None:
  938. symbol = market['symbol']
  939. orderId = self.safe_string(order, 'id')
  940. type = self.safe_string(order, 'type')
  941. timestamp = self.safe_integer(order, 'createdAt')
  942. datetime = self.iso8601(timestamp)
  943. price = self.safe_float(order, 'price')
  944. side = self.safe_string(order, 'side')
  945. feeCurrencyId = self.safe_string(order, 'feeCurrency')
  946. feeCurrency = self.safe_currency_code(feeCurrencyId)
  947. feeCost = self.safe_float(order, 'fee')
  948. amount = self.safe_float(order, 'size')
  949. filled = self.safe_float(order, 'dealSize')
  950. cost = self.safe_float(order, 'dealFunds')
  951. remaining = amount - filled
  952. # bool
  953. isActive = self.safe_value(order, 'isActive', False)
  954. cancelExist = self.safe_value(order, 'cancelExist', False)
  955. status = 'open' if isActive else 'closed'
  956. status = 'canceled' if cancelExist else status
  957. fee = {
  958. 'currency': feeCurrency,
  959. 'cost': feeCost,
  960. }
  961. if type == 'market':
  962. if price == 0.0:
  963. if (cost is not None) and (filled is not None):
  964. if (cost > 0) and (filled > 0):
  965. price = cost / filled
  966. clientOrderId = self.safe_string(order, 'clientOid')
  967. return {
  968. 'id': orderId,
  969. 'clientOrderId': clientOrderId,
  970. 'symbol': symbol,
  971. 'type': type,
  972. 'side': side,
  973. 'amount': amount,
  974. 'price': price,
  975. 'cost': cost,
  976. 'filled': filled,
  977. 'remaining': remaining,
  978. 'timestamp': timestamp,
  979. 'datetime': datetime,
  980. 'fee': fee,
  981. 'status': status,
  982. 'info': order,
  983. 'lastTradeTimestamp': None,
  984. 'average': None,
  985. 'trades': None,
  986. }
  987. def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
  988. self.load_markets()
  989. request = {}
  990. market = None
  991. if symbol is not None:
  992. market = self.market(symbol)
  993. request['symbol'] = market['id']
  994. if limit is not None:
  995. request['pageSize'] = limit
  996. method = self.options['fetchMyTradesMethod']
  997. parseResponseData = False
  998. if method == 'private_get_fills':
  999. # does not return trades earlier than 2019-02-18T00:00:00Z
  1000. if since is not None:
  1001. # only returns trades up to one week after the since param
  1002. request['startAt'] = since
  1003. elif method == 'private_get_limit_fills':
  1004. # does not return trades earlier than 2019-02-18T00:00:00Z
  1005. # takes no params
  1006. # only returns first 1000 trades(not only "in the last 24 hours" as stated in the docs)
  1007. parseResponseData = True
  1008. elif method == 'private_get_hist_orders':
  1009. # despite that self endpoint is called `HistOrders`
  1010. # it returns historical trades instead of orders
  1011. # returns trades earlier than 2019-02-18T00:00:00Z only
  1012. if since is not None:
  1013. request['startAt'] = int(since / 1000)
  1014. else:
  1015. raise ExchangeError(self.id + ' invalid fetchClosedOrder method')
  1016. response = getattr(self, method)(self.extend(request, params))
  1017. #
  1018. # {
  1019. # "currentPage": 1,
  1020. # "pageSize": 50,
  1021. # "totalNum": 1,
  1022. # "totalPage": 1,
  1023. # "items": [
  1024. # {
  1025. # "symbol":"BTC-USDT", # symbol
  1026. # "tradeId":"5c35c02709e4f67d5266954e", # trade id
  1027. # "orderId":"5c35c02703aa673ceec2a168", # order id
  1028. # "counterOrderId":"5c1ab46003aa676e487fa8e3", # counter order id
  1029. # "side":"buy", # transaction direction,include buy and sell
  1030. # "liquidity":"taker", # include taker and maker
  1031. # "forceTaker":true, # forced to become taker
  1032. # "price":"0.083", # order price
  1033. # "size":"0.8424304", # order quantity
  1034. # "funds":"0.0699217232", # order funds
  1035. # "fee":"0", # fee
  1036. # "feeRate":"0", # fee rate
  1037. # "feeCurrency":"USDT", # charge fee currency
  1038. # "stop":"", # stop type
  1039. # "type":"limit", # order type, e.g. limit, market, stop_limit.
  1040. # "createdAt":1547026472000 # time
  1041. # },
  1042. # #------------------------------------------------------
  1043. # # v1(historical) trade response structure
  1044. # {
  1045. # "symbol": "SNOV-ETH",
  1046. # "dealPrice": "0.0000246",
  1047. # "dealValue": "0.018942",
  1048. # "amount": "770",
  1049. # "fee": "0.00001137",
  1050. # "side": "sell",
  1051. # "createdAt": 1540080199
  1052. # "id":"5c4d389e4c8c60413f78e2e5",
  1053. # }
  1054. # ]
  1055. # }
  1056. #
  1057. data = self.safe_value(response, 'data', {})
  1058. trades = None
  1059. if parseResponseData:
  1060. trades = data
  1061. else:
  1062. trades = self.safe_value(data, 'items', [])
  1063. return self.parse_trades(trades, market, since, limit)
  1064. def fetch_trades(self, symbol, since=None, limit=None, params={}):
  1065. self.load_markets()
  1066. market = self.market(symbol)
  1067. request = {
  1068. 'symbol': market['id'],
  1069. }
  1070. if since is not None:
  1071. request['startAt'] = int(math.floor(since / 1000))
  1072. if limit is not None:
  1073. request['pageSize'] = limit
  1074. response = self.publicGetMarketHistories(self.extend(request, params))
  1075. #
  1076. # {
  1077. # "code": "200000",
  1078. # "data": [
  1079. # {
  1080. # "sequence": "1548764654235",
  1081. # "side": "sell",
  1082. # "size":"0.6841354",
  1083. # "price":"0.03202",
  1084. # "time":1548848575203567174
  1085. # }
  1086. # ]
  1087. # }
  1088. #
  1089. trades = self.safe_value(response, 'data', [])
  1090. return self.parse_trades(trades, market, since, limit)
  1091. def parse_trade(self, trade, market=None):
  1092. #
  1093. # fetchTrades(public)
  1094. #
  1095. # {
  1096. # "sequence": "1548764654235",
  1097. # "side": "sell",
  1098. # "size":"0.6841354",
  1099. # "price":"0.03202",
  1100. # "time":1548848575203567174
  1101. # }
  1102. #
  1103. # {
  1104. # sequence: '1568787654360',
  1105. # symbol: 'BTC-USDT',
  1106. # side: 'buy',
  1107. # size: '0.00536577',
  1108. # price: '9345',
  1109. # takerOrderId: '5e356c4a9f1a790008f8d921',
  1110. # time: '1580559434436443257',
  1111. # type: 'match',
  1112. # makerOrderId: '5e356bffedf0010008fa5d7f',
  1113. # tradeId: '5e356c4aeefabd62c62a1ece'
  1114. # }
  1115. #
  1116. # fetchMyTrades(private) v2
  1117. #
  1118. # {
  1119. # "symbol":"BTC-USDT",
  1120. # "tradeId":"5c35c02709e4f67d5266954e",
  1121. # "orderId":"5c35c02703aa673ceec2a168",
  1122. # "counterOrderId":"5c1ab46003aa676e487fa8e3",
  1123. # "side":"buy",
  1124. # "liquidity":"taker",
  1125. # "forceTaker":true,
  1126. # "price":"0.083",
  1127. # "size":"0.8424304",
  1128. # "funds":"0.0699217232",
  1129. # "fee":"0",
  1130. # "feeRate":"0",
  1131. # "feeCurrency":"USDT",
  1132. # "stop":"",
  1133. # "type":"limit",
  1134. # "createdAt":1547026472000
  1135. # }
  1136. #
  1137. # fetchMyTrades v2 alternative format since 2019-05-21 https://github.com/ccxt/ccxt/pull/5162
  1138. #
  1139. # {
  1140. # symbol: "OPEN-BTC",
  1141. # forceTaker: False,
  1142. # orderId: "5ce36420054b4663b1fff2c9",
  1143. # fee: "0",
  1144. # feeCurrency: "",
  1145. # type: "",
  1146. # feeRate: "0",
  1147. # createdAt: 1558417615000,
  1148. # size: "12.8206",
  1149. # stop: "",
  1150. # price: "0",
  1151. # funds: "0",
  1152. # tradeId: "5ce390cf6e0db23b861c6e80"
  1153. # }
  1154. #
  1155. # fetchMyTrades(private) v1(historical)
  1156. #
  1157. # {
  1158. # "symbol": "SNOV-ETH",
  1159. # "dealPrice": "0.0000246",
  1160. # "dealValue": "0.018942",
  1161. # "amount": "770",
  1162. # "fee": "0.00001137",
  1163. # "side": "sell",
  1164. # "createdAt": 1540080199
  1165. # "id":"5c4d389e4c8c60413f78e2e5",
  1166. # }
  1167. #
  1168. symbol = None
  1169. marketId = self.safe_string(trade, 'symbol')
  1170. if marketId is not None:
  1171. if marketId in self.markets_by_id:
  1172. market = self.markets_by_id[marketId]
  1173. symbol = market['symbol']
  1174. else:
  1175. baseId, quoteId = marketId.split('-')
  1176. base = self.safe_currency_code(baseId)
  1177. quote = self.safe_currency_code(quoteId)
  1178. symbol = base + '/' + quote
  1179. if symbol is None:
  1180. if market is not None:
  1181. symbol = market['symbol']
  1182. id = self.safe_string_2(trade, 'tradeId', 'id')
  1183. orderId = self.safe_string(trade, 'orderId')
  1184. takerOrMaker = self.safe_string(trade, 'liquidity')
  1185. amount = self.safe_float_2(trade, 'size', 'amount')
  1186. timestamp = self.safe_integer(trade, 'time')
  1187. if timestamp is not None:
  1188. timestamp = int(timestamp / 1000000)
  1189. else:
  1190. timestamp = self.safe_integer(trade, 'createdAt')
  1191. # if it's a historical v1 trade, the exchange returns timestamp in seconds
  1192. if ('dealValue' in trade) and (timestamp is not None):
  1193. timestamp = timestamp * 1000
  1194. price = self.safe_float_2(trade, 'price', 'dealPrice')
  1195. side = self.safe_string(trade, 'side')
  1196. fee = None
  1197. feeCost = self.safe_float(trade, 'fee')
  1198. if feeCost is not None:
  1199. feeCurrencyId = self.safe_string(trade, 'feeCurrency')
  1200. feeCurrency = self.safe_currency_code(feeCurrencyId)
  1201. if feeCurrency is None:
  1202. if market is not None:
  1203. feeCurrency = market['quote'] if (side == 'sell') else market['base']
  1204. fee = {
  1205. 'cost': feeCost,
  1206. 'currency': feeCurrency,
  1207. 'rate': self.safe_float(trade, 'feeRate'),
  1208. }
  1209. type = self.safe_string(trade, 'type')
  1210. if type == 'match':
  1211. type = None
  1212. cost = self.safe_float_2(trade, 'funds', 'dealValue')
  1213. if cost is None:
  1214. if amount is not None:
  1215. if price is not None:
  1216. cost = amount * price
  1217. return {
  1218. 'info': trade,
  1219. 'id': id,
  1220. 'order': orderId,
  1221. 'timestamp': timestamp,
  1222. 'datetime': self.iso8601(timestamp),
  1223. 'symbol': symbol,
  1224. 'type': type,
  1225. 'takerOrMaker': takerOrMaker,
  1226. 'side': side,
  1227. 'price': price,
  1228. 'amount': amount,
  1229. 'cost': cost,
  1230. 'fee': fee,
  1231. }
  1232. def withdraw(self, code, amount, address, tag=None, params={}):
  1233. self.load_markets()
  1234. self.check_address(address)
  1235. currency = self.currency_id(code)
  1236. request = {
  1237. 'currency': currency,
  1238. 'address': address,
  1239. 'amount': amount,
  1240. }
  1241. if tag is not None:
  1242. request['memo'] = tag
  1243. response = self.privatePostWithdrawals(self.extend(request, params))
  1244. #
  1245. # https://github.com/ccxt/ccxt/issues/5558
  1246. #
  1247. # {
  1248. # "code": 200000,
  1249. # "data": {
  1250. # "withdrawalId": "abcdefghijklmnopqrstuvwxyz"
  1251. # }
  1252. # }
  1253. #
  1254. data = self.safe_value(response, 'data', {})
  1255. return {
  1256. 'id': self.safe_string(data, 'withdrawalId'),
  1257. 'info': response,
  1258. }
  1259. def parse_transaction_status(self, status):
  1260. statuses = {
  1261. 'SUCCESS': 'ok',
  1262. 'PROCESSING': 'ok',
  1263. 'FAILURE': 'failed',
  1264. }
  1265. return self.safe_string(statuses, status)
  1266. def parse_transaction(self, transaction, currency=None):
  1267. #
  1268. # fetchDeposits
  1269. #
  1270. # {
  1271. # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989",
  1272. # "memo": "5c247c8a03aa677cea2a251d",
  1273. # "amount": 1,
  1274. # "fee": 0.0001,
  1275. # "currency": "KCS",
  1276. # "isInner": False,
  1277. # "walletTxId": "5bbb57386d99522d9f954c5a@test004",
  1278. # "status": "SUCCESS",
  1279. # "createdAt": 1544178843000,
  1280. # "updatedAt": 1544178891000
  1281. # }
  1282. #
  1283. # fetchWithdrawals
  1284. #
  1285. # {
  1286. # "id": "5c2dc64e03aa675aa263f1ac",
  1287. # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9",
  1288. # "memo": "",
  1289. # "currency": "ETH",
  1290. # "amount": 1.0000000,
  1291. # "fee": 0.0100000,
  1292. # "walletTxId": "3e2414d82acce78d38be7fe9",
  1293. # "isInner": False,
  1294. # "status": "FAILURE",
  1295. # "createdAt": 1546503758000,
  1296. # "updatedAt": 1546504603000
  1297. # }
  1298. #
  1299. currencyId = self.safe_string(transaction, 'currency')
  1300. code = self.safe_currency_code(currencyId, currency)
  1301. address = self.safe_string(transaction, 'address')
  1302. amount = self.safe_float(transaction, 'amount')
  1303. txid = self.safe_string(transaction, 'walletTxId')
  1304. if txid is not None:
  1305. txidParts = txid.split('@')
  1306. numTxidParts = len(txidParts)
  1307. if numTxidParts > 1:
  1308. if address is None:
  1309. if len(txidParts[1]) > 1:
  1310. address = txidParts[1]
  1311. txid = txidParts[0]
  1312. type = 'withdrawal' if (txid is None) else 'deposit'
  1313. rawStatus = self.safe_string(transaction, 'status')
  1314. status = self.parse_transaction_status(rawStatus)
  1315. fee = None
  1316. feeCost = self.safe_float(transaction, 'fee')
  1317. if feeCost is not None:
  1318. rate = None
  1319. if amount is not None:
  1320. rate = feeCost / amount
  1321. fee = {
  1322. 'cost': feeCost,
  1323. 'rate': rate,
  1324. 'currency': code,
  1325. }
  1326. tag = self.safe_string(transaction, 'memo')
  1327. timestamp = self.safe_integer_2(transaction, 'createdAt', 'createAt')
  1328. id = self.safe_string(transaction, 'id')
  1329. updated = self.safe_integer(transaction, 'updatedAt')
  1330. isV1 = not ('createdAt' in transaction)
  1331. # if it's a v1 structure
  1332. if isV1:
  1333. type = 'withdrawal' if ('address' in transaction) else 'deposit'
  1334. if timestamp is not None:
  1335. timestamp = timestamp * 1000
  1336. if updated is not None:
  1337. updated = updated * 1000
  1338. return {
  1339. 'id': id,
  1340. 'address': address,
  1341. 'tag': tag,
  1342. 'currency': code,
  1343. 'amount': amount,
  1344. 'txid': txid,
  1345. 'type': type,
  1346. 'status': status,
  1347. 'fee': fee,
  1348. 'timestamp': timestamp,
  1349. 'datetime': self.iso8601(timestamp),
  1350. 'updated': updated,
  1351. 'info': transaction,
  1352. }
  1353. def fetch_deposits(self, code=None, since=None, limit=None, params={}):
  1354. self.load_markets()
  1355. request = {}
  1356. currency = None
  1357. if code is not None:
  1358. currency = self.currency(code)
  1359. request['currency'] = currency['id']
  1360. if limit is not None:
  1361. request['pageSize'] = limit
  1362. method = 'privateGetDeposits'
  1363. if since is not None:
  1364. # if since is earlier than 2019-02-18T00:00:00Z
  1365. if since < 1550448000000:
  1366. request['startAt'] = int(since / 1000)
  1367. method = 'privateGetHistDeposits'
  1368. else:
  1369. request['startAt'] = since
  1370. response = getattr(self, method)(self.extend(request, params))
  1371. #
  1372. # {
  1373. # code: '200000',
  1374. # data: {
  1375. # "currentPage": 1,
  1376. # "pageSize": 5,
  1377. # "totalNum": 2,
  1378. # "totalPage": 1,
  1379. # "items": [
  1380. # #--------------------------------------------------
  1381. # # version 2 deposit response structure
  1382. # {
  1383. # "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989",
  1384. # "memo": "5c247c8a03aa677cea2a251d",
  1385. # "amount": 1,
  1386. # "fee": 0.0001,
  1387. # "currency": "KCS",
  1388. # "isInner": False,
  1389. # "walletTxId": "5bbb57386d99522d9f954c5a@test004",
  1390. # "status": "SUCCESS",
  1391. # "createdAt": 1544178843000,
  1392. # "updatedAt": 1544178891000
  1393. # },
  1394. # #--------------------------------------------------
  1395. # # version 1(historical) deposit response structure
  1396. # {
  1397. # "currency": "BTC",
  1398. # "createAt": 1528536998,
  1399. # "amount": "0.03266638",
  1400. # "walletTxId": "55c643bc2c68d6f17266383ac1be9e454038864b929ae7cee0bc408cc5c869e8@12ffGWmMMD1zA1WbFm7Ho3JZ1w6NYXjpFk@234",
  1401. # "isInner": False,
  1402. # "status": "SUCCESS",
  1403. # }
  1404. # ]
  1405. # }
  1406. # }
  1407. #
  1408. responseData = response['data']['items']
  1409. return self.parse_transactions(responseData, currency, since, limit, {'type': 'deposit'})
  1410. def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
  1411. self.load_markets()
  1412. request = {}
  1413. currency = None
  1414. if code is not None:
  1415. currency = self.currency(code)
  1416. request['currency'] = currency['id']
  1417. if limit is not None:
  1418. request['pageSize'] = limit
  1419. method = 'privateGetWithdrawals'
  1420. if since is not None:
  1421. # if since is earlier than 2019-02-18T00:00:00Z
  1422. if since < 1550448000000:
  1423. request['startAt'] = int(since / 1000)
  1424. method = 'privateGetHistWithdrawals'
  1425. else:
  1426. request['startAt'] = since
  1427. response = getattr(self, method)(self.extend(request, params))
  1428. #
  1429. # {
  1430. # code: '200000',
  1431. # data: {
  1432. # "currentPage": 1,
  1433. # "pageSize": 5,
  1434. # "totalNum": 2,
  1435. # "totalPage": 1,
  1436. # "items": [
  1437. # #--------------------------------------------------
  1438. # # version 2 withdrawal response structure
  1439. # {
  1440. # "id": "5c2dc64e03aa675aa263f1ac",
  1441. # "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9",
  1442. # "memo": "",
  1443. # "currency": "ETH",
  1444. # "amount": 1.0000000,
  1445. # "fee": 0.0100000,
  1446. # "walletTxId": "3e2414d82acce78d38be7fe9",
  1447. # "isInner": False,
  1448. # "status": "FAILURE",
  1449. # "createdAt": 1546503758000,
  1450. # "updatedAt": 1546504603000
  1451. # },
  1452. # #--------------------------------------------------
  1453. # # version 1(historical) withdrawal response structure
  1454. # {
  1455. # "currency": "BTC",
  1456. # "createAt": 1526723468,
  1457. # "amount": "0.534",
  1458. # "address": "33xW37ZSW4tQvg443Pc7NLCAs167Yc2XUV",
  1459. # "walletTxId": "aeacea864c020acf58e51606169240e96774838dcd4f7ce48acf38e3651323f4",
  1460. # "isInner": False,
  1461. # "status": "SUCCESS"
  1462. # }
  1463. # ]
  1464. # }
  1465. # }
  1466. #
  1467. responseData = response['data']['items']
  1468. return self.parse_transactions(responseData, currency, since, limit, {'type': 'withdrawal'})
  1469. def fetch_balance(self, params={}):
  1470. self.load_markets()
  1471. type = None
  1472. request = {}
  1473. if 'type' in params:
  1474. type = params['type']
  1475. if type is not None:
  1476. request['type'] = type
  1477. params = self.omit(params, 'type')
  1478. else:
  1479. options = self.safe_value(self.options, 'fetchBalance', {})
  1480. type = self.safe_string(options, 'type', 'trade')
  1481. response = self.privateGetAccounts(self.extend(request, params))
  1482. #
  1483. # {
  1484. # "code":"200000",
  1485. # "data":[
  1486. # {"balance":"0.00009788","available":"0.00009788","holds":"0","currency":"BTC","id":"5c6a4fd399a1d81c4f9cc4d0","type":"trade"},
  1487. # {"balance":"3.41060034","available":"3.41060034","holds":"0","currency":"SOUL","id":"5c6a4d5d99a1d8182d37046d","type":"trade"},
  1488. # {"balance":"0.01562641","available":"0.01562641","holds":"0","currency":"NEO","id":"5c6a4f1199a1d8165a99edb1","type":"trade"},
  1489. # ]
  1490. # }
  1491. #
  1492. data = self.safe_value(response, 'data', [])
  1493. result = {'info': response}
  1494. for i in range(0, len(data)):
  1495. balance = data[i]
  1496. balanceType = self.safe_string(balance, 'type')
  1497. if balanceType == type:
  1498. currencyId = self.safe_string(balance, 'currency')
  1499. code = self.safe_currency_code(currencyId)
  1500. account = self.account()
  1501. account['total'] = self.safe_float(balance, 'balance')
  1502. account['free'] = self.safe_float(balance, 'available')
  1503. account['used'] = self.safe_float(balance, 'holds')
  1504. result[code] = account
  1505. return self.parse_balance(result)
  1506. def fetch_ledger(self, code=None, since=None, limit=None, params={}):
  1507. if code is None:
  1508. raise ArgumentsRequired(self.id + ' fetchLedger requires a code param')
  1509. self.load_markets()
  1510. self.load_accounts()
  1511. currency = self.currency(code)
  1512. accountId = self.safe_string(params, 'accountId')
  1513. if accountId is None:
  1514. for i in range(0, len(self.accounts)):
  1515. account = self.accounts[i]
  1516. if account['currency'] == code and account['type'] == 'main':
  1517. accountId = account['id']
  1518. break
  1519. if accountId is None:
  1520. raise ExchangeError(self.id + ' ' + code + 'main account is not loaded in loadAccounts')
  1521. request = {
  1522. 'accountId': accountId,
  1523. }
  1524. if since is not None:
  1525. request['startAt'] = int(math.floor(since / 1000))
  1526. response = self.privateGetAccountsAccountIdLedgers(self.extend(request, params))
  1527. #
  1528. # {
  1529. # code: '200000',
  1530. # data: {
  1531. # totalNum: 1,
  1532. # totalPage: 1,
  1533. # pageSize: 50,
  1534. # currentPage: 1,
  1535. # items: [
  1536. # {
  1537. # createdAt: 1561897880000,
  1538. # amount: '0.0111123',
  1539. # bizType: 'Exchange',
  1540. # balance: '0.13224427',
  1541. # fee: '0.0000111',
  1542. # context: '{"symbol":"KCS-ETH","orderId":"5d18ab98c788c6426188296f","tradeId":"5d18ab9818996813f539a806"}',
  1543. # currency: 'ETH',
  1544. # direction: 'out'
  1545. # }
  1546. # ]
  1547. # }
  1548. # }
  1549. #
  1550. items = response['data']['items']
  1551. return self.parse_ledger(items, currency, since, limit)
  1552. def parse_ledger_entry(self, item, currency=None):
  1553. #
  1554. # trade
  1555. #
  1556. # {
  1557. # createdAt: 1561897880000,
  1558. # amount: '0.0111123',
  1559. # bizType: 'Exchange',
  1560. # balance: '0.13224427',
  1561. # fee: '0.0000111',
  1562. # context: '{"symbol":"KCS-ETH","orderId":"5d18ab98c788c6426188296f","tradeId":"5d18ab9818996813f539a806"}',
  1563. # currency: 'ETH',
  1564. # direction: 'out'
  1565. # }
  1566. #
  1567. # withdrawal
  1568. #
  1569. # {
  1570. # createdAt: 1561900264000,
  1571. # amount: '0.14333217',
  1572. # bizType: 'Withdrawal',
  1573. # balance: '0',
  1574. # fee: '0.01',
  1575. # context: '{"orderId":"5d18b4e687111437cf1c48b9","txId":"0x1d136ee065c5c4c5caa293faa90d43e213c953d7cdd575c89ed0b54eb87228b8"}',
  1576. # currency: 'ETH',
  1577. # direction: 'out'
  1578. # }
  1579. #
  1580. currencyId = self.safe_string(item, 'currency')
  1581. code = self.safe_currency_code(currencyId, currency)
  1582. fee = {
  1583. 'cost': self.safe_float(item, 'fee'),
  1584. 'code': code,
  1585. }
  1586. amount = self.safe_float(item, 'amount')
  1587. after = self.safe_float(item, 'balance')
  1588. direction = self.safe_string(item, 'direction')
  1589. before = None
  1590. if after is not None and amount is not None:
  1591. difference = amount if (direction == 'out') else -amount
  1592. before = self.sum(after, difference)
  1593. timestamp = self.safe_integer(item, 'createdAt')
  1594. type = self.parse_ledger_entry_type(self.safe_string(item, 'bizType'))
  1595. contextString = self.safe_string(item, 'context')
  1596. id = None
  1597. referenceId = None
  1598. if self.is_json_encoded_object(contextString):
  1599. context = self.parse_json(contextString)
  1600. id = self.safe_string(context, 'orderId')
  1601. if type == 'trade':
  1602. referenceId = self.safe_string(context, 'tradeId')
  1603. elif type == 'transaction':
  1604. referenceId = self.safe_string(context, 'txId')
  1605. return {
  1606. 'id': id,
  1607. 'currency': code,
  1608. 'account': None,
  1609. 'referenceAccount': None,
  1610. 'referenceId': referenceId,
  1611. 'status': None,
  1612. 'amount': amount,
  1613. 'before': before,
  1614. 'after': after,
  1615. 'fee': fee,
  1616. 'direction': direction,
  1617. 'timestamp': timestamp,
  1618. 'datetime': self.iso8601(timestamp),
  1619. 'type': type,
  1620. 'info': item,
  1621. }
  1622. def parse_ledger_entry_type(self, type):
  1623. types = {
  1624. 'Exchange': 'trade',
  1625. 'Withdrawal': 'transaction',
  1626. 'Deposit': 'transaction',
  1627. 'Transfer': 'transfer',
  1628. }
  1629. return self.safe_string(types, type, type)
  1630. def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
  1631. #
  1632. # the v2 URL is https://openapi-v2.kucoin.com/api/v1/endpoint
  1633. #
  1634. #
  1635. versions = self.safe_value(self.options, 'versions', {})
  1636. apiVersions = self.safe_value(versions, api)
  1637. methodVersions = self.safe_value(apiVersions, method, {})
  1638. defaultVersion = self.safe_string(methodVersions, path, self.options['version'])
  1639. version = self.safe_string(params, 'version', defaultVersion)
  1640. params = self.omit(params, 'version')
  1641. endpoint = '/api/' + version + '/' + self.implode_params(path, params)
  1642. query = self.omit(params, self.extract_params(path))
  1643. endpart = ''
  1644. headers = headers if (headers is not None) else {}
  1645. if query:
  1646. if method != 'GET':
  1647. body = self.json(query)
  1648. endpart = body
  1649. headers['Content-Type'] = 'application/json'
  1650. else:
  1651. endpoint += '?' + self.urlencode(query)
  1652. url = self.urls['api'][api] + endpoint
  1653. if api == 'private':
  1654. self.check_required_credentials()
  1655. timestamp = str(self.nonce())
  1656. headers = self.extend({
  1657. 'KC-API-KEY': self.apiKey,
  1658. 'KC-API-TIMESTAMP': timestamp,
  1659. 'KC-API-PASSPHRASE': self.password,
  1660. }, headers)
  1661. payload = timestamp + method + endpoint + endpart
  1662. signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64')
  1663. headers['KC-API-SIGN'] = signature
  1664. partner = self.safe_value(self.options, 'partner', {})
  1665. partnerId = self.safe_string(partner, 'id')
  1666. partnerSecret = self.safe_string(partner, 'secret')
  1667. if (partnerId is not None) and (partnerSecret is not None):
  1668. partnerPayload = timestamp + partnerId + self.apiKey
  1669. partnerSignature = self.hmac(self.encode(partnerPayload), self.encode(partnerSecret), hashlib.sha256, 'base64')
  1670. headers['KC-API-PARTNER-SIGN'] = partnerSignature
  1671. headers['KC-API-PARTNER'] = partnerId
  1672. return {'url': url, 'method': method, 'body': body, 'headers': headers}
  1673. def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
  1674. if not response:
  1675. self.throw_broadly_matched_exception(self.exceptions['broad'], body, body)
  1676. return
  1677. #
  1678. # bad
  1679. # {"code": "400100", "msg": "validation.createOrder.clientOidIsRequired"}
  1680. # good
  1681. # {code: '200000', data: {...}}
  1682. #
  1683. errorCode = self.safe_string(response, 'code')
  1684. message = self.safe_string(response, 'msg')
  1685. self.throw_exactly_matched_exception(self.exceptions['exact'], message, message)
  1686. self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, message)