<?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\abstract\bitflyer as Exchange;

class bitflyer extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'bitflyer',
            'name' => 'bitFlyer',
            'countries' => array( 'JP' ),
            'version' => 'v1',
            'rateLimit' => 1000, // their nonce-timestamp is in seconds...
            'hostname' => 'bitflyer.com', // or bitflyer.com
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => false,
                'swap' => null, // has but not fully implemented
                'future' => null, // has but not fully implemented
                'option' => false,
                'cancelAllOrders' => null,  // https://lightning.bitflyer.com/docs?lang=en#cancel-all-orders
                'cancelOrder' => true,
                'createOrder' => true,
                'fetchBalance' => true,
                'fetchClosedOrders' => 'emulated',
                'fetchCurrencies' => false,
                'fetchDeposits' => true,
                'fetchFundingRate' => true,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchMarginMode' => false,
                'fetchMarkets' => true,
                'fetchMyTrades' => true,
                'fetchOpenOrders' => 'emulated',
                'fetchOrder' => 'emulated',
                'fetchOrderBook' => true,
                'fetchOrders' => true,
                'fetchPositionMode' => false,
                'fetchPositions' => true,
                'fetchTicker' => true,
                'fetchTrades' => true,
                'fetchTradingFee' => true,
                'fetchTradingFees' => false,
                'fetchTransfer' => false,
                'fetchTransfers' => false,
                'fetchWithdrawals' => true,
                'transfer' => false,
                'withdraw' => true,
            ),
            'urls' => array(
                'logo' => 'https://github.com/user-attachments/assets/d0217747-e54d-4533-8416-0d553dca74bb',
                'api' => array(
                    'rest' => 'https://api.{hostname}',
                ),
                'www' => 'https://bitflyer.com',
                'doc' => 'https://lightning.bitflyer.com/docs?lang=en',
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        'getmarkets/usa', // new (wip)
                        'getmarkets/eu',  // new (wip)
                        'getmarkets',     // or 'markets'
                        'getboard',       // ...
                        'getticker',
                        'getexecutions',
                        'gethealth',
                        'getboardstate',
                        'getchats',
                        'getfundingrate',
                    ),
                ),
                'private' => array(
                    'get' => array(
                        'getpermissions',
                        'getbalance',
                        'getbalancehistory',
                        'getcollateral',
                        'getcollateralhistory',
                        'getcollateralaccounts',
                        'getaddresses',
                        'getcoinins',
                        'getcoinouts',
                        'getbankaccounts',
                        'getdeposits',
                        'getwithdrawals',
                        'getchildorders',
                        'getparentorders',
                        'getparentorder',
                        'getexecutions',
                        'getpositions',
                        'gettradingcommission',
                    ),
                    'post' => array(
                        'sendcoin',
                        'withdraw',
                        'sendchildorder',
                        'cancelchildorder',
                        'sendparentorder',
                        'cancelparentorder',
                        'cancelallchildorders',
                    ),
                ),
            ),
            'fees' => array(
                'trading' => array(
                    'maker' => $this->parse_number('0.002'),
                    'taker' => $this->parse_number('0.002'),
                ),
            ),
            'precisionMode' => TICK_SIZE,
            'features' => array(
                'spot' => array(
                    'sandbox' => false,
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => false,
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => false,
                        'takeProfitPrice' => false,
                        'attachedStopLossTakeProfit' => null,
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => true,
                            'GTD' => true, // todo implement
                        ),
                        'hedged' => false,
                        'trailing' => false, // todo recheck
                        'leverage' => false,
                        'marketBuyRequiresPrice' => false,
                        'marketBuyByCost' => false,
                        'selfTradePrevention' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => null,
                        'untilDays' => null,
                        'symbolRequired' => true,
                    ),
                    'fetchOrder' => array(
                        'marginMode' => false,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ),
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ),
                    'fetchOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => null,
                        'untilDays' => null,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ),
                    'fetchClosedOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => null,
                        'daysBackCanceled' => null,
                        'untilDays' => null,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ),
                    'fetchOHLCV' => null,
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'exceptions' => array(
                'exact' => array(
                    '-2' => '\\ccxt\\OnMaintenance', // array("status":-2,"error_message":"Under maintenance","data":null)
                ),
            ),
        ));
    }

    public function parse_expiry_date($expiry) {
        $day = mb_substr($expiry, 0, 2 - 0);
        $monthName = mb_substr($expiry, 2, 5 - 2);
        $year = mb_substr($expiry, 5, 9 - 5);
        $months = array(
            'JAN' => '01',
            'FEB' => '02',
            'MAR' => '03',
            'APR' => '04',
            'MAY' => '05',
            'JUN' => '06',
            'JUL' => '07',
            'AUG' => '08',
            'SEP' => '09',
            'OCT' => '10',
            'NOV' => '11',
            'DEC' => '12',
        );
        $month = $this->safe_string($months, $monthName);
        return $this->parse8601($year . '-' . $month . '-' . $day . 'T00:00:00Z');
    }

    public function safe_market(?string $marketId = null, ?array $market = null, ?string $delimiter = null, ?string $marketType = null): array {
        // Bitflyer has a different type of conflict in markets, because
        // some of their ids (ETH/BTC and BTC/JPY) are duplicated in US, EU and JP.
        // Since they're the same we just need to return one
        return parent::safe_market($marketId, $market, $delimiter, 'spot');
    }

    public function fetch_markets($params = array ()): array {
        /**
         * retrieves data on all $markets for bitflyer
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#$market-list
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} an array of objects representing $market data
         */
        $jp_markets = $this->publicGetGetmarkets ($params);
        //
        //     array(
        //         // $spot
        //         array( "product_code" => "BTC_JPY", "market_type" => "Spot" ),
        //         array( "product_code" => "BCH_BTC", "market_type" => "Spot" ),
        //         // forex $swap
        //         array( "product_code" => "FX_BTC_JPY", "market_type" => "FX" ),
        //
        //         // $future
        //         array(
        //             "product_code" => "BTCJPY11FEB2022",
        //             "alias" => "BTCJPY_MAT1WK",
        //             "market_type" => "Futures",
        //         ),
        //     );
        //
        $us_markets = $this->publicGetGetmarketsUsa ($params);
        //
        //     array(
        //         array( "product_code" => "BTC_USD", "market_type" => "Spot" ),
        //         array( "product_code" => "BTC_JPY", "market_type" => "Spot" ),
        //     );
        //
        $eu_markets = $this->publicGetGetmarketsEu ($params);
        //
        //     array(
        //         array( "product_code" => "BTC_EUR", "market_type" => "Spot" ),
        //         array( "product_code" => "BTC_JPY", "market_type" => "Spot" ),
        //     );
        //
        $markets = $this->array_concat($jp_markets, $us_markets);
        $markets = $this->array_concat($markets, $eu_markets);
        $result = array();
        for ($i = 0; $i < count($markets); $i++) {
            $market = $markets[$i];
            $id = $this->safe_string($market, 'product_code');
            $currencies = explode('_', $id);
            $marketType = $this->safe_string($market, 'market_type');
            $swap = ($marketType === 'FX');
            $future = ($marketType === 'Futures');
            $spot = !$swap && !$future;
            $type = 'spot';
            $settle = null;
            $baseId = null;
            $quoteId = null;
            $expiry = null;
            if ($spot) {
                $baseId = $this->safe_string($currencies, 0);
                $quoteId = $this->safe_string($currencies, 1);
            } elseif ($swap) {
                $type = 'swap';
                $baseId = $this->safe_string($currencies, 1);
                $quoteId = $this->safe_string($currencies, 2);
            } elseif ($future) {
                $alias = $this->safe_string($market, 'alias');
                if ($alias === null) {
                    // no $alias:
                    // array( product_code => 'BTCJPY11MAR2022', market_type => 'Futures' )
                    // TODO this will break if there are products with 4 chars
                    $baseId = mb_substr($id, 0, 3 - 0);
                    $quoteId = mb_substr($id, 3, 6 - 3);
                    // last 9 chars are $expiry date
                    $expiryDate = mb_substr($id, -9);
                    $expiry = $this->parse_expiry_date($expiryDate);
                } else {
                    $splitAlias = explode('_', $alias);
                    $currencyIds = $this->safe_string($splitAlias, 0);
                    $baseId = mb_substr($currencyIds, 0, -3 - 0);
                    $quoteId = mb_substr($currencyIds, -3);
                    $splitId = explode($currencyIds, $id);
                    $expiryDate = $this->safe_string($splitId, 1);
                    $expiry = $this->parse_expiry_date($expiryDate);
                }
                $type = 'future';
            }
            $base = $this->safe_currency_code($baseId);
            $quote = $this->safe_currency_code($quoteId);
            $symbol = $base . '/' . $quote;
            $taker = $this->fees['trading']['taker'];
            $maker = $this->fees['trading']['maker'];
            $contract = $swap || $future;
            if ($contract) {
                $maker = 0.0;
                $taker = 0.0;
                $settle = 'JPY';
                $symbol = $symbol . ':' . $settle;
                if ($future) {
                    $symbol = $symbol . '-' . $this->yymmdd($expiry);
                }
            }
            $result[] = array(
                'id' => $id,
                'symbol' => $symbol,
                'base' => $base,
                'quote' => $quote,
                'settle' => $settle,
                'baseId' => $baseId,
                'quoteId' => $quoteId,
                'settleId' => null,
                'type' => $type,
                'spot' => $spot,
                'margin' => false,
                'swap' => $swap,
                'future' => $future,
                'option' => false,
                'active' => true,
                'contract' => $contract,
                'linear' => $spot ? null : true,
                'inverse' => $spot ? null : false,
                'taker' => $taker,
                'maker' => $maker,
                'contractSize' => null,
                'expiry' => $expiry,
                'expiryDatetime' => $this->iso8601($expiry),
                'strike' => null,
                'optionType' => null,
                'precision' => array(
                    'amount' => null,
                    'price' => null,
                ),
                'limits' => array(
                    'leverage' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'amount' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'price' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'cost' => array(
                        'min' => null,
                        'max' => null,
                    ),
                ),
                'created' => null,
                'info' => $market,
            );
        }
        return $result;
    }

    public function parse_balance($response): array {
        $result = array( 'info' => $response );
        for ($i = 0; $i < count($response); $i++) {
            $balance = $response[$i];
            $currencyId = $this->safe_string($balance, 'currency_code');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['total'] = $this->safe_string($balance, 'amount');
            $account['free'] = $this->safe_string($balance, 'available');
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function fetch_balance($params = array ()): array {
        /**
         * query for balance and get the amount of funds available for trading or funds locked in orders
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#get-account-asset-balance
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
         */
        $this->load_markets();
        $response = $this->privateGetGetbalance ($params);
        //
        //     array(
        //         array(
        //             "currency_code" => "JPY",
        //             "amount" => 1024078,
        //             "available" => 508000
        //         ),
        //         array(
        //             "currency_code" => "BTC",
        //             "amount" => 10.24,
        //             "available" => 4.12
        //         ),
        //         {
        //             "currency_code" => "ETH",
        //             "amount" => 20.48,
        //             "available" => 16.38
        //         }
        //     )
        //
        return $this->parse_balance($response);
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#order-book
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the order book for
         * @param {int} [$limit] the maximum amount of order book entries to return
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
        );
        $orderbook = $this->publicGetGetboard ($this->extend($request, $params));
        return $this->parse_order_book($orderbook, $market['symbol'], null, 'bids', 'asks', 'price', 'size');
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        $symbol = $this->safe_symbol(null, $market);
        $timestamp = $this->parse8601($this->safe_string($ticker, 'timestamp'));
        $last = $this->safe_string($ticker, 'ltp');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => null,
            'low' => null,
            'bid' => $this->safe_string($ticker, 'best_bid'),
            'bidVolume' => null,
            'ask' => $this->safe_string($ticker, 'best_ask'),
            'askVolume' => null,
            'vwap' => null,
            'open' => null,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'volume_by_product'),
            'quoteVolume' => null,
            'info' => $ticker,
        ), $market);
    }

    public function fetch_ticker(string $symbol, $params = array ()): array {
        /**
         * fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#ticker
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the ticker for
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
        );
        $response = $this->publicGetGetticker ($this->extend($request, $params));
        return $this->parse_ticker($response, $market);
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // fetchTrades (public) v1
        //
        //      {
        //          "id":2278466664,
        //          "side":"SELL",
        //          "price":56810.7,
        //          "size":0.08798,
        //          "exec_date":"2021-11-19T11:46:39.323",
        //          "buy_child_order_acceptance_id":"JRF20211119-114209-236525",
        //          "sell_child_order_acceptance_id":"JRF20211119-114639-236919"
        //      }
        //
        // fetchMyTrades
        //
        //      array(
        //          "id" => 37233,
        //          "side" => "BUY",
        //          "price" => 33470,
        //          "size" => 0.01,
        //          "exec_date" => "2015-07-07T09:57:40.397",
        //          "child_order_id" => "JOR20150707-060559-021935",
        //          "child_order_acceptance_id" => "JRF20150707-060559-396699"
        //          "commission" => 0,
        //      ),
        //
        $side = $this->safe_string_lower($trade, 'side');
        if ($side !== null) {
            if (strlen($side) < 1) {
                $side = null;
            }
        }
        $order = null;
        if ($side !== null) {
            $idInner = $side . '_child_order_acceptance_id';
            if (is_array($trade) && array_key_exists($idInner, $trade)) {
                $order = $trade[$idInner];
            }
        }
        if ($order === null) {
            $order = $this->safe_string($trade, 'child_order_acceptance_id');
        }
        $timestamp = $this->parse8601($this->safe_string($trade, 'exec_date'));
        $priceString = $this->safe_string($trade, 'price');
        $amountString = $this->safe_string($trade, 'size');
        $id = $this->safe_string($trade, 'id');
        $market = $this->safe_market(null, $market);
        return $this->safe_trade(array(
            'id' => $id,
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $market['symbol'],
            'order' => $order,
            'type' => null,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $priceString,
            'amount' => $amountString,
            'cost' => null,
            'fee' => null,
        ), $market);
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * get the list of most recent trades for a particular $symbol
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#list-executions
         *
         * @param {string} $symbol unified $symbol of the $market to fetch trades for
         * @param {int} [$since] timestamp in ms of the earliest trade to fetch
         * @param {int} [$limit] the maximum amount of trades to fetch
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
        );
        if ($limit !== null) {
            $request['count'] = $limit;
        }
        $response = $this->publicGetGetexecutions ($this->extend($request, $params));
        //
        //    array(
        //     array(
        //       "id" => 39287,
        //       "side" => "BUY",
        //       "price" => 31690,
        //       "size" => 27.04,
        //       "exec_date" => "2015-07-08T02:43:34.823",
        //       "buy_child_order_acceptance_id" => "JRF20150707-200203-452209",
        //       "sell_child_order_acceptance_id" => "JRF20150708-024334-060234"
        //     ),
        //    )
        //
        return $this->parse_trades($response, $market, $since, $limit);
    }

    public function fetch_trading_fee(string $symbol, $params = array ()): array {
        /**
         * fetch the trading fees for a $market
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#get-trading-commission
         *
         * @param {string} $symbol unified $market $symbol
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$fee-structure $fee structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
        );
        $response = $this->privateGetGettradingcommission ($this->extend($request, $params));
        //
        //   {
        //       commission_rate => '0.0020'
        //   }
        //
        $fee = $this->safe_number($response, 'commission_rate');
        return array(
            'info' => $response,
            'symbol' => $market['symbol'],
            'maker' => $fee,
            'taker' => $fee,
            'percentage' => null,
            'tierBased' => null,
        );
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        /**
         * create a trade order
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#send-a-new-order
         *
         * @param {string} $symbol unified $symbol of the market to create an order in
         * @param {string} $type 'market' or 'limit'
         * @param {string} $side 'buy' or 'sell'
         * @param {float} $amount how much of currency you want to trade in units of base currency
         * @param {float} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $request = array(
            'product_code' => $this->market_id($symbol),
            'child_order_type' => strtoupper($type),
            'side' => strtoupper($side),
            'price' => $price,
            'size' => $amount,
        );
        $result = $this->privatePostSendchildorder ($this->extend($request, $params));
        // array( "status" => - 200, "error_message" => "Insufficient funds", "data" => null )
        $id = $this->safe_string($result, 'child_order_acceptance_id');
        return $this->safe_order(array(
            'id' => $id,
            'info' => $result,
        ));
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * cancels an open order
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#cancel-order
         *
         * @param {string} $id order $id
         * @param {string} $symbol unified $symbol of the market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' cancelOrder() requires a $symbol argument');
        }
        $this->load_markets();
        $request = array(
            'product_code' => $this->market_id($symbol),
            'child_order_acceptance_id' => $id,
        );
        $response = $this->privatePostCancelchildorder ($this->extend($request, $params));
        //
        //    200 OK.
        //
        return $this->safe_order(array(
            'info' => $response,
        ));
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'ACTIVE' => 'open',
            'COMPLETED' => 'closed',
            'CANCELED' => 'canceled',
            'EXPIRED' => 'canceled',
            'REJECTED' => 'canceled',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order(array $order, ?array $market = null): array {
        $timestamp = $this->parse8601($this->safe_string($order, 'child_order_date'));
        $price = $this->safe_string($order, 'price');
        $amount = $this->safe_string($order, 'size');
        $filled = $this->safe_string($order, 'executed_size');
        $remaining = $this->safe_string($order, 'outstanding_size');
        $status = $this->parse_order_status($this->safe_string($order, 'child_order_state'));
        $type = $this->safe_string_lower($order, 'child_order_type');
        $side = $this->safe_string_lower($order, 'side');
        $marketId = $this->safe_string($order, 'product_code');
        $symbol = $this->safe_symbol($marketId, $market);
        $fee = null;
        $feeCost = $this->safe_number($order, 'total_commission');
        if ($feeCost !== null) {
            $fee = array(
                'cost' => $feeCost,
                'currency' => null,
                'rate' => null,
            );
        }
        $id = $this->safe_string($order, 'child_order_acceptance_id');
        return $this->safe_order(array(
            'id' => $id,
            'clientOrderId' => null,
            'info' => $order,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'status' => $status,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'triggerPrice' => null,
            'cost' => null,
            'amount' => $amount,
            'filled' => $filled,
            'remaining' => $remaining,
            'fee' => $fee,
            'average' => null,
            'trades' => null,
        ), $market);
    }

    public function fetch_orders(?string $symbol = null, ?int $since = null, ?int $limit = 100, $params = array ()): array {
        /**
         * fetches information on multiple $orders made by the user
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#list-$orders
         *
         * @param {string} $symbol unified $market $symbol of the $market $orders were made in
         * @param {int} [$since] the earliest time in ms to fetch $orders for
         * @param {int} [$limit] the maximum number of order structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' fetchOrders() requires a $symbol argument');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
            'count' => $limit,
        );
        $response = $this->privateGetGetchildorders ($this->extend($request, $params));
        $orders = $this->parse_orders($response, $market, $since, $limit);
        if ($symbol !== null) {
            $orders = $this->filter_by($orders, 'symbol', $symbol);
        }
        return $orders;
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = 100, $params = array ()): array {
        /**
         * fetch all unfilled currently open orders
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#list-orders
         *
         * @param {string} $symbol unified market $symbol
         * @param {int} [$since] the earliest time in ms to fetch open orders for
         * @param {int} [$limit] the maximum number of  open orders structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $request = array(
            'child_order_state' => 'ACTIVE',
        );
        return $this->fetch_orders($symbol, $since, $limit, $this->extend($request, $params));
    }

    public function fetch_closed_orders(?string $symbol = null, ?int $since = null, ?int $limit = 100, $params = array ()): array {
        /**
         * fetches information on multiple closed orders made by the user
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#list-orders
         *
         * @param {string} $symbol unified market $symbol of the market orders were made in
         * @param {int} [$since] the earliest time in ms to fetch orders for
         * @param {int} [$limit] the maximum number of order structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $request = array(
            'child_order_state' => 'COMPLETED',
        );
        return $this->fetch_orders($symbol, $since, $limit, $this->extend($request, $params));
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an order made by the user
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#list-$orders
         *
         * @param {string} $id the order $id
         * @param {string} $symbol unified $symbol of the market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' fetchOrder() requires a $symbol argument');
        }
        $orders = $this->fetch_orders($symbol);
        $ordersById = $this->index_by($orders, 'id');
        if (is_array($ordersById) && array_key_exists($id, $ordersById)) {
            return $ordersById[$id];
        }
        throw new OrderNotFound($this->id . ' No order found with $id ' . $id);
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         * fetch all trades made by the user
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#list-executions
         *
         * @param {string} $symbol unified $market $symbol
         * @param {int} [$since] the earliest time in ms to fetch trades for
         * @param {int} [$limit] the maximum number of trades structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
         */
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' fetchMyTrades() requires a $symbol argument');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
        );
        if ($limit !== null) {
            $request['count'] = $limit;
        }
        $response = $this->privateGetGetexecutions ($this->extend($request, $params));
        //
        //    array(
        //     array(
        //       "id" => 37233,
        //       "side" => "BUY",
        //       "price" => 33470,
        //       "size" => 0.01,
        //       "exec_date" => "2015-07-07T09:57:40.397",
        //       "child_order_id" => "JOR20150707-060559-021935",
        //       "child_order_acceptance_id" => "JRF20150707-060559-396699"
        //       "commission" => 0,
        //     ),
        //    )
        //
        return $this->parse_trades($response, $market, $since, $limit);
    }

    public function fetch_positions(?array $symbols = null, $params = array ()): array {
        /**
         * fetch all open positions
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#get-open-interest-summary
         *
         * @param {string[]} $symbols list of unified market $symbols
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=position-structure position structure~
         */
        if ($symbols === null) {
            throw new ArgumentsRequired($this->id . ' fetchPositions() requires a `$symbols` argument, exactly one symbol in an array');
        }
        $this->load_markets();
        $request = array(
            'product_code' => $this->market_ids($symbols),
        );
        $response = $this->privateGetGetpositions ($this->extend($request, $params));
        //
        //     array(
        //         {
        //             "product_code" => "FX_BTC_JPY",
        //             "side" => "BUY",
        //             "price" => 36000,
        //             "size" => 10,
        //             "commission" => 0,
        //             "swap_point_accumulate" => -35,
        //             "require_collateral" => 120000,
        //             "open_date" => "2015-11-03T10:04:45.011",
        //             "leverage" => 3,
        //             "pnl" => 965,
        //             "sfd" => -0.5
        //         }
        //     )
        //
        // todo unify parsePosition/parsePositions
        return $response;
    }

    public function withdraw(string $code, float $amount, string $address, ?string $tag = null, $params = array ()): array {
        /**
         * make a withdrawal
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#withdrawing-funds
         *
         * @param {string} $code unified $currency $code
         * @param {float} $amount the $amount to withdraw
         * @param {string} $address the $address to withdraw to
         * @param {string} $tag
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structure~
         */
        $this->check_address($address);
        $this->load_markets();
        if ($code !== 'JPY' && $code !== 'USD' && $code !== 'EUR') {
            throw new ExchangeError($this->id . ' allows withdrawing JPY, USD, EUR only, ' . $code . ' is not supported');
        }
        $currency = $this->currency($code);
        $request = array(
            'currency_code' => $currency['id'],
            'amount' => $amount,
            // 'bank_account_id' => 1234,
        );
        $response = $this->privatePostWithdraw ($this->extend($request, $params));
        //
        //     {
        //         "message_id" => "69476620-5056-4003-bcbe-42658a2b041b"
        //     }
        //
        return $this->parse_transaction($response, $currency);
    }

    public function fetch_deposits(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all deposits made to an account
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#get-crypto-assets-deposit-history
         *
         * @param {string} $code unified $currency $code
         * @param {int} [$since] the earliest time in ms to fetch deposits for
         * @param {int} [$limit] the maximum number of deposits structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
         */
        $this->load_markets();
        $currency = null;
        $request = array();
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        if ($limit !== null) {
            $request['count'] = $limit; // default 100
        }
        $response = $this->privateGetGetcoinins ($this->extend($request, $params));
        //
        //     array(
        //         {
        //             "id" => 100,
        //             "order_id" => "CDP20151227-024141-055555",
        //             "currency_code" => "BTC",
        //             "amount" => 0.00002,
        //             "address" => "1WriteySQufKZ2pVuM1oMhPrTtTVFq35j",
        //             "tx_hash" => "9f92ee65a176bb9545f7becb8706c50d07d4cee5ffca34d8be3ef11d411405ae",
        //             "status" => "COMPLETED",
        //             "event_date" => "2015-11-27T08:59:20.301"
        //         }
        //     )
        //
        return $this->parse_transactions($response, $currency, $since, $limit);
    }

    public function fetch_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all withdrawals made from an account
         *
         * @see https://lightning.bitflyer.com/docs?lang=en#get-crypto-assets-transaction-history
         *
         * @param {string} $code unified $currency $code
         * @param {int} [$since] the earliest time in ms to fetch withdrawals for
         * @param {int} [$limit] the maximum number of withdrawals structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
         */
        $this->load_markets();
        $currency = null;
        $request = array();
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        if ($limit !== null) {
            $request['count'] = $limit; // default 100
        }
        $response = $this->privateGetGetcoinouts ($this->extend($request, $params));
        //
        //     array(
        //         {
        //             "id" => 500,
        //             "order_id" => "CWD20151224-014040-077777",
        //             "currency_code" => "BTC",
        //             "amount" => 0.1234,
        //             "address" => "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
        //             "tx_hash" => "724c07dfd4044abcb390b0412c3e707dd5c4f373f0a52b3bd295ce32b478c60a",
        //             "fee" => 0.0005,
        //             "additional_fee" => 0.0001,
        //             "status" => "COMPLETED",
        //             "event_date" => "2015-12-24T01:40:40.397"
        //         }
        //     )
        //
        return $this->parse_transactions($response, $currency, $since, $limit);
    }

    public function parse_deposit_status($status) {
        $statuses = array(
            'PENDING' => 'pending',
            'COMPLETED' => 'ok',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_withdrawal_status($status) {
        $statuses = array(
            'PENDING' => 'pending',
            'COMPLETED' => 'ok',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        // fetchDeposits
        //
        //     {
        //         "id" => 100,
        //         "order_id" => "CDP20151227-024141-055555",
        //         "currency_code" => "BTC",
        //         "amount" => 0.00002,
        //         "address" => "1WriteySQufKZ2pVuM1oMhPrTtTVFq35j",
        //         "tx_hash" => "9f92ee65a176bb9545f7becb8706c50d07d4cee5ffca34d8be3ef11d411405ae",
        //         "status" => "COMPLETED",
        //         "event_date" => "2015-11-27T08:59:20.301"
        //     }
        //
        // fetchWithdrawals
        //
        //     {
        //         "id" => 500,
        //         "order_id" => "CWD20151224-014040-077777",
        //         "currency_code" => "BTC",
        //         "amount" => 0.1234,
        //         "address" => "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
        //         "tx_hash" => "724c07dfd4044abcb390b0412c3e707dd5c4f373f0a52b3bd295ce32b478c60a",
        //         "fee" => 0.0005,
        //         "additional_fee" => 0.0001,
        //         "status" => "COMPLETED",
        //         "event_date" => "2015-12-24T01:40:40.397"
        //     }
        //
        // withdraw
        //
        //     {
        //         "message_id" => "69476620-5056-4003-bcbe-42658a2b041b"
        //     }
        //
        $id = $this->safe_string_2($transaction, 'id', 'message_id');
        $address = $this->safe_string($transaction, 'address');
        $currencyId = $this->safe_string($transaction, 'currency_code');
        $code = $this->safe_currency_code($currencyId, $currency);
        $timestamp = $this->parse8601($this->safe_string($transaction, 'event_date'));
        $amount = $this->safe_number($transaction, 'amount');
        $txId = $this->safe_string($transaction, 'tx_hash');
        $rawStatus = $this->safe_string($transaction, 'status');
        $type = null;
        $status = null;
        $fee = null;
        if (is_array($transaction) && array_key_exists('fee', $transaction)) {
            $type = 'withdrawal';
            $status = $this->parse_withdrawal_status($rawStatus);
            $feeCost = $this->safe_string($transaction, 'fee');
            $additionalFee = $this->safe_string($transaction, 'additional_fee');
            $fee = array( 'currency' => $code, 'cost' => $this->parse_number(Precise::string_add($feeCost, $additionalFee)) );
        } else {
            $type = 'deposit';
            $status = $this->parse_deposit_status($rawStatus);
        }
        return array(
            'info' => $transaction,
            'id' => $id,
            'txid' => $txId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'network' => null,
            'address' => $address,
            'addressTo' => $address,
            'addressFrom' => null,
            'tag' => null,
            'tagTo' => null,
            'tagFrom' => null,
            'type' => $type,
            'amount' => $amount,
            'currency' => $code,
            'status' => $status,
            'updated' => null,
            'comment' => null,
            'internal' => null,
            'fee' => $fee,
        );
    }

    public function fetch_funding_rate(string $symbol, $params = array ()): array {
        /**
         * fetch the current funding rate
         *
         * @see https://lightning.bitflyer.com/docs#funding-rate
         *
         * @param {string} $symbol unified $market $symbol
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=funding-rate-structure funding rate structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'product_code' => $market['id'],
        );
        $response = $this->publicGetGetfundingrate ($this->extend($request, $params));
        //
        //    {
        //        "current_funding_rate" => -0.003750000000
        //        "next_funding_rate_settledate" => "2024-04-15T13:00:00"
        //    }
        //
        return $this->parse_funding_rate($response, $market);
    }

    public function parse_funding_rate($contract, ?array $market = null): array {
        //
        //    {
        //        "current_funding_rate" => -0.003750000000
        //        "next_funding_rate_settledate" => "2024-04-15T13:00:00"
        //    }
        //
        $nextFundingDatetime = $this->safe_string($contract, 'next_funding_rate_settledate');
        $nextFundingTimestamp = $this->parse8601($nextFundingDatetime);
        return array(
            'info' => $contract,
            'symbol' => $this->safe_string($market, 'symbol'),
            'markPrice' => null,
            'indexPrice' => null,
            'interestRate' => null,
            'estimatedSettlePrice' => null,
            'timestamp' => null,
            'datetime' => null,
            'fundingRate' => null,
            'fundingTimestamp' => null,
            'fundingDatetime' => null,
            'nextFundingRate' => $this->safe_number($contract, 'current_funding_rate'),
            'nextFundingTimestamp' => $nextFundingTimestamp,
            'nextFundingDatetime' => $this->iso8601($nextFundingTimestamp),
            'previousFundingRate' => null,
            'previousFundingTimestamp' => null,
            'previousFundingDatetime' => null,
            'interval' => null,
        );
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $request = '/' . $this->version . '/';
        if ($api === 'private') {
            $request .= 'me/';
        }
        $request .= $path;
        if ($method === 'GET') {
            if ($params) {
                $request .= '?' . $this->urlencode($params);
            }
        }
        $baseUrl = $this->implode_hostname($this->urls['api']['rest']);
        $url = $baseUrl . $request;
        if ($api === 'private') {
            $this->check_required_credentials();
            $nonce = (string) $this->nonce();
            $auth = implode('', array($nonce, $method, $request));
            if ($params) {
                if ($method !== 'GET') {
                    $body = $this->json($params);
                    $auth .= $body;
                }
            }
            $headers = array(
                'ACCESS-KEY' => $this->apiKey,
                'ACCESS-TIMESTAMP' => $nonce,
                'ACCESS-SIGN' => $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256'),
                'Content-Type' => 'application/json',
            );
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function handle_errors(int $code, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        if ($response === null) {
            return null; // fallback to the default error handler
        }
        $feedback = $this->id . ' ' . $body;
        // i.e. array("status":-2,"error_message":"Under maintenance","data":null)
        $errorMessage = $this->safe_string($response, 'error_message');
        $statusCode = $this->safe_integer($response, 'status');
        if ($errorMessage !== null) {
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $statusCode, $feedback);
            throw new ExchangeError($feedback);
        }
        return null;
    }
}
