<?php

namespace ccxt;

// 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

use Exception; // a common import
use \ccxt\ExchangeError;
use \ccxt\ArgumentsRequired;
use \ccxt\InvalidOrder;
use \ccxt\DDoSProtection;

class currencycom extends Exchange {

    public function describe() {
        return $this->deep_extend(parent::describe (), array(
            'id' => 'currencycom',
            'name' => 'Currency.com',
            'countries' => array( 'BY' ), // Belarus
            'rateLimit' => 500,
            'certified' => true,
            'pro' => true,
            'version' => 'v1',
            // new metainfo interface
            'has' => array(
                'cancelOrder' => true,
                'CORS' => false,
                'createOrder' => true,
                'fetchAccounts' => true,
                'fetchBalance' => true,
                'fetchMarkets' => true,
                'fetchMyTrades' => true,
                'fetchOHLCV' => true,
                'fetchOpenOrders' => true,
                'fetchOrderBook' => true,
                'fetchTicker' => true,
                'fetchTickers' => true,
                'fetchTime' => true,
                'fetchTradingFees' => true,
                'fetchTrades' => true,
            ),
            'timeframes' => array(
                '1m' => '1m',
                '3m' => '3m',
                '5m' => '5m',
                '15m' => '15m',
                '30m' => '30m',
                '1h' => '1h',
                '4h' => '4h',
                '1d' => '1d',
                '1w' => '1w',
            ),
            'urls' => array(
                'logo' => 'https://user-images.githubusercontent.com/1294454/83718672-36745c00-a63e-11ea-81a9-677b1f789a4d.jpg',
                'api' => array(
                    'public' => 'https://api-adapter.backend.currency.com/api',
                    'private' => 'https://api-adapter.backend.currency.com/api',
                ),
                'www' => 'https://www.currency.com',
                'referral' => 'https://currency.com/trading/signup?c=362jaimv&pid=referral',
                'doc' => array(
                    'https://currency.com/api',
                ),
                'fees' => 'https://currency.com/fees-charges',
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        'time',
                        'exchangeInfo',
                        'depth',
                        'aggTrades',
                        'klines',
                        'ticker/24hr',
                    ),
                ),
                'private' => array(
                    'get' => array(
                        'leverageSettings',
                        'openOrders',
                        'tradingPositions',
                        'account',
                        'myTrades',
                    ),
                    'post' => array(
                        'order',
                        'updateTradingPosition',
                        'updateTradingOrder',
                        'closeTradingPosition',
                    ),
                    'delete' => array(
                        'order',
                    ),
                ),
            ),
            'fees' => array(
                'trading' => array(
                    'tierBased' => false,
                    'percentage' => true,
                    'taker' => 0.002,
                    'maker' => 0.002,
                ),
            ),
            'precisionMode' => TICK_SIZE,
            // exchange-specific options
            'options' => array(
                'defaultTimeInForce' => 'GTC', // 'GTC' = Good To Cancel (default), 'IOC' = Immediate Or Cancel, 'FOK' = Fill Or Kill
                'warnOnFetchOpenOrdersWithoutSymbol' => true,
                'recvWindow' => 5 * 1000, // 5 sec, default
                'timeDifference' => 0, // the difference between system clock and Binance clock
                'adjustForTimeDifference' => false, // controls the adjustment logic upon instantiation
                'parseOrderToPrecision' => false, // force amounts and costs in parseOrder to precision
                'newOrderRespType' => array(
                    'market' => 'FULL', // 'ACK' for order id, 'RESULT' for full order or 'FULL' for order with fills
                    'limit' => 'RESULT', // we change it from 'ACK' by default to 'RESULT'
                    'stop' => 'RESULT',
                ),
            ),
            'exceptions' => array(
                'broad' => array(
                    'FIELD_VALIDATION_ERROR Cancel is available only for LIMIT order' => '\\ccxt\\InvalidOrder',
                    'API key does not exist' => '\\ccxt\\AuthenticationError',
                    'Order would trigger immediately.' => '\\ccxt\\InvalidOrder',
                    'Account has insufficient balance for requested action.' => '\\ccxt\\InsufficientFunds',
                    'Rest API trading is not enabled.' => '\\ccxt\\ExchangeNotAvailable',
                ),
                'exact' => array(
                    '-1000' => '\\ccxt\\ExchangeNotAvailable', // array("code":-1000,"msg":"An unknown error occured while processing the request.")
                    '-1013' => '\\ccxt\\InvalidOrder', // createOrder -> 'invalid quantity'/'invalid price'/MIN_NOTIONAL
                    '-1021' => '\\ccxt\\InvalidNonce', // 'your time is ahead of server'
                    '-1022' => '\\ccxt\\AuthenticationError', // array("code":-1022,"msg":"Signature for this request is not valid.")
                    '-1100' => '\\ccxt\\InvalidOrder', // createOrder(symbol, 1, asdf) -> 'Illegal characters found in parameter 'price'
                    '-1104' => '\\ccxt\\ExchangeError', // Not all sent parameters were read, read 8 parameters but was sent 9
                    '-1025' => '\\ccxt\\AuthenticationError', // array("code":-1025,"msg":"Invalid API-key, IP, or permissions for action")
                    '-1128' => '\\ccxt\\BadRequest', // array("code":-1128,"msg":"Combination of optional parameters invalid.")
                    '-2010' => '\\ccxt\\ExchangeError', // generic error code for createOrder -> 'Account has insufficient balance for requested action.', array("code":-2010,"msg":"Rest API trading is not enabled."), etc...
                    '-2011' => '\\ccxt\\OrderNotFound', // cancelOrder(1, 'BTC/USDT') -> 'UNKNOWN_ORDER'
                    '-2013' => '\\ccxt\\OrderNotFound', // fetchOrder (1, 'BTC/USDT') -> 'Order does not exist'
                    '-2014' => '\\ccxt\\AuthenticationError', // array( "code":-2014, "msg" => "API-key format invalid." )
                    '-2015' => '\\ccxt\\AuthenticationError', // "Invalid API-key, IP, or permissions for action."
                ),
            ),
            'commonCurrencies' => array(
                'IQ' => 'iQIYI',
            ),
        ));
    }

    public function nonce() {
        return $this->milliseconds() - $this->options['timeDifference'];
    }

    public function fetch_time($params = array ()) {
        $response = $this->publicGetTime ($params);
        //
        //     {
        //         "serverTime" => 1590998366609
        //     }
        //
        return $this->safe_integer($response, 'serverTime');
    }

    public function load_time_difference($params = array ()) {
        $response = $this->publicGetTime ($params);
        $after = $this->milliseconds();
        $this->options['timeDifference'] = intval($after - $response['serverTime']);
        return $this->options['timeDifference'];
    }

    public function fetch_markets($params = array ()) {
        $response = $this->publicGetExchangeInfo ($params);
        //
        //     {
        //         "timezone":"UTC",
        //         "serverTime":1603252990096,
        //         "rateLimits":array(
        //             array("rateLimitType":"REQUEST_WEIGHT","interval":"MINUTE","intervalNum":1,"limit":1200),
        //             array("rateLimitType":"ORDERS","interval":"SECOND","intervalNum":1,"limit":10),
        //             array("rateLimitType":"ORDERS","interval":"DAY","intervalNum":1,"limit":864000),
        //         ),
        //         "exchangeFilters":array(),
        //         "symbols":[
        //             array(
        //                 "$symbol":"EVK",
        //                 "name":"Evonik",
        //                 "$status":"BREAK",
        //                 "baseAsset":"EVK",
        //                 "baseAssetPrecision":3,
        //                 "quoteAsset":"EUR",
        //                 "quoteAssetId":"EUR",
        //                 "quotePrecision":3,
        //                 "orderTypes":["LIMIT","MARKET"],
        //                 "$filters":array(
        //                     array("filterType":"LOT_SIZE","minQty":"1","maxQty":"27000","stepSize":"1"),
        //                     array("filterType":"MIN_NOTIONAL","minNotional":"23")
        //                 ),
        //                 "marketType":"SPOT",
        //                 "country":"DE",
        //                 "sector":"Basic Materials",
        //                 "industry":"Diversified Chemicals",
        //                 "tradingHours":"UTC; Mon 07:02 - 15:30; Tue 07:02 - 15:30; Wed 07:02 - 15:30; Thu 07:02 - 15:30; Fri 07:02 - 15:30",
        //                 "tickSize":0.005,
        //                 "tickValue":0.11125,
        //                 "$exchangeFee":0.05
        //             ),
        //             array(
        //                 "$symbol":"BTC/USD_LEVERAGE",
        //                 "name":"Bitcoin / USD",
        //                 "$status":"TRADING",
        //                 "baseAsset":"BTC",
        //                 "baseAssetPrecision":3,
        //                 "quoteAsset":"USD",
        //                 "quoteAssetId":"USD_LEVERAGE",
        //                 "quotePrecision":3,
        //                 "orderTypes":["LIMIT","MARKET","STOP"],
        //                 "$filters":array(
        //                     array("filterType":"LOT_SIZE","minQty":"0.001","maxQty":"100","stepSize":"0.001"),
        //                     array("filterType":"MIN_NOTIONAL","minNotional":"13")
        //                 ),
        //                 "marketType":"LEVERAGE",
        //                 "longRate":-0.01,
        //                 "shortRate":0.01,
        //                 "swapChargeInterval":480,
        //                 "country":"",
        //                 "sector":"",
        //                 "industry":"",
        //                 "tradingHours":"UTC; Mon - 21:00, 21:05 -; Tue - 21:00, 21:05 -; Wed - 21:00, 21:05 -; Thu - 21:00, 21:05 -; Fri - 21:00, 22:01 -; Sat - 21:00, 21:05 -; Sun - 20:00, 21:05 -",
        //                 "tickSize":0.05,
        //                 "tickValue":610.20875,
        //                 "$makerFee":-0.025,
        //                 "$takerFee":0.075
        //             ),
        //         ]
        //     }
        //
        if ($this->options['adjustForTimeDifference']) {
            $this->load_time_difference();
        }
        $markets = $this->safe_value($response, 'symbols');
        $result = array();
        for ($i = 0; $i < count($markets); $i++) {
            $market = $markets[$i];
            $id = $this->safe_string($market, 'symbol');
            $baseId = $this->safe_string($market, 'baseAsset');
            $quoteId = $this->safe_string($market, 'quoteAsset');
            $base = $this->safe_currency_code($baseId);
            $quote = $this->safe_currency_code($quoteId);
            $symbol = $base . '/' . $quote;
            if (mb_strpos($id, '/') !== false) {
                $symbol = $id;
            }
            $filters = $this->safe_value($market, 'filters', array());
            $filtersByType = $this->index_by($filters, 'filterType');
            $precision = array(
                'amount' => 1 / pow(1, $this->safe_integer($market, 'baseAssetPrecision')),
                'price' => $this->safe_float($market, 'tickSize'),
            );
            $status = $this->safe_string($market, 'status');
            $active = ($status === 'TRADING');
            $type = $this->safe_string_lower($market, 'marketType');
            if ($type === 'leverage') {
                $type = 'margin';
            }
            $spot = ($type === 'spot');
            $margin = ($type === 'margin');
            $entry = array(
                'id' => $id,
                'symbol' => $symbol,
                'base' => $base,
                'quote' => $quote,
                'baseId' => $baseId,
                'quoteId' => $quoteId,
                'type' => $type,
                'spot' => $spot,
                'margin' => $margin,
                'info' => $market,
                'active' => $active,
                'precision' => $precision,
                'limits' => array(
                    'amount' => array(
                        'min' => pow(10, -$precision['amount']),
                        'max' => null,
                    ),
                    'price' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'cost' => array(
                        'min' => -1 * log10 ($precision['amount']),
                        'max' => null,
                    ),
                ),
            );
            $exchangeFee = $this->safe_float_2($market, 'exchangeFee', 'tradingFee');
            $makerFee = $this->safe_float($market, 'makerFee', $exchangeFee);
            $takerFee = $this->safe_float($market, 'takerFee', $exchangeFee);
            if ($makerFee !== null) {
                $entry['maker'] = $makerFee / 100;
            }
            if ($takerFee !== null) {
                $entry['taker'] = $takerFee / 100;
            }
            if (is_array($filtersByType) && array_key_exists('PRICE_FILTER', $filtersByType)) {
                $filter = $this->safe_value($filtersByType, 'PRICE_FILTER', array());
                $entry['precision']['price'] = $this->safe_float($filter, 'tickSize');
                // PRICE_FILTER reports zero values for $maxPrice
                // since they updated $filter types in November 2018
                // https://github.com/ccxt/ccxt/issues/4286
                // therefore limits['price']['max'] doesn't have any meaningful value except null
                $entry['limits']['price'] = array(
                    'min' => $this->safe_float($filter, 'minPrice'),
                    'max' => null,
                );
                $maxPrice = $this->safe_float($filter, 'maxPrice');
                if (($maxPrice !== null) && ($maxPrice > 0)) {
                    $entry['limits']['price']['max'] = $maxPrice;
                }
            }
            if (is_array($filtersByType) && array_key_exists('LOT_SIZE', $filtersByType)) {
                $filter = $this->safe_value($filtersByType, 'LOT_SIZE', array());
                $entry['precision']['amount'] = $this->safe_float($filter, 'stepSize');
                $entry['limits']['amount'] = array(
                    'min' => $this->safe_float($filter, 'minQty'),
                    'max' => $this->safe_float($filter, 'maxQty'),
                );
            }
            if (is_array($filtersByType) && array_key_exists('MARKET_LOT_SIZE', $filtersByType)) {
                $filter = $this->safe_value($filtersByType, 'MARKET_LOT_SIZE', array());
                $entry['limits']['market'] = array(
                    'min' => $this->safe_float($filter, 'minQty'),
                    'max' => $this->safe_float($filter, 'maxQty'),
                );
            }
            if (is_array($filtersByType) && array_key_exists('MIN_NOTIONAL', $filtersByType)) {
                $filter = $this->safe_value($filtersByType, 'MIN_NOTIONAL', array());
                $entry['limits']['cost']['min'] = $this->safe_float($filter, 'minNotional');
            }
            $result[] = $entry;
        }
        return $result;
    }

    public function calculate_fee($symbol, $type, $side, $amount, $price, $takerOrMaker = 'taker', $params = array ()) {
        $market = $this->markets[$symbol];
        $key = 'quote';
        $rate = $market[$takerOrMaker];
        $cost = $amount * $rate;
        $precision = $market['precision']['price'];
        if ($side === 'sell') {
            $cost *= $price;
        } else {
            $key = 'base';
            $precision = $market['precision']['amount'];
        }
        $cost = $this->decimal_to_precision($cost, ROUND, $precision, $this->precisionMode);
        return array(
            'type' => $takerOrMaker,
            'currency' => $market[$key],
            'rate' => $rate,
            'cost' => floatval($cost),
        );
    }

    public function fetch_accounts($params = array ()) {
        $response = $this->privateGetAccount ($params);
        //
        //     {
        //         "makerCommission":0.20,
        //         "takerCommission":0.20,
        //         "buyerCommission":0.20,
        //         "sellerCommission":0.20,
        //         "canTrade":true,
        //         "canWithdraw":true,
        //         "canDeposit":true,
        //         "updateTime":1591056268,
        //         "balances":array(
        //             array(
        //                 "$accountId":5470306579272968,
        //                 "collateralCurrency":true,
        //                 "asset":"ETH",
        //                 "free":0.0,
        //                 "locked":0.0,
        //                 "default":false,
        //             ),
        //         )
        //     }
        //
        $accounts = $this->safe_value($response, 'balances', array());
        $result = array();
        for ($i = 0; $i < count($accounts); $i++) {
            $account = $accounts[$i];
            $accountId = $this->safe_integer($account, 'accountId');
            $currencyId = $this->safe_string($account, 'asset');
            $currencyCode = $this->safe_currency_code($currencyId);
            $result[] = array(
                'id' => $accountId,
                'type' => null,
                'currency' => $currencyCode,
                'info' => $response,
            );
        }
        return $result;
    }

    public function fetch_trading_fees($params = array ()) {
        $this->load_markets();
        $response = $this->privateGetAccount ($params);
        return array(
            'info' => $response,
            'maker' => $this->safe_float($response, 'makerCommission'),
            'taker' => $this->safe_float($response, 'takerCommission'),
        );
    }

    public function parse_balance_response($response) {
        //
        //     {
        //         "makerCommission":0.20,
        //         "takerCommission":0.20,
        //         "buyerCommission":0.20,
        //         "sellerCommission":0.20,
        //         "canTrade":true,
        //         "canWithdraw":true,
        //         "canDeposit":true,
        //         "updateTime":1591056268,
        //         "$balances":array(
        //             array(
        //                 "accountId":5470306579272968,
        //                 "collateralCurrency":true,
        //                 "asset":"ETH",
        //                 "free":0.0,
        //                 "locked":0.0,
        //                 "default":false,
        //             ),
        //         )
        //     }
        //
        $result = array( 'info' => $response );
        $balances = $this->safe_value($response, 'balances', array());
        for ($i = 0; $i < count($balances); $i++) {
            $balance = $balances[$i];
            $currencyId = $this->safe_string($balance, 'asset');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['free'] = $this->safe_float($balance, 'free');
            $account['used'] = $this->safe_float($balance, 'locked');
            $result[$code] = $account;
        }
        return $this->parse_balance($result);
    }

    public function fetch_balance($params = array ()) {
        $this->load_markets();
        $response = $this->privateGetAccount ($params);
        //
        //     {
        //         "makerCommission":0.20,
        //         "takerCommission":0.20,
        //         "buyerCommission":0.20,
        //         "sellerCommission":0.20,
        //         "canTrade":true,
        //         "canWithdraw":true,
        //         "canDeposit":true,
        //         "updateTime":1591056268,
        //         "balances":array(
        //             array(
        //                 "accountId":5470306579272968,
        //                 "collateralCurrency":true,
        //                 "asset":"ETH",
        //                 "free":0.0,
        //                 "locked":0.0,
        //                 "default":false,
        //             ),
        //         )
        //     }
        //
        return $this->parse_balance_response($response);
    }

    public function fetch_order_book($symbol, $limit = null, $params = array ()) {
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'symbol' => $market['id'],
        );
        if ($limit !== null) {
            $request['limit'] = $limit; // default 100, max 1000, valid limits 5, 10, 20, 50, 100, 500, 1000, 5000
        }
        $response = $this->publicGetDepth (array_merge($request, $params));
        //
        //     {
        //         "lastUpdateId":1590999849037,
        //         "asks":[
        //             [0.02495,60.0000],
        //             [0.02496,120.0000],
        //             [0.02497,240.0000],
        //         ],
        //         "bids":[
        //             [0.02487,60.0000],
        //             [0.02486,120.0000],
        //             [0.02485,240.0000],
        //         ]
        //     }
        //
        $orderbook = $this->parse_order_book($response);
        $orderbook['nonce'] = $this->safe_integer($response, 'lastUpdateId');
        return $orderbook;
    }

    public function parse_ticker($ticker, $market = null) {
        //
        // fetchTicker
        //
        //     {
        //         "$symbol":"ETH/BTC",
        //         "priceChange":"0.00030",
        //         "priceChangePercent":"1.21",
        //         "weightedAvgPrice":"0.02481",
        //         "prevClosePrice":"0.02447",
        //         "lastPrice":"0.02477",
        //         "lastQty":"60.0",
        //         "bidPrice":"0.02477",
        //         "askPrice":"0.02484",
        //         "openPrice":"0.02447",
        //         "highPrice":"0.02524",
        //         "lowPrice":"0.02438",
        //         "volume":"11.97",
        //         "quoteVolume":"0.298053",
        //         "openTime":1590969600000,
        //         "closeTime":1591000072693
        //     }
        //
        // fetchTickers
        //
        //     {
        //         "$symbol":"EVK",
        //         "highPrice":"22.57",
        //         "lowPrice":"22.16",
        //         "volume":"1",
        //         "quoteVolume":"22.2",
        //         "openTime":1590699364000,
        //         "closeTime":1590785764000
        //     }
        //
        $timestamp = $this->safe_integer($ticker, 'closeTime');
        $marketId = $this->safe_string($ticker, 'symbol');
        $symbol = $this->safe_symbol($marketId, $market);
        $last = $this->safe_float($ticker, 'lastPrice');
        $open = $this->safe_float($ticker, 'openPrice');
        $average = null;
        if (($open !== null) && ($last !== null)) {
            $average = $this->sum($open, $last) / 2;
        }
        return array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->safe_float($ticker, 'highPrice'),
            'low' => $this->safe_float($ticker, 'lowPrice'),
            'bid' => $this->safe_float($ticker, 'bidPrice'),
            'bidVolume' => $this->safe_float($ticker, 'bidQty'),
            'ask' => $this->safe_float($ticker, 'askPrice'),
            'askVolume' => $this->safe_float($ticker, 'askQty'),
            'vwap' => $this->safe_float($ticker, 'weightedAvgPrice'),
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => $this->safe_float($ticker, 'prevClosePrice'), // previous day close
            'change' => $this->safe_float($ticker, 'priceChange'),
            'percentage' => $this->safe_float($ticker, 'priceChangePercent'),
            'average' => $average,
            'baseVolume' => $this->safe_float($ticker, 'volume'),
            'quoteVolume' => $this->safe_float($ticker, 'quoteVolume'),
            'info' => $ticker,
        );
    }

    public function fetch_ticker($symbol, $params = array ()) {
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'symbol' => $market['id'],
        );
        $response = $this->publicGetTicker24hr (array_merge($request, $params));
        //
        //     {
        //         "$symbol":"ETH/BTC",
        //         "priceChange":"0.00030",
        //         "priceChangePercent":"1.21",
        //         "weightedAvgPrice":"0.02481",
        //         "prevClosePrice":"0.02447",
        //         "lastPrice":"0.02477",
        //         "lastQty":"60.0",
        //         "bidPrice":"0.02477",
        //         "askPrice":"0.02484",
        //         "openPrice":"0.02447",
        //         "highPrice":"0.02524",
        //         "lowPrice":"0.02438",
        //         "volume":"11.97",
        //         "quoteVolume":"0.298053",
        //         "openTime":1590969600000,
        //         "closeTime":1591000072693
        //     }
        //
        return $this->parse_ticker($response, $market);
    }

    public function parse_tickers($rawTickers, $symbols = null) {
        $tickers = array();
        for ($i = 0; $i < count($rawTickers); $i++) {
            $tickers[] = $this->parse_ticker($rawTickers[$i]);
        }
        return $this->filter_by_array($tickers, 'symbol', $symbols);
    }

    public function fetch_tickers($symbols = null, $params = array ()) {
        $this->load_markets();
        $response = $this->publicGetTicker24hr ($params);
        //
        //     array(
        //         {
        //             "symbol":"EVK",
        //             "highPrice":"22.57",
        //             "lowPrice":"22.16",
        //             "volume":"1",
        //             "quoteVolume":"22.2",
        //             "openTime":1590699364000,
        //             "closeTime":1590785764000
        //         }
        //     )
        //
        return $this->parse_tickers($response, $symbols);
    }

    public function parse_ohlcv($ohlcv, $market = null) {
        //
        //     array(
        //         1590971040000,
        //         "0.02454",
        //         "0.02456",
        //         "0.02452",
        //         "0.02456",
        //         249
        //     )
        //
        return array(
            $this->safe_integer($ohlcv, 0),
            $this->safe_float($ohlcv, 1),
            $this->safe_float($ohlcv, 2),
            $this->safe_float($ohlcv, 3),
            $this->safe_float($ohlcv, 4),
            $this->safe_float($ohlcv, 5),
        );
    }

    public function fetch_ohlcv($symbol, $timeframe = '1m', $since = null, $limit = null, $params = array ()) {
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'symbol' => $market['id'],
            'interval' => $this->timeframes[$timeframe],
        );
        if ($since !== null) {
            $request['startTime'] = $since;
        }
        if ($limit !== null) {
            $request['limit'] = $limit; // default 500, max 1000
        }
        $response = $this->publicGetKlines (array_merge($request, $params));
        //
        //     [
        //         [1590971040000,"0.02454","0.02456","0.02452","0.02456",249],
        //         [1590971100000,"0.02455","0.02457","0.02452","0.02456",300],
        //         [1590971160000,"0.02455","0.02456","0.02453","0.02454",286],
        //     ]
        //
        return $this->parse_ohlcvs($response, $market, $timeframe, $since, $limit);
    }

    public function parse_trade($trade, $market = null) {
        //
        // fetchTrades (public aggregate trades)
        //
        //     {
        //         "a":1658318071,
        //         "p":"0.02476",
        //         "q":"0.0",
        //         "T":1591001423382,
        //         "m":false
        //     }
        //
        // createOrder fills (private)
        //
        //     {
        //         "$price" => "9807.05",
        //         "qty" => "0.01",
        //         "commission" => "0",
        //         "commissionAsset" => "dUSD"
        //     }
        //
        // fetchMyTrades
        //
        //     {
        //         "$symbol" => "BNBBTC",
        //         "$id" => 28457,
        //         "$orderId" => 100234,
        //         "$price" => "4.00000100",
        //         "qty" => "12.00000000",
        //         "commission" => "10.10000000",
        //         "commissionAsset" => "BNB",
        //         "time" => 1499865549590,
        //         "isBuyer" => true,
        //         "isMaker" => false,
        //         "isBestMatch" => true
        //     }
        //
        $timestamp = $this->safe_integer_2($trade, 'T', 'time');
        $price = $this->safe_float_2($trade, 'p', 'price');
        $amount = $this->safe_float_2($trade, 'q', 'qty');
        $id = $this->safe_string_2($trade, 'a', 'id');
        $side = null;
        $orderId = $this->safe_string($trade, 'orderId');
        if (is_array($trade) && array_key_exists('m', $trade)) {
            $side = $trade['m'] ? 'sell' : 'buy'; // this is reversed intentionally
        } else if (is_array($trade) && array_key_exists('isBuyerMaker', $trade)) {
            $side = $trade['isBuyerMaker'] ? 'sell' : 'buy';
        } else {
            if (is_array($trade) && array_key_exists('isBuyer', $trade)) {
                $side = ($trade['isBuyer']) ? 'buy' : 'sell'; // this is a true $side
            }
        }
        $fee = null;
        if (is_array($trade) && array_key_exists('commission', $trade)) {
            $fee = array(
                'cost' => $this->safe_float($trade, 'commission'),
                'currency' => $this->safe_currency_code($this->safe_string($trade, 'commissionAsset')),
            );
        }
        $takerOrMaker = null;
        if (is_array($trade) && array_key_exists('isMaker', $trade)) {
            $takerOrMaker = $trade['isMaker'] ? 'maker' : 'taker';
        }
        $marketId = $this->safe_string($trade, 'symbol');
        $symbol = $this->safe_symbol($marketId, $market);
        return array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'id' => $id,
            'order' => $orderId,
            'type' => null,
            'takerOrMaker' => $takerOrMaker,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'cost' => $price * $amount,
            'fee' => $fee,
        );
    }

    public function fetch_trades($symbol, $since = null, $limit = null, $params = array ()) {
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'symbol' => $market['id'],
            // 'limit' => 500, // default 500, max 1000
        );
        if ($limit !== null) {
            $request['limit'] = $limit; // default 500, max 1000
        }
        $response = $this->publicGetAggTrades (array_merge($request, $params));
        //
        //     array(
        //         {
        //             "a":1658318071,
        //             "p":"0.02476",
        //             "q":"0.0",
        //             "T":1591001423382,
        //             "m":false
        //         }
        //     )
        //
        return $this->parse_trades($response, $market, $since, $limit);
    }

    public function parse_order_status($status) {
        $statuses = array(
            'NEW' => 'open',
            'PARTIALLY_FILLED' => 'open',
            'FILLED' => 'closed',
            'CANCELED' => 'canceled',
            'PENDING_CANCEL' => 'canceling', // currently unused
            'REJECTED' => 'rejected',
            'EXPIRED' => 'expired',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order($order, $market = null) {
        //
        //     {
        //         "$symbol" => "BTC/USD",
        //         "orderId" => "00000000-0000-0000-0000-0000000c0263",
        //         "clientOrderId" => "00000000-0000-0000-0000-0000000c0263",
        //         "transactTime" => 1589878206426,
        //         "$price" => "9825.66210000",
        //         "origQty" => "0.01",
        //         "executedQty" => "0.01",
        //         "$status" => "FILLED",
        //         "$timeInForce" => "FOK",
        //         "$type" => "MARKET",
        //         "$side" => "BUY",
        //         "$fills" => array(
        //             {
        //                 "$price" => "9807.05",
        //                 "qty" => "0.01",
        //                 "commission" => "0",
        //                 "commissionAsset" => "dUSD"
        //             }
        //         )
        //     }
        //
        $status = $this->parse_order_status($this->safe_string($order, 'status'));
        $marketId = $this->safe_string($order, 'symbol');
        $symbol = $this->safe_symbol($marketId, $market, '/');
        $timestamp = null;
        if (is_array($order) && array_key_exists('time', $order)) {
            $timestamp = $this->safe_integer($order, 'time');
        } else if (is_array($order) && array_key_exists('transactTime', $order)) {
            $timestamp = $this->safe_integer($order, 'transactTime');
        }
        $price = $this->safe_float($order, 'price');
        $amount = $this->safe_float($order, 'origQty');
        $filled = $this->safe_float($order, 'executedQty');
        $remaining = null;
        $cost = $this->safe_float($order, 'cummulativeQuoteQty');
        if ($filled !== null) {
            if ($amount !== null) {
                $remaining = $amount - $filled;
                if ($this->options['parseOrderToPrecision']) {
                    $remaining = floatval($this->amount_to_precision($symbol, $remaining));
                }
                $remaining = max ($remaining, 0.0);
            }
            if ($price !== null) {
                if ($cost === null) {
                    $cost = $price * $filled;
                }
            }
        }
        $id = $this->safe_string($order, 'orderId');
        $type = $this->safe_string_lower($order, 'type');
        if ($type === 'market') {
            if ($price === 0.0) {
                if (($cost !== null) && ($filled !== null)) {
                    if (($cost > 0) && ($filled > 0)) {
                        $price = $cost / $filled;
                    }
                }
            }
        }
        $side = $this->safe_string_lower($order, 'side');
        $fee = null;
        $trades = null;
        $fills = $this->safe_value($order, 'fills');
        if ($fills !== null) {
            $trades = $this->parse_trades($fills, $market);
            $numTrades = is_array($trades) ? count($trades) : 0;
            if ($numTrades > 0) {
                $cost = $trades[0]['cost'];
                $fee = array(
                    'cost' => $trades[0]['fee']['cost'],
                    'currency' => $trades[0]['fee']['currency'],
                );
                for ($i = 1; $i < count($trades); $i++) {
                    $cost = $this->sum($cost, $trades[$i]['cost']);
                    $fee['cost'] = $this->sum($fee['cost'], $trades[$i]['fee']['cost']);
                }
            }
        }
        $average = null;
        if ($cost !== null) {
            if ($filled) {
                $average = $cost / $filled;
            }
            if ($this->options['parseOrderToPrecision']) {
                $cost = floatval($this->cost_to_precision($symbol, $cost));
            }
        }
        $timeInForce = $this->safe_string($order, 'timeInForce');
        return array(
            'info' => $order,
            'id' => $id,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => $timeInForce,
            'side' => $side,
            'price' => $price,
            'stopPrice' => null,
            'amount' => $amount,
            'cost' => $cost,
            'average' => $average,
            'filled' => $filled,
            'remaining' => $remaining,
            'status' => $status,
            'fee' => $fee,
            'trades' => $trades,
        );
    }

    public function create_order($symbol, $type, $side, $amount, $price = null, $params = array ()) {
        $this->load_markets();
        $market = $this->market($symbol);
        $accountId = null;
        if ($market['margin']) {
            $accountId = $this->safe_integer($params, 'accountId');
            if ($accountId === null) {
                throw new ArgumentsRequired($this->id . ' createOrder() requires an $accountId parameter for ' . $market['type'] . ' $market ' . $symbol);
            }
        }
        $uppercaseType = strtoupper($type);
        $newOrderRespType = $this->safe_value($this->options['newOrderRespType'], $type, 'RESULT');
        $request = array(
            'symbol' => $market['id'],
            'quantity' => $this->amount_to_precision($symbol, $amount),
            'type' => $uppercaseType,
            'side' => strtoupper($side),
            'newOrderRespType' => $newOrderRespType, // 'RESULT' for full order or 'FULL' for order with fills
            // 'leverage' => 1,
            // 'accountId' => 5470306579272968, // required for leverage markets
            // 'takeProfit' => '123.45',
            // 'stopLoss' => '54.321'
            // 'guaranteedStopLoss' => '54.321',
        );
        if ($uppercaseType === 'LIMIT') {
            $request['price'] = $this->price_to_precision($symbol, $price);
            $request['timeInForce'] = $this->options['defaultTimeInForce']; // 'GTC' = Good To Cancel (default), 'IOC' = Immediate Or Cancel, 'FOK' = Fill Or Kill
        } else if ($uppercaseType === 'STOP') {
            $request['price'] = $this->price_to_precision($symbol, $price);
        }
        $response = $this->privatePostOrder (array_merge($request, $params));
        //
        //     {
        //         "$symbol" => "BTC/USD",
        //         "orderId" => "00000000-0000-0000-0000-0000000c0263",
        //         "clientOrderId" => "00000000-0000-0000-0000-0000000c0263",
        //         "transactTime" => 1589878206426,
        //         "$price" => "9825.66210000",
        //         "origQty" => "0.01",
        //         "executedQty" => "0.01",
        //         "status" => "FILLED",
        //         "timeInForce" => "FOK",
        //         "$type" => "MARKET",
        //         "$side" => "BUY",
        //         "fills" => array(
        //             {
        //                 "$price" => "9807.05",
        //                 "qty" => "0.01",
        //                 "commission" => "0",
        //                 "commissionAsset" => "dUSD"
        //             }
        //         )
        //     }
        //
        return $this->parse_order($response, $market);
    }

    public function fetch_open_orders($symbol = null, $since = null, $limit = null, $params = array ()) {
        $this->load_markets();
        $market = null;
        $request = array();
        if ($symbol !== null) {
            $market = $this->market($symbol);
            $request['symbol'] = $market['id'];
        } else if ($this->options['warnOnFetchOpenOrdersWithoutSymbol']) {
            $symbols = $this->symbols;
            $numSymbols = is_array($symbols) ? count($symbols) : 0;
            $fetchOpenOrdersRateLimit = intval($numSymbols / 2);
            throw new ExchangeError($this->id . ' fetchOpenOrders() WARNING => fetching open orders without specifying a $symbol is rate-limited to one call per ' . (string) $fetchOpenOrdersRateLimit . ' seconds. Do not call this method frequently to avoid ban. Set ' . $this->id . '.options["warnOnFetchOpenOrdersWithoutSymbol"] = false to suppress this warning message.');
        }
        $response = $this->privateGetOpenOrders (array_merge($request, $params));
        return $this->parse_orders($response, $market, $since, $limit);
    }

    public function cancel_order($id, $symbol = null, $params = array ()) {
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' cancelOrder() requires a $symbol argument');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $origClientOrderId = $this->safe_value($params, 'origClientOrderId');
        $request = array(
            'symbol' => $market['id'],
            // 'orderId' => intval($id),
            // 'origClientOrderId' => $id,
        );
        if ($origClientOrderId === null) {
            $request['orderId'] = $id;
        } else {
            $request['origClientOrderId'] = $origClientOrderId;
        }
        $response = $this->privateDeleteOrder (array_merge($request, $params));
        //
        //     {
        //         "$symbol":"ETH/USD",
        //         "orderId":"00000000-0000-0000-0000-00000024383b",
        //         "clientOrderId":"00000000-0000-0000-0000-00000024383b",
        //         "price":"150",
        //         "origQty":"0.1",
        //         "executedQty":"0.0",
        //         "status":"CANCELED",
        //         "timeInForce":"GTC",
        //         "type":"LIMIT",
        //         "side":"BUY"
        //     }
        //
        return $this->parse_order($response, $market);
    }

    public function fetch_my_trades($symbol = null, $since = null, $limit = null, $params = array ()) {
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' fetchMyTrades() requires a $symbol argument');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'symbol' => $market['id'],
        );
        if ($limit !== null) {
            $request['limit'] = $limit;
        }
        $response = $this->privateGetMyTrades (array_merge($request, $params));
        //
        //     array(
        //         {
        //             "$symbol" => "BNBBTC",
        //             "id" => 28457,
        //             "orderId" => 100234,
        //             "price" => "4.00000100",
        //             "qty" => "12.00000000",
        //             "commission" => "10.10000000",
        //             "commissionAsset" => "BNB",
        //             "time" => 1499865549590,
        //             "isBuyer" => true,
        //             "isMaker" => false,
        //             "isBestMatch" => true
        //         }
        //     )
        //
        return $this->parse_trades($response, $market, $since, $limit);
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $url = $this->urls['api'][$api] . '/' . $this->version . '/' . $path;
        if ($path === 'historicalTrades') {
            $headers = array(
                'X-MBX-APIKEY' => $this->apiKey,
            );
        }
        if ($api === 'private') {
            $this->check_required_credentials();
            $query = $this->urlencode(array_merge(array(
                'timestamp' => $this->nonce(),
                'recvWindow' => $this->options['recvWindow'],
            ), $params));
            $signature = $this->hmac($this->encode($query), $this->encode($this->secret));
            $query .= '&' . 'signature=' . $signature;
            $headers = array(
                'X-MBX-APIKEY' => $this->apiKey,
            );
            if (($method === 'GET') || ($method === 'DELETE')) {
                $url .= '?' . $query;
            } else {
                $body = $query;
                $headers['Content-Type'] = 'application/x-www-form-urlencoded';
            }
        } else {
            if ($params) {
                $url .= '?' . $this->urlencode($params);
            }
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function handle_errors($httpCode, $reason, $url, $method, $headers, $body, $response, $requestHeaders, $requestBody) {
        if (($httpCode === 418) || ($httpCode === 429)) {
            throw new DDoSProtection($this->id . ' ' . (string) $httpCode . ' ' . $reason . ' ' . $body);
        }
        // error $response in a form => array( "code" => -1013, "msg" => "Invalid quantity." )
        // following block cointains legacy checks against $message patterns in "msg" property
        // will switch "code" checks eventually, when we know all of them
        if ($httpCode >= 400) {
            if (mb_strpos($body, 'Price * QTY is zero or less') !== false) {
                throw new InvalidOrder($this->id . ' order cost = amount * price is zero or less ' . $body);
            }
            if (mb_strpos($body, 'LOT_SIZE') !== false) {
                throw new InvalidOrder($this->id . ' order amount should be evenly divisible by lot size ' . $body);
            }
            if (mb_strpos($body, 'PRICE_FILTER') !== false) {
                throw new InvalidOrder($this->id . ' order price is invalid, i.e. exceeds allowed price precision, exceeds min price or max price limits or is invalid float value in general, use $this->price_to_precision(symbol, amount) ' . $body);
            }
        }
        if ($response === null) {
            return; // fallback to default error handler
        }
        //
        //     array("code":-1128,"msg":"Combination of optional parameters invalid.")
        //
        $errorCode = $this->safe_string($response, 'code');
        if (($errorCode !== null) && ($errorCode !== '0')) {
            $feedback = $this->id . ' ' . $this->json($response);
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback);
            $message = $this->safe_string($response, 'msg');
            $this->throw_broadly_matched_exception($this->exceptions['broad'], $message, $feedback);
            throw new ExchangeError($feedback);
        }
    }
}
