<?php

namespace ccxt\pro;

// 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 \React\Async;
use \React\Promise\PromiseInterface;

class backpack extends \ccxt\async\backpack {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => false,
                'watchBidsAsks' => true,
                'watchMyTrades' => false,
                'watchOHLCV' => true,
                'watchOHLCVForSymbols' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOrders' => true,
                'watchPositions' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'unwatchBidsAsks' => true,
                'unwatchOHLCV' => true,
                'unwatchOHLCVForSymbols' => true,
                'unwatchOrderBook' => true,
                'unwatchOrderBookForSymbols' => true,
                'unwatchTicker' => true,
                'unwatchTickers' => true,
                'unWatchTrades' => true,
                'unWatchTradesForSymbols' => true,
                'unWatchOrders' => true,
                'unWatchPositions' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://ws.backpack.exchange',
                        'private' => 'wss://ws.backpack.exchange',
                    ),
                ),
            ),
            'options' => array(
                'timeframes' => array(
                ),
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
                'keepAlive' => 119000,
            ),
        ));
    }

    public function watch_public($topics, $messageHashes, $params = array (), $unwatch = false) {
        return Async\async(function () use ($topics, $messageHashes, $params, $unwatch) {
            Async\await($this->load_markets());
            $url = $this->urls['api']['ws']['public'];
            $method = $unwatch ? 'UNSUBSCRIBE' : 'SUBSCRIBE';
            $request = array(
                'method' => $method,
                'params' => $topics,
            );
            $message = $this->deep_extend($request, $params);
            if ($unwatch) {
                $this->handle_unsubscriptions($url, $messageHashes, $message);
                return null;
            }
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes));
        }) ();
    }

    public function watch_private($topics, $messageHashes, $params = array (), $unwatch = false) {
        return Async\async(function () use ($topics, $messageHashes, $params, $unwatch) {
            $this->check_required_credentials();
            $url = $this->urls['api']['ws']['private'];
            $instruction = 'subscribe';
            $ts = (string) $this->nonce();
            $method = $unwatch ? 'UNSUBSCRIBE' : 'SUBSCRIBE';
            $recvWindow = $this->safe_string_2($this->options, 'recvWindow', 'X-Window', '5000');
            $payload = 'instruction=' . $instruction . '&' . 'timestamp=' . $ts . '&window=' . $recvWindow;
            $secretBytes = base64_decode($this->secret);
            $seed = $this->array_slice($secretBytes, 0, 32);
            $signature = $this->eddsa($this->encode($payload), $seed, 'ed25519');
            $request = array(
                'method' => $method,
                'params' => $topics,
                'signature' => array( $this->apiKey, $signature, $ts, $recvWindow ),
            );
            $message = $this->deep_extend($request, $params);
            if ($unwatch) {
                $this->handle_unsubscriptions($url, $messageHashes, $message);
                return null;
            }
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes));
        }) ();
    }

    public function handle_unsubscriptions(string $url, array $messageHashes, array $message) {
        $client = $this->client($url);
        $this->watch_multiple($url, $messageHashes, $message, $messageHashes);
        for ($i = 0; $i < count($messageHashes); $i++) {
            $messageHash = $messageHashes[$i];
            $subMessageHash = str_replace('unsubscribe:', '', $messageHash);
            $this->clean_unsubscription($client, $subMessageHash, $messageHash);
            if (mb_strpos($messageHash, 'ticker') !== false) {
                $symbol = str_replace('unsubscribe:ticker:', '', $messageHash);
                if (is_array($this->tickers) && array_key_exists($symbol, $this->tickers)) {
                    unset($this->tickers[$symbol]);
                }
            } elseif (mb_strpos($messageHash, 'bidask') !== false) {
                $symbol = str_replace('unsubscribe:bidask:', '', $messageHash);
                if (is_array($this->bidsasks) && array_key_exists($symbol, $this->bidsasks)) {
                    unset($this->bidsasks[$symbol]);
                }
            } elseif (mb_strpos($messageHash, 'candles') !== false) {
                $splitHashes = explode(':', $messageHash);
                $symbol = $this->safe_string($splitHashes, 2);
                $timeframe = $this->safe_string($splitHashes, 3);
                if (is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs)) {
                    if (is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol])) {
                        unset($this->ohlcvs[$symbol][$timeframe]);
                    }
                }
            } elseif (mb_strpos($messageHash, 'orderbook') !== false) {
                $symbol = str_replace('unsubscribe:orderbook:', '', $messageHash);
                if (is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks)) {
                    unset($this->orderbooks[$symbol]);
                }
            } elseif (mb_strpos($messageHash, 'trades') !== false) {
                $symbol = str_replace('unsubscribe:trades:', '', $messageHash);
                if (is_array($this->trades) && array_key_exists($symbol, $this->trades)) {
                    unset($this->trades[$symbol]);
                }
            } elseif (mb_strpos($messageHash, 'orders') !== false) {
                if ($messageHash === 'unsubscribe:orders') {
                    $cache = $this->orders;
                    $keys = is_array($cache) ? array_keys($cache) : array();
                    for ($j = 0; $j < count($keys); $j++) {
                        $symbol = $keys[$j];
                        unset($this->orders[$symbol]);
                    }
                } else {
                    $symbol = str_replace('unsubscribe:orders:', '', $messageHash);
                    if (is_array($this->orders) && array_key_exists($symbol, $this->orders)) {
                        unset($this->orders[$symbol]);
                    }
                }
            } elseif (mb_strpos($messageHash, 'positions') !== false) {
                if ($messageHash === 'unsubscribe:positions') {
                    $cache = $this->positions;
                    $keys = is_array($cache) ? array_keys($cache) : array();
                    for ($j = 0; $j < count($keys); $j++) {
                        $symbol = $keys[$j];
                        unset($this->positions[$symbol]);
                    }
                } else {
                    $symbol = str_replace('unsubscribe:positions:', '', $messageHash);
                    if (is_array($this->positions) && array_key_exists($symbol, $this->positions)) {
                        unset($this->positions[$symbol]);
                    }
                }
            }
        }
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/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~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $topic = 'ticker' . '.' . $market['id'];
            $messageHash = 'ticker' . ':' . $symbol;
            return Async\await($this->watch_public(array( $topic ), array( $messageHash ), $params));
        }) ();
    }

    public function un_watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/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~
             */
            return Async\await($this->un_watch_tickers(array( $symbol ), $params));
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Ticker
             *
             * @param {string[]} $symbols 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $messageHashes = array();
            $topics = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketId = $this->market_id($symbol);
                $messageHashes[] = 'ticker:' . $symbol;
                $topics[] = 'ticker.' . $marketId;
            }
            Async\await($this->watch_public($topics, $messageHashes, $params));
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function un_watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Ticker
             *
             * @param {string[]} $symbols 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketId = $this->market_id($symbol);
                $topics[] = 'ticker.' . $marketId;
                $messageHashes[] = 'unsubscribe:ticker:' . $symbol;
            }
            return Async\await($this->watch_public($topics, $messageHashes, $params, true));
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         data => array(
        //             E => '1754176123312507',
        //             V => '19419526.742584',
        //             c => '3398.57',
        //             e => 'ticker',
        //             h => '3536.65',
        //             l => '3371.8',
        //             n => 17152,
        //             o => '3475.45',
        //             s => 'ETH_USDC',
        //             v => '5573.5827'
        //         ),
        //         stream => 'bookTicker.ETH_USDC'
        //     }
        //
        $ticker = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($ticker, 's');
        $market = $this->safe_market($marketId);
        $symbol = $this->safe_symbol($marketId, $market);
        $parsedTicker = $this->parse_ws_ticker($ticker, $market);
        $messageHash = 'ticker' . ':' . $symbol;
        $this->tickers[$symbol] = $parsedTicker;
        $client->resolve ($parsedTicker, $messageHash);
    }

    public function parse_ws_ticker(array $ticker, ?array $market = null): array {
        //
        //     {
        //         E => '1754178406415232',
        //         V => '19303818.6923',
        //         c => '3407.54',
        //         e => 'ticker',
        //         h => '3536.65',
        //         l => '3369.18',
        //         n => 17272,
        //         o => '3481.71',
        //         s => 'ETH_USDC',
        //         v => '5542.3911'
        //     }
        //
        $microseconds = $this->safe_integer($ticker, 'E');
        $timestamp = $this->parse_to_int($microseconds / 1000);
        $marketId = $this->safe_string($ticker, 's');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_symbol($marketId, $market);
        $last = $this->safe_string($ticker, 'c');
        $open = $this->safe_string($ticker, 'o');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->safe_number($ticker, 'h'),
            'low' => $this->safe_number($ticker, 'l'),
            'bid' => null,
            'bidVolume' => null,
            'ask' => null,
            'askVolume' => null,
            'vwap' => null,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'v'),
            'quoteVolume' => $this->safe_string($ticker, 'V'),
            'info' => $ticker,
        ), $market);
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches best bid & ask for $symbols
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Book-ticker
             *
             * @param {string[]} $symbols 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketId = $this->market_id($symbol);
                $topics[] = 'bookTicker.' . $marketId;
                $messageHashes[] = 'bidask:' . $symbol;
            }
            Async\await($this->watch_public($topics, $messageHashes, $params));
            return $this->filter_by_array($this->bidsasks, 'symbol', $symbols);
        }) ();
    }

    public function un_watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches best bid & ask for $symbols
             * @param {string[]} $symbols 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketId = $this->market_id($symbol);
                $topics[] = 'bookTicker.' . $marketId;
                $messageHashes[] = 'unsubscribe:bidask:' . $symbol;
            }
            return Async\await($this->watch_public($topics, $messageHashes, $params, true));
        }) ();
    }

    public function handle_bid_ask(Client $client, $message) {
        //
        //     {
        //         $data => array(
        //             A => '0.4087',
        //             B => '0.0020',
        //             E => '1754517402450016',
        //             T => '1754517402449064',
        //             a => '3667.50',
        //             b => '3667.49',
        //             e => 'bookTicker',
        //             s => 'ETH_USDC',
        //             u => 1328288557
        //         ),
        //         stream => 'bookTicker.ETH_USDC'
        //     }
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 's');
        $market = $this->safe_market($marketId);
        $symbol = $this->safe_symbol($marketId, $market);
        $parsedBidAsk = $this->parse_ws_bid_ask($data, $market);
        $messageHash = 'bidask' . ':' . $symbol;
        $this->bidsasks[$symbol] = $parsedBidAsk;
        $client->resolve ($parsedBidAsk, $messageHash);
    }

    public function parse_ws_bid_ask($ticker, $market = null) {
        //
        //     {
        //         A => '0.4087',
        //         B => '0.0020',
        //         E => '1754517402450016',
        //         T => '1754517402449064',
        //         a => '3667.50',
        //         b => '3667.49',
        //         e => 'bookTicker',
        //         s => 'ETH_USDC',
        //         u => 1328288557
        //     }
        //
        $marketId = $this->safe_string($ticker, 's');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_string($market, 'symbol');
        $microseconds = $this->safe_integer($ticker, 'E');
        $timestamp = $this->parse_to_int($microseconds / 1000);
        $ask = $this->safe_string($ticker, 'a');
        $askVolume = $this->safe_string($ticker, 'A');
        $bid = $this->safe_string($ticker, 'b');
        $bidVolume = $this->safe_string($ticker, 'B');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'ask' => $ask,
            'askVolume' => $askVolume,
            'bid' => $bid,
            'bidVolume' => $bidVolume,
            'info' => $ticker,
        ), $market);
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, close price, and the volume of a market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/K-Line
             *
             * @param {string} $symbol unified $symbol of the market to fetch OHLCV data for
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            $result = Async\await($this->watch_ohlcv_for_symbols(array( array( $symbol, $timeframe ) ), $since, $limit, $params));
            return $result[$symbol][$timeframe];
        }) ();
    }

    public function un_watch_ohlcv(string $symbol, $timeframe = '1m', $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/K-Line
             *
             * @param {string} $symbol unified $symbol of the market to fetch OHLCV data for
             * @param {string} $timeframe the length of time each candle represents
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            return Async\await($this->un_watch_ohlcv_for_symbols(array( array( $symbol, $timeframe ) ), $params));
        }) ();
    }

    public function watch_ohlcv_for_symbols(array $symbolsAndTimeframes, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($symbolsAndTimeframes, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, close price, and the volume of a $market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/K-Line
             *
             * @param {string[][]} $symbolsAndTimeframes array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of $candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of $candles ordered, open, high, low, close, volume
             */
            $symbolsLength = count($symbolsAndTimeframes);
            if ($symbolsLength === 0 || gettype($symbolsAndTimeframes[0]) !== 'array' || array_keys($symbolsAndTimeframes[0]) !== array_keys(array_keys($symbolsAndTimeframes[0]))) {
                throw new ArgumentsRequired($this->id . " watchOHLCVForSymbols() requires a an array of symbols and timeframes, like  ['ETH/USDC', '1m']");
            }
            Async\await($this->load_markets());
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbolsAndTimeframes); $i++) {
                $symbolAndTimeframe = $symbolsAndTimeframes[$i];
                $marketId = $this->safe_string($symbolAndTimeframe, 0);
                $market = $this->market($marketId);
                $tf = $this->safe_string($symbolAndTimeframe, 1);
                $interval = $this->safe_string($this->timeframes, $tf, $tf);
                $topics[] = 'kline.' . $interval . '.' . $market['id'];
                $messageHashes[] = 'candles:' . $market['symbol'] . ':' . $interval;
            }
            list($symbol, $timeframe, $candles) = Async\await($this->watch_public($topics, $messageHashes, $params));
            if ($this->newUpdates) {
                $limit = $candles->getLimit ($symbol, $limit);
            }
            $filtered = $this->filter_by_since_limit($candles, $since, $limit, 0, true);
            return $this->create_ohlcv_object($symbol, $timeframe, $filtered);
        }) ();
    }

    public function un_watch_ohlcv_for_symbols(array $symbolsAndTimeframes, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbolsAndTimeframes, $params) {
            /**
             * unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/K-Line
             *
             * @param {string[][]} $symbolsAndTimeframes array of arrays containing unified symbols and timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            $symbolsLength = count($symbolsAndTimeframes);
            if ($symbolsLength === 0 || gettype($symbolsAndTimeframes[0]) !== 'array' || array_keys($symbolsAndTimeframes[0]) !== array_keys(array_keys($symbolsAndTimeframes[0]))) {
                throw new ArgumentsRequired($this->id . " unWatchOHLCVForSymbols() requires a an array of symbols and timeframes, like  ['ETH/USDC', '1m']");
            }
            Async\await($this->load_markets());
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbolsAndTimeframes); $i++) {
                $symbolAndTimeframe = $symbolsAndTimeframes[$i];
                $marketId = $this->safe_string($symbolAndTimeframe, 0);
                $market = $this->market($marketId);
                $tf = $this->safe_string($symbolAndTimeframe, 1);
                $interval = $this->safe_string($this->timeframes, $tf, $tf);
                $topics[] = 'kline.' . $interval . '.' . $market['id'];
                $messageHashes[] = 'unsubscribe:candles:' . $market['symbol'] . ':' . $interval;
            }
            return Async\await($this->watch_public($topics, $messageHashes, $params, true));
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         $data => array(
        //             E => '1754519557526056',
        //             T => '2025-08-07T00:00:00',
        //             X => false,
        //             c => '3680.520000000',
        //             e => 'kline',
        //             h => '3681.370000000',
        //             l => '3667.650000000',
        //             n => 255,
        //             o => '3670.150000000',
        //             s => 'ETH_USDC',
        //             t => '2025-08-06T22:00:00',
        //             v => '62.2621000'
        //         ),
        //         $stream => 'kline.2h.ETH_USDC'
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 's');
        $market = $this->market($marketId);
        $symbol = $market['symbol'];
        $stream = $this->safe_string($message, 'stream');
        $parts = explode('.', $stream);
        $timeframe = $this->safe_string($parts, 1);
        if (!(is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs))) {
            $this->ohlcvs[$symbol] = array();
        }
        if (!(is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol]))) {
            $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
            $stored = new ArrayCacheByTimestamp ($limit);
            $this->ohlcvs[$symbol][$timeframe] = $stored;
        }
        $ohlcv = $this->ohlcvs[$symbol][$timeframe];
        $parsed = $this->parse_ws_ohlcv($data);
        $ohlcv->append ($parsed);
        $messageHash = 'candles:' . $symbol . ':' . $timeframe;
        $client->resolve (array( $symbol, $timeframe, $ohlcv ), $messageHash);
    }

    public function parse_ws_ohlcv($ohlcv, $market = null): array {
        //
        //     array(
        //         E => '1754519557526056',
        //         T => '2025-08-07T00:00:00',
        //         X => false,
        //         c => '3680.520000000',
        //         e => 'kline',
        //         h => '3681.370000000',
        //         l => '3667.650000000',
        //         n => 255,
        //         o => '3670.150000000',
        //         s => 'ETH_USDC',
        //         t => '2025-08-06T22:00:00',
        //         v => '62.2621000'
        //     ),
        //
        return array(
            $this->parse8601($this->safe_string($ohlcv, 'T')),
            $this->safe_number($ohlcv, 'o'),
            $this->safe_number($ohlcv, 'h'),
            $this->safe_number($ohlcv, 'l'),
            $this->safe_number($ohlcv, 'c'),
            $this->safe_number($ohlcv, 'v'),
        );
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple trades made in a market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Trade
             *
             * @param {string} $symbol unified $symbol of the market to fetch the ticker for
             * @param {int} [$since] the earliest time in ms to fetch trades for
             * @param {int} [$limit] the maximum number of trade structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            return Async\await($this->watch_trades_for_symbols(array( $symbol ), $since, $limit, $params));
        }) ();
    }

    public function un_watch_trades(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches from the stream channel
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Trade
             *
             * @param {string} $symbol unified $symbol of the market to fetch trades for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            return Async\await($this->un_watch_trades_for_symbols(array( $symbol ), $params));
        }) ();
    }

    public function watch_trades_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * watches information on multiple $trades made in a market
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Trade
             *
             * @param {string[]} $symbols unified $symbol of the market to fetch $trades for
             * @param {int} [$since] the earliest time in ms to fetch $trades for
             * @param {int} [$limit] the maximum number of trade structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $symbolsLength = count($symbols);
            if ($symbolsLength === 0) {
                throw new ArgumentsRequired($this->id . ' watchTradesForSymbols() requires a non-empty array of symbols');
            }
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketId = $this->market_id($symbol);
                $topics[] = 'trade.' . $marketId;
                $messageHashes[] = 'trades:' . $symbol;
            }
            $trades = Async\await($this->watch_public($topics, $messageHashes, $params));
            if ($this->newUpdates) {
                $first = $this->safe_value($trades, 0);
                $tradeSymbol = $this->safe_string($first, 'symbol');
                $limit = $trades->getLimit ($tradeSymbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function un_watch_trades_for_symbols(array $symbols, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches from the stream channel
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Trade
             *
             * @param {string[]} $symbols unified $symbol of the market to fetch trades for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $symbolsLength = count($symbols);
            if ($symbolsLength === 0) {
                throw new ArgumentsRequired($this->id . ' unWatchTradesForSymbols() requires a non-empty array of symbols');
            }
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketId = $this->market_id($symbol);
                $topics[] = 'trade.' . $marketId;
                $messageHashes[] = 'unsubscribe:trades:' . $symbol;
            }
            return Async\await($this->watch_public($topics, $messageHashes, $params, true));
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         $data => array(
        //             E => '1754601477746429',
        //             T => '1754601477744000',
        //             a => '5121860761',
        //             b => '5121861755',
        //             e => 'trade',
        //             m => false,
        //             p => '3870.25',
        //             q => '0.0008',
        //             s => 'ETH_USDC_PERP',
        //             t => 10782547
        //         ),
        //         stream => 'trade.ETH_USDC_PERP'
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 's');
        $market = $this->market($marketId);
        $symbol = $market['symbol'];
        if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $cache = $this->trades[$symbol];
        $trade = $this->parse_ws_trade($data, $market);
        $cache->append ($trade);
        $messageHash = 'trades:' . $symbol;
        $client->resolve ($cache, $messageHash);
        $client->resolve ($cache, 'trades');
    }

    public function parse_ws_trade($trade, $market = null) {
        //
        //     {
        //         E => '1754601477746429',
        //         T => '1754601477744000',
        //         a => '5121860761',
        //         b => '5121861755',
        //         e => 'trade',
        //         m => false,
        //         p => '3870.25',
        //         q => '0.0008',
        //         s => 'ETH_USDC_PERP',
        //         t => 10782547
        //     }
        //
        $microseconds = $this->safe_integer($trade, 'E');
        $timestamp = $this->parse_to_int($microseconds / 1000);
        $id = $this->safe_string($trade, 't');
        $marketId = $this->safe_string($trade, 's');
        $market = $this->safe_market($marketId, $market);
        $isMaker = $this->safe_bool($trade, 'm');
        $side = $isMaker ? 'sell' : 'buy';
        $takerOrMaker = $isMaker ? 'maker' : 'taker';
        $price = $this->safe_string($trade, 'p');
        $amount = $this->safe_string($trade, 'q');
        $orderId = null;
        if ($side === 'buy') {
            $orderId = $this->safe_string($trade, 'b');
        } else {
            $orderId = $this->safe_string($trade, 'a');
        }
        return $this->safe_trade(array(
            'info' => $trade,
            'id' => $id,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $market['symbol'],
            'order' => $orderId,
            'type' => null,
            'side' => $side,
            'takerOrMaker' => $takerOrMaker,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => array(
                'currency' => null,
                'cost' => null,
            ),
        ), $market);
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Depth
             *
             * @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
             */
            return Async\await($this->watch_order_book_for_symbols(array( $symbol ), $limit, $params));
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Public/Depth
             *
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             * @param {string[]} $symbols unified array of $symbols
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->method] either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2'
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market $symbols
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $marketIds = $this->market_ids($symbols);
            $messageHashes = array();
            $topics = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $messageHashes[] = 'orderbook:' . $symbol;
                $marketId = $marketIds[$i];
                $topic = 'depth.' . $marketId;
                $topics[] = $topic;
            }
            $orderbook = Async\await($this->watch_public($topics, $messageHashes, $params));
            return $orderbook->limit (); // todo check if $limit is needed
        }) ();
    }

    public function un_watch_order_book(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             * @param {string} $symbol unified array of symbols
             * @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
             */
            return Async\await($this->un_watch_order_book_for_symbols(array( $symbol ), $params));
        }) ();
    }

    public function un_watch_order_book_for_symbols(array $symbols, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             * @param {string[]} $symbols unified array of $symbols
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->method] either '/market/level2' or '/spotMarket/level2Depth5' or '/spotMarket/level2Depth50' default is '/market/level2'
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market $symbols
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $marketIds = $this->market_ids($symbols);
            $messageHashes = array();
            $topics = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $messageHashes[] = 'unsubscribe:orderbook:' . $symbol;
                $marketId = $marketIds[$i];
                $topic = 'depth.' . $marketId;
                $topics[] = $topic;
            }
            return Async\await($this->watch_public($topics, $messageHashes, $params, true));
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        // initial snapshot is fetched with ccxt's fetchOrderBook
        // the feed does not include a snapshot, just the deltas
        //
        //     {
        //         "data" => array(
        //             "E" => "1754903057555305",
        //             "T" => "1754903057554352",
        //             "U" => 1345937436,
        //             "a" => array(),
        //             "b" => array(),
        //             "e" => "depth",
        //             "s" => "ETH_USDC",
        //             "u" => 1345937436
        //         ),
        //         "stream" => "depth.ETH_USDC"
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 's');
        $symbol = $this->safe_symbol($marketId);
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $this->orderbooks[$symbol] = $this->order_book();
        }
        $storedOrderBook = $this->orderbooks[$symbol];
        $nonce = $this->safe_integer($storedOrderBook, 'nonce');
        $deltaNonce = $this->safe_integer($data, 'u');
        $messageHash = 'orderbook:' . $symbol;
        if ($nonce === null) {
            $cacheLength = count($storedOrderBook->cache);
            // the rest API is very delayed
            // usually it takes at least 9 deltas to resolve
            $snapshotDelay = $this->handle_option('watchOrderBook', 'snapshotDelay', 10);
            if ($cacheLength === $snapshotDelay) {
                $this->spawn(array($this, 'load_order_book'), $client, $messageHash, $symbol, null, array());
            }
            $storedOrderBook->cache[] = $data;
            return;
        } elseif ($nonce > $deltaNonce) {
            return;
        }
        $this->handle_delta($storedOrderBook, $data);
        $client->resolve ($storedOrderBook, $messageHash);
    }

    public function handle_delta($orderbook, $delta) {
        $timestamp = $this->parse_to_int($this->safe_integer($delta, 'T') / 1000);
        $orderbook['timestamp'] = $timestamp;
        $orderbook['datetime'] = $this->iso8601($timestamp);
        $orderbook['nonce'] = $this->safe_integer($delta, 'u');
        $bids = $this->safe_list($delta, 'b', array());
        $asks = $this->safe_list($delta, 'a', array());
        $storedBids = $orderbook['bids'];
        $storedAsks = $orderbook['asks'];
        $this->handle_bid_asks($storedBids, $bids);
        $this->handle_bid_asks($storedAsks, $asks);
    }

    public function handle_bid_asks($bookSide, $bidAsks) {
        for ($i = 0; $i < count($bidAsks); $i++) {
            $bidAsk = $this->parse_bid_ask($bidAsks[$i]);
            $bookSide->storeArray ($bidAsk);
        }
    }

    public function get_cache_index($orderbook, $cache) {
        //
        // array("E":"1759338824897386","T":"1759338824895616","U":1662976171,"a":array(),"b":[["117357.0","0.00000"]],"e":"depth","s":"BTC_USDC_PERP","u":1662976171)
        $firstDelta = $this->safe_dict($cache, 0);
        $nonce = $this->safe_integer($orderbook, 'nonce');
        $firstDeltaStart = $this->safe_integer($firstDelta, 'U');
        if ($nonce < $firstDeltaStart - 1) {
            return -1;
        }
        for ($i = 0; $i < count($cache); $i++) {
            $delta = $cache[$i];
            $deltaStart = $this->safe_integer($delta, 'U');
            $deltaEnd = $this->safe_integer($delta, 'u');
            if (($nonce >= $deltaStart - 1) && ($nonce < $deltaEnd)) {
                return $i;
            }
        }
        return count($cache);
    }

    public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $orders made by the user
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Private/Order-update
             *
             * @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 {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
            }
            $topic = 'account.orderUpdate';
            $messageHash = 'orders';
            if ($market !== null) {
                $topic = 'account.orderUpdate.' . $market['id'];
                $messageHash = 'orders:' . $symbol;
            }
            $orders = Async\await($this->watch_private(array( $topic ), array( $messageHash ), $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function un_watch_orders(?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches information on multiple orders made by the user
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Private/Order-update
             *
             * @param {string} [$symbol] unified $market $symbol of the $market orders were made in
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
            }
            $topic = 'account.orderUpdate';
            $messageHash = 'unsubscribe:orders';
            if ($market !== null) {
                $topic = 'account.orderUpdate.' . $market['id'];
                $messageHash = 'unsubscribe:orders:' . $symbol;
            }
            return Async\await($this->watch_private(array( $topic ), array( $messageHash ), $params, true));
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        //     {
        //         $data => array(
        //             E => '1754939110175843',
        //             O => 'USER',
        //             Q => '4.30',
        //             S => 'Bid',
        //             T => '1754939110174703',
        //             V => 'RejectTaker',
        //             X => 'New',
        //             Z => '0',
        //             e => 'orderAccepted',
        //             f => 'GTC',
        //             i => '5406825793',
        //             o => 'MARKET',
        //             q => '0.0010',
        //             r => false,
        //             s => 'ETH_USDC',
        //             t => null,
        //             z => '0'
        //         ),
        //         stream => 'account.orderUpdate.ETH_USDC'
        //     }
        //
        $messageHash = 'orders';
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 's');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $parsed = $this->parse_ws_order($data, $market);
        $orders = $this->orders;
        if ($orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $orders = new ArrayCacheBySymbolById ($limit);
            $this->orders = $orders;
        }
        $orders->append ($parsed);
        $client->resolve ($orders, $messageHash);
        $symbolSpecificMessageHash = $messageHash . ':' . $symbol;
        $client->resolve ($orders, $symbolSpecificMessageHash);
    }

    public function parse_ws_order($order, $market = null) {
        //
        //     array(
        //         E => '1754939110175879',
        //         L => '4299.16',
        //         N => 'ETH',
        //         O => 'USER',
        //         Q => '4.30',
        //         S => 'Bid',
        //         T => '1754939110174705',
        //         V => 'RejectTaker',
        //         X => 'Filled',
        //         Z => '4.299160',
        //         e => 'orderFill',
        //         f => 'GTC',
        //         i => '5406825793',
        //         l => '0.0010',
        //         m => false,
        //         n => '0.000001',
        //         o => 'MARKET',
        //         q => '0.0010',
        //         r => false,
        //         s => 'ETH_USDC',
        //         t => 2888471,
        //         z => '0.0010'
        //     ),
        //
        $id = $this->safe_string($order, 'i');
        $clientOrderId = $this->safe_string($order, 'c');
        $microseconds = $this->safe_integer($order, 'E');
        $timestamp = $this->parse_to_int($microseconds / 1000);
        $status = $this->parse_ws_order_status($this->safe_string($order, 'X'), $market);
        $marketId = $this->safe_string($order, 's');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $type = $this->safe_string_lower($order, 'o');
        $timeInForce = $this->safe_string($order, 'f');
        $side = $this->parse_ws_order_side($this->safe_string($order, 'S'));
        $price = $this->safe_string($order, 'p');
        $triggerPrice = $this->safe_number($order, 'P');
        $amount = $this->safe_string($order, 'q');
        $cost = $this->safe_string($order, 'Z');
        $filled = $this->safe_string($order, 'l');
        $fee = null;
        $feeCurrency = $this->safe_string($order, 'N');
        if ($feeCurrency !== null) {
            $fee = array(
                'currency' => $feeCurrency,
                'cost' => null,
            );
        }
        return $this->safe_order(array(
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'status' => $status,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => $timeInForce,
            'side' => $side,
            'price' => $price,
            'stopPrice' => null,
            'triggerPrice' => $triggerPrice,
            'average' => null,
            'amount' => $amount,
            'cost' => $cost,
            'filled' => $filled,
            'remaining' => null,
            'fee' => $fee,
            'trades' => null,
            'info' => $order,
        ), $market);
    }

    public function parse_ws_order_status($status, $market = null) {
        $statuses = array(
            'New' => 'open',
            'Filled' => 'closed',
            'Cancelled' => 'canceled',
            'Expired' => 'canceled',
            'PartiallyFilled' => 'open',
            'TriggerPending' => 'open',
            'TriggerFailed' => 'rejected',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_ws_order_side(?string $side) {
        $sides = array(
            'Bid' => 'buy',
            'Ask' => 'sell',
        );
        return $this->safe_string($sides, $side, $side);
    }

    public function watch_positions(?array $symbols = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * watch all open $positions
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Private/Position-update
             *
             * @param {string[]} [$symbols] list of unified market $symbols to watch $positions for
             * @param {int} [$since] the earliest time in ms to fetch $positions for
             * @param {int} [$limit] the maximum number of $positions to retrieve
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $messageHashes = array();
            $topics = array();
            if ($symbols !== null) {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $messageHashes[] = 'positions' . ':' . $symbol;
                    $topics[] = 'account.positionUpdate.' . $this->market_id($symbol);
                }
            } else {
                $messageHashes[] = 'positions';
                $topics[] = 'account.positionUpdate';
            }
            $positions = Async\await($this->watch_private($topics, $messageHashes, $params));
            if ($this->newUpdates) {
                return $positions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function un_watch_positions(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches from the stream channel
             *
             * @see https://docs.backpack.exchange/#tag/Streams/Private/Position-update
             *
             * @param {string[]} [$symbols] list of unified market $symbols to watch positions for
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $messageHashes = array();
            $topics = array();
            if ($symbols !== null) {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $messageHashes[] = 'unsubscribe:positions' . ':' . $symbol;
                    $topics[] = 'account.positionUpdate.' . $this->market_id($symbol);
                }
            } else {
                $messageHashes[] = 'unsubscribe:positions';
                $topics[] = 'account.positionUpdate';
            }
            return Async\await($this->watch_private($topics, $messageHashes, $params, true));
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        //     {
        //         $data => array(
        //             B => '4236.36',
        //             E => '1754943862040486',
        //             M => '4235.88650933',
        //             P => '-0.000473',
        //             Q => '0.0010',
        //             T => '1754943862040487',
        //             b => '4238.479',
        //             e => 'positionOpened',
        //             f => '0.02',
        //             i => 5411399049,
        //             l => '0',
        //             m => '0.0125',
        //             n => '4.23588650933',
        //             p => '0',
        //             q => '0.0010',
        //             s => 'ETH_USDC_PERP'
        //         ),
        //         stream => 'account.positionUpdate'
        //     }
        //
        $messageHash = 'positions';
        $data = $this->safe_dict($message, 'data', array());
        if ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolById ();
        }
        $cache = $this->positions;
        $parsedPosition = $this->parse_ws_position($data);
        $microseconds = $this->safe_integer($data, 'E');
        $timestamp = $this->parse_to_int($microseconds / 1000);
        $parsedPosition['timestamp'] = $timestamp;
        $parsedPosition['datetime'] = $this->iso8601($timestamp);
        $cache->append ($parsedPosition);
        $symbolSpecificMessageHash = $messageHash . ':' . $parsedPosition['symbol'];
        $client->resolve (array( $parsedPosition ), $messageHash);
        $client->resolve (array( $parsedPosition ), $symbolSpecificMessageHash);
    }

    public function parse_ws_position($position, $market = null) {
        //
        //     {
        //         B => '4236.36',
        //         E => '1754943862040486',
        //         M => '4235.88650933',
        //         P => '-0.000473',
        //         Q => '0.0010',
        //         T => '1754943862040487',
        //         b => '4238.479',
        //         e => 'positionOpened',
        //         f => '0.02',
        //         i => 5411399049,
        //         l => '0',
        //         m => '0.0125',
        //         n => '4.23588650933',
        //         p => '0',
        //         q => '0.0010',
        //         s => 'ETH_USDC_PERP'
        //     }
        //
        $id = $this->safe_string($position, 'i');
        $marketId = $this->safe_string($position, 's');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $notional = $this->safe_string($position, 'n');
        $liquidationPrice = $this->safe_string($position, 'l');
        $entryPrice = $this->safe_string($position, 'b');
        $realizedPnl = $this->safe_string($position, 'p');
        $unrealisedPnl = $this->safe_string($position, 'P');
        $contracts = $this->safe_string($position, 'Q');
        $markPrice = $this->safe_string($position, 'M');
        $netQuantity = $this->safe_number($position, 'q');
        $hedged = false;
        $side = 'long';
        if ($netQuantity < 0) {
            $side = 'short';
        }
        if ($netQuantity === null) {
            $hedged = null;
            $side = null;
        }
        $microseconds = $this->safe_integer($position, 'E');
        $timestamp = $this->parse_to_int($microseconds / 1000);
        $maintenanceMarginPercentage = $this->safe_number($position, 'm');
        $initialMarginPercentage = $this->safe_number($position, 'f');
        return $this->safe_position(array(
            'info' => $position,
            'id' => $id,
            'symbol' => $symbol,
            'notional' => $notional,
            'marginMode' => null,
            'liquidationPrice' => $liquidationPrice,
            'entryPrice' => $entryPrice,
            'realizedPnl' => $realizedPnl,
            'unrealizedPnl' => $unrealisedPnl,
            'percentage' => null,
            'contracts' => $contracts,
            'contractSize' => null,
            'markPrice' => $markPrice,
            'side' => $side,
            'hedged' => $hedged,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'maintenanceMargin' => null,
            'maintenanceMarginPercentage' => $maintenanceMarginPercentage,
            'collateral' => null,
            'initialMargin' => null,
            'initialMarginPercentage' => $initialMarginPercentage,
            'leverage' => null,
            'marginRatio' => null,
        ));
    }

    public function handle_message(Client $client, $message) {
        if (!$this->handle_error_message($client, $message)) {
            return;
        }
        $data = $this->safe_dict($message, 'data');
        $event = $this->safe_string($data, 'e');
        if ($event === 'ticker') {
            $this->handle_ticker($client, $message);
        } elseif ($event === 'bookTicker') {
            $this->handle_bid_ask($client, $message);
        } elseif ($event === 'kline') {
            $this->handle_ohlcv($client, $message);
        } elseif ($event === 'trade') {
            $this->handle_trades($client, $message);
        } elseif ($event === 'depth') {
            $this->handle_order_book($client, $message);
        } elseif ($event === 'orderAccepted' || $event === 'orderUpdate' || $event === 'orderFill' || $event === 'orderCancelled' || $event === 'orderExpired' || $event === 'orderModified' || $event === 'triggerPlaced' || $event === 'triggerFailed') {
            $this->handle_order($client, $message);
        } elseif ($event === 'positionAdjusted' || $event === 'positionOpened' || $event === 'positionClosed' || $event === 'positionUpdated') {
            $this->handle_positions($client, $message);
        }
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //     {
        //         id => null,
        //         $error => {
        //             $code => 4006,
        //             $message => 'Invalid stream'
        //         }
        //     }
        //
        $error = $this->safe_dict($message, 'error', array());
        $code = $this->safe_integer($error, 'code');
        try {
            if ($code !== null) {
                $msg = $this->safe_string($error, 'message');
                throw new ExchangeError($this->id . ' ' . $msg);
            }
            return true;
        } catch (Exception $e) {
            $client->reject ($e);
        }
        return true;
    }
}
