# -*- 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
import hashlib
import math
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import PermissionDenied
from ccxt.base.errors import AccountSuspended
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import NotSupported
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.errors import InvalidNonce


class kucoin(Exchange):

    def describe(self):
        return self.deep_extend(super(kucoin, self).describe(), {
            'id': 'kucoin',
            'name': 'KuCoin',
            'countries': ['SC'],
            'rateLimit': 334,
            'version': 'v2',
            'certified': False,
            'pro': True,
            'comment': 'Platform 2.0',
            'has': {
                'CORS': False,
                'fetchStatus': True,
                'fetchTime': True,
                'fetchMarkets': True,
                'fetchCurrencies': True,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchOrderBook': True,
                'fetchOrder': True,
                'fetchClosedOrders': True,
                'fetchOpenOrders': True,
                'fetchDepositAddress': True,
                'createDepositAddress': True,
                'withdraw': True,
                'fetchDeposits': True,
                'fetchWithdrawals': True,
                'fetchBalance': True,
                'fetchTrades': True,
                'fetchMyTrades': True,
                'createOrder': True,
                'cancelOrder': True,
                'fetchAccounts': True,
                'fetchFundingFee': True,
                'fetchOHLCV': True,
                'fetchLedger': True,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg',
                'referral': 'https://www.kucoin.com/?rcode=E5wkqe',
                'api': {
                    'public': 'https://openapi-v2.kucoin.com',
                    'private': 'https://openapi-v2.kucoin.com',
                },
                'test': {
                    'public': 'https://openapi-sandbox.kucoin.com',
                    'private': 'https://openapi-sandbox.kucoin.com',
                },
                'www': 'https://www.kucoin.com',
                'doc': [
                    'https://docs.kucoin.com',
                ],
            },
            'requiredCredentials': {
                'apiKey': True,
                'secret': True,
                'password': True,
            },
            'api': {
                'public': {
                    'get': [
                        'timestamp',
                        'status',
                        'symbols',
                        'markets',
                        'market/allTickers',
                        'market/orderbook/level{level}',
                        'market/orderbook/level2',
                        'market/orderbook/level2_20',
                        'market/orderbook/level2_100',
                        'market/orderbook/level3',
                        'market/histories',
                        'market/candles',
                        'market/stats',
                        'currencies',
                        'currencies/{currency}',
                        'prices',
                        'mark-price/{symbol}/current',
                        'margin/config',
                    ],
                    'post': [
                        'bullet-public',
                    ],
                },
                'private': {
                    'get': [
                        'accounts',
                        'accounts/{accountId}',
                        'accounts/{accountId}/ledgers',
                        'accounts/{accountId}/holds',
                        'accounts/transferable',
                        'sub/user',
                        'sub-accounts',
                        'sub-accounts/{subUserId}',
                        'deposit-addresses',
                        'deposits',
                        'hist-deposits',
                        'hist-orders',
                        'hist-withdrawals',
                        'withdrawals',
                        'withdrawals/quotas',
                        'orders',
                        'orders/{orderId}',
                        'limit/orders',
                        'fills',
                        'limit/fills',
                        'margin/account',
                        'margin/borrow',
                        'margin/borrow/outstanding',
                        'margin/borrow/borrow/repaid',
                        'margin/lend/active',
                        'margin/lend/done',
                        'margin/lend/trade/unsettled',
                        'margin/lend/trade/settled',
                        'margin/lend/assets',
                        'margin/market',
                        'margin/trade/last',
                    ],
                    'post': [
                        'accounts',
                        'accounts/inner-transfer',
                        'accounts/sub-transfer',
                        'deposit-addresses',
                        'withdrawals',
                        'orders',
                        'orders/multi',
                        'margin/borrow',
                        'margin/repay/all',
                        'margin/repay/single',
                        'margin/lend',
                        'margin/toggle-auto-lend',
                        'bullet-private',
                    ],
                    'delete': [
                        'withdrawals/{withdrawalId}',
                        'orders',
                        'orders/{orderId}',
                        'margin/lend/{orderId}',
                    ],
                },
            },
            'timeframes': {
                '1m': '1min',
                '3m': '3min',
                '5m': '5min',
                '15m': '15min',
                '30m': '30min',
                '1h': '1hour',
                '2h': '2hour',
                '4h': '4hour',
                '6h': '6hour',
                '8h': '8hour',
                '12h': '12hour',
                '1d': '1day',
                '1w': '1week',
            },
            'exceptions': {
                'exact': {
                    'order not exist': OrderNotFound,
                    'order not exist.': OrderNotFound,  # duplicated error temporarily
                    'order_not_exist': OrderNotFound,  # {"code":"order_not_exist","msg":"order_not_exist"} ¯\_(ツ)_/¯
                    'order_not_exist_or_not_allow_to_cancel': InvalidOrder,  # {"code":"400100","msg":"order_not_exist_or_not_allow_to_cancel"}
                    'Order size below the minimum requirement.': InvalidOrder,  # {"code":"400100","msg":"Order size below the minimum requirement."}
                    'The withdrawal amount is below the minimum requirement.': ExchangeError,  # {"code":"400100","msg":"The withdrawal amount is below the minimum requirement."}
                    '400': BadRequest,
                    '401': AuthenticationError,
                    '403': NotSupported,
                    '404': NotSupported,
                    '405': NotSupported,
                    '429': RateLimitExceeded,
                    '500': ExchangeNotAvailable,  # Internal Server Error -- We had a problem with our server. Try again later.
                    '503': ExchangeNotAvailable,
                    '101030': PermissionDenied,  # {"code":"101030","msg":"You haven't yet enabled the margin trading"}
                    '200004': InsufficientFunds,
                    '230003': InsufficientFunds,  # {"code":"230003","msg":"Balance insufficient!"}
                    '260100': InsufficientFunds,  # {"code":"260100","msg":"account.noBalance"}
                    '300000': InvalidOrder,
                    '400000': BadSymbol,
                    '400001': AuthenticationError,
                    '400002': InvalidNonce,
                    '400003': AuthenticationError,
                    '400004': AuthenticationError,
                    '400005': AuthenticationError,
                    '400006': AuthenticationError,
                    '400007': AuthenticationError,
                    '400008': NotSupported,
                    '400100': BadRequest,
                    '411100': AccountSuspended,
                    '415000': BadRequest,  # {"code":"415000","msg":"Unsupported Media Type"}
                    '500000': ExchangeError,
                },
                'broad': {
                    'Exceeded the access frequency': RateLimitExceeded,
                    'require more permission': PermissionDenied,
                },
            },
            'fees': {
                'trading': {
                    'tierBased': False,
                    'percentage': True,
                    'taker': 0.001,
                    'maker': 0.001,
                },
                'funding': {
                    'tierBased': False,
                    'percentage': False,
                    'withdraw': {},
                    'deposit': {},
                },
            },
            'commonCurrencies': {
                'HOT': 'HOTNOW',
                'EDGE': 'DADI',  # https://github.com/ccxt/ccxt/issues/5756
                'WAX': 'WAXP',
                'TRY': 'Trias',
            },
            'options': {
                'version': 'v1',
                'symbolSeparator': '-',
                'fetchMyTradesMethod': 'private_get_fills',
                'fetchBalance': {
                    'type': 'trade',  # or 'main'
                },
                # endpoint versions
                'versions': {
                    'public': {
                        'GET': {
                            'status': 'v1',
                            'market/orderbook/level{level}': 'v2',
                            'market/orderbook/level2': 'v2',
                            'market/orderbook/level2_20': 'v2',
                            'market/orderbook/level2_100': 'v2',
                        },
                    },
                    'private': {
                        'POST': {
                            'accounts/inner-transfer': 'v2',
                            'accounts/sub-transfer': 'v2',
                        },
                    },
                },
            },
        })

    def nonce(self):
        return self.milliseconds()

    async def load_time_difference(self, params={}):
        response = await self.publicGetTimestamp(params)
        after = self.milliseconds()
        kucoinTime = self.safe_integer(response, 'data')
        self.options['timeDifference'] = int(after - kucoinTime)
        return self.options['timeDifference']

    async def fetch_time(self, params={}):
        response = await self.publicGetTimestamp(params)
        #
        #     {
        #         "code":"200000",
        #         "msg":"success",
        #         "data":1546837113087
        #     }
        #
        return self.safe_integer(response, 'data')

    async def fetch_status(self, params={}):
        response = await self.publicGetStatus(params)
        #
        #     {
        #         "code":"200000",
        #         "data":{
        #             "msg":"",
        #             "status":"open"
        #         }
        #     }
        #
        data = self.safe_value(response, 'data', {})
        status = self.safe_value(data, 'status')
        if status is not None:
            status = 'ok' if (status == 'open') else 'maintenance'
            self.status = self.extend(self.status, {
                'status': status,
                'updated': self.milliseconds(),
            })
        return self.status

    async def fetch_markets(self, params={}):
        response = await self.publicGetSymbols(params)
        #
        #     {
        #         quoteCurrency: 'BTC',
        #         symbol: 'KCS-BTC',
        #         quoteMaxSize: '9999999',
        #         quoteIncrement: '0.000001',
        #         baseMinSize: '0.01',
        #         quoteMinSize: '0.00001',
        #         enableTrading: True,
        #         priceIncrement: '0.00000001',
        #         name: 'KCS-BTC',
        #         baseIncrement: '0.01',
        #         baseMaxSize: '9999999',
        #         baseCurrency: 'KCS'
        #     }
        #
        data = response['data']
        result = []
        for i in range(0, len(data)):
            market = data[i]
            id = self.safe_string(market, 'symbol')
            baseId, quoteId = id.split('-')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            symbol = base + '/' + quote
            active = self.safe_value(market, 'enableTrading')
            baseMaxSize = self.safe_float(market, 'baseMaxSize')
            baseMinSize = self.safe_float(market, 'baseMinSize')
            quoteMaxSize = self.safe_float(market, 'quoteMaxSize')
            quoteMinSize = self.safe_float(market, 'quoteMinSize')
            # quoteIncrement = self.safe_float(market, 'quoteIncrement')
            precision = {
                'amount': self.precision_from_string(self.safe_string(market, 'baseIncrement')),
                'price': self.precision_from_string(self.safe_string(market, 'priceIncrement')),
            }
            limits = {
                'amount': {
                    'min': baseMinSize,
                    'max': baseMaxSize,
                },
                'price': {
                    'min': self.safe_float(market, 'priceIncrement'),
                    'max': quoteMaxSize / baseMinSize,
                },
                'cost': {
                    'min': quoteMinSize,
                    'max': quoteMaxSize,
                },
            }
            result.append({
                'id': id,
                'symbol': symbol,
                'baseId': baseId,
                'quoteId': quoteId,
                'base': base,
                'quote': quote,
                'active': active,
                'precision': precision,
                'limits': limits,
                'info': market,
            })
        return result

    async def fetch_currencies(self, params={}):
        response = await self.publicGetCurrencies(params)
        #
        #     {
        #         "currency": "OMG",
        #         "name": "OMG",
        #         "fullName": "OmiseGO",
        #         "precision": 8,
        #         "confirms": 12,
        #         "withdrawalMinSize": "4",
        #         "withdrawalMinFee": "1.25",
        #         "isWithdrawEnabled": False,
        #         "isDepositEnabled": False,
        #         "isMarginEnabled": False,
        #         "isDebitEnabled": False
        #     }
        #
        data = self.safe_value(response, 'data', [])
        result = {}
        for i in range(0, len(data)):
            entry = data[i]
            id = self.safe_string(entry, 'currency')
            name = self.safe_string(entry, 'fullName')
            code = self.safe_currency_code(id)
            precision = self.safe_integer(entry, 'precision')
            isWithdrawEnabled = self.safe_value(entry, 'isWithdrawEnabled', False)
            isDepositEnabled = self.safe_value(entry, 'isDepositEnabled', False)
            fee = self.safe_float(entry, 'withdrawalMinFee')
            active = (isWithdrawEnabled and isDepositEnabled)
            result[code] = {
                'id': id,
                'name': name,
                'code': code,
                'precision': precision,
                'info': entry,
                'active': active,
                'fee': fee,
                'limits': self.limits,
            }
        return result

    async def fetch_accounts(self, params={}):
        response = await self.privateGetAccounts(params)
        #
        #     {
        #         code: "200000",
        #         data: [
        #             {
        #                 balance: "0.00009788",
        #                 available: "0.00009788",
        #                 holds: "0",
        #                 currency: "BTC",
        #                 id: "5c6a4fd399a1d81c4f9cc4d0",
        #                 type: "trade"
        #             },
        #             {
        #                 balance: "0.00000001",
        #                 available: "0.00000001",
        #                 holds: "0",
        #                 currency: "ETH",
        #                 id: "5c6a49ec99a1d819392e8e9f",
        #                 type: "trade"
        #             }
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data')
        result = []
        for i in range(0, len(data)):
            account = data[i]
            accountId = self.safe_string(account, 'id')
            currencyId = self.safe_string(account, 'currency')
            code = self.safe_currency_code(currencyId)
            type = self.safe_string(account, 'type')  # main or trade
            result.append({
                'id': accountId,
                'type': type,
                'currency': code,
                'info': account,
            })
        return result

    async def fetch_funding_fee(self, code, params={}):
        currencyId = self.currency_id(code)
        request = {
            'currency': currencyId,
        }
        response = await self.privateGetWithdrawalsQuotas(self.extend(request, params))
        data = response['data']
        withdrawFees = {}
        withdrawFees[code] = self.safe_float(data, 'withdrawMinFee')
        return {
            'info': response,
            'withdraw': withdrawFees,
            'deposit': {},
        }

    def parse_ticker(self, ticker, market=None):
        #
        #     {
        #         symbol: "ETH-BTC",
        #         high: "0.019518",
        #         vol: "7997.82836194",
        #         last: "0.019329",
        #         low: "0.019",
        #         buy: "0.019329",
        #         sell: "0.01933",
        #         changePrice: "-0.000139",
        #         time:  1580553706304,
        #         averagePrice: "0.01926386",
        #         changeRate: "-0.0071",
        #         volValue: "154.40791568183474"
        #     }
        #
        #     {
        #         "trading": True,
        #         "symbol": "KCS-BTC",
        #         "buy": 0.00011,
        #         "sell": 0.00012,
        #         "sort": 100,
        #         "volValue": 3.13851792584,   #total
        #         "baseCurrency": "KCS",
        #         "market": "BTC",
        #         "quoteCurrency": "BTC",
        #         "symbolCode": "KCS-BTC",
        #         "datetime": 1548388122031,
        #         "high": 0.00013,
        #         "vol": 27514.34842,
        #         "low": 0.0001,
        #         "changePrice": -1.0e-5,
        #         "changeRate": -0.0769,
        #         "lastTradedPrice": 0.00012,
        #         "board": 0,
        #         "mark": 0
        #     }
        #
        percentage = self.safe_float(ticker, 'changeRate')
        if percentage is not None:
            percentage = percentage * 100
        last = self.safe_float_2(ticker, 'last', 'lastTradedPrice')
        marketId = self.safe_string(ticker, 'symbol')
        symbol = self.safe_symbol(marketId, market, '-')
        baseVolume = self.safe_float(ticker, 'vol')
        quoteVolume = self.safe_float(ticker, 'volValue')
        vwap = self.vwap(baseVolume, quoteVolume)
        timestamp = self.safe_integer_2(ticker, 'time', 'datetime')
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': self.safe_float(ticker, 'high'),
            'low': self.safe_float(ticker, 'low'),
            'bid': self.safe_float(ticker, 'buy'),
            'bidVolume': None,
            'ask': self.safe_float(ticker, 'sell'),
            'askVolume': None,
            'vwap': vwap,
            'open': self.safe_float(ticker, 'open'),
            'close': last,
            'last': last,
            'previousClose': None,
            'change': self.safe_float(ticker, 'changePrice'),
            'percentage': percentage,
            'average': self.safe_float(ticker, 'averagePrice'),
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        }

    async def fetch_tickers(self, symbols=None, params={}):
        await self.load_markets()
        response = await self.publicGetMarketAllTickers(params)
        #
        #     {
        #         "code": "200000",
        #         "data": {
        #             "date": 1550661940645,
        #             "ticker": [
        #                 'buy': '0.00001168',
        #                 'changePrice': '-0.00000018',
        #                 'changeRate': '-0.0151',
        #                 'datetime': 1550661146316,
        #                 'high': '0.0000123',
        #                 'last': '0.00001169',
        #                 'low': '0.00001159',
        #                 'sell': '0.00001182',
        #                 'symbol': 'LOOM-BTC',
        #                 'vol': '44399.5669'
        #             },
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data', {})
        tickers = self.safe_value(data, 'ticker', [])
        result = {}
        for i in range(0, len(tickers)):
            ticker = self.parse_ticker(tickers[i])
            symbol = self.safe_string(ticker, 'symbol')
            if symbol is not None:
                result[symbol] = ticker
        return self.filter_by_array(result, 'symbol', symbols)

    async def fetch_ticker(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbol': market['id'],
        }
        response = await self.publicGetMarketStats(self.extend(request, params))
        #
        #     {
        #         "code": "200000",
        #         "data": {
        #             'buy': '0.00001168',
        #             'changePrice': '-0.00000018',
        #             'changeRate': '-0.0151',
        #             'datetime': 1550661146316,
        #             'high': '0.0000123',
        #             'last': '0.00001169',
        #             'low': '0.00001159',
        #             'sell': '0.00001182',
        #             'symbol': 'LOOM-BTC',
        #             'vol': '44399.5669'
        #         },
        #     }
        #
        return self.parse_ticker(response['data'], market)

    def parse_ohlcv(self, ohlcv, market=None):
        #
        #     [
        #         "1545904980",             # Start time of the candle cycle
        #         "0.058",                  # opening price
        #         "0.049",                  # closing price
        #         "0.058",                  # highest price
        #         "0.049",                  # lowest price
        #         "0.018",                  # base volume
        #         "0.000945",               # quote volume
        #     ]
        #
        return [
            self.safe_timestamp(ohlcv, 0),
            self.safe_float(ohlcv, 1),
            self.safe_float(ohlcv, 3),
            self.safe_float(ohlcv, 4),
            self.safe_float(ohlcv, 2),
            self.safe_float(ohlcv, 5),
        ]

    async def fetch_ohlcv(self, symbol, timeframe='15m', since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        marketId = market['id']
        request = {
            'symbol': marketId,
            'type': self.timeframes[timeframe],
        }
        duration = self.parse_timeframe(timeframe) * 1000
        endAt = self.milliseconds()  # required param
        if since is not None:
            request['startAt'] = int(int(math.floor(since / 1000)))
            if limit is None:
                # https://docs.kucoin.com/#get-klines
                # https://docs.kucoin.com/#details
                # For each query, the system would return at most 1500 pieces of data.
                # To obtain more data, please page the data by time.
                limit = self.safe_integer(self.options, 'fetchOHLCVLimit', 1500)
            endAt = self.sum(since, limit * duration)
        elif limit is not None:
            since = endAt - limit * duration
            request['startAt'] = int(int(math.floor(since / 1000)))
        request['endAt'] = int(int(math.floor(endAt / 1000)))
        response = await self.publicGetMarketCandles(self.extend(request, params))
        #
        #     {
        #         "code":"200000",
        #         "data":[
        #             ["1591517700","0.025078","0.025069","0.025084","0.025064","18.9883256","0.4761861079404"],
        #             ["1591516800","0.025089","0.025079","0.025089","0.02506","99.4716622","2.494143499081"],
        #             ["1591515900","0.025079","0.02509","0.025091","0.025068","59.83701271","1.50060885172798"],
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data', [])
        return self.parse_ohlcvs(data, market, timeframe, since, limit)

    async def create_deposit_address(self, code, params={}):
        await self.load_markets()
        currencyId = self.currency_id(code)
        request = {'currency': currencyId}
        response = await self.privatePostDepositAddresses(self.extend(request, params))
        # BCH {"code":"200000","data":{"address":"bitcoincash:qza3m4nj9rx7l9r0cdadfqxts6f92shvhvr5ls4q7z","memo":""}}
        # BTC {"code":"200000","data":{"address":"36SjucKqQpQSvsak9A7h6qzFjrVXpRNZhE","memo":""}}
        data = self.safe_value(response, 'data', {})
        address = self.safe_string(data, 'address')
        # BCH/BSV is returned with a "bitcoincash:" prefix, which we cut off here and only keep the address
        if address is not None:
            address = address.replace('bitcoincash:', '')
        tag = self.safe_string(data, 'memo')
        if code != 'NIM':
            # contains spaces
            self.check_address(address)
        return {
            'info': response,
            'currency': code,
            'address': address,
            'tag': tag,
        }

    async def fetch_deposit_address(self, code, params={}):
        await self.load_markets()
        currencyId = self.currency_id(code)
        request = {'currency': currencyId}
        response = await self.privateGetDepositAddresses(self.extend(request, params))
        # BCH {"code":"200000","data":{"address":"bitcoincash:qza3m4nj9rx7l9r0cdadfqxts6f92shvhvr5ls4q7z","memo":""}}
        # BTC {"code":"200000","data":{"address":"36SjucKqQpQSvsak9A7h6qzFjrVXpRNZhE","memo":""}}
        data = self.safe_value(response, 'data', {})
        address = self.safe_string(data, 'address')
        tag = self.safe_string(data, 'memo')
        if code != 'NIM':
            # contains spaces
            self.check_address(address)
        return {
            'info': response,
            'currency': code,
            'address': address,
            'tag': tag,
        }

    async def fetch_l3_order_book(self, symbol, limit=None, params={}):
        return await self.fetch_order_book(symbol, limit, {'level': 3})

    async def fetch_order_book(self, symbol, limit=None, params={}):
        level = self.safe_integer(params, 'level', 2)
        levelLimit = str(level)
        if levelLimit == '2':
            if limit is not None:
                if (limit != 20) and (limit != 100):
                    raise ExchangeError(self.id + ' fetchOrderBook limit argument must be None, 20 or 100')
                levelLimit += '_' + str(limit)
        await self.load_markets()
        marketId = self.market_id(symbol)
        request = {'symbol': marketId, 'level': levelLimit}
        response = await self.publicGetMarketOrderbookLevelLevel(self.extend(request, params))
        #
        # 'market/orderbook/level2'
        # 'market/orderbook/level2_20'
        # 'market/orderbook/level2_100'
        #
        #     {
        #         "code":"200000",
        #         "data":{
        #             "sequence":"1583235112106",
        #             "asks":[
        #                 # ...
        #                 ["0.023197","12.5067468"],
        #                 ["0.023194","1.8"],
        #                 ["0.023191","8.1069672"]
        #             ],
        #             "bids":[
        #                 ["0.02319","1.6000002"],
        #                 ["0.023189","2.2842325"],
        #             ],
        #             "time":1586584067274
        #         }
        #     }
        #
        # 'market/orderbook/level3'
        #
        #     {
        #         "code":"200000",
        #         "data":{
        #             "sequence":"1583731857120",
        #             "asks":[
        #                 # id, price, size, timestamp in nanoseconds
        #                 ["5e915f8acd26670009675300","6925.7","0.2","1586585482194286069"],
        #                 ["5e915f8ace35a200090bba48","6925.7","0.001","1586585482229569826"],
        #                 ["5e915f8a8857740009ca7d33","6926","0.00001819","1586585482149148621"],
        #             ],
        #             "bids":[
        #                 ["5e915f8acca406000ac88194","6925.6","0.05","1586585482384384842"],
        #                 ["5e915f93cd26670009676075","6925.6","0.08","1586585491334914600"],
        #                 ["5e915f906aa6e200099b49f6","6925.4","0.2","1586585488941126340"],
        #             ],
        #             "time":1586585492487
        #         }
        #     }
        #
        data = self.safe_value(response, 'data', {})
        timestamp = self.safe_integer(data, 'time')
        orderbook = self.parse_order_book(data, timestamp, 'bids', 'asks', level - 2, level - 1)
        orderbook['nonce'] = self.safe_integer(data, 'sequence')
        return orderbook

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        marketId = self.market_id(symbol)
        # required param, cannot be used twice
        clientOrderId = self.safe_string_2(params, 'clientOid', 'clientOrderId', self.uuid())
        params = self.omit(params, ['clientOid', 'clientOrderId'])
        request = {
            'clientOid': clientOrderId,
            'side': side,
            'symbol': marketId,
            'type': type,
        }
        if type != 'market':
            request['price'] = self.price_to_precision(symbol, price)
            request['size'] = self.amount_to_precision(symbol, amount)
        else:
            if self.safe_value(params, 'quoteAmount'):
                # used to create market order by quote amount - https://github.com/ccxt/ccxt/issues/4876
                request['funds'] = self.amount_to_precision(symbol, amount)
            else:
                request['size'] = self.amount_to_precision(symbol, amount)
        response = await self.privatePostOrders(self.extend(request, params))
        #
        #     {
        #         code: '200000',
        #         data: {
        #             "orderId": "5bd6e9286d99522a52e458de"
        #         }
        #    }
        #
        data = self.safe_value(response, 'data', {})
        timestamp = self.milliseconds()
        id = self.safe_string(data, 'orderId')
        order = {
            'id': id,
            'clientOrderId': clientOrderId,
            'info': data,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            'symbol': symbol,
            'type': type,
            'side': side,
            'price': price,
            'amount': None,
            'cost': None,
            'average': None,
            'filled': None,
            'remaining': None,
            'status': None,
            'fee': None,
            'trades': None,
        }
        if not self.safe_value(params, 'quoteAmount'):
            order['amount'] = amount
        return order

    async def cancel_order(self, id, symbol=None, params={}):
        request = {'orderId': id}
        response = await self.privateDeleteOrdersOrderId(self.extend(request, params))
        return response

    async def fetch_orders_by_status(self, status, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {
            'status': status,
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['symbol'] = market['id']
        if since is not None:
            request['startAt'] = since
        if limit is not None:
            request['pageSize'] = limit
        response = await self.privateGetOrders(self.extend(request, params))
        #
        #     {
        #         code: '200000',
        #         data: {
        #             "currentPage": 1,
        #             "pageSize": 1,
        #             "totalNum": 153408,
        #             "totalPage": 153408,
        #             "items": [
        #                 {
        #                     "id": "5c35c02703aa673ceec2a168",   #orderid
        #                     "symbol": "BTC-USDT",   #symbol
        #                     "opType": "DEAL",      # operation type,deal is pending order,cancel is cancel order
        #                     "type": "limit",       # order type,e.g. limit,markrt,stop_limit.
        #                     "side": "buy",         # transaction direction,include buy and sell
        #                     "price": "10",         # order price
        #                     "size": "2",           # order quantity
        #                     "funds": "0",          # order funds
        #                     "dealFunds": "0.166",  # deal funds
        #                     "dealSize": "2",       # deal quantity
        #                     "fee": "0",            # fee
        #                     "feeCurrency": "USDT",  # charge fee currency
        #                     "stp": "",             # self trade prevention,include CN,CO,DC,CB
        #                     "stop": "",            # stop type
        #                     "stopTriggered": False,  # stop order is triggered
        #                     "stopPrice": "0",      # stop price
        #                     "timeInForce": "GTC",  # time InForce,include GTC,GTT,IOC,FOK
        #                     "postOnly": False,     # postOnly
        #                     "hidden": False,       # hidden order
        #                     "iceberg": False,      # iceberg order
        #                     "visibleSize": "0",    # display quantity for iceberg order
        #                     "cancelAfter": 0,      # cancel orders time，requires timeInForce to be GTT
        #                     "channel": "IOS",      # order source
        #                     "clientOid": "",       # user-entered order unique mark
        #                     "remark": "",          # remark
        #                     "tags": "",            # tag order source
        #                     "isActive": False,     # status before unfilled or uncancelled
        #                     "cancelExist": False,   # order cancellation transaction record
        #                     "createdAt": 1547026471000  # time
        #                 },
        #             ]
        #         }
        #    }
        responseData = self.safe_value(response, 'data', {})
        orders = self.safe_value(responseData, 'items', [])
        return self.parse_orders(orders, market, since, limit)

    async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
        return await self.fetch_orders_by_status('done', symbol, since, limit, params)

    async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        return await self.fetch_orders_by_status('active', symbol, since, limit, params)

    async def fetch_order(self, id, symbol=None, params={}):
        await self.load_markets()
        # a special case for None ids
        # otherwise a wrong endpoint for all orders will be triggered
        # https://github.com/ccxt/ccxt/issues/7234
        if id is None:
            raise InvalidOrder(self.id + ' fetchOrder() requires an order id')
        request = {
            'orderId': id,
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
        response = await self.privateGetOrdersOrderId(self.extend(request, params))
        responseData = self.safe_value(response, 'data')
        return self.parse_order(responseData, market)

    def parse_order(self, order, market=None):
        #
        # fetchOpenOrders, fetchClosedOrders
        #
        #     {
        #         "id": "5c35c02703aa673ceec2a168",   #orderid
        #         "symbol": "BTC-USDT",   #symbol
        #         "opType": "DEAL",      # operation type,deal is pending order,cancel is cancel order
        #         "type": "limit",       # order type,e.g. limit,markrt,stop_limit.
        #         "side": "buy",         # transaction direction,include buy and sell
        #         "price": "10",         # order price
        #         "size": "2",           # order quantity
        #         "funds": "0",          # order funds
        #         "dealFunds": "0.166",  # deal funds
        #         "dealSize": "2",       # deal quantity
        #         "fee": "0",            # fee
        #         "feeCurrency": "USDT",  # charge fee currency
        #         "stp": "",             # self trade prevention,include CN,CO,DC,CB
        #         "stop": "",            # stop type
        #         "stopTriggered": False,  # stop order is triggered
        #         "stopPrice": "0",      # stop price
        #         "timeInForce": "GTC",  # time InForce,include GTC,GTT,IOC,FOK
        #         "postOnly": False,     # postOnly
        #         "hidden": False,       # hidden order
        #         "iceberg": False,      # iceberg order
        #         "visibleSize": "0",    # display quantity for iceberg order
        #         "cancelAfter": 0,      # cancel orders time，requires timeInForce to be GTT
        #         "channel": "IOS",      # order source
        #         "clientOid": "",       # user-entered order unique mark
        #         "remark": "",          # remark
        #         "tags": "",            # tag order source
        #         "isActive": False,     # status before unfilled or uncancelled
        #         "cancelExist": False,   # order cancellation transaction record
        #         "createdAt": 1547026471000  # time
        #     }
        #
        marketId = self.safe_string(order, 'symbol')
        symbol = self.safe_symbol(marketId, market, '-')
        orderId = self.safe_string(order, 'id')
        type = self.safe_string(order, 'type')
        timestamp = self.safe_integer(order, 'createdAt')
        datetime = self.iso8601(timestamp)
        price = self.safe_float(order, 'price')
        side = self.safe_string(order, 'side')
        feeCurrencyId = self.safe_string(order, 'feeCurrency')
        feeCurrency = self.safe_currency_code(feeCurrencyId)
        feeCost = self.safe_float(order, 'fee')
        amount = self.safe_float(order, 'size')
        filled = self.safe_float(order, 'dealSize')
        cost = self.safe_float(order, 'dealFunds')
        remaining = amount - filled
        # bool
        isActive = self.safe_value(order, 'isActive', False)
        cancelExist = self.safe_value(order, 'cancelExist', False)
        status = 'open' if isActive else 'closed'
        status = 'canceled' if cancelExist else status
        fee = {
            'currency': feeCurrency,
            'cost': feeCost,
        }
        if type == 'market':
            if price == 0.0:
                if (cost is not None) and (filled is not None):
                    if (cost > 0) and (filled > 0):
                        price = cost / filled
        clientOrderId = self.safe_string(order, 'clientOid')
        timeInForce = self.safe_string(order, 'timeInForce')
        stopPrice = self.safe_float(order, 'stopPrice')
        postOnly = self.safe_value(order, 'postOnly')
        return {
            'id': orderId,
            'clientOrderId': clientOrderId,
            'symbol': symbol,
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': side,
            'amount': amount,
            'price': price,
            'stopPrice': stopPrice,
            'cost': cost,
            'filled': filled,
            'remaining': remaining,
            'timestamp': timestamp,
            'datetime': datetime,
            'fee': fee,
            'status': status,
            'info': order,
            'lastTradeTimestamp': None,
            'average': None,
            'trades': None,
        }

    async def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['symbol'] = market['id']
        if limit is not None:
            request['pageSize'] = limit
        method = self.options['fetchMyTradesMethod']
        parseResponseData = False
        if method == 'private_get_fills':
            # does not return trades earlier than 2019-02-18T00:00:00Z
            if since is not None:
                # only returns trades up to one week after the since param
                request['startAt'] = since
        elif method == 'private_get_limit_fills':
            # does not return trades earlier than 2019-02-18T00:00:00Z
            # takes no params
            # only returns first 1000 trades(not only "in the last 24 hours" as stated in the docs)
            parseResponseData = True
        elif method == 'private_get_hist_orders':
            # despite that self endpoint is called `HistOrders`
            # it returns historical trades instead of orders
            # returns trades earlier than 2019-02-18T00:00:00Z only
            if since is not None:
                request['startAt'] = int(since / 1000)
        else:
            raise ExchangeError(self.id + ' invalid fetchClosedOrder method')
        response = await getattr(self, method)(self.extend(request, params))
        #
        #     {
        #         "currentPage": 1,
        #         "pageSize": 50,
        #         "totalNum": 1,
        #         "totalPage": 1,
        #         "items": [
        #             {
        #                 "symbol":"BTC-USDT",       # symbol
        #                 "tradeId":"5c35c02709e4f67d5266954e",        # trade id
        #                 "orderId":"5c35c02703aa673ceec2a168",        # order id
        #                 "counterOrderId":"5c1ab46003aa676e487fa8e3",  # counter order id
        #                 "side":"buy",              # transaction direction,include buy and sell
        #                 "liquidity":"taker",       # include taker and maker
        #                 "forceTaker":true,         # forced to become taker
        #                 "price":"0.083",           # order price
        #                 "size":"0.8424304",        # order quantity
        #                 "funds":"0.0699217232",    # order funds
        #                 "fee":"0",                 # fee
        #                 "feeRate":"0",             # fee rate
        #                 "feeCurrency":"USDT",      # charge fee currency
        #                 "stop":"",                 # stop type
        #                 "type":"limit",            # order type, e.g. limit, market, stop_limit.
        #                 "createdAt":1547026472000  # time
        #             },
        #             #------------------------------------------------------
        #             # v1(historical) trade response structure
        #             {
        #                 "symbol": "SNOV-ETH",
        #                 "dealPrice": "0.0000246",
        #                 "dealValue": "0.018942",
        #                 "amount": "770",
        #                 "fee": "0.00001137",
        #                 "side": "sell",
        #                 "createdAt": 1540080199
        #                 "id":"5c4d389e4c8c60413f78e2e5",
        #             }
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data', {})
        trades = None
        if parseResponseData:
            trades = data
        else:
            trades = self.safe_value(data, 'items', [])
        return self.parse_trades(trades, market, since, limit)

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbol': market['id'],
        }
        if since is not None:
            request['startAt'] = int(math.floor(since / 1000))
        if limit is not None:
            request['pageSize'] = limit
        response = await self.publicGetMarketHistories(self.extend(request, params))
        #
        #     {
        #         "code": "200000",
        #         "data": [
        #             {
        #                 "sequence": "1548764654235",
        #                 "side": "sell",
        #                 "size":"0.6841354",
        #                 "price":"0.03202",
        #                 "time":1548848575203567174
        #             }
        #         ]
        #     }
        #
        trades = self.safe_value(response, 'data', [])
        return self.parse_trades(trades, market, since, limit)

    def parse_trade(self, trade, market=None):
        #
        # fetchTrades(public)
        #
        #     {
        #         "sequence": "1548764654235",
        #         "side": "sell",
        #         "size":"0.6841354",
        #         "price":"0.03202",
        #         "time":1548848575203567174
        #     }
        #
        #     {
        #         sequence: '1568787654360',
        #         symbol: 'BTC-USDT',
        #         side: 'buy',
        #         size: '0.00536577',
        #         price: '9345',
        #         takerOrderId: '5e356c4a9f1a790008f8d921',
        #         time: '1580559434436443257',
        #         type: 'match',
        #         makerOrderId: '5e356bffedf0010008fa5d7f',
        #         tradeId: '5e356c4aeefabd62c62a1ece'
        #     }
        #
        # fetchMyTrades(private) v2
        #
        #     {
        #         "symbol":"BTC-USDT",
        #         "tradeId":"5c35c02709e4f67d5266954e",
        #         "orderId":"5c35c02703aa673ceec2a168",
        #         "counterOrderId":"5c1ab46003aa676e487fa8e3",
        #         "side":"buy",
        #         "liquidity":"taker",
        #         "forceTaker":true,
        #         "price":"0.083",
        #         "size":"0.8424304",
        #         "funds":"0.0699217232",
        #         "fee":"0",
        #         "feeRate":"0",
        #         "feeCurrency":"USDT",
        #         "stop":"",
        #         "type":"limit",
        #         "createdAt":1547026472000
        #     }
        #
        # fetchMyTrades v2 alternative format since 2019-05-21 https://github.com/ccxt/ccxt/pull/5162
        #
        #     {
        #         symbol: "OPEN-BTC",
        #         forceTaker:  False,
        #         orderId: "5ce36420054b4663b1fff2c9",
        #         fee: "0",
        #         feeCurrency: "",
        #         type: "",
        #         feeRate: "0",
        #         createdAt: 1558417615000,
        #         size: "12.8206",
        #         stop: "",
        #         price: "0",
        #         funds: "0",
        #         tradeId: "5ce390cf6e0db23b861c6e80"
        #     }
        #
        # fetchMyTrades(private) v1(historical)
        #
        #     {
        #         "symbol": "SNOV-ETH",
        #         "dealPrice": "0.0000246",
        #         "dealValue": "0.018942",
        #         "amount": "770",
        #         "fee": "0.00001137",
        #         "side": "sell",
        #         "createdAt": 1540080199
        #         "id":"5c4d389e4c8c60413f78e2e5",
        #     }
        #
        marketId = self.safe_string(trade, 'symbol')
        symbol = self.safe_symbol(marketId, market, '-')
        id = self.safe_string_2(trade, 'tradeId', 'id')
        orderId = self.safe_string(trade, 'orderId')
        takerOrMaker = self.safe_string(trade, 'liquidity')
        amount = self.safe_float_2(trade, 'size', 'amount')
        timestamp = self.safe_integer(trade, 'time')
        if timestamp is not None:
            timestamp = int(timestamp / 1000000)
        else:
            timestamp = self.safe_integer(trade, 'createdAt')
            # if it's a historical v1 trade, the exchange returns timestamp in seconds
            if ('dealValue' in trade) and (timestamp is not None):
                timestamp = timestamp * 1000
        price = self.safe_float_2(trade, 'price', 'dealPrice')
        side = self.safe_string(trade, 'side')
        fee = None
        feeCost = self.safe_float(trade, 'fee')
        if feeCost is not None:
            feeCurrencyId = self.safe_string(trade, 'feeCurrency')
            feeCurrency = self.safe_currency_code(feeCurrencyId)
            if feeCurrency is None:
                if market is not None:
                    feeCurrency = market['quote'] if (side == 'sell') else market['base']
            fee = {
                'cost': feeCost,
                'currency': feeCurrency,
                'rate': self.safe_float(trade, 'feeRate'),
            }
        type = self.safe_string(trade, 'type')
        if type == 'match':
            type = None
        cost = self.safe_float_2(trade, 'funds', 'dealValue')
        if cost is None:
            if amount is not None:
                if price is not None:
                    cost = amount * price
        return {
            'info': trade,
            'id': id,
            'order': orderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'type': type,
            'takerOrMaker': takerOrMaker,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        }

    async def withdraw(self, code, amount, address, tag=None, params={}):
        await self.load_markets()
        self.check_address(address)
        currency = self.currency_id(code)
        request = {
            'currency': currency,
            'address': address,
            'amount': amount,
        }
        if tag is not None:
            request['memo'] = tag
        response = await self.privatePostWithdrawals(self.extend(request, params))
        #
        # https://github.com/ccxt/ccxt/issues/5558
        #
        #     {
        #         "code":  200000,
        #         "data": {
        #             "withdrawalId":  "abcdefghijklmnopqrstuvwxyz"
        #         }
        #     }
        #
        data = self.safe_value(response, 'data', {})
        return {
            'id': self.safe_string(data, 'withdrawalId'),
            'info': response,
        }

    def parse_transaction_status(self, status):
        statuses = {
            'SUCCESS': 'ok',
            'PROCESSING': 'ok',
            'FAILURE': 'failed',
        }
        return self.safe_string(statuses, status)

    def parse_transaction(self, transaction, currency=None):
        #
        # fetchDeposits
        #
        #     {
        #         "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989",
        #         "memo": "5c247c8a03aa677cea2a251d",
        #         "amount": 1,
        #         "fee": 0.0001,
        #         "currency": "KCS",
        #         "isInner": False,
        #         "walletTxId": "5bbb57386d99522d9f954c5a@test004",
        #         "status": "SUCCESS",
        #         "createdAt": 1544178843000,
        #         "updatedAt": 1544178891000
        #     }
        #
        # fetchWithdrawals
        #
        #     {
        #         "id": "5c2dc64e03aa675aa263f1ac",
        #         "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9",
        #         "memo": "",
        #         "currency": "ETH",
        #         "amount": 1.0000000,
        #         "fee": 0.0100000,
        #         "walletTxId": "3e2414d82acce78d38be7fe9",
        #         "isInner": False,
        #         "status": "FAILURE",
        #         "createdAt": 1546503758000,
        #         "updatedAt": 1546504603000
        #     }
        #
        currencyId = self.safe_string(transaction, 'currency')
        code = self.safe_currency_code(currencyId, currency)
        address = self.safe_string(transaction, 'address')
        amount = self.safe_float(transaction, 'amount')
        txid = self.safe_string(transaction, 'walletTxId')
        if txid is not None:
            txidParts = txid.split('@')
            numTxidParts = len(txidParts)
            if numTxidParts > 1:
                if address is None:
                    if len(txidParts[1]) > 1:
                        address = txidParts[1]
            txid = txidParts[0]
        type = 'withdrawal' if (txid is None) else 'deposit'
        rawStatus = self.safe_string(transaction, 'status')
        status = self.parse_transaction_status(rawStatus)
        fee = None
        feeCost = self.safe_float(transaction, 'fee')
        if feeCost is not None:
            rate = None
            if amount is not None:
                rate = feeCost / amount
            fee = {
                'cost': feeCost,
                'rate': rate,
                'currency': code,
            }
        tag = self.safe_string(transaction, 'memo')
        timestamp = self.safe_integer_2(transaction, 'createdAt', 'createAt')
        id = self.safe_string(transaction, 'id')
        updated = self.safe_integer(transaction, 'updatedAt')
        isV1 = not ('createdAt' in transaction)
        # if it's a v1 structure
        if isV1:
            type = 'withdrawal' if ('address' in transaction) else 'deposit'
            if timestamp is not None:
                timestamp = timestamp * 1000
            if updated is not None:
                updated = updated * 1000
        return {
            'id': id,
            'address': address,
            'tag': tag,
            'currency': code,
            'amount': amount,
            'txid': txid,
            'type': type,
            'status': status,
            'fee': fee,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'updated': updated,
            'info': transaction,
        }

    async def fetch_deposits(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        if limit is not None:
            request['pageSize'] = limit
        method = 'privateGetDeposits'
        if since is not None:
            # if since is earlier than 2019-02-18T00:00:00Z
            if since < 1550448000000:
                request['startAt'] = int(since / 1000)
                method = 'privateGetHistDeposits'
            else:
                request['startAt'] = since
        response = await getattr(self, method)(self.extend(request, params))
        #
        #     {
        #         code: '200000',
        #         data: {
        #             "currentPage": 1,
        #             "pageSize": 5,
        #             "totalNum": 2,
        #             "totalPage": 1,
        #             "items": [
        #                 #--------------------------------------------------
        #                 # version 2 deposit response structure
        #                 {
        #                     "address": "0x5f047b29041bcfdbf0e4478cdfa753a336ba6989",
        #                     "memo": "5c247c8a03aa677cea2a251d",
        #                     "amount": 1,
        #                     "fee": 0.0001,
        #                     "currency": "KCS",
        #                     "isInner": False,
        #                     "walletTxId": "5bbb57386d99522d9f954c5a@test004",
        #                     "status": "SUCCESS",
        #                     "createdAt": 1544178843000,
        #                     "updatedAt": 1544178891000
        #                 },
        #                 #--------------------------------------------------
        #                 # version 1(historical) deposit response structure
        #                 {
        #                     "currency": "BTC",
        #                     "createAt": 1528536998,
        #                     "amount": "0.03266638",
        #                     "walletTxId": "55c643bc2c68d6f17266383ac1be9e454038864b929ae7cee0bc408cc5c869e8@12ffGWmMMD1zA1WbFm7Ho3JZ1w6NYXjpFk@234",
        #                     "isInner": False,
        #                     "status": "SUCCESS",
        #                 }
        #             ]
        #         }
        #     }
        #
        responseData = response['data']['items']
        return self.parse_transactions(responseData, currency, since, limit, {'type': 'deposit'})

    async def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        if limit is not None:
            request['pageSize'] = limit
        method = 'privateGetWithdrawals'
        if since is not None:
            # if since is earlier than 2019-02-18T00:00:00Z
            if since < 1550448000000:
                request['startAt'] = int(since / 1000)
                method = 'privateGetHistWithdrawals'
            else:
                request['startAt'] = since
        response = await getattr(self, method)(self.extend(request, params))
        #
        #     {
        #         code: '200000',
        #         data: {
        #             "currentPage": 1,
        #             "pageSize": 5,
        #             "totalNum": 2,
        #             "totalPage": 1,
        #             "items": [
        #                 #--------------------------------------------------
        #                 # version 2 withdrawal response structure
        #                 {
        #                     "id": "5c2dc64e03aa675aa263f1ac",
        #                     "address": "0x5bedb060b8eb8d823e2414d82acce78d38be7fe9",
        #                     "memo": "",
        #                     "currency": "ETH",
        #                     "amount": 1.0000000,
        #                     "fee": 0.0100000,
        #                     "walletTxId": "3e2414d82acce78d38be7fe9",
        #                     "isInner": False,
        #                     "status": "FAILURE",
        #                     "createdAt": 1546503758000,
        #                     "updatedAt": 1546504603000
        #                 },
        #                 #--------------------------------------------------
        #                 # version 1(historical) withdrawal response structure
        #                 {
        #                     "currency": "BTC",
        #                     "createAt": 1526723468,
        #                     "amount": "0.534",
        #                     "address": "33xW37ZSW4tQvg443Pc7NLCAs167Yc2XUV",
        #                     "walletTxId": "aeacea864c020acf58e51606169240e96774838dcd4f7ce48acf38e3651323f4",
        #                     "isInner": False,
        #                     "status": "SUCCESS"
        #                 }
        #             ]
        #         }
        #     }
        #
        responseData = response['data']['items']
        return self.parse_transactions(responseData, currency, since, limit, {'type': 'withdrawal'})

    async def fetch_balance(self, params={}):
        await self.load_markets()
        type = None
        request = {}
        if 'type' in params:
            type = params['type']
            if type is not None:
                request['type'] = type
            params = self.omit(params, 'type')
        else:
            options = self.safe_value(self.options, 'fetchBalance', {})
            type = self.safe_string(options, 'type', 'trade')
        response = await self.privateGetAccounts(self.extend(request, params))
        #
        #     {
        #         "code":"200000",
        #         "data":[
        #             {"balance":"0.00009788","available":"0.00009788","holds":"0","currency":"BTC","id":"5c6a4fd399a1d81c4f9cc4d0","type":"trade"},
        #             {"balance":"3.41060034","available":"3.41060034","holds":"0","currency":"SOUL","id":"5c6a4d5d99a1d8182d37046d","type":"trade"},
        #             {"balance":"0.01562641","available":"0.01562641","holds":"0","currency":"NEO","id":"5c6a4f1199a1d8165a99edb1","type":"trade"},
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data', [])
        result = {'info': response}
        for i in range(0, len(data)):
            balance = data[i]
            balanceType = self.safe_string(balance, 'type')
            if balanceType == type:
                currencyId = self.safe_string(balance, 'currency')
                code = self.safe_currency_code(currencyId)
                account = self.account()
                account['total'] = self.safe_float(balance, 'balance')
                account['free'] = self.safe_float(balance, 'available')
                account['used'] = self.safe_float(balance, 'holds')
                result[code] = account
        return self.parse_balance(result)

    async def fetch_ledger(self, code=None, since=None, limit=None, params={}):
        if code is None:
            raise ArgumentsRequired(self.id + ' fetchLedger() requires a code param')
        await self.load_markets()
        await self.load_accounts()
        currency = self.currency(code)
        accountId = self.safe_string(params, 'accountId')
        if accountId is None:
            for i in range(0, len(self.accounts)):
                account = self.accounts[i]
                if account['currency'] == code and account['type'] == 'main':
                    accountId = account['id']
                    break
        if accountId is None:
            raise ExchangeError(self.id + ' ' + code + 'main account is not loaded in loadAccounts')
        request = {
            'accountId': accountId,
        }
        if since is not None:
            request['startAt'] = int(math.floor(since / 1000))
        response = await self.privateGetAccountsAccountIdLedgers(self.extend(request, params))
        #
        #     {
        #         code: '200000',
        #         data: {
        #             totalNum: 1,
        #             totalPage: 1,
        #             pageSize: 50,
        #             currentPage: 1,
        #             items: [
        #                 {
        #                     createdAt: 1561897880000,
        #                     amount: '0.0111123',
        #                     bizType: 'Exchange',
        #                     balance: '0.13224427',
        #                     fee: '0.0000111',
        #                     context: '{"symbol":"KCS-ETH","orderId":"5d18ab98c788c6426188296f","tradeId":"5d18ab9818996813f539a806"}',
        #                     currency: 'ETH',
        #                     direction: 'out'
        #                 }
        #             ]
        #         }
        #     }
        #
        items = response['data']['items']
        return self.parse_ledger(items, currency, since, limit)

    def parse_ledger_entry(self, item, currency=None):
        #
        # trade
        #
        #     {
        #         createdAt: 1561897880000,
        #         amount: '0.0111123',
        #         bizType: 'Exchange',
        #         balance: '0.13224427',
        #         fee: '0.0000111',
        #         context: '{"symbol":"KCS-ETH","orderId":"5d18ab98c788c6426188296f","tradeId":"5d18ab9818996813f539a806"}',
        #         currency: 'ETH',
        #         direction: 'out'
        #     }
        #
        # withdrawal
        #
        #     {
        #         createdAt: 1561900264000,
        #         amount: '0.14333217',
        #         bizType: 'Withdrawal',
        #         balance: '0',
        #         fee: '0.01',
        #         context: '{"orderId":"5d18b4e687111437cf1c48b9","txId":"0x1d136ee065c5c4c5caa293faa90d43e213c953d7cdd575c89ed0b54eb87228b8"}',
        #         currency: 'ETH',
        #         direction: 'out'
        #     }
        #
        currencyId = self.safe_string(item, 'currency')
        code = self.safe_currency_code(currencyId, currency)
        fee = {
            'cost': self.safe_float(item, 'fee'),
            'code': code,
        }
        amount = self.safe_float(item, 'amount')
        after = self.safe_float(item, 'balance')
        direction = self.safe_string(item, 'direction')
        before = None
        if after is not None and amount is not None:
            difference = amount if (direction == 'out') else -amount
            before = self.sum(after, difference)
        timestamp = self.safe_integer(item, 'createdAt')
        type = self.parse_ledger_entry_type(self.safe_string(item, 'bizType'))
        contextString = self.safe_string(item, 'context')
        id = None
        referenceId = None
        if self.is_json_encoded_object(contextString):
            context = self.parse_json(contextString)
            id = self.safe_string(context, 'orderId')
            if type == 'trade':
                referenceId = self.safe_string(context, 'tradeId')
            elif type == 'transaction':
                referenceId = self.safe_string(context, 'txId')
        return {
            'id': id,
            'currency': code,
            'account': None,
            'referenceAccount': None,
            'referenceId': referenceId,
            'status': None,
            'amount': amount,
            'before': before,
            'after': after,
            'fee': fee,
            'direction': direction,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'type': type,
            'info': item,
        }

    def parse_ledger_entry_type(self, type):
        types = {
            'Exchange': 'trade',
            'Withdrawal': 'transaction',
            'Deposit': 'transaction',
            'Transfer': 'transfer',
        }
        return self.safe_string(types, type, type)

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        #
        # the v2 URL is https://openapi-v2.kucoin.com/api/v1/endpoint
        #                                †                 ↑
        #
        versions = self.safe_value(self.options, 'versions', {})
        apiVersions = self.safe_value(versions, api)
        methodVersions = self.safe_value(apiVersions, method, {})
        defaultVersion = self.safe_string(methodVersions, path, self.options['version'])
        version = self.safe_string(params, 'version', defaultVersion)
        params = self.omit(params, 'version')
        endpoint = '/api/' + version + '/' + self.implode_params(path, params)
        query = self.omit(params, self.extract_params(path))
        endpart = ''
        headers = headers if (headers is not None) else {}
        if query:
            if method != 'GET':
                body = self.json(query)
                endpart = body
                headers['Content-Type'] = 'application/json'
            else:
                endpoint += '?' + self.urlencode(query)
        url = self.urls['api'][api] + endpoint
        if api == 'private':
            self.check_required_credentials()
            timestamp = str(self.nonce())
            headers = self.extend({
                'KC-API-KEY-VERSION': '2',
                'KC-API-KEY': self.apiKey,
                'KC-API-TIMESTAMP': timestamp,
            }, headers)
            apiKeyVersion = self.safe_string(headers, 'KC-API-KEY-VERSION')
            if apiKeyVersion == '2':
                passphrase = self.hmac(self.encode(self.password), self.encode(self.secret), hashlib.sha256, 'base64')
                headers['KC-API-PASSPHRASE'] = passphrase
            else:
                headers['KC-API-PASSPHRASE'] = self.password
            payload = timestamp + method + endpoint + endpart
            signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64')
            headers['KC-API-SIGN'] = signature
            partner = self.safe_value(self.options, 'partner', {})
            partnerId = self.safe_string(partner, 'id')
            partnerSecret = self.safe_string(partner, 'secret')
            if (partnerId is not None) and (partnerSecret is not None):
                partnerPayload = timestamp + partnerId + self.apiKey
                partnerSignature = self.hmac(self.encode(partnerPayload), self.encode(partnerSecret), hashlib.sha256, 'base64')
                headers['KC-API-PARTNER-SIGN'] = partnerSignature
                headers['KC-API-PARTNER'] = partnerId
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if not response:
            self.throw_broadly_matched_exception(self.exceptions['broad'], body, body)
            return
        #
        # bad
        #     {"code": "400100", "msg": "validation.createOrder.clientOidIsRequired"}
        # good
        #     {code: '200000', data: {...}}
        #
        errorCode = self.safe_string(response, 'code')
        message = self.safe_string(response, 'msg')
        self.throw_exactly_matched_exception(self.exceptions['exact'], message, message)
        self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, message)
