'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { AuthenticationError, ExchangeError, PermissionDenied, BadRequest, ArgumentsRequired, OrderNotFound, InsufficientFunds, ExchangeNotAvailable, DDoSProtection, InvalidAddress, InvalidOrder } = require ('./base/errors');

//  ---------------------------------------------------------------------------

module.exports = class bitpanda extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'bitpanda',
            'name': 'Bitpanda Pro',
            'countries': [ 'AT' ], // Austria
            'rateLimit': 300,
            'version': 'v1',
            // new metainfo interface
            'has': {
                'CORS': false,
                'publicAPI': true,
                'privateAPI': true,
                'cancelAllOrders': true,
                'cancelOrder': true,
                'cancelOrders': true,
                'createDepositAddress': true,
                'createOrder': true,
                'fetchBalance': true,
                'fetchClosedOrders': true,
                'fetchCurrencies': true,
                'fetchDeposits': true,
                'fetchDepositAddress': true,
                'fetchMarkets': true,
                'fetchMyTrades': true,
                'fetchOHLCV': true,
                'fetchOpenOrders': true,
                'fetchOrder': true,
                'fetchOrderBook': true,
                'fetchOrderTrades': true,
                'fetchTime': true,
                'fetchTrades': true,
                'fetchTradingFees': true,
                'fetchTicker': true,
                'fetchTickers': true,
                'fetchWithdrawals': true,
                'withdraw': true,
            },
            'timeframes': {
                '1m': '1/MINUTES',
                '5m': '5/MINUTES',
                '15m': '15/MINUTES',
                '30m': '30/MINUTES',
                '1h': '1/HOURS',
                '4h': '4/HOURS',
                '1d': '1/DAYS',
                '1w': '1/WEEKS',
                '1M': '1/MONTHS',
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/51840849/87591171-9a377d80-c6f0-11ea-94ac-97a126eac3bc.jpg',
                'api': {
                    'public': 'https://api.exchange.bitpanda.com/public',
                    'private': 'https://api.exchange.bitpanda.com/public',
                },
                'www': 'https://www.bitpanda.com/en/pro',
                'doc': [
                    'https://developers.bitpanda.com/exchange/',
                ],
                'fees': 'https://www.bitpanda.com/en/pro/fees',
            },
            'api': {
                'public': {
                    'get': [
                        'currencies',
                        'candlesticks/{instrument_code}',
                        'fees',
                        'instruments',
                        'order-book/{instrument_code}',
                        'market-ticker',
                        'market-ticker/{instrument_code}',
                        'price-ticks/{instrument_code}',
                        'time',
                    ],
                },
                'private': {
                    'get': [
                        'account/balances',
                        'account/deposit/crypto/{currency_code}',
                        'account/deposit/fiat/EUR',
                        'account/deposits',
                        'account/deposits/bitpanda',
                        'account/withdrawals',
                        'account/withdrawals/bitpanda',
                        'account/fees',
                        'account/orders',
                        'account/orders/{order_id}',
                        'account/orders/{order_id}/trades',
                        'account/trades',
                        'account/trades/{trade_id}',
                        'account/trading-volume',
                    ],
                    'post': [
                        'account/deposit/crypto',
                        'account/withdraw/crypto',
                        'account/withdraw/fiat',
                        'account/fees',
                        'account/orders',
                    ],
                    'delete': [
                        'account/orders',
                        'account/orders/{order_id}',
                        'account/orders/client/{client_id}',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'tierBased': true,
                    'percentage': true,
                    'taker': 0.15 / 100,
                    'maker': 0.10 / 100,
                    'tiers': [
                        // volume in BTC
                        {
                            'taker': [
                                [0, 0.15 / 100],
                                [100, 0.13 / 100],
                                [250, 0.13 / 100],
                                [1000, 0.1 / 100],
                                [5000, 0.09 / 100],
                                [10000, 0.075 / 100],
                                [20000, 0.065 / 100],
                            ],
                            'maker': [
                                [0, 0.1 / 100],
                                [100, 0.1 / 100],
                                [250, 0.09 / 100],
                                [1000, 0.075 / 100],
                                [5000, 0.06 / 100],
                                [10000, 0.05 / 100],
                                [20000, 0.05 / 100],
                            ],
                        },
                    ],
                },
            },
            'requiredCredentials': {
                'apiKey': true,
                'secret': false,
            },
            'exceptions': {
                'exact': {
                    'INVALID_CLIENT_UUID': InvalidOrder,
                    'ORDER_NOT_FOUND': OrderNotFound,
                    'ONLY_ONE_ERC20_ADDRESS_ALLOWED': InvalidAddress,
                    'DEPOSIT_ADDRESS_NOT_USED': InvalidAddress,
                    'INVALID_CREDENTIALS': AuthenticationError,
                    'MISSING_CREDENTIALS': AuthenticationError,
                    'INVALID_APIKEY': AuthenticationError,
                    'INVALID_SCOPES': AuthenticationError,
                    'INVALID_SUBJECT': AuthenticationError,
                    'INVALID_ISSUER': AuthenticationError,
                    'INVALID_AUDIENCE': AuthenticationError,
                    'INVALID_DEVICE_ID': AuthenticationError,
                    'INVALID_IP_RESTRICTION': AuthenticationError,
                    'APIKEY_REVOKED': AuthenticationError,
                    'APIKEY_EXPIRED': AuthenticationError,
                    'SYNCHRONIZER_TOKEN_MISMATCH': AuthenticationError,
                    'SESSION_EXPIRED': AuthenticationError,
                    'INTERNAL_ERROR': AuthenticationError,
                    'CLIENT_IP_BLOCKED': PermissionDenied,
                    'MISSING_PERMISSION': PermissionDenied,
                    'ILLEGAL_CHARS': BadRequest,
                    'UNSUPPORTED_MEDIA_TYPE': BadRequest,
                    'ACCOUNT_HISTORY_TIME_RANGE_TOO_BIG': BadRequest,
                    'CANDLESTICKS_TIME_RANGE_TOO_BIG': BadRequest,
                    'INVALID_INSTRUMENT_CODE': BadRequest,
                    'INVALID_ORDER_TYPE': BadRequest,
                    'INVALID_UNIT': BadRequest,
                    'INVALID_PERIOD': BadRequest,
                    'INVALID_TIME': BadRequest,
                    'INVALID_DATE': BadRequest,
                    'INVALID_CURRENCY': BadRequest,
                    'INVALID_AMOUNT': BadRequest,
                    'INVALID_PRICE': BadRequest,
                    'INVALID_LIMIT': BadRequest,
                    'INVALID_QUERY': BadRequest,
                    'INVALID_CURSOR': BadRequest,
                    'INVALID_ACCOUNT_ID': BadRequest,
                    'INVALID_SIDE': InvalidOrder,
                    'INVALID_ACCOUNT_HISTORY_FROM_TIME': BadRequest,
                    'INVALID_ACCOUNT_HISTORY_MAX_PAGE_SIZE': BadRequest,
                    'INVALID_ACCOUNT_HISTORY_TIME_PERIOD': BadRequest,
                    'INVALID_ACCOUNT_HISTORY_TO_TIME': BadRequest,
                    'INVALID_CANDLESTICKS_GRANULARITY': BadRequest,
                    'INVALID_CANDLESTICKS_UNIT': BadRequest,
                    'INVALID_ORDER_BOOK_DEPTH': BadRequest,
                    'INVALID_ORDER_BOOK_LEVEL': BadRequest,
                    'INVALID_PAGE_CURSOR': BadRequest,
                    'INVALID_TIME_RANGE': BadRequest,
                    'INVALID_TRADE_ID': BadRequest,
                    'INVALID_UI_ACCOUNT_SETTINGS': BadRequest,
                    'NEGATIVE_AMOUNT': InvalidOrder,
                    'NEGATIVE_PRICE': InvalidOrder,
                    'MIN_SIZE_NOT_SATISFIED': InvalidOrder,
                    'BAD_AMOUNT_PRECISION': InvalidOrder,
                    'BAD_PRICE_PRECISION': InvalidOrder,
                    'BAD_TRIGGER_PRICE_PRECISION': InvalidOrder,
                    'MAX_OPEN_ORDERS_EXCEEDED': BadRequest,
                    'MISSING_PRICE': InvalidOrder,
                    'MISSING_ORDER_TYPE': InvalidOrder,
                    'MISSING_SIDE': InvalidOrder,
                    'MISSING_CANDLESTICKS_PERIOD_PARAM': ArgumentsRequired,
                    'MISSING_CANDLESTICKS_UNIT_PARAM': ArgumentsRequired,
                    'MISSING_FROM_PARAM': ArgumentsRequired,
                    'MISSING_INSTRUMENT_CODE': ArgumentsRequired,
                    'MISSING_ORDER_ID': InvalidOrder,
                    'MISSING_TO_PARAM': ArgumentsRequired,
                    'MISSING_TRADE_ID': ArgumentsRequired,
                    'INVALID_ORDER_ID': OrderNotFound,
                    'NOT_FOUND': OrderNotFound,
                    'INSUFFICIENT_LIQUIDITY': InsufficientFunds,
                    'INSUFFICIENT_FUNDS': InsufficientFunds,
                    'NO_TRADING': ExchangeNotAvailable,
                    'SERVICE_UNAVAILABLE': ExchangeNotAvailable,
                    'GATEWAY_TIMEOUT': ExchangeNotAvailable,
                    'RATELIMIT': DDoSProtection,
                    'CF_RATELIMIT': DDoSProtection,
                    'INTERNAL_SERVER_ERROR': ExchangeError,
                },
                'broad': {
                },
            },
            'commonCurrencies': {
                'MIOTA': 'IOTA', // https://github.com/ccxt/ccxt/issues/7487
            },
            // exchange-specific options
            'options': {
                'fetchTradingFees': {
                    'method': 'fetchPrivateTradingFees', // or 'fetchPublicTradingFees'
                },
                'fiat': [ 'EUR', 'CHF' ],
            },
        });
    }

    async fetchTime (params = {}) {
        const response = await this.publicGetTime (params);
        //
        //     {
        //         iso: '2020-07-10T05:17:26.716Z',
        //         epoch_millis: 1594358246716,
        //     }
        //
        return this.safeInteger (response, 'epoch_millis');
    }

    async fetchCurrencies (params = {}) {
        const response = await this.publicGetCurrencies (params);
        //
        //     [
        //         {
        //             "code":"BEST",
        //             "precision":8
        //         }
        //     ]
        //
        const result = {};
        for (let i = 0; i < response.length; i++) {
            const currency = response[i];
            const id = this.safeString (currency, 'code');
            const code = this.safeCurrencyCode (id);
            result[code] = {
                'id': id,
                'code': code,
                'name': undefined,
                'info': currency, // the original payload
                'active': undefined,
                'fee': undefined,
                'precision': this.safeInteger (currency, 'precision'),
                'limits': {
                    'amount': { 'min': undefined, 'max': undefined },
                    'price': { 'min': undefined, 'max': undefined },
                    'cost': { 'min': undefined, 'max': undefined },
                    'withdraw': { 'min': undefined, 'max': undefined },
                },
            };
        }
        return result;
    }

    async fetchMarkets (params = {}) {
        const response = await this.publicGetInstruments (params);
        //
        //     [
        //         {
        //             state: 'ACTIVE',
        //             base: { code: 'ETH', precision: 8 },
        //             quote: { code: 'CHF', precision: 2 },
        //             amount_precision: 4,
        //             market_precision: 2,
        //             min_size: '10.0'
        //         }
        //     ]
        //
        const result = [];
        for (let i = 0; i < response.length; i++) {
            const market = response[i];
            const baseAsset = this.safeValue (market, 'base', {});
            const quoteAsset = this.safeValue (market, 'quote', {});
            const baseId = this.safeString (baseAsset, 'code');
            const quoteId = this.safeString (quoteAsset, 'code');
            const id = baseId + '_' + quoteId;
            const base = this.safeCurrencyCode (baseId);
            const quote = this.safeCurrencyCode (quoteId);
            const symbol = base + '/' + quote;
            const precision = {
                'amount': this.safeInteger (market, 'amount_precision'),
                'price': this.safeInteger (market, 'market_precision'),
            };
            const limits = {
                'amount': {
                    'min': undefined,
                    'max': undefined,
                },
                'price': {
                    'min': undefined,
                    'max': undefined,
                },
                'cost': {
                    'min': this.safeFloat (market, 'min_size'),
                    'max': undefined,
                },
            };
            const state = this.safeString (market, 'state');
            const active = (state === 'ACTIVE');
            result.push ({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'precision': precision,
                'limits': limits,
                'info': market,
                'active': active,
            });
        }
        return result;
    }

    async fetchTradingFees (params = {}) {
        let method = this.safeString (params, 'method');
        params = this.omit (params, 'method');
        if (method === undefined) {
            const options = this.safeValue (this.options, 'fetchTradingFees', {});
            method = this.safeString (options, 'method', 'fetchPrivateTradingFees');
        }
        return await this[method] (params);
    }

    async fetchPublicTradingFees (params = {}) {
        await this.loadMarkets ();
        const response = await this.publicGetFees (params);
        //
        //     [
        //         {
        //             "fee_group_id":"default",
        //             "display_text":"The standard fee plan.",
        //             "fee_tiers":[
        //                 {"volume":"0.0","fee_group_id":"default","maker_fee":"0.1","taker_fee":"0.15"},
        //                 {"volume":"100.0","fee_group_id":"default","maker_fee":"0.1","taker_fee":"0.13"},
        //                 {"volume":"250.0","fee_group_id":"default","maker_fee":"0.09","taker_fee":"0.13"},
        //                 {"volume":"1000.0","fee_group_id":"default","maker_fee":"0.075","taker_fee":"0.1"},
        //                 {"volume":"5000.0","fee_group_id":"default","maker_fee":"0.06","taker_fee":"0.09"},
        //                 {"volume":"10000.0","fee_group_id":"default","maker_fee":"0.05","taker_fee":"0.075"},
        //                 {"volume":"20000.0","fee_group_id":"default","maker_fee":"0.05","taker_fee":"0.065"}
        //             ],
        //             "fee_discount_rate":"25.0",
        //             "minimum_price_value":"0.12"
        //         }
        //     ]
        //
        const feeGroupsById = this.indexBy (response, 'fee_group_id');
        const feeGroupId = this.safeValue (this.options, 'fee_group_id', 'default');
        const feeGroup = this.safeValue (feeGroupsById, feeGroupId, {});
        const feeTiers = this.safeValue (feeGroup, 'fee_tiers');
        const result = {};
        for (let i = 0; i < this.symbols.length; i++) {
            const symbol = this.symbols[i];
            const fee = {
                'info': feeGroup,
                'symbol': symbol,
                'maker': undefined,
                'taker': undefined,
                'percentage': true,
                'tierBased': true,
            };
            const takerFees = [];
            const makerFees = [];
            for (let i = 0; i < feeTiers.length; i++) {
                const tier = feeTiers[i];
                const volume = this.safeFloat (tier, 'volume');
                let taker = this.safeFloat (tier, 'taker_fee');
                let maker = this.safeFloat (tier, 'maker_fee');
                taker /= 100;
                maker /= 100;
                takerFees.push ([ volume, taker ]);
                makerFees.push ([ volume, maker ]);
                if (i === 0) {
                    fee['taker'] = taker;
                    fee['maker'] = maker;
                }
            }
            const tiers = {
                'taker': takerFees,
                'maker': makerFees,
            };
            fee['tiers'] = tiers;
            result[symbol] = fee;
        }
        return result;
    }

    async fetchPrivateTradingFees (params = {}) {
        await this.loadMarkets ();
        const response = await this.privateGetAccountFees (params);
        //
        //     {
        //         "account_id": "ed524d00-820a-11e9-8f1e-69602df16d85",
        //         "running_trading_volume": "0.0",
        //         "fee_group_id": "default",
        //         "collect_fees_in_best": false,
        //         "fee_discount_rate": "25.0",
        //         "minimum_price_value": "0.12",
        //         "fee_tiers": [
        //             { "volume": "0.0", "fee_group_id": "default", "maker_fee": "0.1", "taker_fee": "0.1" },
        //             { "volume": "100.0", "fee_group_id": "default", "maker_fee": "0.09", "taker_fee": "0.1" },
        //             { "volume": "250.0", "fee_group_id": "default", "maker_fee": "0.08", "taker_fee": "0.1" },
        //             { "volume": "1000.0", "fee_group_id": "default", "maker_fee": "0.07", "taker_fee": "0.09" },
        //             { "volume": "5000.0", "fee_group_id": "default", "maker_fee": "0.06", "taker_fee": "0.08" },
        //             { "volume": "10000.0", "fee_group_id": "default", "maker_fee": "0.05", "taker_fee": "0.07" },
        //             { "volume": "20000.0", "fee_group_id": "default", "maker_fee": "0.05", "taker_fee": "0.06" },
        //             { "volume": "50000.0", "fee_group_id": "default", "maker_fee": "0.05", "taker_fee": "0.05" }
        //         ],
        //         "active_fee_tier": { "volume": "0.0", "fee_group_id": "default", "maker_fee": "0.1", "taker_fee": "0.1" }
        //     }
        //
        const activeFeeTier = this.safeValue (response, 'active_fee_tier', {});
        const result = {
            'info': response,
            'maker': this.safeFloat (activeFeeTier, 'maker_fee'),
            'taker': this.safeFloat (activeFeeTier, 'taker_fee'),
            'percentage': true,
            'tierBased': true,
        };
        const feeTiers = this.safeValue (response, 'fee_tiers');
        const takerFees = [];
        const makerFees = [];
        for (let i = 0; i < feeTiers.length; i++) {
            const tier = feeTiers[i];
            const volume = this.safeFloat (tier, 'volume');
            let taker = this.safeFloat (tier, 'taker_fee');
            let maker = this.safeFloat (tier, 'maker_fee');
            taker /= 100;
            maker /= 100;
            takerFees.push ([ volume, taker ]);
            makerFees.push ([ volume, maker ]);
        }
        const tiers = {
            'taker': takerFees,
            'maker': makerFees,
        };
        result['tiers'] = tiers;
        return result;
    }

    parseTicker (ticker, market = undefined) {
        //
        // fetchTicker, fetchTickers
        //
        //     {
        //         "instrument_code":"BTC_EUR",
        //         "sequence":602562,
        //         "time":"2020-07-10T06:27:34.951Z",
        //         "state":"ACTIVE",
        //         "is_frozen":0,
        //         "quote_volume":"1695555.1783768",
        //         "base_volume":"205.67436",
        //         "last_price":"8143.91",
        //         "best_bid":"8143.71",
        //         "best_ask":"8156.9",
        //         "price_change":"-147.47",
        //         "price_change_percentage":"-1.78",
        //         "high":"8337.45",
        //         "low":"8110.0"
        //     }
        //
        const timestamp = this.parse8601 (this.safeString (ticker, 'time'));
        const marketId = this.safeString (ticker, 'instrument_code');
        const symbol = this.safeSymbol (marketId, market, '_');
        const last = this.safeFloat (ticker, 'last_price');
        const percentage = this.safeFloat (ticker, 'price_change_percentage');
        const change = this.safeFloat (ticker, 'price_change');
        let open = undefined;
        let average = undefined;
        if ((last !== undefined) && (change !== undefined)) {
            open = last - change;
            average = this.sum (last, open) / 2;
        }
        const baseVolume = this.safeFloat (ticker, 'base_volume');
        const quoteVolume = this.safeFloat (ticker, 'quote_volume');
        const vwap = this.vwap (baseVolume, quoteVolume);
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': this.safeFloat (ticker, 'high'),
            'low': this.safeFloat (ticker, 'low'),
            'bid': this.safeFloat (ticker, 'best_bid'),
            'bidVolume': undefined,
            'ask': this.safeFloat (ticker, 'best_ask'),
            'askVolume': undefined,
            'vwap': vwap,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': change,
            'percentage': percentage,
            'average': average,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        };
    }

    async fetchTicker (symbol, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'instrument_code': market['id'],
        };
        const response = await this.publicGetMarketTickerInstrumentCode (this.extend (request, params));
        //
        //     {
        //         "instrument_code":"BTC_EUR",
        //         "sequence":602562,
        //         "time":"2020-07-10T06:27:34.951Z",
        //         "state":"ACTIVE",
        //         "is_frozen":0,
        //         "quote_volume":"1695555.1783768",
        //         "base_volume":"205.67436",
        //         "last_price":"8143.91",
        //         "best_bid":"8143.71",
        //         "best_ask":"8156.9",
        //         "price_change":"-147.47",
        //         "price_change_percentage":"-1.78",
        //         "high":"8337.45",
        //         "low":"8110.0"
        //     }
        //
        return this.parseTicker (response, market);
    }

    async fetchTickers (symbols = undefined, params = {}) {
        await this.loadMarkets ();
        const response = await this.publicGetMarketTicker (params);
        //
        //     [
        //         {
        //             "instrument_code":"BTC_EUR",
        //             "sequence":602562,
        //             "time":"2020-07-10T06:27:34.951Z",
        //             "state":"ACTIVE",
        //             "is_frozen":0,
        //             "quote_volume":"1695555.1783768",
        //             "base_volume":"205.67436",
        //             "last_price":"8143.91",
        //             "best_bid":"8143.71",
        //             "best_ask":"8156.9",
        //             "price_change":"-147.47",
        //             "price_change_percentage":"-1.78",
        //             "high":"8337.45",
        //             "low":"8110.0"
        //         }
        //     ]
        //
        const result = {};
        for (let i = 0; i < response.length; i++) {
            const ticker = this.parseTicker (response[i]);
            const symbol = ticker['symbol'];
            result[symbol] = ticker;
        }
        return this.filterByArray (result, 'symbol', symbols);
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            'instrument_code': this.marketId (symbol),
            // level 1 means only the best bid and ask
            // level 2 is a compiled order book up to market precision
            // level 3 is a full orderbook
            // if you wish to get regular updates about orderbooks please use the Websocket channel
            // heavy usage of this endpoint may result in limited access according to rate limits rules
            // 'level': 3, // default
        };
        if (limit !== undefined) {
            request['depth'] = limit;
        }
        const response = await this.publicGetOrderBookInstrumentCode (this.extend (request, params));
        //
        // level 1
        //
        //     {
        //         "instrument_code":"BTC_EUR",
        //         "time":"2020-07-10T07:39:06.343Z",
        //         "asks":{
        //             "value":{
        //                 "price":"8145.29",
        //                 "amount":"0.96538",
        //                 "number_of_orders":1
        //             }
        //         },
        //         "bids":{
        //             "value":{
        //                 "price":"8134.0",
        //                 "amount":"1.5978",
        //                 "number_of_orders":5
        //             }
        //         }
        //     }
        //
        // level 2
        //
        //     {
        //         "instrument_code":"BTC_EUR","time":"2020-07-10T07:36:43.538Z",
        //         "asks":[
        //             {"price":"8146.59","amount":"0.89691","number_of_orders":1},
        //             {"price":"8146.89","amount":"1.92062","number_of_orders":1},
        //             {"price":"8169.5","amount":"0.0663","number_of_orders":1},
        //         ],
        //         "bids":[
        //             {"price":"8143.49","amount":"0.01329","number_of_orders":1},
        //             {"price":"8137.01","amount":"5.34748","number_of_orders":1},
        //             {"price":"8137.0","amount":"2.0","number_of_orders":1},
        //         ]
        //     }
        //
        // level 3
        //
        //     {
        //         "instrument_code":"BTC_EUR",
        //         "time":"2020-07-10T07:32:31.525Z",
        //         "bids":[
        //             {"price":"8146.79","amount":"0.01537","order_id":"5d717da1-a8f4-422d-afcc-03cb6ab66825"},
        //             {"price":"8139.32","amount":"3.66009","order_id":"d0715c68-f28d-4cf1-a450-d56cf650e11c"},
        //             {"price":"8137.51","amount":"2.61049","order_id":"085fd6f4-e835-4ca5-9449-a8f165772e60"},
        //         ],
        //         "asks":[
        //             {"price":"8153.49","amount":"0.93384","order_id":"755d3aa3-42b5-46fa-903d-98f42e9ae6c4"},
        //             {"price":"8153.79","amount":"1.80456","order_id":"62034cf3-b70d-45ff-b285-ba6307941e7c"},
        //             {"price":"8167.9","amount":"0.0018","order_id":"036354e0-71cd-492f-94f2-01f7d4b66422"},
        //         ]
        //     }
        //
        const timestamp = this.parse8601 (this.safeString (response, 'time'));
        return this.parseOrderBook (response, timestamp, 'bids', 'asks', 'price', 'amount');
    }

    parseOHLCV (ohlcv, market = undefined) {
        //
        //     {
        //         "instrument_code":"BTC_EUR",
        //         "granularity":{"unit":"HOURS","period":1},
        //         "high":"9252.65",
        //         "low":"9115.27",
        //         "open":"9250.0",
        //         "close":"9132.35",
        //         "total_amount":"33.85924",
        //         "volume":"311958.9635744",
        //         "time":"2020-05-08T22:59:59.999Z",
        //         "last_sequence":461123
        //     }
        //
        const granularity = this.safeValue (ohlcv, 'granularity');
        const unit = this.safeString (granularity, 'unit');
        const period = this.safeString (granularity, 'period');
        const units = {
            'MINUTES': 'm',
            'HOURS': 'h',
            'DAYS': 'd',
            'WEEKS': 'w',
            'MONTHS': 'M',
        };
        const lowercaseUnit = this.safeString (units, unit);
        const timeframe = period + lowercaseUnit;
        const durationInSeconds = this.parseTimeframe (timeframe);
        const duration = durationInSeconds * 1000;
        const timestamp = this.parse8601 (this.safeString (ohlcv, 'time'));
        const alignedTimestamp = duration * parseInt (timestamp / duration);
        const options = this.safeValue (this.options, 'fetchOHLCV', {});
        const volumeField = this.safeString (options, 'volume', 'total_amount');
        return [
            alignedTimestamp,
            this.safeFloat (ohlcv, 'open'),
            this.safeFloat (ohlcv, 'high'),
            this.safeFloat (ohlcv, 'low'),
            this.safeFloat (ohlcv, 'close'),
            this.safeFloat (ohlcv, volumeField),
        ];
    }

    async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const periodUnit = this.safeString (this.timeframes, timeframe);
        const [ period, unit ] = periodUnit.split ('/');
        const durationInSeconds = this.parseTimeframe (timeframe);
        const duration = durationInSeconds * 1000;
        if (limit === undefined) {
            limit = 1500;
        }
        const request = {
            'instrument_code': market['id'],
            // 'from': this.iso8601 (since),
            // 'to': this.iso8601 (this.milliseconds ()),
            'period': period,
            'unit': unit,
        };
        if (since === undefined) {
            const now = this.milliseconds ();
            request['to'] = this.iso8601 (now);
            request['from'] = this.iso8601 (now - limit * duration);
        } else {
            request['from'] = this.iso8601 (since);
            request['to'] = this.iso8601 (this.sum (since, limit * duration));
        }
        const response = await this.publicGetCandlesticksInstrumentCode (this.extend (request, params));
        //
        //     [
        //         {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9252.65","low":"9115.27","open":"9250.0","close":"9132.35","total_amount":"33.85924","volume":"311958.9635744","time":"2020-05-08T22:59:59.999Z","last_sequence":461123},
        //         {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9162.49","low":"9040.0","open":"9132.53","close":"9083.69","total_amount":"26.19685","volume":"238553.7812365","time":"2020-05-08T23:59:59.999Z","last_sequence":461376},
        //         {"instrument_code":"BTC_EUR","granularity":{"unit":"HOURS","period":1},"high":"9135.7","low":"9002.59","open":"9055.45","close":"9133.98","total_amount":"26.21919","volume":"238278.8724959","time":"2020-05-09T00:59:59.999Z","last_sequence":461521},
        //     ]
        //
        return this.parseOHLCVs (response, market, timeframe, since, limit);
    }

    parseTrade (trade, market = undefined) {
        //
        // fetchTrades (public)
        //
        //     {
        //         "instrument_code":"BTC_EUR",
        //         "price":"8137.28",
        //         "amount":"0.22269",
        //         "taker_side":"BUY",
        //         "volume":"1812.0908832",
        //         "time":"2020-07-10T14:44:32.299Z",
        //         "trade_timestamp":1594392272299,
        //         "sequence":603047
        //     }
        //
        // fetchMyTrades, fetchOrder, fetchOpenOrders, fetchClosedOrders trades (private)
        //
        //     {
        //         "fee": {
        //             "fee_amount": "0.0014",
        //             "fee_currency": "BTC",
        //             "fee_percentage": "0.1",
        //             "fee_group_id": "default",
        //             "fee_type": "TAKER",
        //             "running_trading_volume": "0.0"
        //         },
        //         "trade": {
        //             "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664",
        //             "order_id": "36bb2437-7402-4794-bf26-4bdf03526439",
        //             "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9",
        //             "amount": "1.4",
        //             "side": "BUY",
        //             "instrument_code": "BTC_EUR",
        //             "price": "7341.4",
        //             "time": "2019-09-27T15:05:32.564Z",
        //             "sequence": 48670
        //         }
        //     }
        //
        const feeInfo = this.safeValue (trade, 'fee', {});
        trade = this.safeValue (trade, 'trade', trade);
        let timestamp = this.safeInteger (trade, 'trade_timestamp');
        if (timestamp === undefined) {
            timestamp = this.parse8601 (this.safeString (trade, 'time'));
        }
        const side = this.safeStringLower2 (trade, 'side', 'taker_side');
        const price = this.safeFloat (trade, 'price');
        const amount = this.safeFloat (trade, 'amount');
        let cost = this.safeFloat (trade, 'volume');
        if ((cost === undefined) && (amount !== undefined) && (price !== undefined)) {
            cost = amount * price;
        }
        const marketId = this.safeString (trade, 'instrument_code');
        const symbol = this.safeSymbol (marketId, market, '_');
        const feeCost = this.safeFloat (feeInfo, 'fee_amount');
        let takerOrMaker = undefined;
        let fee = undefined;
        if (feeCost !== undefined) {
            const feeCurrencyId = this.safeString (feeInfo, 'fee_currency');
            const feeCurrencyCode = this.safeCurrencyCode (feeCurrencyId);
            const feeRate = this.safeFloat (feeInfo, 'fee_percentage');
            fee = {
                'cost': feeCost,
                'currency': feeCurrencyCode,
                'rate': feeRate,
            };
            takerOrMaker = this.safeStringLower (feeInfo, 'fee_type');
        }
        return {
            'id': this.safeString2 (trade, 'trade_id', 'sequence'),
            'order': this.safeString (trade, 'order_id'),
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': symbol,
            'type': undefined,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': cost,
            'takerOrMaker': takerOrMaker,
            'fee': fee,
            'info': trade,
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'instrument_code': market['id'],
            // 'from': this.iso8601 (since),
            // 'to': this.iso8601 (this.milliseconds ()),
        };
        if (since !== undefined) {
            // returns price ticks for a specific market with an interval of maximum of 4 hours
            // sorted by latest first
            request['from'] = this.iso8601 (since);
            request['to'] = this.iso8601 (this.sum (since, 14400000));
        }
        const response = await this.publicGetPriceTicksInstrumentCode (this.extend (request, params));
        //
        //     [
        //         {
        //             "instrument_code":"BTC_EUR",
        //             "price":"8137.28",
        //             "amount":"0.22269",
        //             "taker_side":"BUY",
        //             "volume":"1812.0908832",
        //             "time":"2020-07-10T14:44:32.299Z",
        //             "trade_timestamp":1594392272299,
        //             "sequence":603047
        //         }
        //     ]
        //
        return this.parseTrades (response, market, since, limit);
    }

    async fetchBalance (params = {}) {
        await this.loadMarkets ();
        const response = await this.privateGetAccountBalances (params);
        //
        //     {
        //         "account_id":"4b95934f-55f1-460c-a525-bd5afc0cf071",
        //         "balances":[
        //             {
        //                 "account_id":"4b95934f-55f1-460c-a525-bd5afc0cf071",
        //                 "currency_code":"BTC",
        //                 "change":"10.0",
        //                 "available":"10.0",
        //                 "locked":"0.0",
        //                 "sequence":142135994,
        //                 "time":"2020-07-01T10:57:32.959Z"
        //             }
        //         ]
        //     }
        //
        const balances = this.safeValue (response, 'balances', []);
        const result = { 'info': response };
        for (let i = 0; i < balances.length; i++) {
            const balance = balances[i];
            const currencyId = this.safeString (balance, 'currency_code');
            const code = this.safeCurrencyCode (currencyId);
            const account = this.account ();
            account['free'] = this.safeFloat (balance, 'available');
            account['used'] = this.safeFloat (balance, 'locked');
            result[code] = account;
        }
        return this.parseBalance (result);
    }

    parseDepositAddress (depositAddress, currency = undefined) {
        let code = undefined;
        if (currency !== undefined) {
            code = currency['code'];
        }
        const address = this.safeString (depositAddress, 'address');
        const tag = this.safeString (depositAddress, 'destination_tag');
        this.checkAddress (address);
        return {
            'currency': code,
            'address': address,
            'tag': tag,
            'info': depositAddress,
        };
    }

    async createDepositAddress (code, params = {}) {
        await this.loadMarkets ();
        const currency = this.currency (code);
        const request = {
            'currency': currency['id'],
        };
        const response = await this.privatePostAccountDepositCrypto (this.extend (request, params));
        //
        //     {
        //         "address":"rBnNhk95FrdNisZtXcStzriFS8vEzz53DM",
        //         "destination_tag":"865690307",
        //         "enabled":true,
        //         "is_smart_contract":false
        //     }
        //
        return this.parseDepositAddress (response, currency);
    }

    async fetchDepositAddress (code, params = {}) {
        await this.loadMarkets ();
        const currency = this.currency (code);
        const request = {
            'currency_code': currency['id'],
        };
        const response = await this.privateGetAccountDepositCryptoCurrencyCode (this.extend (request, params));
        //
        //     {
        //         "address":"rBnNhk95FrdNisZtXcStzriFS8vEzz53DM",
        //         "destination_tag":"865690307",
        //         "enabled":true,
        //         "is_smart_contract":false,
        //         "can_create_more":false
        //     }
        //
        return this.parseDepositAddress (response, currency);
    }

    async fetchDeposits (code = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            // 'cursor': 'string', // pointer specifying the position from which the next pages should be returned
        };
        let currency = undefined;
        if (code !== undefined) {
            currency = this.currency (code);
            request['currency_code'] = currency['id'];
        }
        if (limit !== undefined) {
            request['max_page_size'] = limit;
        }
        if (since !== undefined) {
            const to = this.safeString (params, 'to');
            if (to === undefined) {
                throw new ArgumentsRequired (this.id + ' fetchDeposits() requires a "to" iso8601 string param with the since argument is specified');
            }
            request['from'] = this.iso8601 (since);
        }
        const response = await this.privateGetAccountDeposits (this.extend (request, params));
        //
        //     {
        //         "deposit_history": [
        //             {
        //                 "transaction_id": "e5342efcd-d5b7-4a56-8e12-b69ffd68c5ef",
        //                 "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58",
        //                 "amount": "100",
        //                 "type": "CRYPTO",
        //                 "funds_source": "INTERNAL",
        //                 "time": "2020-04-22T09:57:47Z",
        //                 "currency": "BTC",
        //                 "fee_amount": "0.0",
        //                 "fee_currency": "BTC"
        //             },
        //             {
        //                 "transaction_id": "79793d00-2899-4a4d-95b7-73ae6b31384f",
        //                 "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58",
        //                 "time": "2020-05-05T11:22:07.925Z",
        //                 "currency": "EUR",
        //                 "funds_source": "EXTERNAL",
        //                 "type": "FIAT",
        //                 "amount": "50.0",
        //                 "fee_amount": "0.01",
        //                 "fee_currency": "EUR"
        //             }
        //         ],
        //         "max_page_size": 2,
        //         "cursor": "eyJhY2NvdW50X2lkIjp7InMiOiJlMzY5YWM4MC00NTc3LTExZTktYWUwOC05YmVkYzQ3OTBiODQiLCJzcyI6W10sIm5zIjpbXSwiYnMiOltdLCJtIjp7fSwibCI6W119LCJpdGVtX2tleSI6eyJzIjoiV0lUSERSQVdBTDo6MmFlMjYwY2ItOTk3MC00YmNiLTgxNmEtZGY4MDVmY2VhZTY1Iiwic3MiOltdLCJucyI6W10sImJzIjpbXSwibSI6e30sImwiOltdfSwiZ2xvYmFsX3dpdGhkcmF3YWxfaW5kZXhfaGFzaF9rZXkiOnsicyI6ImUzNjlhYzgwLTQ1NzctMTFlOS1hZTA4LTliZWRjNDc5MGI4NCIsInNzIjpbXSwibnMiOltdLCJicyI6W10sIm0iOnt9LCJsIjpbXX0sInRpbWVzdGFtcCI6eyJuIjoiMTU4ODA1ODc2Nzk0OCIsInNzIjpbXSwibnMiOltdLCJicyI6W10sIm0iOnt9LCJsIjpbXX19"
        //     }
        //
        const depositHistory = this.safeValue (response, 'deposit_history', []);
        return this.parseTransactions (depositHistory, currency, since, limit, { 'type': 'deposit' });
    }

    async fetchWithdrawals (code = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            // 'cursor': 'string', // pointer specifying the position from which the next pages should be returned
        };
        let currency = undefined;
        if (code !== undefined) {
            currency = this.currency (code);
            request['currency_code'] = currency['id'];
        }
        if (limit !== undefined) {
            request['max_page_size'] = limit;
        }
        if (since !== undefined) {
            const to = this.safeString (params, 'to');
            if (to === undefined) {
                throw new ArgumentsRequired (this.id + ' fetchWithdrawals() requires a "to" iso8601 string param with the since argument is specified');
            }
            request['from'] = this.iso8601 (since);
        }
        const response = await this.privateGetAccountWithdrawals (this.extend (request, params));
        //
        //     {
        //         "withdrawal_history": [
        //             {
        //                 "account_id": "e369ac80-4577-11e9-ae08-9bedc4790b84",
        //                 "amount": "0.1",
        //                 "currency": "BTC",
        //                 "fee_amount": "0.00002",
        //                 "fee_currency": "BTC",
        //                 "funds_source": "EXTERNAL",
        //                 "related_transaction_id": "e298341a-3855-405e-bce3-92db368a3157",
        //                 "time": "2020-05-05T11:11:32.110Z",
        //                 "transaction_id": "6693ff40-bb10-4dcf-ada7-3b287727c882",
        //                 "type": "CRYPTO"
        //             },
        //             {
        //                 "account_id": "e369ac80-4577-11e9-ae08-9bedc4790b84",
        //                 "amount": "0.1",
        //                 "currency": "BTC",
        //                 "fee_amount": "0.0",
        //                 "fee_currency": "BTC",
        //                 "funds_source": "INTERNAL",
        //                 "time": "2020-05-05T10:29:53.464Z",
        //                 "transaction_id": "ec9703b1-954b-4f76-adea-faac66eabc0b",
        //                 "type": "CRYPTO"
        //             }
        //         ],
        //         "cursor": "eyJhY2NvdW50X2lkIjp7InMiOiJlMzY5YWM4MC00NTc3LTExZTktYWUwOC05YmVkYzQ3OTBiODQiLCJzcyI6W10sIm5zIjpbXSwiYnMiOltdLCJtIjp7fSwibCI6W119LCJpdGVtX2tleSI6eyJzIjoiV0lUSERSQVdBTDo6ZWM5NzAzYjEtOTU0Yi00Zjc2LWFkZWEtZmFhYzY2ZWFiYzBiIiwic3MiOltdLCJucyI6W10sImJzIjpbXSwibSI6e30sImwiOltdfSwiZ2xvYmFsX3dpdGhkcmF3YWxfaW5kZXhfaGFzaF9rZXkiOnsicyI6ImUzNjlhYzgwLTQ1NzctMTFlOS1hZTA4LTliZWRjNDc5MGI4NCIsInNzIjpbXSwibnMiOltdLCJicyI6W10sIm0iOnt9LCJsIjpbXX0sInRpbWVzdGFtcCI6eyJuIjoiMTU4ODY3NDU5MzQ2NCIsInNzIjpbXSwibnMiOltdLCJicyI6W10sIm0iOnt9LCJsIjpbXX19",
        //         "max_page_size": 2
        //     }
        //
        const withdrawalHistory = this.safeValue (response, 'withdrawal_history', []);
        return this.parseTransactions (withdrawalHistory, currency, since, limit, { 'type': 'withdrawal' });
    }

    async withdraw (code, amount, address, tag = undefined, params = {}) {
        this.checkAddress (address);
        await this.loadMarkets ();
        const currency = this.currency (code);
        const request = {
            'currency': code,
            'amount': this.currencyToPrecision (code, amount),
            // 'payout_account_id': '66756a10-3e86-48f4-9678-b634c4b135b2', // fiat only
            // 'recipient': { // crypto only
            //     'address': address,
            //     // 'destination_tag': '',
            // },
        };
        const options = this.safeValue (this.options, 'fiat', []);
        const isFiat = this.inArray (code, options);
        const method = isFiat ? 'privatePostAccountWithdrawFiat' : 'privatePostAccountWithdrawCrypto';
        if (isFiat) {
            const payoutAccountId = this.safeString (params, 'payout_account_id');
            if (payoutAccountId === undefined) {
                throw ArgumentsRequired (this.id + ' withdraw() requires a payout_account_id param for fiat ' + code + ' withdrawals');
            }
        } else {
            const recipient = { 'address': address };
            if (tag !== undefined) {
                recipient['destination_tag'] = tag;
            }
            request['recipient'] = recipient;
        }
        const response = await this[method] (this.extend (request, params));
        //
        // crypto
        //
        //     {
        //         "amount": "1234.5678",
        //         "fee": "1234.5678",
        //         "recipient": "3NacQ7rzZdhfyAtfJ5a11k8jFPdcMP2Bq7",
        //         "destination_tag": "",
        //         "transaction_id": "d0f8529f-f832-4e6a-9dc5-b8d5797badb2"
        //     }
        //
        // fiat
        //
        //     {
        //         "transaction_id": "54236cd0-4413-11e9-93fb-5fea7e5b5df6"
        //     }
        //
        return this.parseTransaction (response, currency);
    }

    parseTransaction (transaction, currency = undefined) {
        //
        // fetchDeposits, fetchWithdrawals
        //
        //     {
        //         "transaction_id": "C2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef",
        //         "type": "FIAT",
        //         "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58",
        //         "amount": "1234.5678",
        //         "time": "2019-08-24T14:15:22Z",
        //         "funds_source": "INTERNAL",
        //         "currency": "BTC",
        //         "fee_amount": "1234.5678",
        //         "fee_currency": "BTC",
        //         "blockchain_transaction_id": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16",
        //         "related_transaction_id": "e298341a-3855-405e-bce3-92db368a3157"
        //     }
        //
        // withdraw
        //
        //
        //     crypto
        //
        //     {
        //         "amount": "1234.5678",
        //         "fee": "1234.5678",
        //         "recipient": "3NacQ7rzZdhfyAtfJ5a11k8jFPdcMP2Bq7",
        //         "destination_tag": "",
        //         "transaction_id": "d0f8529f-f832-4e6a-9dc5-b8d5797badb2"
        //     }
        //
        //     fiat
        //
        //     {
        //         "transaction_id": "54236cd0-4413-11e9-93fb-5fea7e5b5df6"
        //     }
        //
        const id = this.safeString (transaction, 'transaction_id');
        const amount = this.safeFloat (transaction, 'amount');
        const timestamp = this.parse8601 (this.safeString (transaction, 'time'));
        const currencyId = this.safeString (transaction, 'currency');
        currency = this.safeCurrency (currencyId, currency);
        const status = 'ok'; // the exchange returns cleared transactions only
        const feeCost = this.safeFloat2 (transaction, 'fee_amount', 'fee');
        let fee = undefined;
        const addressTo = this.safeString (transaction, 'recipient');
        const tagTo = this.safeString (transaction, 'destination_tag');
        if (feeCost !== undefined) {
            const feeCurrencyId = this.safeString (transaction, 'fee_currency', currencyId);
            const feeCurrencyCode = this.safeCurrencyCode (feeCurrencyId);
            fee = {
                'cost': feeCost,
                'currency': feeCurrencyCode,
            };
        }
        return {
            'info': transaction,
            'id': id,
            'currency': currency['code'],
            'amount': amount,
            'address': addressTo,
            'addressFrom': undefined,
            'addressTo': addressTo,
            'tag': tagTo,
            'tagFrom': undefined,
            'tagTo': tagTo,
            'status': status,
            'type': undefined,
            'updated': undefined,
            'txid': this.safeString (transaction, 'blockchain_transaction_id'),
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'fee': fee,
        };
    }

    parseOrderStatus (status) {
        const statuses = {
            'FILLED': 'open',
            'FILLED_FULLY': 'closed',
            'FILLED_CLOSED': 'canceled',
            'FILLED_REJECTED': 'rejected',
            'OPEN': 'open',
            'REJECTED': 'rejected',
            'CLOSED': 'canceled',
            'FAILED': 'failed',
            'STOP_TRIGGERED': 'triggered',
        };
        return this.safeString (statuses, status, status);
    }

    parseOrder (order, market = undefined) {
        //
        // createOrder
        //
        //     {
        //         "order_id": "d5492c24-2995-4c18-993a-5b8bf8fffc0d",
        //         "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206",
        //         "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9",
        //         "instrument_code": "BTC_EUR",
        //         "time": "2019-08-01T08:00:44.026Z",
        //         "side": "BUY",
        //         "price": "5000",
        //         "amount": "1",
        //         "filled_amount": "0.5",
        //         "type": "LIMIT",
        //         "time_in_force": "GOOD_TILL_CANCELLED"
        //     }
        //
        // fetchOrder, fetchOpenOrders, fetchClosedOrders
        //
        //     {
        //         "order": {
        //             "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2",
        //             "account_id": "1eb2ad5d-55f1-40b5-bc92-7dc05869e905",
        //             "instrument_code": "BTC_EUR",
        //             "amount": "1234.5678",
        //             "filled_amount": "1234.5678",
        //             "side": "BUY",
        //             "type": "LIMIT",
        //             "status": "OPEN",
        //             "sequence": 123456789,
        //             "price": "1234.5678",
        //             "average_price": "1234.5678",
        //             "reason": "INSUFFICIENT_FUNDS",
        //             "time": "2019-08-24T14:15:22Z",
        //             "time_in_force": "GOOD_TILL_CANCELLED",
        //             "time_last_updated": "2019-08-24T14:15:22Z",
        //             "expire_after": "2019-08-24T14:15:22Z",
        //             "is_post_only": false,
        //             "time_triggered": "2019-08-24T14:15:22Z",
        //             "trigger_price": "1234.5678"
        //         },
        //         "trades": [
        //             {
        //                 "fee": {
        //                     "fee_amount": "0.0014",
        //                     "fee_currency": "BTC",
        //                     "fee_percentage": "0.1",
        //                     "fee_group_id": "default",
        //                     "fee_type": "TAKER",
        //                     "running_trading_volume": "0.0"
        //                 },
        //                 "trade": {
        //                     "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664",
        //                     "order_id": "36bb2437-7402-4794-bf26-4bdf03526439",
        //                     "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9",
        //                     "amount": "1.4",
        //                     "side": "BUY",
        //                     "instrument_code": "BTC_EUR",
        //                     "price": "7341.4",
        //                     "time": "2019-09-27T15:05:32.564Z",
        //                     "sequence": 48670
        //                 }
        //             }
        //         ]
        //     }
        //
        const rawTrades = this.safeValue (order, 'trades', []);
        order = this.safeValue (order, 'order', order);
        const id = this.safeString (order, 'order_id');
        const clientOrderId = this.safeString (order, 'client_id');
        const timestamp = this.parse8601 (this.safeString (order, 'time'));
        let status = this.parseOrderStatus (this.safeString (order, 'status'));
        const marketId = this.safeString (order, 'instrument_code');
        const symbol = this.safeSymbol (marketId, market, '_');
        const price = this.safeFloat (order, 'price');
        const amount = this.safeFloat (order, 'amount');
        let cost = undefined;
        const filled = this.safeFloat (order, 'filled_amount');
        let remaining = undefined;
        if (filled !== undefined) {
            if (amount !== undefined) {
                remaining = Math.max (0, amount - filled);
                if (status === undefined) {
                    if (remaining > 0) {
                        status = 'open';
                    } else {
                        status = 'closed';
                    }
                }
            }
        }
        const side = this.safeStringLower (order, 'side');
        const type = this.safeStringLower (order, 'type');
        const trades = this.parseTrades (rawTrades, market, undefined, undefined);
        const fees = [];
        const numTrades = trades.length;
        let lastTradeTimestamp = undefined;
        let tradeCost = undefined;
        let tradeAmount = undefined;
        if (numTrades > 0) {
            lastTradeTimestamp = trades[0]['timestamp'];
            tradeCost = 0;
            tradeAmount = 0;
            for (let i = 0; i < trades.length; i++) {
                const trade = trades[i];
                fees.push (trade['fee']);
                lastTradeTimestamp = Math.max (lastTradeTimestamp, trade['timestamp']);
                tradeCost = this.sum (tradeCost, trade['cost']);
                tradeAmount = this.sum (tradeAmount, trade['amount']);
            }
        }
        let average = this.safeFloat (order, 'average_price');
        if (average === undefined) {
            if ((tradeCost !== undefined) && (tradeAmount !== undefined) && (tradeAmount !== 0)) {
                average = tradeCost / tradeAmount;
            }
        }
        if (cost === undefined) {
            if ((average !== undefined) && (filled !== undefined)) {
                cost = average * filled;
            }
        }
        const timeInForce = this.parseTimeInForce (this.safeString (order, 'time_in_force'));
        const stopPrice = this.safeFloat (order, 'trigger_price');
        const postOnly = this.safeValue (order, 'is_post_only');
        const result = {
            'id': id,
            'clientOrderId': clientOrderId,
            'info': order,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': side,
            'price': price,
            'stopPrice': stopPrice,
            'amount': amount,
            'cost': cost,
            'average': average,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            // 'fee': undefined,
            'trades': trades,
        };
        const numFees = fees.length;
        if (numFees > 0) {
            if (numFees === 1) {
                result['fee'] = fees[0];
            } else {
                const feesByCurrency = this.groupBy (fees, 'currency');
                const feeCurrencies = Object.keys (feesByCurrency);
                const numFeesByCurrency = feeCurrencies.length;
                if (numFeesByCurrency === 1) {
                    const feeCurrency = feeCurrencies[0];
                    const feeArray = this.safeValue (feesByCurrency, feeCurrency);
                    let feeCost = 0;
                    for (let i = 0; i < feeArray.length; i++) {
                        feeCost = this.sum (feeCost, feeArray[i]['cost']);
                    }
                    result['fee'] = {
                        'cost': feeCost,
                        'currency': feeCurrency,
                    };
                } else {
                    result['fees'] = fees;
                }
            }
        } else {
            result['fee'] = undefined;
        }
        return result;
    }

    parseTimeInForce (timeInForce) {
        const timeInForces = {
            'GOOD_TILL_CANCELLED': 'GTC',
            'GOOD_TILL_TIME': 'GTT',
            'IMMEDIATE_OR_CANCELLED': 'IOC',
            'FILL_OR_KILL': 'FOK',
        };
        return this.safeString (timeInForces, timeInForce, timeInForce);
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const uppercaseType = type.toUpperCase ();
        const request = {
            'instrument_code': market['id'],
            'type': uppercaseType, // LIMIT, MARKET, STOP
            'side': side.toUpperCase (), // or SELL
            'amount': this.amountToPrecision (symbol, amount),
            // "price": "1234.5678", // required for LIMIT and STOP orders
            // "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206", // optional
            // "time_in_force": "GOOD_TILL_CANCELLED", // limit orders only, GOOD_TILL_CANCELLED, GOOD_TILL_TIME, IMMEDIATE_OR_CANCELLED and FILL_OR_KILL
            // "expire_after": "2020-07-02T19:40:13Z", // required for GOOD_TILL_TIME
            // "is_post_only": false, // limit orders only, optional
            // "trigger_price": "1234.5678" // required for stop orders
        };
        let priceIsRequired = false;
        if (uppercaseType === 'LIMIT' || uppercaseType === 'STOP') {
            priceIsRequired = true;
        }
        if (uppercaseType === 'STOP') {
            const triggerPrice = this.safeFloat (params, 'trigger_price');
            if (triggerPrice === undefined) {
                throw new ArgumentsRequired (this.id + ' createOrder() requires a trigger_price param for ' + type + ' orders');
            }
            request['trigger_price'] = this.priceToPrecision (symbol, triggerPrice);
            params = this.omit (params, 'trigger_price');
        }
        if (priceIsRequired) {
            request['price'] = this.priceToPrecision (symbol, price);
        }
        const clientOrderId = this.safeString2 (params, 'clientOrderId', 'client_id');
        if (clientOrderId !== undefined) {
            request['client_id'] = clientOrderId;
            params = this.omit (params, [ 'clientOrderId', 'client_id' ]);
        }
        const response = await this.privatePostAccountOrders (this.extend (request, params));
        //
        //     {
        //         "order_id": "d5492c24-2995-4c18-993a-5b8bf8fffc0d",
        //         "client_id": "d75fb03b-b599-49e9-b926-3f0b6d103206",
        //         "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9",
        //         "instrument_code": "BTC_EUR",
        //         "time": "2019-08-01T08:00:44.026Z",
        //         "side": "BUY",
        //         "price": "5000",
        //         "amount": "1",
        //         "filled_amount": "0.5",
        //         "type": "LIMIT",
        //         "time_in_force": "GOOD_TILL_CANCELLED"
        //     }
        //
        return this.parseOrder (response, market);
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        const clientOrderId = this.safeString2 (params, 'clientOrderId', 'client_id');
        params = this.omit (params, [ 'clientOrderId', 'client_id' ]);
        let method = 'privateDeleteAccountOrdersOrderId';
        const request = {};
        if (clientOrderId !== undefined) {
            method = 'privateDeleteAccountOrdersClientClientId';
            request['client_id'] = clientOrderId;
        } else {
            request['order_id'] = id;
        }
        const response = await this[method] (this.extend (request, params));
        //
        // responds with an empty body
        //
        return response;
    }

    async cancelAllOrders (symbol = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {};
        if (symbol !== undefined) {
            const market = this.market (symbol);
            request['instrument_code'] = market['id'];
        }
        const response = await this.privateDeleteAccountOrders (this.extend (request, params));
        //
        //     [
        //         "a10e9bd1-8f72-4cfe-9f1b-7f1c8a9bd8ee"
        //     ]
        //
        return response;
    }

    async cancelOrders (ids, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            'ids': ids.join (','),
        };
        const response = await this.privateDeleteAccountOrders (this.extend (request, params));
        //
        //     [
        //         "a10e9bd1-8f72-4cfe-9f1b-7f1c8a9bd8ee"
        //     ]
        //
        return response;
    }

    async fetchOrder (id, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            'order_id': id,
        };
        const response = await this.privateGetAccountOrdersOrderId (this.extend (request, params));
        //
        //     {
        //         "order": {
        //             "order_id": "36bb2437-7402-4794-bf26-4bdf03526439",
        //             "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9",
        //             "time_last_updated": "2019-09-27T15:05:35.096Z",
        //             "sequence": 48782,
        //             "price": "7349.2",
        //             "filled_amount": "100.0",
        //             "status": "FILLED_FULLY",
        //             "amount": "100.0",
        //             "instrument_code": "BTC_EUR",
        //             "side": "BUY",
        //             "time": "2019-09-27T15:05:32.063Z",
        //             "type": "MARKET"
        //         },
        //         "trades": [
        //             {
        //                 "fee": {
        //                     "fee_amount": "0.0014",
        //                     "fee_currency": "BTC",
        //                     "fee_percentage": "0.1",
        //                     "fee_group_id": "default",
        //                     "fee_type": "TAKER",
        //                     "running_trading_volume": "0.0"
        //                 },
        //                 "trade": {
        //                     "trade_id": "fdff2bcc-37d6-4a2d-92a5-46e09c868664",
        //                     "order_id": "36bb2437-7402-4794-bf26-4bdf03526439",
        //                     "account_id": "a4c699f6-338d-4a26-941f-8f9853bfc4b9",
        //                     "amount": "1.4",
        //                     "side": "BUY",
        //                     "instrument_code": "BTC_EUR",
        //                     "price": "7341.4",
        //                     "time": "2019-09-27T15:05:32.564Z",
        //                     "sequence": 48670
        //                 }
        //             }
        //         ]
        //     }
        //
        return this.parseOrder (response);
    }

    async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            // 'from': this.iso8601 (since),
            // 'to': this.iso8601 (this.milliseconds ()), // max range is 100 days
            // 'instrument_code': market['id'],
            // 'with_cancelled_and_rejected': false, // default is false, orders which have been cancelled by the user before being filled or rejected by the system as invalid, additionally, all inactive filled orders which would return with "with_just_filled_inactive"
            // 'with_just_filled_inactive': false, // orders which have been filled and are no longer open, use of "with_cancelled_and_rejected" extends "with_just_filled_inactive" and in case both are specified the latter is ignored
            // 'with_just_orders': false, // do not return any trades corresponsing to the orders, it may be significanly faster and should be used if user is not interesting in trade information
            // 'max_page_size': 100,
            // 'cursor': 'string', // pointer specifying the position from which the next pages should be returned
        };
        let market = undefined;
        if (symbol !== undefined) {
            market = this.market (symbol);
            request['instrument_code'] = market['id'];
        }
        if (since !== undefined) {
            const to = this.safeString (params, 'to');
            if (to === undefined) {
                throw new ArgumentsRequired (this.id + ' fetchOrders() requires a "to" iso8601 string param with the since argument is specified, max range is 100 days');
            }
            request['from'] = this.iso8601 (since);
        }
        if (limit !== undefined) {
            request['max_page_size'] = limit;
        }
        const response = await this.privateGetAccountOrders (this.extend (request, params));
        //
        //     {
        //         "order_history": [
        //             {
        //                 "order": {
        //                     "trigger_price": "12089.88",
        //                     "order_id": "d453ca12-c650-46dd-9dee-66910d96bfc0",
        //                     "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a",
        //                     "instrument_code": "BTC_USDT",
        //                     "time": "2019-08-23T10:02:31.663Z",
        //                     "side": "SELL",
        //                     "price": "10159.76",
        //                     "average_price": "10159.76",
        //                     "amount": "0.2",
        //                     "filled_amount": "0.2",
        //                     "type": "STOP",
        //                     "sequence": 8,
        //                     "status": "FILLED_FULLY"
        //                 },
        //                 "trades": [
        //                     {
        //                         "fee": {
        //                             "fee_amount": "0.4188869",
        //                             "fee_currency": "USDT",
        //                             "fee_percentage": "0.1",
        //                             "fee_group_id": "default",
        //                             "fee_type": "TAKER",
        //                             "running_trading_volume": "0.0"
        //                         },
        //                         "trade": {
        //                             "trade_id": "ec82896f-fd1b-4cbb-89df-a9da85ccbb4b",
        //                             "order_id": "d453ca12-c650-46dd-9dee-66910d96bfc0",
        //                             "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a",
        //                             "amount": "0.2",
        //                             "side": "SELL",
        //                             "instrument_code": "BTC_USDT",
        //                             "price": "10159.76",
        //                             "time": "2019-08-23T10:02:32.663Z",
        //                             "sequence": 9
        //                         }
        //                     }
        //                 ]
        //             },
        //             {
        //                 "order": {
        //                     "order_id": "5151a99e-f414-418f-8cf1-2568d0a63ea5",
        //                     "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a",
        //                     "instrument_code": "BTC_USDT",
        //                     "time": "2019-08-23T10:01:36.773Z",
        //                     "side": "SELL",
        //                     "price": "12289.88",
        //                     "amount": "0.5",
        //                     "filled_amount": "0.0",
        //                     "type": "LIMIT",
        //                     "sequence": 7,
        //                     "status": "OPEN"
        //                 },
        //                 "trades": []
        //             },
        //             {
        //                 "order": {
        //                     "order_id": "ac80d857-75e1-4733-9070-fd4288395fdc",
        //                     "account_id": "ef3a5f4c-cfcd-415e-ba89-5a9abf47b28a",
        //                     "instrument_code": "BTC_USDT",
        //                     "time": "2019-08-23T10:01:25.031Z",
        //                     "side": "SELL",
        //                     "price": "11089.88",
        //                     "amount": "0.1",
        //                     "filled_amount": "0.0",
        //                     "type": "LIMIT",
        //                     "sequence": 6,
        //                     "status": "OPEN"
        //                 },
        //                 "trades": []
        //             }
        //         ],
        //         "max_page_size": 100
        //     }
        //
        const orderHistory = this.safeValue (response, 'order_history', []);
        return this.parseOrders (orderHistory, market, since, limit);
    }

    async fetchClosedOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        const request = {
            'with_cancelled_and_rejected': true, // default is false, orders which have been cancelled by the user before being filled or rejected by the system as invalid, additionally, all inactive filled orders which would return with "with_just_filled_inactive"
        };
        return await this.fetchOpenOrders (symbol, since, limit, this.extend (request, params));
    }

    async fetchOrderTrades (id, symbol = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            'order_id': id,
            // 'max_page_size': 100,
            // 'cursor': 'string', // pointer specifying the position from which the next pages should be returned
        };
        if (limit !== undefined) {
            request['max_page_size'] = limit;
        }
        const response = await this.privateGetAccountOrdersOrderIdTrades (this.extend (request, params));
        //
        //     {
        //         "trade_history": [
        //             {
        //                 "trade": {
        //                     "trade_id": "2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef",
        //                     "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2",
        //                     "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58",
        //                     "amount": "1234.5678",
        //                     "side": "BUY",
        //                     "instrument_code": "BTC_EUR",
        //                     "price": "1234.5678",
        //                     "time": "2019-08-24T14:15:22Z",
        //                     "price_tick_sequence": 0,
        //                     "sequence": 123456789
        //                 },
        //                 "fee": {
        //                     "fee_amount": "1234.5678",
        //                     "fee_percentage": "1234.5678",
        //                     "fee_group_id": "default",
        //                     "running_trading_volume": "1234.5678",
        //                     "fee_currency": "BTC",
        //                     "fee_type": "TAKER"
        //                 }
        //             }
        //         ],
        //         "max_page_size": 0,
        //         "cursor": "string"
        //     }
        //
        const tradeHistory = this.safeValue (response, 'trade_history', []);
        let market = undefined;
        if (symbol !== undefined) {
            market = this.market (symbol);
        }
        return this.parseTrades (tradeHistory, market, since, limit);
    }

    async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            // 'from': this.iso8601 (since),
            // 'to': this.iso8601 (this.milliseconds ()), // max range is 100 days
            // 'instrument_code': market['id'],
            // 'max_page_size': 100,
            // 'cursor': 'string', // pointer specifying the position from which the next pages should be returned
        };
        let market = undefined;
        if (symbol !== undefined) {
            market = this.market (symbol);
            request['instrument_code'] = market['id'];
        }
        if (since !== undefined) {
            const to = this.safeString (params, 'to');
            if (to === undefined) {
                throw new ArgumentsRequired (this.id + ' fetchMyTrades() requires a "to" iso8601 string param with the since argument is specified, max range is 100 days');
            }
            request['from'] = this.iso8601 (since);
        }
        if (limit !== undefined) {
            request['max_page_size'] = limit;
        }
        const response = await this.privateGetAccountTrades (this.extend (request, params));
        //
        //     {
        //         "trade_history": [
        //             {
        //                 "trade": {
        //                     "trade_id": "2b42efcd-d5b7-4a56-8e12-b69ffd68c5ef",
        //                     "order_id": "66756a10-3e86-48f4-9678-b634c4b135b2",
        //                     "account_id": "c2d0076a-c20d-41f8-9e9a-1a1d028b2b58",
        //                     "amount": "1234.5678",
        //                     "side": "BUY",
        //                     "instrument_code": "BTC_EUR",
        //                     "price": "1234.5678",
        //                     "time": "2019-08-24T14:15:22Z",
        //                     "price_tick_sequence": 0,
        //                     "sequence": 123456789
        //                 },
        //                 "fee": {
        //                     "fee_amount": "1234.5678",
        //                     "fee_percentage": "1234.5678",
        //                     "fee_group_id": "default",
        //                     "running_trading_volume": "1234.5678",
        //                     "fee_currency": "BTC",
        //                     "fee_type": "TAKER"
        //                 }
        //             }
        //         ],
        //         "max_page_size": 0,
        //         "cursor": "string"
        //     }
        //
        const tradeHistory = this.safeValue (response, 'trade_history', []);
        return this.parseTrades (tradeHistory, market, since, limit);
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) {
        let url = this.urls['api'][api] + '/' + this.version + '/' + this.implodeParams (path, params);
        const query = this.omit (params, this.extractParams (path));
        if (api === 'public') {
            if (Object.keys (query).length) {
                url += '?' + this.urlencode (query);
            }
        } else if (api === 'private') {
            this.checkRequiredCredentials ();
            headers = {
                'Accept': 'application/json',
                'Authorization': 'Bearer ' + this.apiKey,
            };
            if (method === 'POST') {
                body = this.json (query);
                headers['Content-Type'] = 'application/json';
            } else {
                if (Object.keys (query).length) {
                    url += '?' + this.urlencode (query);
                }
            }
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
        if (response === undefined) {
            return;
        }
        //
        //     {"error":"MISSING_FROM_PARAM"}
        //     {"error":"MISSING_TO_PARAM"}
        //     {"error":"CANDLESTICKS_TIME_RANGE_TOO_BIG"}
        //
        const message = this.safeString (response, 'error');
        if (message !== undefined) {
            const feedback = this.id + ' ' + body;
            this.throwExactlyMatchedException (this.exceptions['exact'], message, feedback);
            this.throwBroadlyMatchedException (this.exceptions['broad'], message, feedback);
            throw new ExchangeError (feedback); // unknown message
        }
    }
};
