# -*- coding: utf-8 -*-

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

import ccxt.async_support
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp
import hashlib
from ccxt.base.types import Any, Int, Order, OrderBook, Str, Strings, Tickers, Trade
from ccxt.async_support.base.ws.client import Client
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import NotSupported
from ccxt.base.precise import Precise


class gemini(ccxt.async_support.gemini):

    def describe(self) -> Any:
        return self.deep_extend(super(gemini, self).describe(), {
            'has': {
                'ws': True,
                'watchBalance': False,
                'watchTicker': False,
                'watchTickers': False,
                'watchBidsAsks': True,
                'watchTrades': True,
                'watchTradesForSymbols': True,
                'watchMyTrades': False,
                'watchOrders': True,
                'watchOrderBook': True,
                'watchOrderBookForSymbols': True,
                'watchOHLCV': True,
            },
            'hostname': 'api.gemini.com',
            'urls': {
                'api': {
                    'ws': 'wss://api.gemini.com',
                },
                'test': {
                    'ws': 'wss://api.sandbox.gemini.com',
                },
            },
        })

    async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        watch the list of most recent trades for a particular symbol

        https://docs.gemini.com/websocket-api/#market-data-version-2

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
        """
        await self.load_markets()
        market = self.market(symbol)
        messageHash = 'trades:' + market['symbol']
        marketId = market['id']
        request: dict = {
            'type': 'subscribe',
            'subscriptions': [
                {
                    'name': 'l2',
                    'symbols': [
                        marketId.upper(),
                    ],
                },
            ],
        }
        subscribeHash = 'l2:' + market['symbol']
        url = self.urls['api']['ws'] + '/v2/marketdata'
        trades = await self.watch(url, messageHash, request, subscribeHash)
        if self.newUpdates:
            limit = trades.getLimit(market['symbol'], limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

    async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """

        https://docs.gemini.com/websocket-api/#multi-market-data

        get the list of most recent trades for a list of symbols
        :param str[] symbols: 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
        """
        trades = await self.helper_for_watch_multiple_construct('trades', symbols, params)
        if self.newUpdates:
            first = self.safe_list(trades, 0)
            tradeSymbol = self.safe_string(first, 'symbol')
            limit = trades.getLimit(tradeSymbol, limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

    def parse_ws_trade(self, trade, market=None) -> Trade:
        #
        # regular v2 trade
        #
        #     {
        #         "type": "trade",
        #         "symbol": "BTCUSD",
        #         "event_id": 122258166738,
        #         "timestamp": 1655330221424,
        #         "price": "22269.14",
        #         "quantity": "0.00004473",
        #         "side": "buy"
        #     }
        #
        # multi data trade
        #
        #    {
        #        "type": "trade",
        #        "symbol": "ETHUSD",
        #        "tid": "1683002242170204",  # self is not TS, but somewhat ID
        #        "price": "2299.24",
        #        "amount": "0.002662",
        #        "makerSide": "bid"
        #    }
        #
        timestamp = self.safe_integer(trade, 'timestamp')
        id = self.safe_string_2(trade, 'event_id', 'tid')
        priceString = self.safe_string(trade, 'price')
        amountString = self.safe_string_2(trade, 'quantity', 'amount')
        side = self.safe_string_lower(trade, 'side')
        if side is None:
            marketSide = self.safe_string_lower(trade, 'makerSide')
            if marketSide == 'bid':
                side = 'sell'
            elif marketSide == 'ask':
                side = 'buy'
        marketId = self.safe_string_lower(trade, 'symbol')
        symbol = self.safe_symbol(marketId, market)
        return self.safe_trade({
            'id': id,
            'order': None,
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'type': None,
            'side': side,
            'takerOrMaker': None,
            'price': priceString,
            'cost': None,
            'amount': amountString,
            'fee': None,
        }, market)

    def handle_trade(self, client: Client, message):
        #
        #     {
        #         "type": "trade",
        #         "symbol": "BTCUSD",
        #         "event_id": 122278173770,
        #         "timestamp": 1655335880981,
        #         "price": "22530.80",
        #         "quantity": "0.04",
        #         "side": "buy"
        #     }
        #
        trade = self.parse_ws_trade(message)
        symbol = trade['symbol']
        tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
        stored = self.safe_value(self.trades, symbol)
        if stored is None:
            stored = ArrayCache(tradesLimit)
            self.trades[symbol] = stored
        stored.append(trade)
        messageHash = 'trades:' + symbol
        client.resolve(stored, messageHash)

    def handle_trades(self, client: Client, message):
        #
        #     {
        #         "type": "l2_updates",
        #         "symbol": "BTCUSD",
        #         "changes": [
        #             ["buy", '22252.37', "0.02"],
        #             ["buy", '22251.61', "0.04"],
        #             ["buy", '22251.60', "0.04"],
        #             # some asks
        #         ],
        #         "trades": [
        #             {type: 'trade', symbol: 'BTCUSD', event_id: 122258166738, timestamp: 1655330221424, price: '22269.14', quantity: "0.00004473", side: "buy"},
        #             {type: 'trade', symbol: 'BTCUSD', event_id: 122258141090, timestamp: 1655330213216, price: '22250.00', quantity: "0.00704098", side: "buy"},
        #             {type: 'trade', symbol: 'BTCUSD', event_id: 122258118291, timestamp: 1655330206753, price: '22250.00', quantity: "0.03", side: "buy"},
        #         ],
        #         "auction_events": [
        #             {
        #                 "type": "auction_result",
        #                 "symbol": "BTCUSD",
        #                 "time_ms": 1655323200000,
        #                 "result": "failure",
        #                 "highest_bid_price": "21590.88",
        #                 "lowest_ask_price": "21602.30",
        #                 "collar_price": "21634.73"
        #             },
        #             {
        #                 "type": "auction_indicative",
        #                 "symbol": "BTCUSD",
        #                 "time_ms": 1655323185000,
        #                 "result": "failure",
        #                 "highest_bid_price": "21661.90",
        #                 "lowest_ask_price": "21663.78",
        #                 "collar_price": "21662.845"
        #             },
        #         ]
        #     }
        #
        marketId = self.safe_string_lower(message, 'symbol')
        market = self.safe_market(marketId)
        trades = self.safe_value(message, 'trades')
        if trades is not None:
            symbol = market['symbol']
            tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
            stored = self.safe_value(self.trades, symbol)
            if stored is None:
                stored = ArrayCache(tradesLimit)
                self.trades[symbol] = stored
            for i in range(0, len(trades)):
                trade = self.parse_ws_trade(trades[i], market)
                stored.append(trade)
            messageHash = 'trades:' + symbol
            client.resolve(stored, messageHash)

    def handle_trades_for_multidata(self, client: Client, trades, timestamp: Int):
        if trades is not None:
            tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
            storesForSymbols: dict = {}
            for i in range(0, len(trades)):
                marketId = trades[i]['symbol']
                market = self.safe_market(marketId.lower())
                symbol = market['symbol']
                trade = self.parse_ws_trade(trades[i], market)
                trade['timestamp'] = timestamp
                trade['datetime'] = self.iso8601(timestamp)
                stored = self.safe_value(self.trades, symbol)
                if stored is None:
                    stored = ArrayCache(tradesLimit)
                    self.trades[symbol] = stored
                stored.append(trade)
                storesForSymbols[symbol] = stored
            symbols = list(storesForSymbols.keys())
            for i in range(0, len(symbols)):
                symbol = symbols[i]
                stored = storesForSymbols[symbol]
                messageHash = 'trades:' + symbol
                client.resolve(stored, messageHash)

    async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """
        watches historical candlestick data containing the open, high, low, and close price, and the volume of a market

        https://docs.gemini.com/websocket-api/#candles-data-feed

        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str timeframe: the length of time each candle represents
        :param int [since]: timestamp in ms of the earliest candle to fetch
        :param int [limit]: the maximum amount of candles to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        timeframeId = self.safe_string(self.timeframes, timeframe, timeframe)
        request: dict = {
            'type': 'subscribe',
            'subscriptions': [
                {
                    'name': 'candles_' + timeframeId,
                    'symbols': [
                        market['id'].upper(),
                    ],
                },
            ],
        }
        messageHash = 'ohlcv:' + market['symbol'] + ':' + timeframeId
        url = self.urls['api']['ws'] + '/v2/marketdata'
        ohlcv = await self.watch(url, messageHash, request, messageHash)
        if self.newUpdates:
            limit = ohlcv.getLimit(symbol, limit)
        return self.filter_by_since_limit(ohlcv, since, limit, 0, True)

    def handle_ohlcv(self, client: Client, message):
        #
        #     {
        #         "type": "candles_15m_updates",
        #         "symbol": "BTCUSD",
        #         "changes": [
        #             [
        #                 1561054500000,
        #                 9350.18,
        #                 9358.35,
        #                 9350.18,
        #                 9355.51,
        #                 2.07
        #             ],
        #             [
        #                 1561053600000,
        #                 9357.33,
        #                 9357.33,
        #                 9350.18,
        #                 9350.18,
        #                 1.5900161
        #             ]
        #             ...
        #         ]
        #     }
        #
        type = self.safe_string(message, 'type', '')
        timeframeId = type[8:]
        timeframeEndIndex = timeframeId.find('_')
        timeframeId = timeframeId[0:timeframeEndIndex]
        marketId = self.safe_string(message, 'symbol', '').lower()
        market = self.safe_market(marketId)
        symbol = self.safe_symbol(marketId, market)
        changes = self.safe_value(message, 'changes', [])
        timeframe = self.find_timeframe(timeframeId)
        ohlcvsBySymbol = self.safe_value(self.ohlcvs, symbol)
        if ohlcvsBySymbol is None:
            self.ohlcvs[symbol] = {}
        stored = self.safe_value(self.ohlcvs[symbol], timeframe)
        if stored is None:
            limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
            stored = ArrayCacheByTimestamp(limit)
            self.ohlcvs[symbol][timeframe] = stored
        changesLength = len(changes)
        # reverse order of array to store candles in ascending order
        for i in range(0, changesLength):
            index = changesLength - i - 1
            parsed = self.parse_ohlcv(changes[index], market)
            stored.append(parsed)
        messageHash = 'ohlcv:' + symbol + ':' + timeframeId
        client.resolve(stored, messageHash)
        return message

    async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data

        https://docs.gemini.com/websocket-api/#market-data-version-2

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        messageHash = 'orderbook:' + market['symbol']
        marketId = market['id']
        request: dict = {
            'type': 'subscribe',
            'subscriptions': [
                {
                    'name': 'l2',
                    'symbols': [
                        marketId.upper(),
                    ],
                },
            ],
        }
        subscribeHash = 'l2:' + market['symbol']
        url = self.urls['api']['ws'] + '/v2/marketdata'
        orderbook = await self.watch(url, messageHash, request, subscribeHash)
        return orderbook.limit()

    def handle_order_book(self, client: Client, message):
        changes = self.safe_value(message, 'changes', [])
        marketId = self.safe_string_lower(message, 'symbol')
        market = self.safe_market(marketId)
        symbol = market['symbol']
        messageHash = 'orderbook:' + symbol
        # orderbook = self.safe_value(self.orderbooks, symbol)
        if not (symbol in self.orderbooks):
            self.orderbooks[symbol] = self.order_book()
        orderbook = self.orderbooks[symbol]
        for i in range(0, len(changes)):
            delta = changes[i]
            price = self.safe_number(delta, 1)
            size = self.safe_number(delta, 2)
            side = 'bids' if (delta[0] == 'buy') else 'asks'
            bookside = orderbook[side]
            bookside.store(price, size)
            orderbook[side] = bookside
        orderbook['symbol'] = symbol
        self.orderbooks[symbol] = orderbook
        client.resolve(orderbook, messageHash)

    async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook:
        """
        watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data

        https://docs.gemini.com/websocket-api/#multi-market-data

        :param str[] symbols: unified array of symbols
        :param int [limit]: the maximum amount of order book entries to return
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
        """
        orderbook = await self.helper_for_watch_multiple_construct('orderbook', symbols, params)
        return orderbook.limit()

    async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
        """
        watches best bid & ask for symbols

        https://docs.gemini.com/websocket-api/#multi-market-data

        :param str[] symbols: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
        """
        return await self.helper_for_watch_multiple_construct('bidsasks', symbols, params)

    def handle_bids_asks_for_multidata(self, client: Client, rawBidAskChanges, timestamp: Int, nonce: Int):
        #
        # {
        #     eventId: '1683002916916153',
        #     events: [
        #       {
        #         price: '50945.37',
        #         reason: 'top-of-book',
        #         remaining: '0.0',
        #         side: 'bid',
        #         symbol: 'BTCUSDT',
        #         type: 'change'
        #       },
        #       {
        #         price: '50947.75',
        #         reason: 'top-of-book',
        #         remaining: '0.11725',
        #         side: 'bid',
        #         symbol: 'BTCUSDT',
        #         type: 'change'
        #       }
        #     ],
        #     socket_sequence: 322,
        #     timestamp: 1708674495,
        #     timestampms: 1708674495174,
        #     type: 'update'
        # }
        #
        marketId = rawBidAskChanges[0]['symbol']
        market = self.safe_market(marketId.lower())
        symbol = market['symbol']
        if not (symbol in self.bidsasks):
            self.bidsasks[symbol] = self.parse_ticker({})
            self.bidsasks[symbol]['symbol'] = symbol
        currentBidAsk = self.bidsasks[symbol]
        messageHash = 'bidsasks:' + symbol
        # last update always overwrites the previous state and is the latest state
        for i in range(0, len(rawBidAskChanges)):
            entry = rawBidAskChanges[i]
            rawSide = self.safe_string(entry, 'side')
            price = self.safe_number(entry, 'price')
            sizeString = self.safe_string(entry, 'remaining')
            if Precise.string_eq(sizeString, '0'):
                continue
            size = self.parse_number(sizeString)
            if rawSide == 'bid':
                currentBidAsk['bid'] = price
                currentBidAsk['bidVolume'] = size
            else:
                currentBidAsk['ask'] = price
                currentBidAsk['askVolume'] = size
        currentBidAsk['timestamp'] = timestamp
        currentBidAsk['datetime'] = self.iso8601(timestamp)
        currentBidAsk['info'] = rawBidAskChanges
        bidsAsksDict = {}
        bidsAsksDict[symbol] = currentBidAsk
        self.bidsasks[symbol] = currentBidAsk
        client.resolve(bidsAsksDict, messageHash)

    async def helper_for_watch_multiple_construct(self, itemHashName: str, symbols: List[str] = None, params={}):
        await self.load_markets()
        if symbols is None:
            raise NotSupported(self.id + ' watchMultiple requires at least one symbol')
        symbols = self.market_symbols(symbols, None, False, True, True)
        firstMarket = self.market(symbols[0])
        if not firstMarket['spot'] and not firstMarket['linear']:
            raise NotSupported(self.id + ' watchMultiple supports only spot or linear-swap symbols')
        messageHashes = []
        marketIds = []
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            messageHash = itemHashName + ':' + symbol
            messageHashes.append(messageHash)
            market = self.market(symbol)
            marketIds.append(market['id'])
        queryStr = ','.join(marketIds)
        url = self.urls['api']['ws'] + '/v1/multimarketdata?symbols=' + queryStr + '&heartbeat=true&'
        if itemHashName == 'orderbook':
            url += 'trades=false&bids=true&offers=true'
        elif itemHashName == 'bidsasks':
            url += 'trades=false&bids=true&offers=true&top_of_book=true'
        elif itemHashName == 'trades':
            url += 'trades=true&bids=false&offers=false'
        return await self.watch_multiple(url, messageHashes, None)

    def handle_order_book_for_multidata(self, client: Client, rawOrderBookChanges, timestamp: Int, nonce: Int):
        #
        # rawOrderBookChanges
        #
        # [
        #   {
        #     delta: "4105123935484.817624",
        #     price: "0.000000001",
        #     reason: "initial",  # initial|cancel|place
        #     remaining: "4105123935484.817624",
        #     side: "bid",  # bid|ask
        #     symbol: "SHIBUSD",
        #     type: "change",  # seems always change
        #   },
        #   ...
        #
        marketId = rawOrderBookChanges[0]['symbol']
        market = self.safe_market(marketId.lower())
        symbol = market['symbol']
        messageHash = 'orderbook:' + symbol
        if not (symbol in self.orderbooks):
            ob = self.order_book()
            self.orderbooks[symbol] = ob
        orderbook = self.orderbooks[symbol]
        bids = orderbook['bids']
        asks = orderbook['asks']
        for i in range(0, len(rawOrderBookChanges)):
            entry = rawOrderBookChanges[i]
            price = self.safe_number(entry, 'price')
            size = self.safe_number(entry, 'remaining')
            rawSide = self.safe_string(entry, 'side')
            if rawSide == 'bid':
                bids.store(price, size)
            else:
                asks.store(price, size)
        orderbook['bids'] = bids
        orderbook['asks'] = asks
        orderbook['symbol'] = symbol
        orderbook['nonce'] = nonce
        orderbook['timestamp'] = timestamp
        orderbook['datetime'] = self.iso8601(timestamp)
        self.orderbooks[symbol] = orderbook
        client.resolve(orderbook, messageHash)

    def handle_l2_updates(self, client: Client, message):
        #
        #     {
        #         "type": "l2_updates",
        #         "symbol": "BTCUSD",
        #         "changes": [
        #             ["buy", '22252.37', "0.02"],
        #             ["buy", '22251.61', "0.04"],
        #             ["buy", '22251.60', "0.04"],
        #             # some asks
        #         ],
        #         "trades": [
        #             {type: 'trade', symbol: 'BTCUSD', event_id: 122258166738, timestamp: 1655330221424, price: '22269.14', quantity: "0.00004473", side: "buy"},
        #             {type: 'trade', symbol: 'BTCUSD', event_id: 122258141090, timestamp: 1655330213216, price: '22250.00', quantity: "0.00704098", side: "buy"},
        #             {type: 'trade', symbol: 'BTCUSD', event_id: 122258118291, timestamp: 1655330206753, price: '22250.00', quantity: "0.03", side: "buy"},
        #         ],
        #         "auction_events": [
        #             {
        #                 "type": "auction_result",
        #                 "symbol": "BTCUSD",
        #                 "time_ms": 1655323200000,
        #                 "result": "failure",
        #                 "highest_bid_price": "21590.88",
        #                 "lowest_ask_price": "21602.30",
        #                 "collar_price": "21634.73"
        #             },
        #             {
        #                 "type": "auction_indicative",
        #                 "symbol": "BTCUSD",
        #                 "time_ms": 1655323185000,
        #                 "result": "failure",
        #                 "highest_bid_price": "21661.90",
        #                 "lowest_ask_price": "21663.79",
        #                 "collar_price": "21662.845"
        #             },
        #         ]
        #     }
        #
        self.handle_order_book(client, message)
        self.handle_trades(client, message)

    async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        watches information on multiple orders made by the user

        https://docs.gemini.com/websocket-api/#order-events

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
        """
        url = self.urls['api']['ws'] + '/v1/order/events?eventTypeFilter=initial&eventTypeFilter=accepted&eventTypeFilter=rejected&eventTypeFilter=fill&eventTypeFilter=cancelled&eventTypeFilter=booked'
        await self.load_markets()
        authParams: dict = {
            'url': url,
        }
        await self.authenticate(authParams)
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
        messageHash = 'orders'
        orders = await self.watch(url, messageHash, None, messageHash)
        if self.newUpdates:
            limit = orders.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)

    def handle_heartbeat(self, client: Client, message):
        #
        #     {
        #         "type": "heartbeat",
        #         "timestampms": 1659740268958,
        #         "sequence": 7,
        #         "trace_id": "25b3d92476dd3a9a5c03c9bd9e0a0dba",
        #         "socket_sequence": 7
        #     }
        #
        client.lastPong = self.milliseconds()
        return message

    def handle_subscription(self, client: Client, message):
        #
        #     {
        #         "type": "subscription_ack",
        #         "accountId": 19433282,
        #         "subscriptionId": "orderevents-websocket-25b3d92476dd3a9a5c03c9bd9e0a0dba",
        #         "symbolFilter": [],
        #         "apiSessionFilter": [],
        #         "eventTypeFilter": []
        #     }
        #
        return message

    def handle_order(self, client: Client, message):
        #
        #     [
        #         {
        #             "type": "accepted",
        #             "order_id": "134150423884",
        #             "event_id": "134150423886",
        #             "account_name": "primary",
        #             "client_order_id": "1659739406916",
        #             "api_session": "account-pnBFSS0XKGvDamX4uEIt",
        #             "symbol": "batbtc",
        #             "side": "sell",
        #             "order_type": "exchange limit",
        #             "timestamp": "1659739407",
        #             "timestampms": 1659739407576,
        #             "is_live": True,
        #             "is_cancelled": False,
        #             "is_hidden": False,
        #             "original_amount": "1",
        #             "price": "1",
        #             "socket_sequence": 139
        #         }
        #     ]
        #
        messageHash = 'orders'
        if self.orders is None:
            limit = self.safe_integer(self.options, 'ordersLimit', 1000)
            self.orders = ArrayCacheBySymbolById(limit)
        orders = self.orders
        for i in range(0, len(message)):
            order = self.parse_ws_order(message[i])
            orders.append(order)
        client.resolve(self.orders, messageHash)

    def parse_ws_order(self, order, market=None):
        #
        #     {
        #         "type": "accepted",
        #         "order_id": "134150423884",
        #         "event_id": "134150423886",
        #         "account_name": "primary",
        #         "client_order_id": "1659739406916",
        #         "api_session": "account-pnBFSS0XKGvDamX4uEIt",
        #         "symbol": "batbtc",
        #         "side": "sell",
        #         "order_type": "exchange limit",
        #         "timestamp": "1659739407",
        #         "timestampms": 1659739407576,
        #         "is_live": True,
        #         "is_cancelled": False,
        #         "is_hidden": False,
        #         "original_amount": "1",
        #         "price": "1",
        #         "socket_sequence": 139
        #     }
        #
        timestamp = self.safe_integer(order, 'timestampms')
        status = self.safe_string(order, 'type')
        marketId = self.safe_string(order, 'symbol')
        typeId = self.safe_string(order, 'order_type')
        behavior = self.safe_string(order, 'behavior')
        timeInForce = 'GTC'
        postOnly = False
        if behavior == 'immediate-or-cancel':
            timeInForce = 'IOC'
        elif behavior == 'fill-or-kill':
            timeInForce = 'FOK'
        elif behavior == 'maker-or-cancel':
            timeInForce = 'PO'
            postOnly = True
        return self.safe_order({
            'id': self.safe_string(order, 'order_id'),
            'clientOrderId': self.safe_string(order, 'client_order_id'),
            'info': order,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            'status': self.parse_ws_order_status(status),
            'symbol': self.safe_symbol(marketId, market),
            'type': self.parse_ws_order_type(typeId),
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': self.safe_string(order, 'side'),
            'price': self.safe_number(order, 'price'),
            'stopPrice': None,
            'average': self.safe_number(order, 'avg_execution_price'),
            'cost': None,
            'amount': self.safe_number(order, 'original_amount'),
            'filled': self.safe_number(order, 'executed_amount'),
            'remaining': self.safe_number(order, 'remaining_amount'),
            'fee': None,
            'trades': None,
        }, market)

    def parse_ws_order_status(self, status):
        statuses: dict = {
            'accepted': 'open',
            'booked': 'open',
            'fill': 'closed',
            'cancelled': 'canceled',
            'cancel_rejected': 'rejected',
            'rejected': 'rejected',
        }
        return self.safe_string(statuses, status, status)

    def parse_ws_order_type(self, type):
        types: dict = {
            'exchange limit': 'limit',
            'market buy': 'market',
            'market sell': 'market',
        }
        return self.safe_string(types, type, type)

    def handle_error(self, client: Client, message):
        #
        #     {
        #         "reason": "NoValidTradingPairs",
        #         "result": "error"
        #     }
        #
        raise ExchangeError(self.json(message))

    def handle_message(self, client: Client, message):
        #
        #  public
        #     {
        #         "type": "trade",
        #         "symbol": "BTCUSD",
        #         "event_id": 122278173770,
        #         "timestamp": 1655335880981,
        #         "price": "22530.80",
        #         "quantity": "0.04",
        #         "side": "buy"
        #     }
        #
        #  private
        #     [
        #         {
        #             "type": "accepted",
        #             "order_id": "134150423884",
        #             "event_id": "134150423886",
        #             "account_name": "primary",
        #             "client_order_id": "1659739406916",
        #             "api_session": "account-pnBFSS0XKGvDamX4uEIt",
        #             "symbol": "batbtc",
        #             "side": "sell",
        #             "order_type": "exchange limit",
        #             "timestamp": "1659739407",
        #             "timestampms": 1659739407576,
        #             "is_live": True,
        #             "is_cancelled": False,
        #             "is_hidden": False,
        #             "original_amount": "1",
        #             "price": "1",
        #             "socket_sequence": 139
        #         }
        #     ]
        #
        isArray = isinstance(message, list)
        if isArray:
            self.handle_order(client, message)
            return
        reason = self.safe_string(message, 'reason')
        if reason == 'error':
            self.handle_error(client, message)
        methods: dict = {
            'l2_updates': self.handle_l2_updates,
            'trade': self.handle_trade,
            'subscription_ack': self.handle_subscription,
            'heartbeat': self.handle_heartbeat,
        }
        type = self.safe_string(message, 'type', '')
        if type.find('candles') >= 0:
            self.handle_ohlcv(client, message)
            return
        method = self.safe_value(methods, type)
        if method is not None:
            method(client, message)
        # handle multimarketdata
        if type == 'update':
            ts = self.safe_integer(message, 'timestampms', self.milliseconds())
            eventId = self.safe_integer(message, 'eventId')
            events = self.safe_list(message, 'events')
            orderBookItems = []
            bidaskItems = []
            collectedEventsOfTrades = []
            eventsLength = len(events)
            for i in range(0, len(events)):
                event = events[i]
                eventType = self.safe_string(event, 'type')
                isOrderBook = (eventType == 'change') and ('side' in event) and self.in_array(event['side'], ['ask', 'bid'])
                eventReason = self.safe_string(event, 'reason')
                isBidAsk = (eventReason == 'top-of-book') or (isOrderBook and (eventReason == 'initial') and eventsLength == 2)
                if isBidAsk:
                    bidaskItems.append(event)
                elif isOrderBook:
                    orderBookItems.append(event)
                elif eventType == 'trade':
                    collectedEventsOfTrades.append(events[i])
            lengthBa = len(bidaskItems)
            if lengthBa > 0:
                self.handle_bids_asks_for_multidata(client, bidaskItems, ts, eventId)
            lengthOb = len(orderBookItems)
            if lengthOb > 0:
                self.handle_order_book_for_multidata(client, orderBookItems, ts, eventId)
            lengthTrades = len(collectedEventsOfTrades)
            if lengthTrades > 0:
                self.handle_trades_for_multidata(client, collectedEventsOfTrades, ts)

    async def authenticate(self, params={}):
        url = self.safe_string(params, 'url')
        if (self.clients is not None) and (url in self.clients):
            return
        self.check_required_credentials()
        startIndex = len(self.urls['api']['ws'])
        urlParamsIndex = url.find('?')
        urlLength = len(url)
        endIndex = urlParamsIndex if (urlParamsIndex >= 0) else urlLength
        request = url[startIndex:endIndex]
        payload: dict = {
            'request': request,
            'nonce': self.nonce(),
        }
        b64 = self.string_to_base64(self.json(payload))
        signature = self.hmac(self.encode(b64), self.encode(self.secret), hashlib.sha384, 'hex')
        defaultOptions: dict = {
            'ws': {
                'options': {
                    'headers': {},
                },
            },
        }
        # self.options = self.extend(defaultOptions, self.options)
        self.extend_exchange_options(defaultOptions)
        originalHeaders = self.options['ws']['options']['headers']
        headers: dict = {
            'X-GEMINI-APIKEY': self.apiKey,
            'X-GEMINI-PAYLOAD': b64,
            'X-GEMINI-SIGNATURE': signature,
        }
        self.options['ws']['options']['headers'] = headers
        self.client(url)
        self.options['ws']['options']['headers'] = originalHeaders
