# -*- 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 BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import CancelPending
from ccxt.base.errors import DDoSProtection
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.errors import OnMaintenance
from ccxt.base.errors import InvalidNonce
from ccxt.base.errors import RequestTimeout


class poloniex(Exchange):

    def describe(self):
        return self.deep_extend(super(poloniex, self).describe(), {
            'id': 'poloniex',
            'name': 'Poloniex',
            'countries': ['US'],
            'rateLimit': 1000,  # up to 6 calls per second
            'certified': False,
            'pro': True,
            'has': {
                'cancelOrder': True,
                'CORS': False,
                'createDepositAddress': True,
                'createMarketOrder': False,
                'createOrder': True,
                'editOrder': True,
                'fetchBalance': True,
                'fetchCurrencies': True,
                'fetchDepositAddress': True,
                'fetchDeposits': True,
                'fetchMarkets': True,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrder': True,  # True endpoint for a single open order
                'fetchOpenOrders': True,  # True endpoint for open orders
                'fetchOrderBook': True,
                'fetchOrderBooks': True,
                'fetchOrderTrades': True,  # True endpoint for trades of a single open or closed order
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTrades': True,
                'fetchTradingFee': True,
                'fetchTradingFees': True,
                'fetchTransactions': True,
                'fetchWithdrawals': True,
                'cancelAllOrders': True,
                'withdraw': True,
            },
            'timeframes': {
                '5m': 300,
                '15m': 900,
                '30m': 1800,
                '2h': 7200,
                '4h': 14400,
                '1d': 86400,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/27766817-e9456312-5ee6-11e7-9b3c-b628ca5626a5.jpg',
                'api': {
                    'public': 'https://poloniex.com/public',
                    'private': 'https://poloniex.com/tradingApi',
                },
                'www': 'https://www.poloniex.com',
                'doc': 'https://docs.poloniex.com',
                'fees': 'https://poloniex.com/fees',
                'referral': 'https://poloniex.com/signup?c=UBFZJRPJ',
            },
            'api': {
                'public': {
                    'get': [
                        'return24hVolume',
                        'returnChartData',
                        'returnCurrencies',
                        'returnLoanOrders',
                        'returnOrderBook',
                        'returnTicker',
                        'returnTradeHistory',
                    ],
                },
                'private': {
                    'post': [
                        'buy',
                        'cancelLoanOffer',
                        'cancelOrder',
                        'cancelAllOrders',
                        'closeMarginPosition',
                        'createLoanOffer',
                        'generateNewAddress',
                        'getMarginPosition',
                        'marginBuy',
                        'marginSell',
                        'moveOrder',
                        'returnActiveLoans',
                        'returnAvailableAccountBalances',
                        'returnBalances',
                        'returnCompleteBalances',
                        'returnDepositAddresses',
                        'returnDepositsWithdrawals',
                        'returnFeeInfo',
                        'returnLendingHistory',
                        'returnMarginAccountSummary',
                        'returnOpenLoanOffers',
                        'returnOpenOrders',
                        'returnOrderTrades',
                        'returnOrderStatus',
                        'returnTradableBalances',
                        'returnTradeHistory',
                        'sell',
                        'toggleAutoRenew',
                        'transferBalance',
                        'withdraw',
                    ],
                },
            },
            'fees': {
                'trading': {
                    # starting from Jan 8 2020
                    'maker': 0.0009,
                    'taker': 0.0009,
                },
                'funding': {},
            },
            'limits': {
                'amount': {
                    'min': 0.000001,
                    'max': 1000000000,
                },
                'price': {
                    'min': 0.00000001,
                    'max': 1000000000,
                },
                'cost': {
                    'min': 0.00000000,
                    'max': 1000000000,
                },
            },
            'precision': {
                'amount': 8,
                'price': 8,
            },
            'commonCurrencies': {
                'AIR': 'AirCoin',
                'APH': 'AphroditeCoin',
                'BCC': 'BTCtalkcoin',
                'BDG': 'Badgercoin',
                'BTM': 'Bitmark',
                'CON': 'Coino',
                'GOLD': 'GoldEagles',
                'GPUC': 'GPU',
                'HOT': 'Hotcoin',
                'ITC': 'Information Coin',
                'KEY': 'KEYCoin',
                'PLX': 'ParallaxCoin',
                'REPV2': 'REP',
                'STR': 'XLM',
                'SOC': 'SOCC',
                'XAP': 'API Coin',
                # self is not documented in the API docs for Poloniex
                # https://github.com/ccxt/ccxt/issues/7084
                # when the user calls withdraw('USDT', amount, address, tag, params)
                # with params = {'currencyToWithdrawAs': 'USDTTRON'}
                # or params = {'currencyToWithdrawAs': 'USDTETH'}
                # fetchWithdrawals('USDT') returns the corresponding withdrawals
                # with a USDTTRON or a USDTETH currency id, respectfully
                # therefore we have map them back to the original code USDT
                # otherwise the returned withdrawals are filtered out
                'USDTTRON': 'USDT',
                'USDTETH': 'USDT',
            },
            'options': {
                'limits': {
                    'cost': {
                        'min': {
                            'BTC': 0.0001,
                            'ETH': 0.0001,
                            'XMR': 0.0001,
                            'USDT': 1.0,
                        },
                    },
                },
            },
            'exceptions': {
                'exact': {
                    'You may only place orders that reduce your position.': InvalidOrder,
                    'Invalid order number, or you are not the person who placed the order.': OrderNotFound,
                    'Permission denied': PermissionDenied,
                    'Connection timed out. Please try again.': RequestTimeout,
                    'Internal error. Please try again.': ExchangeNotAvailable,
                    'Currently in maintenance mode.': OnMaintenance,
                    'Order not found, or you are not the person who placed it.': OrderNotFound,
                    'Invalid API key/secret pair.': AuthenticationError,
                    'Please do not make more than 8 API calls per second.': DDoSProtection,
                    'Rate must be greater than zero.': InvalidOrder,  # {"error":"Rate must be greater than zero."}
                    'Invalid currency pair.': BadSymbol,  # {"error":"Invalid currency pair."}
                    'Invalid currencyPair parameter.': BadSymbol,  # {"error":"Invalid currencyPair parameter."}
                },
                'broad': {
                    'Total must be at least': InvalidOrder,  # {"error":"Total must be at least 0.0001."}
                    'This account is frozen.': AccountSuspended,
                    'Not enough': InsufficientFunds,
                    'Nonce must be greater': InvalidNonce,
                    'You have already called cancelOrder or moveOrder on self order.': CancelPending,
                    'Amount must be at least': InvalidOrder,  # {"error":"Amount must be at least 0.000001."}
                    'is either completed or does not exist': InvalidOrder,  # {"error":"Order 587957810791 is either completed or does not exist."}
                    'Error pulling ': ExchangeError,  # {"error":"Error pulling order book"}
                },
            },
            'orders': {},  # orders cache / emulation
        })

    def calculate_fee(self, symbol, type, side, amount, price, takerOrMaker='taker', params={}):
        market = self.markets[symbol]
        key = 'quote'
        rate = market[takerOrMaker]
        cost = float(self.cost_to_precision(symbol, amount * rate))
        if side == 'sell':
            cost *= price
        else:
            key = 'base'
        return {
            'type': takerOrMaker,
            'currency': market[key],
            'rate': rate,
            'cost': float(self.fee_to_precision(symbol, cost)),
        }

    def parse_ohlcv(self, ohlcv, market=None):
        #
        #     {
        #         "date":1590913773,
        #         "high":0.02491611,
        #         "low":0.02491611,
        #         "open":0.02491611,
        #         "close":0.02491611,
        #         "volume":0,
        #         "quoteVolume":0,
        #         "weightedAverage":0.02491611
        #     }
        #
        return [
            self.safe_timestamp(ohlcv, 'date'),
            self.safe_float(ohlcv, 'open'),
            self.safe_float(ohlcv, 'high'),
            self.safe_float(ohlcv, 'low'),
            self.safe_float(ohlcv, 'close'),
            self.safe_float(ohlcv, 'quoteVolume'),
        ]

    async def fetch_ohlcv(self, symbol, timeframe='5m', since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'currencyPair': market['id'],
            'period': self.timeframes[timeframe],
        }
        if since is None:
            request['end'] = self.seconds()
            if limit is None:
                request['start'] = request['end'] - self.parse_timeframe('1w')  # max range = 1 week
            else:
                request['start'] = request['end'] - limit * self.parse_timeframe(timeframe)
        else:
            request['start'] = int(since / 1000)
            if limit is not None:
                end = self.sum(request['start'], limit * self.parse_timeframe(timeframe))
                request['end'] = end
        response = await self.publicGetReturnChartData(self.extend(request, params))
        #
        #     [
        #         {"date":1590913773,"high":0.02491611,"low":0.02491611,"open":0.02491611,"close":0.02491611,"volume":0,"quoteVolume":0,"weightedAverage":0.02491611},
        #         {"date":1590913800,"high":0.02495324,"low":0.02489501,"open":0.02491797,"close":0.02493693,"volume":0.0927415,"quoteVolume":3.7227869,"weightedAverage":0.02491185},
        #         {"date":1590914100,"high":0.02498596,"low":0.02488503,"open":0.02493033,"close":0.02497896,"volume":0.21196348,"quoteVolume":8.50291888,"weightedAverage":0.02492832},
        #     ]
        #
        return self.parse_ohlcvs(response, market, timeframe, since, limit)

    async def load_markets(self, reload=False, params={}):
        markets = await super(poloniex, self).load_markets(reload, params)
        currenciesByNumericId = self.safe_value(self.options, 'currenciesByNumericId')
        if (currenciesByNumericId is None) or reload:
            self.options['currenciesByNumericId'] = self.index_by(self.currencies, 'numericId')
        return markets

    async def fetch_markets(self, params={}):
        markets = await self.publicGetReturnTicker(params)
        keys = list(markets.keys())
        result = []
        for i in range(0, len(keys)):
            id = keys[i]
            market = markets[id]
            quoteId, baseId = id.split('_')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            symbol = base + '/' + quote
            limits = self.extend(self.limits, {
                'cost': {
                    'min': self.safe_value(self.options['limits']['cost']['min'], quote),
                },
            })
            isFrozen = self.safe_string(market, 'isFrozen')
            active = (isFrozen != '1')
            numericId = self.safe_integer(market, 'id')
            result.append({
                'id': id,
                'numericId': numericId,
                'symbol': symbol,
                'baseId': baseId,
                'quoteId': quoteId,
                'base': base,
                'quote': quote,
                'active': active,
                'limits': limits,
                'info': market,
            })
        return result

    async def fetch_balance(self, params={}):
        await self.load_markets()
        request = {
            'account': 'all',
        }
        response = await self.privatePostReturnCompleteBalances(self.extend(request, params))
        result = {'info': response}
        currencyIds = list(response.keys())
        for i in range(0, len(currencyIds)):
            currencyId = currencyIds[i]
            balance = self.safe_value(response, currencyId, {})
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['free'] = self.safe_float(balance, 'available')
            account['used'] = self.safe_float(balance, 'onOrders')
            result[code] = account
        return self.parse_balance(result)

    async def fetch_trading_fees(self, params={}):
        await self.load_markets()
        fees = await self.privatePostReturnFeeInfo(params)
        #
        #     {
        #         makerFee: '0.00100000',
        #         takerFee: '0.00200000',
        #         marginMakerFee: '0.00100000',
        #         marginTakerFee: '0.00200000',
        #         thirtyDayVolume: '106.08463302',
        #         nextTier: 500000,
        #     }
        #
        return {
            'info': fees,
            'maker': self.safe_float(fees, 'makerFee'),
            'taker': self.safe_float(fees, 'takerFee'),
            'withdraw': {},
            'deposit': {},
        }

    async def fetch_order_book(self, symbol, limit=None, params={}):
        await self.load_markets()
        request = {
            'currencyPair': self.market_id(symbol),
        }
        if limit is not None:
            request['depth'] = limit  # 100
        response = await self.publicGetReturnOrderBook(self.extend(request, params))
        orderbook = self.parse_order_book(response)
        orderbook['nonce'] = self.safe_integer(response, 'seq')
        return orderbook

    async def fetch_order_books(self, symbols=None, limit=None, params={}):
        await self.load_markets()
        request = {
            'currencyPair': 'all',
        }
        if limit is not None:
            request['depth'] = limit  # 100
        response = await self.publicGetReturnOrderBook(self.extend(request, params))
        marketIds = list(response.keys())
        result = {}
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            symbol = None
            if marketId in self.markets_by_id:
                symbol = self.markets_by_id[marketId]['symbol']
            else:
                quoteId, baseId = marketId.split('_')
                base = self.safe_currency_code(baseId)
                quote = self.safe_currency_code(quoteId)
                symbol = base + '/' + quote
            orderbook = self.parse_order_book(response[marketId])
            orderbook['nonce'] = self.safe_integer(response[marketId], 'seq')
            result[symbol] = orderbook
        return result

    def parse_ticker(self, ticker, market=None):
        timestamp = self.milliseconds()
        symbol = None
        if market:
            symbol = market['symbol']
        open = None
        change = None
        average = None
        last = self.safe_float(ticker, 'last')
        relativeChange = self.safe_float(ticker, 'percentChange')
        if relativeChange != -1:
            open = last / self.sum(1, relativeChange)
            change = last - open
            average = self.sum(last, open) / 2
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': self.safe_float(ticker, 'high24hr'),
            'low': self.safe_float(ticker, 'low24hr'),
            'bid': self.safe_float(ticker, 'highestBid'),
            'bidVolume': None,
            'ask': self.safe_float(ticker, 'lowestAsk'),
            'askVolume': None,
            'vwap': None,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': change,
            'percentage': relativeChange * 100,
            'average': average,
            'baseVolume': self.safe_float(ticker, 'quoteVolume'),
            'quoteVolume': self.safe_float(ticker, 'baseVolume'),
            'info': ticker,
        }

    async def fetch_tickers(self, symbols=None, params={}):
        await self.load_markets()
        response = await self.publicGetReturnTicker(params)
        ids = list(response.keys())
        result = {}
        for i in range(0, len(ids)):
            id = ids[i]
            symbol = None
            market = None
            if id in self.markets_by_id:
                market = self.markets_by_id[id]
                symbol = market['symbol']
            else:
                quoteId, baseId = id.split('_')
                base = self.safe_currency_code(baseId)
                quote = self.safe_currency_code(quoteId)
                symbol = base + '/' + quote
                market = {'symbol': symbol}
            ticker = response[id]
            result[symbol] = self.parse_ticker(ticker, market)
        return self.filter_by_array(result, 'symbol', symbols)

    async def fetch_currencies(self, params={}):
        response = await self.publicGetReturnCurrencies(params)
        ids = list(response.keys())
        result = {}
        for i in range(0, len(ids)):
            id = ids[i]
            currency = response[id]
            precision = 8  # default precision, todo: fix "magic constants"
            code = self.safe_currency_code(id)
            active = (currency['delisted'] == 0) and not currency['disabled']
            numericId = self.safe_integer(currency, 'id')
            fee = self.safe_float(currency, 'txFee')
            result[code] = {
                'id': id,
                'numericId': numericId,
                'code': code,
                'info': currency,
                'name': currency['name'],
                'active': active,
                'fee': fee,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': math.pow(10, -precision),
                        'max': math.pow(10, precision),
                    },
                    'price': {
                        'min': math.pow(10, -precision),
                        'max': math.pow(10, precision),
                    },
                    'cost': {
                        'min': None,
                        'max': None,
                    },
                    'withdraw': {
                        'min': fee,
                        'max': math.pow(10, precision),
                    },
                },
            }
        return result

    async def fetch_ticker(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        response = await self.publicGetReturnTicker(params)
        ticker = response[market['id']]
        return self.parse_ticker(ticker, market)

    def parse_trade(self, trade, market=None):
        #
        # fetchMyTrades
        #
        #     {
        #       globalTradeID: 471030550,
        #       tradeID: '42582',
        #       date: '2020-06-16 09:47:50',
        #       rate: '0.000079980000',
        #       amount: '75215.00000000',
        #       total: '6.01569570',
        #       fee: '0.00095000',
        #       feeDisplay: '0.26636100 TRX(0.07125%)',
        #       orderNumber: '5963454848',
        #       type: 'sell',
        #       category: 'exchange'
        #     }
        #
        # createOrder(taker trades)
        #
        #     {
        #         'amount': '200.00000000',
        #         'date': '2019-12-15 16:04:10',
        #         'rate': '0.00000355',
        #         'total': '0.00071000',
        #         'tradeID': '119871',
        #         'type': 'buy',
        #         'takerAdjustment': '200.00000000'
        #     }
        #
        id = self.safe_string_2(trade, 'globalTradeID', 'tradeID')
        orderId = self.safe_string(trade, 'orderNumber')
        timestamp = self.parse8601(self.safe_string(trade, 'date'))
        symbol = None
        if (not market) and ('currencyPair' in trade):
            marketId = self.safe_string(trade, 'currencyPair')
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
            else:
                quoteId, baseId = marketId.split('_')
                base = self.safe_currency_code(baseId)
                quote = self.safe_currency_code(quoteId)
                symbol = base + '/' + quote
        if (symbol is None) and (market is not None):
            symbol = market['symbol']
        side = self.safe_string(trade, 'type')
        fee = None
        price = self.safe_float(trade, 'rate')
        cost = self.safe_float(trade, 'total')
        amount = self.safe_float(trade, 'amount')
        feeDisplay = self.safe_string(trade, 'feeDisplay')
        if feeDisplay is not None:
            parts = feeDisplay.split(' ')
            feeCost = self.safe_float(parts, 0)
            if feeCost is not None:
                feeCurrencyId = self.safe_string(parts, 1)
                feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
                feeRate = self.safe_string(parts, 2)
                if feeRate is not None:
                    feeRate = feeRate.replace('(', '')
                    feeRateParts = feeRate.split('%')
                    feeRate = self.safe_string(feeRateParts, 0)
                    feeRate = float(feeRate) / 100
                fee = {
                    'cost': feeCost,
                    'currency': feeCurrencyCode,
                    'rate': feeRate,
                }
        takerOrMaker = None
        takerAdjustment = self.safe_float(trade, 'takerAdjustment')
        if takerAdjustment is not None:
            takerOrMaker = 'taker'
        return {
            'id': id,
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'order': orderId,
            'type': 'limit',
            'side': side,
            'takerOrMaker': takerOrMaker,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        }

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'currencyPair': market['id'],
        }
        if since is not None:
            request['start'] = int(since / 1000)
            request['end'] = self.seconds()  # last 50000 trades by default
        trades = await self.publicGetReturnTradeHistory(self.extend(request, params))
        return self.parse_trades(trades, market, since, limit)

    async def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        pair = market['id'] if market else 'all'
        request = {'currencyPair': pair}
        if since is not None:
            request['start'] = int(since / 1000)
            request['end'] = self.sum(self.seconds(), 1)  # adding 1 is a fix for  #3411
        # limit is disabled(does not really work as expected)
        if limit is not None:
            request['limit'] = int(limit)
        response = await self.privatePostReturnTradeHistory(self.extend(request, params))
        #
        # specific market(symbol defined)
        #
        #     [
        #         {
        #             globalTradeID: 470912587,
        #             tradeID: '42543',
        #             date: '2020-06-15 17:31:22',
        #             rate: '0.000083840000',
        #             amount: '95237.60321429',
        #             total: '7.98472065',
        #             fee: '0.00095000',
        #             feeDisplay: '0.36137761 TRX(0.07125%)',
        #             orderNumber: '5926344995',
        #             type: 'sell',
        #             category: 'exchange'
        #         },
        #         {
        #             globalTradeID: 470974497,
        #             tradeID: '42560',
        #             date: '2020-06-16 00:41:23',
        #             rate: '0.000078220000',
        #             amount: '1000000.00000000',
        #             total: '78.22000000',
        #             fee: '0.00095000',
        #             feeDisplay: '3.48189819 TRX(0.07125%)',
        #             orderNumber: '5945490830',
        #             type: 'sell',
        #             category: 'exchange'
        #         }
        #     ]
        #
        # all markets(symbol None)
        #
        #     {
        #        BTC_GNT: [{
        #             globalTradeID: 470839947,
        #             tradeID: '4322347',
        #             date: '2020-06-15 12:25:24',
        #             rate: '0.000005810000',
        #             amount: '1702.04429303',
        #             total: '0.00988887',
        #             fee: '0.00095000',
        #             feeDisplay: '4.18235294 TRX(0.07125%)',
        #             orderNumber: '102290272520',
        #             type: 'buy',
        #             category: 'exchange'
        #     }, {
        #             globalTradeID: 470895902,
        #             tradeID: '4322413',
        #             date: '2020-06-15 16:19:00',
        #             rate: '0.000005980000',
        #             amount: '18.66879219',
        #             total: '0.00011163',
        #             fee: '0.00095000',
        #             feeDisplay: '0.04733727 TRX(0.07125%)',
        #             orderNumber: '102298304480',
        #             type: 'buy',
        #             category: 'exchange'
        #         }],
        #     }
        #
        result = []
        if market is not None:
            result = self.parse_trades(response, market)
        else:
            if response:
                ids = list(response.keys())
                for i in range(0, len(ids)):
                    id = ids[i]
                    market = None
                    if id in self.markets_by_id:
                        market = self.markets_by_id[id]
                        trades = self.parse_trades(response[id], market)
                        for j in range(0, len(trades)):
                            result.append(trades[j])
                    else:
                        quoteId, baseId = id.split('_')
                        base = self.safe_currency_code(baseId)
                        quote = self.safe_currency_code(quoteId)
                        symbol = base + '/' + quote
                        trades = response[id]
                        for j in range(0, len(trades)):
                            market = {
                                'symbol': symbol,
                                'base': base,
                                'quote': quote,
                            }
                            result.append(self.parse_trade(trades[j], market))
        return self.filter_by_since_limit(result, since, limit)

    def parse_order_status(self, status):
        statuses = {
            'Open': 'open',
            'Partially filled': 'open',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        #
        # fetchOpenOrder
        #
        #     {
        #         status: 'Open',
        #         rate: '0.40000000',
        #         amount: '1.00000000',
        #         currencyPair: 'BTC_ETH',
        #         date: '2018-10-17 17:04:50',
        #         total: '0.40000000',
        #         type: 'buy',
        #         startingAmount: '1.00000',
        #     }
        #
        # fetchOpenOrders
        #
        #     {
        #         orderNumber: '514514894224',
        #         type: 'buy',
        #         rate: '0.00001000',
        #         startingAmount: '100.00000000',
        #         amount: '100.00000000',
        #         total: '0.00100000',
        #         date: '2018-10-23 17:38:53',
        #         margin: 0,
        #     }
        #
        # createOrder
        #
        #     {
        #         'orderNumber': '9805453960',
        #         'resultingTrades': [
        #             {
        #                 'amount': '200.00000000',
        #                 'date': '2019-12-15 16:04:10',
        #                 'rate': '0.00000355',
        #                 'total': '0.00071000',
        #                 'tradeID': '119871',
        #                 'type': 'buy',
        #                 'takerAdjustment': '200.00000000',
        #             },
        #         ],
        #         'fee': '0.00000000',
        #         'clientOrderId': '12345',
        #         'currencyPair': 'BTC_MANA',
        #         # ---------------------------------------------------------
        #         # the following fields are injected by createOrder
        #         'timestamp': timestamp,
        #         'status': 'open',
        #         'type': type,
        #         'side': side,
        #         'price': price,
        #         'amount': amount,
        #         # ---------------------------------------------------------
        #         # 'resultingTrades' in editOrder
        #         'resultingTrades': {
        #             'BTC_MANA': [],
        #          }
        #     }
        #
        timestamp = self.safe_integer(order, 'timestamp')
        if timestamp is None:
            timestamp = self.parse8601(self.safe_string(order, 'date'))
        symbol = None
        marketId = self.safe_string(order, 'currencyPair')
        if marketId is not None:
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
            else:
                quoteId, baseId = marketId.split('_')
                base = self.safe_currency_code(baseId)
                quote = self.safe_currency_code(quoteId)
                symbol = base + '/' + quote
        if (symbol is None) and (market is not None):
            symbol = market['symbol']
        trades = None
        resultingTrades = self.safe_value(order, 'resultingTrades')
        if not isinstance(resultingTrades, list):
            resultingTrades = self.safe_value(resultingTrades, self.safe_string(market, 'id', marketId))
        if resultingTrades is not None:
            trades = self.parse_trades(resultingTrades, market)
        price = self.safe_float_2(order, 'price', 'rate')
        remaining = self.safe_float(order, 'amount')
        amount = self.safe_float(order, 'startingAmount')
        filled = None
        cost = 0
        if amount is not None:
            if remaining is not None:
                filled = amount - remaining
                if price is not None:
                    cost = filled * price
        else:
            amount = remaining
        status = self.parse_order_status(self.safe_string(order, 'status'))
        average = None
        lastTradeTimestamp = None
        if filled is None:
            if trades is not None:
                filled = 0
                cost = 0
                tradesLength = len(trades)
                if tradesLength > 0:
                    lastTradeTimestamp = trades[0]['timestamp']
                    for i in range(0, tradesLength):
                        trade = trades[i]
                        tradeAmount = trade['amount']
                        tradePrice = trade['price']
                        filled = self.sum(filled, tradeAmount)
                        cost = self.sum(cost, tradePrice * tradeAmount)
                        lastTradeTimestamp = max(lastTradeTimestamp, trade['timestamp'])
                if amount is not None:
                    remaining = max(amount - filled, 0)
                    if filled >= amount:
                        status = 'closed'
        if (filled is not None) and (cost is not None) and (filled > 0):
            average = cost / filled
        type = self.safe_string(order, 'type')
        side = self.safe_string(order, 'side', type)
        if type == side:
            type = None
        id = self.safe_string(order, 'orderNumber')
        fee = None
        feeCost = self.safe_float(order, 'fee')
        if feeCost is not None:
            feeCurrencyCode = None
            if market is not None:
                feeCurrencyCode = market['base'] if (side == 'buy') else market['quote']
            fee = {
                'cost': feeCost,
                'currency': feeCurrencyCode,
            }
        clientOrderId = self.safe_string(order, 'clientOrderId')
        return {
            'info': order,
            'id': id,
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'status': status,
            'symbol': symbol,
            'type': type,
            'timeInForce': None,
            'postOnly': None,
            'side': side,
            'price': price,
            'stopPrice': None,
            'cost': cost,
            'average': average,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'trades': trades,
            'fee': fee,
        }

    def parse_open_orders(self, orders, market, result):
        for i in range(0, len(orders)):
            order = orders[i]
            extended = self.extend(order, {
                'status': 'open',
                'type': 'limit',
                'side': order['type'],
                'price': order['rate'],
            })
            result.append(self.parse_order(extended, market))
        return result

    async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        pair = market['id'] if market else 'all'
        request = {
            'currencyPair': pair,
        }
        response = await self.privatePostReturnOpenOrders(self.extend(request, params))
        extension = {'status': 'open'}
        if market is None:
            marketIds = list(response.keys())
            openOrders = []
            for i in range(0, len(marketIds)):
                marketId = marketIds[i]
                orders = response[marketId]
                m = self.markets_by_id[marketId]
                openOrders = self.array_concat(openOrders, self.parse_orders(orders, m, None, None, extension))
            return self.filter_by_since_limit(openOrders, since, limit)
        else:
            return self.parse_orders(response, market, since, limit, extension)

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        if type == 'market':
            raise ExchangeError(self.id + ' createOrder() does not accept market orders')
        await self.load_markets()
        method = 'privatePost' + self.capitalize(side)
        market = self.market(symbol)
        amount = self.amount_to_precision(symbol, amount)
        request = {
            'currencyPair': market['id'],
            'rate': self.price_to_precision(symbol, price),
            'amount': amount,
        }
        clientOrderId = self.safe_string(params, 'clientOrderId')
        if clientOrderId is not None:
            request['clientOrderId'] = clientOrderId
            params = self.omit(params, 'clientOrderId')
        # remember the timestamp before issuing the request
        timestamp = self.milliseconds()
        response = await getattr(self, method)(self.extend(request, params))
        #
        #     {
        #         'orderNumber': '9805453960',
        #         'resultingTrades': [
        #             {
        #                 'amount': '200.00000000',
        #                 'date': '2019-12-15 16:04:10',
        #                 'rate': '0.00000355',
        #                 'total': '0.00071000',
        #                 'tradeID': '119871',
        #                 'type': 'buy',
        #                 'takerAdjustment': '200.00000000',
        #             },
        #         ],
        #         'fee': '0.00000000',
        #         'currencyPair': 'BTC_MANA',
        #     }
        #
        return self.parse_order(self.extend({
            'timestamp': timestamp,
            'status': 'open',
            'type': type,
            'side': side,
            'price': price,
            'amount': amount,
        }, response), market)

    async def edit_order(self, id, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        price = float(price)
        request = {
            'orderNumber': id,
            'rate': self.price_to_precision(symbol, price),
        }
        if amount is not None:
            request['amount'] = self.amount_to_precision(symbol, amount)
        response = await self.privatePostMoveOrder(self.extend(request, params))
        return self.parse_order(response)

    async def cancel_order(self, id, symbol=None, params={}):
        await self.load_markets()
        request = {}
        clientOrderId = self.safe_value(params, 'clientOrderId')
        if clientOrderId is None:
            request['orderNumber'] = id
        else:
            request['clientOrderId'] = clientOrderId
        params = self.omit(params, 'clientOrderId')
        return await self.privatePostCancelOrder(self.extend(request, params))

    async def cancel_all_orders(self, symbol=None, params={}):
        request = {}
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['currencyPair'] = market['id']
        response = await self.privatePostCancelAllOrders(self.extend(request, params))
        #
        #     {
        #         "success": 1,
        #         "message": "Orders canceled",
        #         "orderNumbers": [
        #             503749,
        #             888321,
        #             7315825,
        #             7316824
        #         ]
        #     }
        #
        return response

    async def fetch_open_order(self, id, symbol=None, params={}):
        await self.load_markets()
        id = str(id)
        request = {
            'orderNumber': id,
        }
        response = await self.privatePostReturnOrderStatus(self.extend(request, params))
        #
        #     {
        #         success: 1,
        #         result: {
        #             '6071071': {
        #                 status: 'Open',
        #                 rate: '0.40000000',
        #                 amount: '1.00000000',
        #                 currencyPair: 'BTC_ETH',
        #                 date: '2018-10-17 17:04:50',
        #                 total: '0.40000000',
        #                 type: 'buy',
        #                 startingAmount: '1.00000',
        #             },
        #         },
        #     }
        #
        result = self.safe_value(response['result'], id)
        if result is None:
            raise OrderNotFound(self.id + ' order id ' + id + ' not found')
        return self.parse_order(result)

    async def fetch_order_status(self, id, symbol=None, params={}):
        await self.load_markets()
        orders = await self.fetch_open_orders(symbol, None, None, params)
        indexed = self.index_by(orders, 'id')
        return 'open' if (id in indexed) else 'closed'

    async def fetch_order_trades(self, id, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {
            'orderNumber': id,
        }
        trades = await self.privatePostReturnOrderTrades(self.extend(request, params))
        return self.parse_trades(trades)

    async def create_deposit_address(self, code, params={}):
        await self.load_markets()
        # USDT, USDTETH, USDTTRON
        currencyId = None
        currency = None
        if code in self.currencies:
            currency = self.currency(code)
            currencyId = currency['id']
        else:
            currencyId = code
        request = {
            'currency': currencyId,
        }
        response = await self.privatePostGenerateNewAddress(self.extend(request, params))
        address = None
        tag = None
        if response['success'] == 1:
            address = self.safe_string(response, 'response')
        self.check_address(address)
        if currency is not None:
            depositAddress = self.safe_string(currency['info'], 'depositAddress')
            if depositAddress is not None:
                tag = address
                address = depositAddress
        return {
            'currency': code,
            'address': address,
            'tag': tag,
            'info': response,
        }

    async def fetch_deposit_address(self, code, params={}):
        await self.load_markets()
        response = await self.privatePostReturnDepositAddresses(params)
        # USDT, USDTETH, USDTTRON
        currencyId = None
        currency = None
        if code in self.currencies:
            currency = self.currency(code)
            currencyId = currency['id']
        else:
            currencyId = code
        address = self.safe_string(response, currencyId)
        tag = None
        self.check_address(address)
        if currency is not None:
            depositAddress = self.safe_string(currency['info'], 'depositAddress')
            if depositAddress is not None:
                tag = address
                address = depositAddress
        return {
            'currency': code,
            'address': address,
            'tag': tag,
            'info': response,
        }

    async def withdraw(self, code, amount, address, tag=None, params={}):
        self.check_address(address)
        await self.load_markets()
        currency = self.currency(code)
        request = {
            'currency': currency['id'],
            'amount': amount,
            'address': address,
        }
        if tag is not None:
            request['paymentId'] = tag
        response = await self.privatePostWithdraw(self.extend(request, params))
        #
        #     {
        #         response: 'Withdrew 1.00000000 USDT.',
        #         email2FA: False,
        #         withdrawalNumber: 13449869
        #     }
        #
        return {
            'info': response,
            'id': self.safe_string(response, 'withdrawalNumber'),
        }

    async def fetch_transactions_helper(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        year = 31104000  # 60 * 60 * 24 * 30 * 12 = one year of history, why not
        now = self.seconds()
        start = int(since / 1000) if (since is not None) else now - 10 * year
        request = {
            'start': start,  # UNIX timestamp, required
            'end': now,  # UNIX timestamp, required
        }
        if limit is not None:
            request['limit'] = limit
        response = await self.privatePostReturnDepositsWithdrawals(self.extend(request, params))
        #
        #     {
        #         "adjustments":[],
        #         "deposits":[
        #             {
        #                 currency: "BTC",
        #                 address: "1MEtiqJWru53FhhHrfJPPvd2tC3TPDVcmW",
        #                 amount: "0.01063000",
        #                 confirmations:  1,
        #                 txid: "952b0e1888d6d491591facc0d37b5ebec540ac1efb241fdbc22bcc20d1822fb6",
        #                 timestamp:  1507916888,
        #                 status: "COMPLETE"
        #             },
        #             {
        #                 currency: "ETH",
        #                 address: "0x20108ba20b65c04d82909e91df06618107460197",
        #                 amount: "4.00000000",
        #                 confirmations: 38,
        #                 txid: "0x4be260073491fe63935e9e0da42bd71138fdeb803732f41501015a2d46eb479d",
        #                 timestamp: 1525060430,
        #                 status: "COMPLETE"
        #             }
        #         ],
        #         "withdrawals":[
        #             {
        #                 "withdrawalNumber":13449869,
        #                 "currency":"USDTTRON",  # not documented in API docs, see commonCurrencies in describe()
        #                 "address":"TXGaqPW23JdRWhsVwS2mRsGsegbdnAd3Rw",
        #                 "amount":"1.00000000",
        #                 "fee":"0.00000000",
        #                 "timestamp":1591573420,
        #                 "status":"COMPLETE: dadf427224b3d44b38a2c13caa4395e4666152556ca0b2f67dbd86a95655150f",
        #                 "ipAddress":"74.116.3.247",
        #                 "canCancel":0,
        #                 "canResendEmail":0,
        #                 "paymentID":null,
        #                 "scope":"crypto"
        #             },
        #             {
        #                 withdrawalNumber: 8224394,
        #                 currency: "EMC2",
        #                 address: "EYEKyCrqTNmVCpdDV8w49XvSKRP9N3EUyF",
        #                 amount: "63.10796020",
        #                 fee: "0.01000000",
        #                 timestamp: 1510819838,
        #                 status: "COMPLETE: d37354f9d02cb24d98c8c4fc17aa42f475530b5727effdf668ee5a43ce667fd6",
        #                 ipAddress: "5.220.220.200"
        #             },
        #             {
        #                 withdrawalNumber: 9290444,
        #                 currency: "ETH",
        #                 address: "0x191015ff2e75261d50433fbd05bd57e942336149",
        #                 amount: "0.15500000",
        #                 fee: "0.00500000",
        #                 timestamp: 1514099289,
        #                 status: "COMPLETE: 0x12d444493b4bca668992021fd9e54b5292b8e71d9927af1f076f554e4bea5b2d",
        #                 ipAddress: "5.228.227.214"
        #             },
        #             {
        #                 withdrawalNumber: 11518260,
        #                 currency: "BTC",
        #                 address: "8JoDXAmE1GY2LRK8jD1gmAmgRPq54kXJ4t",
        #                 amount: "0.20000000",
        #                 fee: "0.00050000",
        #                 timestamp: 1527918155,
        #                 status: "COMPLETE: 1864f4ebb277d90b0b1ff53259b36b97fa1990edc7ad2be47c5e0ab41916b5ff",
        #                 ipAddress: "211.8.195.26"
        #             }
        #         ]
        #     }
        #
        return response

    async def fetch_transactions(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        response = await self.fetch_transactions_helper(code, since, limit, params)
        currency = None
        if code is not None:
            currency = self.currency(code)
        withdrawals = self.safe_value(response, 'withdrawals', [])
        deposits = self.safe_value(response, 'deposits', [])
        withdrawalTransactions = self.parse_transactions(withdrawals, currency, since, limit)
        depositTransactions = self.parse_transactions(deposits, currency, since, limit)
        transactions = self.array_concat(depositTransactions, withdrawalTransactions)
        return self.filter_by_currency_since_limit(self.sort_by(transactions, 'timestamp'), code, since, limit)

    async def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
        response = await self.fetch_transactions_helper(code, since, limit, params)
        currency = None
        if code is not None:
            currency = self.currency(code)
        withdrawals = self.safe_value(response, 'withdrawals', [])
        transactions = self.parse_transactions(withdrawals, currency, since, limit)
        return self.filter_by_currency_since_limit(transactions, code, since, limit)

    async def fetch_deposits(self, code=None, since=None, limit=None, params={}):
        response = await self.fetch_transactions_helper(code, since, limit, params)
        currency = None
        if code is not None:
            currency = self.currency(code)
        deposits = self.safe_value(response, 'deposits', [])
        transactions = self.parse_transactions(deposits, currency, since, limit)
        return self.filter_by_currency_since_limit(transactions, code, since, limit)

    def parse_transaction_status(self, status):
        statuses = {
            'COMPLETE': 'ok',
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction(self, transaction, currency=None):
        #
        # deposits
        #
        #     {
        #         "txid": "f49d489616911db44b740612d19464521179c76ebe9021af85b6de1e2f8d68cd",
        #         "type": "deposit",
        #         "amount": "49798.01987021",
        #         "status": "COMPLETE",
        #         "address": "DJVJZ58tJC8UeUv9Tqcdtn6uhWobouxFLT",
        #         "currency": "DOGE",
        #         "timestamp": 1524321838,
        #         "confirmations": 3371,
        #         "depositNumber": 134587098
        #     }
        #
        # withdrawals
        #
        #     {
        #         "fee": "0.00050000",
        #         "type": "withdrawal",
        #         "amount": "0.40234387",
        #         "status": "COMPLETE: fbabb2bf7d81c076f396f3441166d5f60f6cea5fdfe69e02adcc3b27af8c2746",
        #         "address": "1EdAqY4cqHoJGAgNfUFER7yZpg1Jc9DUa3",
        #         "currency": "BTC",
        #         "canCancel": 0,
        #         "ipAddress": "185.230.101.31",
        #         "paymentID": null,
        #         "timestamp": 1523834337,
        #         "canResendEmail": 0,
        #         "withdrawalNumber": 11162900
        #     }
        #
        timestamp = self.safe_timestamp(transaction, 'timestamp')
        currencyId = self.safe_string(transaction, 'currency')
        code = self.safe_currency_code(currencyId)
        status = self.safe_string(transaction, 'status', 'pending')
        txid = self.safe_string(transaction, 'txid')
        if status is not None:
            parts = status.split(': ')
            numParts = len(parts)
            status = parts[0]
            if (numParts > 1) and (txid is None):
                txid = parts[1]
            status = self.parse_transaction_status(status)
        defaultType = 'withdrawal' if ('withdrawalNumber' in transaction) else 'deposit'
        type = self.safe_string(transaction, 'type', defaultType)
        id = self.safe_string_2(transaction, 'withdrawalNumber', 'depositNumber')
        amount = self.safe_float(transaction, 'amount')
        address = self.safe_string(transaction, 'address')
        tag = self.safe_string(transaction, 'paymentID')
        # according to https://poloniex.com/fees/
        feeCost = self.safe_float(transaction, 'fee', 0)
        if type == 'withdrawal':
            # poloniex withdrawal amount includes the fee
            amount = amount - feeCost
        return {
            'info': transaction,
            'id': id,
            'currency': code,
            'amount': amount,
            'address': address,
            'tag': tag,
            'status': status,
            'type': type,
            'updated': None,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'fee': {
                'currency': code,
                'cost': feeCost,
            },
        }

    async def fetch_position(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'currencyPair': market['id'],
        }
        response = await self.privatePostGetMarginPosition(self.extend(request, params))
        #
        #     {
        #         type: "none",
        #         amount: "0.00000000",
        #         total: "0.00000000",
        #         basePrice: "0.00000000",
        #         liquidationPrice: -1,
        #         pl: "0.00000000",
        #         lendingFees: "0.00000000"
        #     }
        #
        # todo unify parsePosition/parsePositions
        return response

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

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        url = self.urls['api'][api]
        query = self.extend({'command': path}, params)
        if api == 'public':
            url += '?' + self.urlencode(query)
        else:
            self.check_required_credentials()
            query['nonce'] = self.nonce()
            body = self.urlencode(query)
            headers = {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Key': self.apiKey,
                'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512),
            }
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if response is None:
            return
        # {"error":"Permission denied."}
        if 'error' in response:
            message = response['error']
            feedback = self.id + ' ' + body
            self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
            self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
            raise ExchangeError(feedback)  # unknown message
