# -*- 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
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
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 OrderImmediatelyFillable
from ccxt.base.errors import OrderNotFillable
from ccxt.base.errors import DuplicateOrderId
from ccxt.base.errors import NotSupported
from ccxt.base.errors import DDoSProtection
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.errors import InvalidNonce
from ccxt.base.decimal_to_precision import TICK_SIZE


class krakenfu(Exchange):

    def describe(self):
        return self.deep_extend(super(krakenfu, self).describe(), {
            'id': 'krakenfu',
            'name': 'Kraken Futures',
            'countries': ['US'],
            'version': 'v3',
            'userAgent': None,
            'rateLimit': 600,
            'has': {
                'cancelAllOrders': True,
                'createMarketOrder': False,
                'editOrder': True,
                'fetchMyTrades': True,
                'fetchOpenOrders': True,
                'fetchOrders': False,
                'fetchTicker': False,
                'fetchTickers': True,
            },
            'urls': {
                'test': {
                    'public': 'https://demo-futures.kraken.com/derivatives',
                    'private': 'https://demo-futures.kraken.com/derivatives',
                    'www': 'https://demo-futures.kraken.com',
                },
                'logo': 'https://user-images.githubusercontent.com/24300605/81436764-b22fd580-9172-11ea-9703-742783e6376d.jpg',
                'api': {
                    'public': 'https://futures.kraken.com/derivatives',
                    'private': 'https://futures.kraken.com/derivatives',
                },
                'www': 'https://futures.kraken.com/',
                'doc': [
                    'https://support.kraken.com/hc/en-us/categories/360001806372-Futures-API',
                ],
                'fees': 'https://support.kraken.com/hc/en-us/articles/360022835771-Transaction-fees-and-rebates-for-Kraken-Futures',
                'referral': None,
            },
            'api': {
                'public': {
                    'get': [
                        'instruments',
                        'orderbook',
                        'tickers',
                        'history',
                    ],
                },
                'private': {
                    'get': [
                        'openpositions',
                        'notifications',
                        'accounts',
                        'openorders',
                        'recentorders',
                        'fills',
                        'transfers',
                    ],
                    'post': [
                        'sendorder',
                        'editorder',
                        'cancelorder',
                        'transfer',
                        'batchorder',
                        'cancelallorders',
                        'cancelallordersafter',
                        'withdrawal',                              # for futures wallet -> kraken spot wallet
                    ],
                },
            },
            'fees': {
                'trading': {
                    'tierBased': False,
                    'percentage': True,
                    'maker': -0.0002,
                    'taker': 0.00075,
                },
            },
            'exceptions': {
                'exact': {
                    'apiLimitExceeded': RateLimitExceeded,
                    'marketUnavailable': ExchangeNotAvailable,
                    'requiredArgumentMissing': BadRequest,
                    'unavailable': ExchangeNotAvailable,
                    'authenticationError': AuthenticationError,
                    'accountInactive': ExchangeError,              # When account has no trade history / no order history. Should self error be ignored in some cases?
                    'invalidAccount': BadRequest,                  # the fromAccount or the toAccount are invalid
                    'invalidAmount': BadRequest,
                    'insufficientFunds': InsufficientFunds,
                    'Bad Request': BadRequest,                     # The URL contains invalid characters.(Please encode the json URL parameter)
                    'Unavailable': InsufficientFunds,              # Insufficient funds in Futures account [withdraw]
                },
                'broad': {
                    'invalidArgument': BadRequest,
                    'nonceBelowThreshold': InvalidNonce,
                    'nonceDuplicate': InvalidNonce,
                },
            },
            'precisionMode': TICK_SIZE,
            'options': {
                'symbol': {
                    'quoteIds': ['USD', 'XBT'],
                    'reversed': False,
                },
                'orderTypes': {
                    'limit': 'lmt',
                    'stop': 'stp',
                    'IOC': 'ioc',
                },
            },
        })

    async def fetch_markets(self, params={}):
        response = await self.publicGetInstruments(params)
        # {
        #   "result":"success",
        #   "instruments":[
        #     {
        #       "symbol":"fi_ethusd_180928",
        #       "type":"futures_inverse",                      # futures_vanilla  # spot index
        #       "underlying":"rr_ethusd",
        #       "lastTradingTime":"2018-09-28T15:00:00.000Z",
        #       "tickSize":0.1,
        #       "contractSize":1,
        #       "tradeable":true,
        #       "marginLevels":[
        #          {
        #           "contracts":0,
        #           "initialMargin":0.02,
        #           "maintenanceMargin":0.01
        #         },
        #         {
        #           "contracts":250000,
        #           "initialMargin":0.04,
        #           "maintenanceMargin":0.02
        #         },
        #         {
        #           "contracts":500000,
        #           "initialMargin":0.06,
        #           "maintenanceMargin":0.03
        #         }
        #       ]
        #     },
        # ...
        #     {
        #       "symbol":"in_xbtusd",
        #       "type":"spot index",
        #       "tradeable":false
        #     }
        #   ],
        #   "serverTime":"2018-07-19T11:32:39.433Z"
        # }
        instruments = response['instruments']
        result = []
        for i in range(0, len(instruments)):
            market = instruments[i]
            active = True
            id = market['symbol']
            type = None
            index = (market['type'].find(' index') >= 0)
            linear = None
            inverse = None
            if not index:
                linear = (market['type'].find('_vanilla') >= 0)
                inverse = not linear
                settleTime = self.safe_string(market, 'lastTradingTime')
                type = 'swap' if (settleTime is None) else 'future'
            else:
                type = 'index'
            swap = (type == 'swap')
            future = (type == 'future')
            symbol = id
            split = id.split('_')
            parsed = self.parse_symbol_id_joined(split[1])
            baseId = parsed['baseId']
            quoteId = parsed['quoteId']
            base = parsed['base']
            quote = parsed['quote']
            # swap == perpetual
            if swap:
                symbol = base + '/' + quote
            lotSize = self.safe_float(market, 'contractSize')
            precision = {
                'amount': None,
                'price': self.safe_float(market, 'tickSize'),
            }
            if not index:
                precision['amount'] = 1.0  # self seems to be the case for all markets
            limits = {
                'amount': {
                    'min': precision['amount'],
                    'max': None,
                },
                'price': {
                    'min': precision['price'],
                    'max': None,
                },
                'cost': {
                    'min': None,
                    'max': None,
                },
            }
            result.append({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'active': active,
                'precision': precision,
                'limits': limits,
                'type': type,
                'spot': False,
                'swap': swap,
                'future': future,
                'prediction': False,
                'linear': linear,
                'inverse': inverse,
                'lotSize': lotSize,
                'info': market,
            })
        return result

    async def fetch_order_book(self, symbol, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbol': market['id'],
        }
        response = await self.publicGetOrderbook(self.extend(request, params))
        # {
        #    "result":"success",
        #    "serverTime":"2016-02-25T09:45:53.818Z",
        #    "orderBook":{
        #       "bids":[
        #          [
        #             4213,
        #             2000,
        #          ],
        #          [
        #             4210,
        #             4000,
        #          ],
        #          ...,
        #       ],
        #       "asks":[
        #          [
        #             4218,
        #             4000,
        #          ],
        #          [
        #             4220,
        #             5000,
        #          ],
        #          ...,
        #       ],
        #    },
        # }
        timestamp = self.parse8601(response['serverTime'])
        return self.parse_order_book(response['orderBook'], timestamp)

    async def fetch_tickers(self, symbols=None, params={}):
        await self.load_markets()
        response = await self.publicGetTickers(params)
        tickers = response['tickers']
        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 result

    def parse_ticker(self, ticker, market=None):
        # {
        #    "tag":"quarter",
        #    "pair":"XRP:USD",
        #    "symbol":"fi_xrpusd_180615",
        #    "markPrice":0.8036,
        #    "bid":0.8154,
        #    "bidSize":15000,
        #    "ask":0.8166,
        #    "askSize":45000,
        #    "vol24h":5314577,
        #    "openInterest":3807948,
        #    "open24h":0.82890000,
        #    "last":0.814,
        #    "lastTime":"2018-05-10T17:14:29.301Z",
        #    "lastSize":1000,
        #    "suspended":false
        # }
        symbol = None
        marketId = self.safe_string(ticker, 'symbol')
        market = self.safe_value(self.markets_by_id, marketId, market)
        if market is not None:
            symbol = market['symbol']
        timestamp = self.parse8601(self.safe_string(ticker, 'lastTime'))
        open = self.safe_float(ticker, 'open24h')
        last = self.safe_float(ticker, 'last')
        change = None
        percentage = None
        average = None
        if last is not None and open is not None:
            change = last - open
            if open > 0:
                percentage = change / open * 100
                average = (open + last) / 2
        volume = self.safe_float(ticker, 'vol24h')
        baseVolume = None
        quoteVolume = None
        if (market is not None) and (market['type'] != 'index'):
            if market['linear']:
                baseVolume = volume  # pv_xrpxbt volume given in XRP
            else:
                quoteVolume = volume  # pi_xbtusd volume given in USD
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': None,
            'low': None,
            'bid': self.safe_float(ticker, 'bid'),
            'bidVolume': self.safe_float(ticker, 'bidSize'),
            'ask': self.safe_float(ticker, 'ask'),
            'askVolume': self.safe_float(ticker, 'askSize'),
            'vwap': None,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': change,
            'percentage': percentage,
            'average': average,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        }

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbol': market['id'],
        }
        # Returns the last 100 trades from the specified lastTime value
        response = await self.publicGetHistory(self.extend(request, params))
        return self.parse_trades(response['history'], market, since, limit)

    def parse_trade(self, trade, market=None):
        #
        # fetchTrades(public)
        #
        # {
        #    "time":"2019-02-14T09:25:33.920Z",
        #    "trade_id":100,
        #    "price":3574,
        #    "size":100,
        #    "side":"buy",
        #    "type":"fill"                                          # fill, liquidation, assignment, termination
        #    "uid":"11c3d82c-9e70-4fe9-8115-f643f1b162d4"
        # }
        #
        # fetchMyTrades(private)
        #
        # {
        #    "fillTime":"2016-02-25T09:47:01.000Z",
        #    "order_id":"c18f0c17-9971-40e6-8e5b-10df05d422f0",
        #    "fill_id":"522d4e08-96e7-4b44-9694-bfaea8fe215e",
        #    "cliOrdId":"d427f920-ec55-4c18-ba95-5fe241513b30",     # OPTIONAL
        #    "symbol":"fi_xbtusd_180615",
        #    "side":"buy",
        #    "size":2000,
        #    "price":4255,
        #    "fillType":"maker"                                     # taker, takerAfterEdit, maker, liquidation, assignee
        # },
        #
        # execution report(createOrder, editOrder)
        # {
        #    "executionId":"e1ec9f63-2338-4c44-b40a-43486c6732d7",
        #    "price":7244.5,
        #    "amount":10,
        #    "orderPriorEdit":null,
        #    "orderPriorExecution":{
        #       "orderId":"61ca5732-3478-42fe-8362-abbfd9465294",
        #       "cliOrdId":null,
        #       "type":"lmt",
        #       "symbol":"pi_xbtusd",
        #       "side":"buy",
        #       "quantity":10,
        #       "filled":0,
        #       "limitPrice":7500,
        #       "reduceOnly":false,
        #       "timestamp":"2019-12-11T17:17:33.888Z",
        #       "lastUpdateTimestamp":"2019-12-11T17:17:33.888Z"
        #    },
        #    "takerReducedQuantity":null,
        #    "type":"EXECUTION"
        # }
        timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'fillTime'))
        price = self.safe_float(trade, 'price')
        amount = self.safe_float_2(trade, 'size', 'amount', 0.0)
        id = self.safe_string_2(trade, 'uid', 'fill_id')
        if id is None:
            id = self.safe_string(trade, 'executionId')
        order = self.safe_string(trade, 'order_id')
        symbolId = self.safe_string(trade, 'symbol')
        side = self.safe_string(trade, 'side')
        type = None
        priorEdit = self.safe_value(trade, 'orderPriorEdit')
        priorExecution = self.safe_value(trade, 'orderPriorExecution')
        if priorExecution is not None:
            order = self.safe_string(priorExecution, 'orderId')
            symbolId = self.safe_string(priorExecution, 'symbol')
            side = self.safe_string(priorExecution, 'side')
            type = self.safe_string(priorExecution, 'type')
        elif priorEdit is not None:
            order = self.safe_string(priorEdit, 'orderId')
            symbolId = self.safe_string(priorEdit, 'symbol')
            side = self.safe_string(priorEdit, 'type')
            type = self.safe_string(priorEdit, 'type')
        if type is not None:
            type = self.parse_order_type(type)
        symbol = None
        if symbolId is not None:
            if symbolId in self.markets_by_id:
                market = self.markets_by_id[symbolId]
            else:
                market = None
                symbol = symbolId
        if market is not None:
            symbol = market['symbol']
        cost = None
        if (amount is not None) and (price is not None) and (market is not None):
            if market['linear']:
                cost = amount * price  # in quote
            else:
                cost = amount / price  # in base
            cost *= market['lotSize']
        fee = None
        takerOrMaker = None
        fillType = self.safe_string(trade, 'fillType')
        if fillType is not None:
            if fillType.find('taker') >= 0:
                takerOrMaker = 'taker'
            elif fillType.find('maker') >= 0:
                takerOrMaker = 'maker'
        return {
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'id': id,
            'order': order,
            'type': type,
            'takerOrMaker': takerOrMaker,
            'side': side,
            'price': price,
            'cost': cost,
            'amount': amount,
            'fee': fee,
            'info': trade,
        }

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        # type            string     'lmt'/'limit' for a limit order
        #                            'post' for a post-only limit order
        #                            'stp'/'stop' for a stop order
        #                            'take_profit' for a take profit order
        #                            'ioc' for an immediate-or-cancel order
        # stopPrice       float      The stop price associated with a stop or take profit order.
        #                            Required if orderType is stp or take_profit. Must not have
        #                            more than 2 decimal places. Note that for stop orders, limitPrice denotes
        #                            the worst price at which the stop or take_profit order can get filled at.
        #                            If no limitPrice is provided the stop or take_profit order will trigger a market order.
        # triggerSignal   string     If placing a stp or take_profit, the signal used for trigger. One of:
        #                              mark - the mark price
        #                              index - the index price
        #                              last - the last executed trade
        # cliOrdId        UUID       The order identity that is specified from the user. It must be globally unique.
        # reduceOnly      string     Set as True if you wish the order to only reduce an existing position.
        #                            Any order which increases an existing position will be rejected. Default False.
        await self.load_markets()
        typeId = self.safe_string(self.options['orderTypes'], type, type)
        request = {
            'orderType': typeId,
            'symbol': self.market_id(symbol),
            'side': side,
            'size': amount,
        }
        if price is not None:
            request['limitPrice'] = price
        response = await self.privatePostSendorder(self.extend(request, params))
        status = self.safe_string(response['sendStatus'], 'status')
        self.verify_order_action_success(status, 'placed', ['filled'])
        order = self.parse_order(response['sendStatus'])
        id = self.safe_string(order, 'id')
        self.orders[id] = order
        return self.extend({'info': response}, order)

    async def edit_order(self, id, symbol, type, side, amount=None, price=None, params={}):
        await self.load_markets()
        request = {
            'orderId': id,
        }
        if amount is not None:
            request['size'] = amount
        if price is not None:
            request['limitPrice'] = price
        response = await self.privatePostEditorder(self.extend(request, params))
        status = self.safe_string(response['editStatus'], 'status')
        self.verify_order_action_success(status, 'edited', ['filled'])
        order = self.parse_order(response['editStatus'])
        self.orders[order['id']] = order
        return self.extend({'info': response}, order)

    async def cancel_order(self, id, symbol=None, params={}):
        await self.load_markets()
        response = await self.privatePostCancelorder(self.extend({'order_id': id}, params))
        status = self.safe_string(self.safe_value(response, 'cancelStatus', {}), 'status')
        self.verify_order_action_success(status, 'canceled')
        order = {}
        if 'cancelStatus' in response:
            order = self.parse_order(response['cancelStatus'])
            self.orders[order['id']] = order
        return self.extend({'info': response}, order)

    async def cancel_all_orders(self, symbol=None, params={}):
        request = {}
        if symbol is not None:
            request['symbol'] = self.market_id(symbol)
        response = await self.privatePostCancelallorders(self.extend(request, params))
        cancelStatus = self.safe_value(response, 'cancelStatus', {})
        cancelledOrders = self.safe_value(cancelStatus, 'cancelledOrders', [])
        for i in range(0, len(cancelledOrders)):
            id = self.safe_string(cancelledOrders[i], 'order_id')
            if id in self.orders:
                self.orders[id]['status'] = 'canceled'
                self.orders[id]['remaining'] = 0.0
        return response

    async def fetch_orders(self, symbol=None, since=None, limit=None, params={}):
        # The returned orderEvents are yet again in entirely different format, what a mess
        raise NotSupported(self.id + ' fetchOrders not supprted yet')
        # await self.load_markets()
        # market = None
        # request = {}
        # if symbol is not None:
        #     market = self.market(symbol)
        #     request['symbol'] = market['id']
        # }
        # response = await self.privateGetRecentorders(self.extend(request, params))
        # return self.parse_orders([response], market, since, limit)

    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)
        response = await self.privateGetOpenorders(params)
        return self.parse_orders(response['openOrders'], market, since, limit)

    def parse_order_type(self, orderType):
        map = {
            'lmt': 'limit',
            'stp': 'stop',
        }
        return self.safe_string(map, orderType, orderType)

    def verify_order_action_success(self, status, action='placed/edited/canceled', omit=[]):
        errors = {
            'invalidOrderType': InvalidOrder,
            'invalidSide': InvalidOrder,
            'invalidSize': InvalidOrder,
            'invalidPrice': InvalidOrder,
            'insufficientAvailableFunds': InsufficientFunds,
            'selfFill': ExchangeError,
            'tooManySmallOrders': ExchangeError,
            'maxPositionViolation': BadRequest,
            'marketSuspended': ExchangeNotAvailable,
            'marketInactive': ExchangeNotAvailable,
            'clientOrderIdAlreadyExist': DuplicateOrderId,
            'clientOrderIdTooLong': BadRequest,
            'outsidePriceCollar': InvalidOrder,
            'postWouldExecute': OrderImmediatelyFillable,  # the unplaced order could actually be parsed(with status = "rejected"), but there is self specific error for self
            'iocWouldNotExecute': OrderNotFillable,  # -||-
            'wouldNotReducePosition': ExchangeError,
            'orderForEditNotFound': OrderNotFound,
            'orderForEditNotAStop': InvalidOrder,
            'filled': OrderNotFound,
            'notFound': OrderNotFound,
        }
        if (status in errors) and not self.in_array(status, omit):
            raise errors[status](self.id + ' order cannot be ' + action + ': ' + status)

    def parse_order_status(self, status):
        statuses = {
            'placed': 'open',  # the order was placed successfully
            'cancelled': 'canceled',  # the order was cancelled successfully
            'invalidOrderType': 'rejected',  # the order was not placed because orderType is invalid
            'invalidSide': 'rejected',  # the order was not placed because side is invalid
            'invalidSize': 'rejected',  # the order was not placed because size is invalid
            'invalidPrice': 'rejected',  # the order was not placed because limitPrice and/or stopPrice are invalid
            'insufficientAvailableFunds': 'rejected',  # the order was not placed because available funds are insufficient
            'selfFill': 'rejected',  # the order was not placed because it would be filled against an existing order belonging to the same account
            'tooManySmallOrders': 'rejected',  # the order was not placed because the number of small open orders would exceed the permissible limit
            'maxPositionViolation': 'rejected',  # Order would cause you to exceed your maximum position in self contract.
            'marketSuspended': 'rejected',  # the order was not placed because the market is suspended
            'marketInactive': 'rejected',  # the order was not placed because the market is inactive
            'clientOrderIdAlreadyExist': 'rejected',  # the specified client id already exist
            'clientOrderIdTooLong': 'rejected',  # the client id is longer than the permissible limit
            'outsidePriceCollar': 'rejected',  # the limit order crosses the spread but is an order of magnitude away from the mark price - fat finger control
            # Should the next two be 'expired' ?
            'postWouldExecute': 'rejected',  # the post-only order would be filled upon placement, thus is cancelled
            'iocWouldNotExecute': 'rejected',  # the immediate-or-cancel order would not execute.
            'wouldNotReducePosition': 'rejected',  # the reduce only order would not reduce position.
            'edited': 'open',  # the order was edited successfully
            'orderForEditNotFound': 'rejected',  # the requested order for edit has not been found
            'orderForEditNotAStop': 'rejected',  # the supplied stopPrice cannot be applied because order is not a stop order
            'filled': 'closed',  # the order was found completely filled and could not be cancelled
            'notFound': 'rejected',  # the order was not found, either because it had already been cancelled or it never existed
            'untouched': 'open',  # the entire size of the order is unfilled
            'partiallyFilled': 'open',  # the size of the order is partially but not entirely filled
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        # "PLACE ORDER"
        #
        # LIMIT
        # {
        #   "order_id":"179f9af8-e45e-469d-b3e9-2fd4675cb7d0",
        #   "status":"placed",
        #   "receivedTime":"2019-09-05T16:33:50.734Z",
        #   "orderEvents":[
        #      {
        #         "order":{
        #            "orderId":"179f9af8-e45e-469d-b3e9-2fd4675cb7d0",
        #            "cliOrdId":null,
        #            "type":"lmt",
        #            "symbol":"pi_xbtusd",
        #            "side":"buy",
        #            "quantity":10000,
        #            "filled":0,
        #            "limitPrice":9400,
        #            "reduceOnly":false,
        #            "timestamp":"2019-09-05T16:33:50.734Z",
        #            "lastUpdateTimestamp":"2019-09-05T16:33:50.734Z"
        #         },
        #         "reducedQuantity":null,
        #         "type":"PLACE"
        #      }
        #   ]
        # }
        #
        # LIMIT REJECTED
        # {
        #   "order_id":"614a5298-0071-450f-83c6-0617ce8c6bc4",
        #   "status":"wouldNotReducePosition",
        #   "receivedTime":"2019-09-05T16:32:54.076Z",
        #   "orderEvents":[
        #      {
        #         "uid":"614a5298-0071-450f-83c6-0617ce8c6bc4",
        #         "order":{
        #            "orderId":"614a5298-0071-450f-83c6-0617ce8c6bc4",
        #            "cliOrdId":null,
        #            "type":"lmt",
        #            "symbol":"pi_xbtusd",
        #            "side":"buy",
        #            "quantity":10000,
        #            "filled":0,
        #            "limitPrice":9400,
        #            "reduceOnly":true,
        #            "timestamp":"2019-09-05T16:32:54.076Z",
        #            "lastUpdateTimestamp":"2019-09-05T16:32:54.076Z"
        #         },
        #         "reason":"WOULD_NOT_REDUCE_POSITION",
        #         "type":"REJECT"
        #      }
        #   ]
        # }
        #
        # CONDITIONAL
        # {
        #   "order_id":"1abfd3c6-af93-4b30-91cc-e4a93797f3f5",
        #   "status":"placed",
        #   "receivedTime":"2019-12-05T10:20:50.701Z",
        #   "orderEvents":[
        #      {
        #         "orderTrigger":{
        #            "uid":"1abfd3c6-af93-4b30-91cc-e4a93797f3f5",
        #            "clientId":null,
        #            "type":"lmt",                                         # "ioc" if stop market
        #            "symbol":"pi_xbtusd",
        #            "side":"buy",
        #            "quantity":10,
        #            "limitPrice":15000,
        #            "triggerPrice":9500,
        #            "triggerSide":"trigger_below",
        #            "triggerSignal":"mark_price",
        #            "reduceOnly":false,
        #            "timestamp":"2019-12-05T10:20:50.701Z",
        #            "lastUpdateTimestamp":"2019-12-05T10:20:50.701Z"
        #         },
        #         "type":"PLACE"
        #      }
        #   ]
        # }
        #
        # EXECUTION
        # {
        #    "order_id":"61ca5732-3478-42fe-8362-abbfd9465294",
        #    "status":"placed",
        #    "receivedTime":"2019-12-11T17:17:33.888Z",
        #    "orderEvents":[
        #       {
        #          "executionId":"e1ec9f63-2338-4c44-b40a-43486c6732d7",
        #          "price":7244.5,
        #          "amount":10,
        #          "orderPriorEdit":null,
        #          "orderPriorExecution":{
        #             "orderId":"61ca5732-3478-42fe-8362-abbfd9465294",
        #             "cliOrdId":null,
        #             "type":"lmt",
        #             "symbol":"pi_xbtusd",
        #             "side":"buy",
        #             "quantity":10,
        #             "filled":0,
        #             "limitPrice":7500,
        #             "reduceOnly":false,
        #             "timestamp":"2019-12-11T17:17:33.888Z",
        #             "lastUpdateTimestamp":"2019-12-11T17:17:33.888Z"
        #          },
        #          "takerReducedQuantity":null,
        #          "type":"EXECUTION"
        #       }
        #    ]
        # }
        #
        # "EDIT ORDER"
        # {
        #    "status":"edited",
        #    "orderId":"022774bc-2c4a-4f26-9317-436c8d85746d",
        #    "receivedTime":"2019-09-05T16:47:47.521Z",
        #    "orderEvents":[
        #       {
        #          "old":{
        #             "orderId":"022774bc-2c4a-4f26-9317-436c8d85746d",
        #             "cliOrdId":null,
        #             "type":"lmt",
        #             "symbol":"pi_xbtusd",
        #             "side":"buy",
        #             "quantity":1000,
        #             "filled":0,
        #             "limitPrice":9400.0,
        #             "reduceOnly":false,
        #             "timestamp":"2019-09-05T16:41:35.173Z",
        #             "lastUpdateTimestamp":"2019-09-05T16:41:35.173Z"
        #          },
        #          "new":{
        #             "orderId":"022774bc-2c4a-4f26-9317-436c8d85746d",
        #             "cliOrdId":null,
        #             "type":"lmt",
        #             "symbol":"pi_xbtusd",
        #             "side":"buy",
        #             "quantity":1501,
        #             "filled":0,
        #             "limitPrice":7200,
        #             "reduceOnly":false,
        #             "timestamp":"2019-09-05T16:41:35.173Z",
        #             "lastUpdateTimestamp":"2019-09-05T16:47:47.519Z"
        #          },
        #          "reducedQuantity":null,
        #          "type":"EDIT"
        #       }
        #    ]
        # }
        #
        # "CANCEL ORDER"
        # {
        #    "status":"cancelled",
        #    "orderEvents":[
        #       {
        #          "uid":"85c40002-3f20-4e87-9302-262626c3531b",
        #          "order":{
        #             "orderId":"85c40002-3f20-4e87-9302-262626c3531b",
        #             "cliOrdId":null,
        #             "type":"lmt",
        #             "symbol":"pi_xbtusd",
        #             "side":"buy",
        #             "quantity":1000,
        #             "filled":0,
        #             "limitPrice":10144,
        #             "stopPrice":null,
        #             "reduceOnly":false,
        #             "timestamp":"2019-08-01T15:26:27.790Z"
        #          },
        #          "type":"CANCEL"
        #       }
        #    ]
        # }
        #
        # "FETCH OPEN ORDERS"
        # {
        #     "order_id":"59302619-41d2-4f0b-941f-7e7914760ad3",
        #     "symbol":"pi_xbtusd",
        #     "side":"sell",
        #     "orderType":"lmt",
        #     "limitPrice":10640,
        #     "unfilledSize":304,
        #     "receivedTime":"2019-09-05T17:01:17.410Z",
        #     "status":"untouched",
        #     "filledSize":0,
        #     "reduceOnly":true,
        #     "lastUpdateTime":"2019-09-05T17:01:17.410Z"
        # }
        #
        orderEvents = self.safe_value(order, 'orderEvents', [])
        details = None
        isPrior = False
        fixed = False
        statusId = None
        price = None
        trades = []
        if len(orderEvents) > 0:
            executions = []
            for i in range(0, len(orderEvents)):
                item = orderEvents[i]
                if self.safe_string(item, 'type') == 'EXECUTION':
                    executions.append(item)
                # Final order(after placement / editing / execution / canceling)
                if ('new' in item) or ('order' in item) or ('orderTrigger' in item):
                    details = self.safe_value_2(item, 'new', 'order')
                    if details is None:
                        details = item['orderTrigger']
                    isPrior = False
                    fixed = True
                elif (('orderPriorEdit' in item) or ('orderPriorExecution' in item)) and (not fixed) and (details is None):
                    details = self.safe_value_2(item, 'orderPriorExecution', 'orderPriorEdit')
                    if 'orderPriorExecution' in item:
                        price = self.safe_float(item['orderPriorExecution'], 'limitPrice')
                    isPrior = True
            trades = self.parse_trades(executions)
            statusId = self.safe_string(order, 'status')
        if details is None:
            details = order
        if statusId is None:
            statusId = self.safe_string(details, 'status')
        # This may be incorrectly marked as "open" if only execution report is given,
        # but will be fixed below
        status = self.parse_order_status(statusId)
        isClosed = self.in_array(status, ['canceled', 'rejected', 'closed'])
        symbol = None
        if market is not None:
            symbol = market['symbol']
        else:
            marketId = self.safe_string(details, 'symbol')
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
                symbol = market['symbol']
        timestamp = self.parse8601(self.safe_string_2(details, 'timestamp', 'receivedTime'))
        lastTradeTimestamp = None
        if price is None:
            price = self.safe_float(details, 'limitPrice')
        amount = self.safe_float(details, 'quantity')
        filled = self.safe_float_2(details, 'filledSize', 'filled', 0.0)
        remaining = self.safe_float(details, 'unfilledSize')
        average = None
        filled2 = 0.0
        if len(trades) > 0:
            vwapSum = 0.0
            for i in range(0, len(trades)):
                trade = trades[i]
                filled2 += trade['amount']
                vwapSum += trade['amount'] * trade['price']
            average = vwapSum / filled2
            if (amount is not None) and (not isClosed) and isPrior and (filled2 >= amount):
                status = 'closed'
                isClosed = True
            if isPrior:
                filled = filled + filled2
            else:
                filled = max(filled, filled2)
        if remaining is None:
            if isPrior:
                if amount is not None:
                    # remaining amount before execution minus executed amount
                    remaining = amount - filled2
            else:
                remaining = amount
        # if fetchOpenOrders are parsed
        if (amount is None) and (not isPrior) and (remaining is not None):
            amount = filled + remaining
        cost = None
        if (filled is not None) and (market is not None):
            whichPrice = average if (average is not None) else price
            if whichPrice is not None:
                if market['linear']:
                    cost = filled * whichPrice  # in quote
                else:
                    cost = filled / whichPrice  # in base
                cost *= market['lotSize']
        id = self.safe_string_2(order, 'order_id', 'orderId')
        if id is None:
            id = self.safe_string_2(details, 'orderId', 'uid')
        type = self.parse_order_type(self.safe_string_lower_2(details, 'type', 'orderType'))
        side = self.safe_string(details, 'side')
        return {
            'id': id,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': type,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': cost,
            'average': average,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': None,
            'trades': trades,
            'info': order,
        }

    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)
        response = await self.privateGetFills(params)
        # {
        #    "result":"success",
        #    "serverTime":"2016-02-25T09:45:53.818Z",
        #    "fills":[
        #       {
        #          "fillTime":"2016-02-25T09:47:01.000Z",
        #          "order_id":"c18f0c17-9971-40e6-8e5b-10df05d422f0",
        #          "fill_id":"522d4e08-96e7-4b44-9694-bfaea8fe215e",
        #          "cliOrdId":"d427f920-ec55-4c18-ba95-5fe241513b30",  # EXTRA
        #          "symbol":"fi_xbtusd_180615",
        #          "side":"buy",
        #          "size":2000,
        #          "price":4255,
        #          "fillType":"maker"
        #       },
        #       ...
        #    ]
        # }
        return self.parse_trades(response['fills'], market, since, limit)

    async def fetch_balance(self, params={}):
        await self.load_markets()
        response = await self.privateGetAccounts(params)
        # {
        #    "result":"success",
        #    "serverTime":"2016-02-25T09:45:53.818Z",
        #    "accounts":{
        #        "cash":{
        #            "type":"cashAccount",
        #            "balances":{
        #                "xbt":141.31756797,
        #                "xrp":52465.1254,
        #            },
        #        },
        #        "fi_xbtusd":{
        #            "type":"marginAccount",
        #            "currency":"xbt",
        #            "balances":{
        #                "fi_xbtusd_171215":50000,
        #                "fi_xbtusd_180615":-15000,
        #                ...,
        #                "xbt":141.31756797,
        #                "xrp":0,
        #            },
        #            "auxiliary":{
        #                "af":100.73891563,
        #                "pnl":12.42134766,
        #                "pv":153.73891563,
        #            },
        #            "marginRequirements":{
        #                "im":52.8,
        #                "mm":23.76,
        #                "lt":39.6,
        #                "tt":15.84,
        #            },
        #            "triggerEstimates":{
        #                "im":3110,
        #                "mm":3000,
        #                "lt":2890,
        #                "tt":2830,
        #            },
        #        },
        #        ...
        #    },
        # }
        result = {'info': response}
        accounts = self.safe_value(response, 'accounts', {})
        cash = self.safe_value(accounts, 'cash', {})
        cashBalances = self.safe_value(cash, 'balances', {})
        # This contains the actually usable margin by each market,
        # but ccxt does not support such format
        # bySymbol = self.omit(accounts, 'cash')
        currencyIds = list(cashBalances.keys())
        for i in range(0, len(currencyIds)):
            currencyId = currencyIds[i]
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['total'] = self.safe_float(cashBalances, currencyId)
            result[code] = account
        return self.parse_balance(result)

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if response is None:
            return
        if code == 429:
            raise DDoSProtection(self.id + ' ' + body)
        message = self.safe_string(response, 'error')
        if message is None:
            return
        feedback = self.id + ' ' + body
        self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
        self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
        if code == 400:
            raise BadRequest(feedback)
        raise ExchangeError(feedback)  # unknown message

    def parse_symbol_id_joined(self, symbolId):
        # Convert by detecting and converting currencies in symbol
        symbolIdLower = symbolId.lower()
        quoteIds = self.options['symbol']['quoteIds']
        reversed = self.options['symbol']['reversed']
        method = 'startsWith' if reversed else 'endsWith'
        quoteId = None
        baseId = None
        for i in range(0, len(quoteIds)):
            if getattr(self, method)(symbolIdLower, quoteIds[i].lower()):
                quoteId = quoteIds[i]
                break
        if quoteId is None:
            raise BadSymbol(self.id + ' symbolId could not be parsed: ' + symbolId)
        if not reversed:
            baseIdLength = len(symbolId) - len(quoteId)
            baseId = self.slice_string(symbolId, 0, baseIdLength)
            quoteId = self.slice_string(symbolId, baseIdLength)
        else:
            quoteId = self.slice_string(symbolId, 0, len(quoteId))
            baseId = self.slice_string(symbolId, len(quoteId))
        return {
            'baseId': baseId,
            'quoteId': quoteId,
            'base': self.safe_currency_code(baseId),
            'quote': self.safe_currency_code(quoteId),
        }

    def starts_with(self, string, x):
        return self.slice_string(string, 0, len(x)) == x

    def ends_with(self, string, x):
        start = max(0, len(string) - len(x))
        return self.slice_string(string, start) == x

    def slice_string(self, string, start=None, end=None):
        if start is None:
            start = 0
        if end is None:
            end = len(string)
        return string[start:end]

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

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        endpoint = '/api/' + self.version + '/' + path
        query = endpoint
        postData = ''
        if params:
            postData = self.urlencode(params)
            query += '?' + postData
        url = self.urls['api'][api] + query
        if api == 'private':
            nonce = ''  # self.nonce()
            auth = postData + nonce + endpoint  # 1
            hash = self.hash(self.encode(auth), 'sha256', 'binary')  # 2
            secret = self.base64_to_binary(self.secret)  # 3
            signature = self.hmac(hash, secret, hashlib.sha512, 'base64')  # 4-5
            headers = {
                'Content-Type': 'application/json',
                'APIKey': self.apiKey,
                'Authent': signature,
            }
            # headers['Nonce'] = nonce
        return {'url': url, 'method': method, 'body': body, 'headers': headers}
