<?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\AuthenticationError;
use ccxt\ArgumentsRequired;
use ccxt\NotSupported;
use ccxt\ChecksumError;
use ccxt\UnsubscribeError;
use ccxt\Precise;
use \React\Async;
use \React\Promise\PromiseInterface;

class coincatch extends \ccxt\async\coincatch {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOHLCV' => true,
                'watchOHLCVForSymbols' => false, // todo
                'watchOrders' => true,
                'watchMyTrades' => false,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchBalance' => true,
                'watchPositions' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://ws.coincatch.com/public/v1/stream',
                        'private' => 'wss://ws.coincatch.com/private/v1/stream',
                    ),
                ),
            ),
            'options' => array(
                'tradesLimit' => 1000,
                'OHLCVLimit' => 200,
                'timeframesForWs' => array(
                    '1m' => '1m',
                    '5m' => '5m',
                    '15m' => '15m',
                    '30m' => '30m',
                    '1h' => '1H',
                    '4h' => '4H',
                    '12h' => '12H',
                    '1d' => '1D',
                    '1w' => '1W',
                ),
                'watchOrderBook' => array(
                    'checksum' => true,
                ),
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                        '30001' => '\\ccxt\\BadRequest', // Channel does not exist
                        '30002' => '\\ccxt\\AuthenticationError', // illegal request
                        '30003' => '\\ccxt\\BadRequest', // invalid op
                        '30004' => '\\ccxt\\AuthenticationError', // User needs to log in
                        '30005' => '\\ccxt\\AuthenticationError', // login failed
                        '30006' => '\\ccxt\\RateLimitExceeded', // request too many
                        '30007' => '\\ccxt\\RateLimitExceeded', // request over limit,connection close
                        '30011' => '\\ccxt\\AuthenticationError', // invalid ACCESS_KEY
                        '30012' => '\\ccxt\\AuthenticationError', // invalid ACCESS_PASSPHRASE
                        '30013' => '\\ccxt\\AuthenticationError', // invalid ACCESS_TIMESTAMP
                        '30014' => '\\ccxt\\BadRequest', // Request timestamp expired
                        '30015' => '\\ccxt\\AuthenticationError', // array( event => 'error', code => 30015, msg => 'Invalid sign' )
                    ),
                    'broad' => array(),
                ),
            ),
        ));
    }

    public function get_market_from_arg($entry) {
        $instId = $this->safe_string($entry, 'instId');
        $instType = $this->safe_string($entry, 'instType');
        $baseAndQuote = $this->parseSpotMarketId ($instId);
        $baseId = $baseAndQuote['baseId'];
        $quoteId = $baseAndQuote['quoteId'];
        $suffix = '_SPBL'; // spot $suffix
        if ($instType === 'mc') {
            if ($quoteId === 'USD') {
                $suffix = '_DMCBL';
            } else {
                $suffix = '_UMCBL';
            }
        }
        $marketId = $this->safe_currency_code($baseId) . $this->safe_currency_code($quoteId) . $suffix;
        return $this->safeMarketCustom ($marketId);
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $this->check_required_credentials();
            $url = $this->urls['api']['ws']['private'];
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $timestamp = (string) $this->seconds();
                $auth = $timestamp . 'GET' . '/user/verify';
                $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256', 'base64');
                $operation = 'login';
                $request = array(
                    'op' => $operation,
                    'args' => array(
                        array(
                            'apiKey' => $this->apiKey,
                            'passphrase' => $this->password,
                            'timestamp' => $timestamp,
                            'sign' => $signature,
                        ),
                    ),
                );
                $message = $this->extend($request, $params);
                $this->watch($url, $messageHash, $message, $messageHash);
            }
            return Async\await($future);
        }) ();
    }

    public function watch_public($messageHash, $subscribeHash, $args, $params = array ()) {
        return Async\async(function () use ($messageHash, $subscribeHash, $args, $params) {
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'op' => 'subscribe',
                'args' => array( $args ),
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $subscribeHash));
        }) ();
    }

    public function un_watch_public($messageHash, $args, $params = array ()) {
        return Async\async(function () use ($messageHash, $args, $params) {
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'op' => 'unsubscribe',
                'args' => array( $args ),
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash));
        }) ();
    }

    public function watch_private($messageHash, $subscribeHash, $args, $params = array ()) {
        return Async\async(function () use ($messageHash, $subscribeHash, $args, $params) {
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['private'];
            $request = array(
                'op' => 'subscribe',
                'args' => array( $args ),
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $subscribeHash));
        }) ();
    }

    public function watch_private_multiple($messageHashes, $subscribeHashes, $args, $params = array ()) {
        return Async\async(function () use ($messageHashes, $subscribeHashes, $args, $params) {
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['private'];
            $request = array(
                'op' => 'subscribe',
                'args' => $args,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $subscribeHashes));
        }) ();
    }

    public function handle_authenticate(Client $client, $message) {
        //
        //  array( event => "login", code => 0 )
        //
        $messageHash = 'authenticated';
        $future = $this->safe_value($client->futures, $messageHash);
        $future->resolve (true);
    }

    public function watch_public_multiple($messageHashes, $subscribeHashes, $argsArray, $params = array ()) {
        return Async\async(function () use ($messageHashes, $subscribeHashes, $argsArray, $params) {
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'op' => 'subscribe',
                'args' => $argsArray,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $subscribeHashes));
        }) ();
    }

    public function un_watch_channel(string $symbol, string $channel, string $messageHashTopic, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $channel, $messageHashTopic, $params) {
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            list($instType, $instId) = $this->get_public_inst_type_and_id($market);
            $messageHash = 'unsubscribe:' . $messageHashTopic . ':' . $symbol;
            $args = array(
                'instType' => $instType,
                'channel' => $channel,
                'instId' => $instId,
            );
            return Async\await($this->un_watch_public($messageHash, $args, $params));
        }) ();
    }

    public function get_public_inst_type_and_id(array $market) {
        $instId = $market['baseId'] . $market['quoteId'];
        $instType = null;
        if ($market['spot']) {
            $instType = 'SP';
        } elseif ($market['swap']) {
            $instType = 'MC';
        } else {
            throw new NotSupported($this->id . ' supports only spot and swap markets');
        }
        return array( $instType, $instId );
    }

    public function handle_dmcbl_market_by_message_hashes(array $market, string $hash, Client $client, ?string $timeframe = null) {
        $marketId = $market['id'];
        $messageHashes = $this->find_message_hashes($client, $hash);
        // the exchange counts DMCBL markets same $market with different quote currencies
        // for example symbols ETHUSD:ETH and ETH/USD:BTC both have the same $marketId ETHUSD_DMCBL
        // we need to check all markets with the same $marketId to find the correct $market that is in $messageHashes
        $marketsWithCurrentId = $this->safe_list($this->markets_by_id, $marketId, array());
        $suffix = '';
        if ($timeframe !== null) {
            $suffix = ':' . $timeframe;
        }
        for ($i = 0; $i < count($marketsWithCurrentId); $i++) {
            $market = $marketsWithCurrentId[$i];
            $symbol = $market['symbol'];
            $messageHash = $hash . $symbol . $suffix;
            if ($this->in_array($messageHash, $messageHashes)) {
                return $market;
            }
        }
        return $market;
    }

    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://coincatch.github.io/github.io/en/spot/#tickers-$channel
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->instType] the type of the instrument to fetch the ticker for, 'SP' for spot markets, 'MC' for futures markets (default is 'SP')
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            list($instType, $instId) = $this->get_public_inst_type_and_id($market);
            $channel = 'ticker';
            $messageHash = $channel . ':' . $symbol;
            $args = array(
                'instType' => $instType,
                'channel' => $channel,
                'instId' => $instId,
            );
            return Async\await($this->watch_public($messageHash, $messageHash, $args, $params));
        }) ();
    }

    public function un_watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unsubscribe from the ticker channel
             *
             * @see https://coincatch.github.io/github.io/en/mix/#tickers-channel
             *
             * @param {string} $symbol unified $symbol of the market to unwatch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {any} status of the unwatch request
             */
            Async\await($this->load_markets());
            return Async\await($this->un_watch_channel($symbol, 'ticker', 'ticker', $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://coincatch.github.io/github.io/en/mix/#$tickers-channel
             *
             * @param {string[]} $symbols unified $symbol of the $market to watch the $tickers 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());
            if ($symbols === null) {
                $symbols = $this->symbols;
            }
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                list($instType, $instId) = $this->get_public_inst_type_and_id($market);
                $args = array(
                    'instType' => $instType,
                    'channel' => 'ticker',
                    'instId' => $instId,
                );
                $topics[] = $args;
                $messageHashes[] = 'ticker:' . $symbol;
            }
            $tickers = Async\await($this->watch_public_multiple($messageHashes, $messageHashes, $topics, $params));
            if ($this->newUpdates) {
                $result = array();
                $result[$tickers['symbol']] = $tickers;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     action => 'snapshot',
        //     $arg => array( instType => 'sp', channel => 'ticker', instId => 'ETHUSDT' ),
        //     $data => array(
        //         {
        //             instId => 'ETHUSDT',
        //             last => '2421.06',
        //             open24h => '2416.93',
        //             high24h => '2441.47',
        //             low24h => '2352.99',
        //             bestBid => '2421.03',
        //             bestAsk => '2421.06',
        //             baseVolume => '9445.2043',
        //             quoteVolume => '22807159.1148',
        //             ts => 1728131730687,
        //             labeId => 0,
        //             openUtc => '2414.50',
        //             chgUTC => '0.00272',
        //             bidSz => '3.866',
        //             askSz => '0.124'
        //         }
        //     ),
        //     ts => 1728131730688
        //
        $arg = $this->safe_dict($message, 'arg', array());
        $market = $this->get_market_from_arg($arg);
        $marketId = $market['id'];
        $hash = 'ticker:';
        if (mb_strpos($marketId, '_DMCBL') !== false) {
            $market = $this->handle_dmcbl_market_by_message_hashes($market, $hash, $client);
        }
        $data = $this->safe_list($message, 'data', array());
        $ticker = $this->parse_ws_ticker($this->safe_dict($data, 0, array()), $market);
        $symbol = $market['symbol'];
        $this->tickers[$symbol] = $ticker;
        $messageHash = $hash . $symbol;
        $client->resolve ($this->tickers[$symbol], $messageHash);
    }

    public function parse_ws_ticker($ticker, $market = null) {
        //
        // spot
        //     {
        //         instId => 'ETHUSDT',
        //         $last => '2421.06',
        //         open24h => '2416.93',
        //         high24h => '2441.47',
        //         low24h => '2352.99',
        //         bestBid => '2421.03',
        //         bestAsk => '2421.06',
        //         baseVolume => '9445.2043',
        //         quoteVolume => '22807159.1148',
        //         ts => 1728131730687,
        //         labeId => 0,
        //         openUtc => '2414.50',
        //         chgUTC => '0.00272',
        //         bidSz => '3.866',
        //         askSz => '0.124'
        //     }
        //
        // swap
        //     {
        //         instId => 'ETHUSDT',
        //         $last => '2434.47',
        //         bestAsk => '2434.48',
        //         bestBid => '2434.47',
        //         high24h => '2471.68',
        //         low24h => '2400.01',
        //         priceChangePercent => '0.00674',
        //         capitalRate => '0.000082',
        //         nextSettleTime => 1728489600000,
        //         systemTime => 1728471993602,
        //         markPrice => '2434.46',
        //         indexPrice => '2435.44',
        //         holding => '171450.25',
        //         baseVolume => '1699298.91',
        //         quoteVolume => '4144522832.32',
        //         openUtc => '2439.67',
        //         chgUTC => '-0.00213',
        //         symbolType => 1,
        //         symbolId => 'ETHUSDT_UMCBL',
        //         deliveryPrice => '0',
        //         bidSz => '26.12',
        //         askSz => '49.6'
        //     }
        //
        $last = $this->safe_string($ticker, 'last');
        $timestamp = $this->safe_integer_2($ticker, 'ts', 'systemTime');
        return $this->safe_ticker(array(
            'symbol' => $market['symbol'],
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->safe_string($ticker, 'high24h'),
            'low' => $this->safe_string($ticker, 'low24h'),
            'bid' => $this->safe_string($ticker, 'bestBid'),
            'bidVolume' => $this->safe_string($ticker, 'bidSz'),
            'ask' => $this->safe_string($ticker, 'bestAsk'),
            'askVolume' => $this->safe_string($ticker, 'askSz'),
            'vwap' => null,
            'open' => $this->safe_string_2($ticker, 'open24h', 'openUtc'),
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => Precise::string_mul($this->safe_string($ticker, 'chgUTC'), '100'),
            'average' => null,
            'baseVolume' => $this->safe_number($ticker, 'baseVolume'),
            'quoteVolume' => $this->safe_number($ticker, 'quoteVolume'),
            'indexPrice' => $this->safe_string($ticker, 'indexPrice'),
            'markPrice' => $this->safe_string($ticker, 'markPrice'),
            '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, and close price, and the volume of a $market
             *
             * @see https://coincatch.github.io/github.io/en/spot/#candlesticks-$channel
             *
             * @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 (not including)
             * @param {int} [$limit] the maximum amount of candles to fetch (not including)
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->instType] the type of the instrument to fetch the OHLCV data for, 'SP' for spot markets, 'MC' for futures markets (default is 'SP')
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $timeframes = $this->options['timeframesForWs'];
            $channel = 'candle' . $this->safe_string($timeframes, $timeframe);
            list($instType, $instId) = $this->get_public_inst_type_and_id($market);
            $args = array(
                'instType' => $instType,
                'channel' => $channel,
                'instId' => $instId,
            );
            $messageHash = 'ohlcv:' . $symbol . ':' . $timeframe;
            $ohlcv = Async\await($this->watch_public($messageHash, $messageHash, $args, $params));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function un_watch_ohlcv(string $symbol, $timeframe = '1m', $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $params) {
            /**
             * unsubscribe from the ohlcv $channel
             *
             * @see https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel
             *
             * @param {string} $symbol unified $symbol of the market to unwatch the ohlcv for
             * @param $timeframe
             * @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
             */
            Async\await($this->load_markets());
            $timeframes = $this->options['timeframesForWs'];
            $interval = $this->safe_string($timeframes, $timeframe);
            $channel = 'candle' . $interval;
            return Async\await($this->un_watch_channel($symbol, $channel, 'ohlcv:' . $interval, $params));
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         action => 'update',
        //         $arg => array( instType => 'sp', $channel => 'candle1D', instId => 'ETHUSDT' ),
        //         $data => array(
        //             array(
        //                 '1728316800000',
        //                 '2474.5',
        //                 '2478.21',
        //                 '2459.8',
        //                 '2463.51',
        //                 '86.0551'
        //             )
        //         ),
        //         ts => 1728317607657
        //     }
        //
        $arg = $this->safe_dict($message, 'arg', array());
        $market = $this->get_market_from_arg($arg);
        $marketId = $market['id'];
        $hash = 'ohlcv:';
        $data = $this->safe_list($message, 'data', array());
        $channel = $this->safe_string($arg, 'channel');
        $klineType = mb_substr($channel, 6);
        $timeframe = $this->find_timeframe($klineType);
        if (mb_strpos($marketId, '_DMCBL') !== false) {
            $market = $this->handle_dmcbl_market_by_message_hashes($market, $hash, $client, $timeframe);
        }
        $symbol = $market['symbol'];
        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);
            $this->ohlcvs[$symbol][$timeframe] = new ArrayCacheByTimestamp ($limit);
        }
        $stored = $this->ohlcvs[$symbol][$timeframe];
        for ($i = 0; $i < count($data); $i++) {
            $candle = $this->safe_list($data, $i, array());
            $parsed = $this->parse_ws_ohlcv($candle, $market);
            $stored->append ($parsed);
        }
        $messageHash = $hash . $symbol . ':' . $timeframe;
        $client->resolve ($stored, $messageHash);
    }

    public function parse_ws_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     array(
        //         '1728316800000',
        //         '2474.5',
        //         '2478.21',
        //         '2459.8',
        //         '2463.51',
        //         '86.0551'
        //     )
        //
        return array(
            $this->safe_integer($ohlcv, 0),
            $this->safe_number($ohlcv, 1),
            $this->safe_number($ohlcv, 2),
            $this->safe_number($ohlcv, 3),
            $this->safe_number($ohlcv, 4),
            $this->safe_number($ohlcv, 5),
        );
    }

    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://coincatch.github.io/github.io/en/spot/#depth-channel
             *
             * @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 un_watch_order_book(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unsubscribe from the orderbook $channel
             *
             * @see https://coincatch.github.io/github.io/en/spot/#depth-$channel
             *
             * @param {string} $symbol unified $symbol of the market to fetch the order book for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->limit] orderbook $limit, default is null
             * @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());
            $channel = 'books';
            $limit = $this->safe_integer($params, 'limit');
            if (($limit === 5) || ($limit === 15)) {
                $params = $this->omit($params, 'limit');
                $channel .= (string) $limit;
            }
            return Async\await($this->un_watch_channel($symbol, $channel, $channel, $params));
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://coincatch.github.io/github.io/en/spot/#depth-$channel
             *
             * @param $symbols
             * @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
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $channel = 'books';
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                list($instType, $instId) = $this->get_public_inst_type_and_id($market);
                $args = array(
                    'instType' => $instType,
                    'channel' => $channel,
                    'instId' => $instId,
                );
                $topics[] = $args;
                $messageHashes[] = $channel . ':' . $symbol;
            }
            $orderbook = Async\await($this->watch_public_multiple($messageHashes, $messageHashes, $topics, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         action => 'update',
        //         $arg => array( instType => 'sp', $channel => 'books', instId => 'ETHUSDT' ),
        //         $data => array(
        //             {
        //                 $asks => array( array( 2507.07, 0.4248 ) ),
        //                 $bids => array( array( 2507.05, 0.1198 ) ),
        //                 $checksum => -1400923312,
        //                 ts => '1728339446908'
        //             }
        //         ),
        //         ts => 1728339446908
        //     }
        //
        $arg = $this->safe_dict($message, 'arg', array());
        $market = $this->get_market_from_arg($arg);
        $marketId = $market['id'];
        $hash = 'books:';
        if (mb_strpos($marketId, '_DMCBL') !== false) {
            $market = $this->handle_dmcbl_market_by_message_hashes($market, $hash, $client);
        }
        $symbol = $market['symbol'];
        $channel = $this->safe_string($arg, 'channel');
        $messageHash = $hash . $symbol;
        $data = $this->safe_list($message, 'data', array());
        $rawOrderBook = $this->safe_dict($data, 0);
        $timestamp = $this->safe_integer($rawOrderBook, 'ts');
        $incrementalBook = $channel;
        if ($incrementalBook) {
            if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
                $ob = $this->counted_order_book(array());
                $ob['symbol'] = $symbol;
                $this->orderbooks[$symbol] = $ob;
            }
            $storedOrderBook = $this->orderbooks[$symbol];
            $asks = $this->safe_list($rawOrderBook, 'asks', array());
            $bids = $this->safe_list($rawOrderBook, 'bids', array());
            $this->handle_deltas($storedOrderBook['asks'], $asks);
            $this->handle_deltas($storedOrderBook['bids'], $bids);
            $storedOrderBook['timestamp'] = $timestamp;
            $storedOrderBook['datetime'] = $this->iso8601($timestamp);
            $checksum = $this->safe_bool($this->options, 'checksum', true);
            $isSnapshot = $this->safe_string($message, 'action') === 'snapshot';
            if (!$isSnapshot && $checksum) {
                $storedAsks = $storedOrderBook['asks'];
                $storedBids = $storedOrderBook['bids'];
                $asksLength = count($storedAsks);
                $bidsLength = count($storedBids);
                $payloadArray = array();
                for ($i = 0; $i < 25; $i++) {
                    if ($i < $bidsLength) {
                        $payloadArray[] = $storedBids[$i][2][0];
                        $payloadArray[] = $storedBids[$i][2][1];
                    }
                    if ($i < $asksLength) {
                        $payloadArray[] = $storedAsks[$i][2][0];
                        $payloadArray[] = $storedAsks[$i][2][1];
                    }
                }
                $payload = implode(':', $payloadArray);
                $calculatedChecksum = $this->crc32($payload, true);
                $responseChecksum = $this->safe_integer($rawOrderBook, 'checksum');
                if ($calculatedChecksum !== $responseChecksum) {
                    $this->spawn(array($this, 'handle_check_sum_error'), $client, $symbol, $messageHash);
                    return;
                }
            }
        } else {
            $orderbook = $this->order_book(array());
            $parsedOrderbook = $this->parse_order_book($rawOrderBook, $symbol, $timestamp);
            $orderbook->reset ($parsedOrderbook);
            $this->orderbooks[$symbol] = $orderbook;
        }
        $client->resolve ($this->orderbooks[$symbol], $messageHash);
    }

    public function handle_check_sum_error(Client $client, string $symbol, string $messageHash) {
        return Async\async(function () use ($client, $symbol, $messageHash) {
            Async\await($this->un_watch_order_book($symbol));
            $error = new ChecksumError ($this->id . ' ' . $this->orderbook_checksum_message($symbol));
            $client->reject ($error, $messageHash);
        }) ();
    }

    public function handle_delta($bookside, $delta) {
        $bidAsk = $this->parse_bid_ask($delta, 0, 1);
        $bidAsk[] = $delta;
        $bookside->storeArray ($bidAsk);
    }

    public function handle_deltas($bookside, $deltas) {
        for ($i = 0; $i < count($deltas); $i++) {
            $this->handle_delta($bookside, $deltas[$i]);
        }
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent trades for a particular $symbol
             *
             * @see https://coincatch.github.io/github.io/en/spot/#trades-channel
             *
             * @param {string} $symbol unified $symbol of the market to fetch trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of trades to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            return Async\await($this->watch_trades_for_symbols(array( $symbol ), $since, $limit, $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://coincatch.github.io/github.io/en/spot/#$trades-channel
             *
             * @param $symbols
             * @param {int} [$since] the earliest time in ms to fetch orders 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 list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            $symbolsLength = count($symbols);
            if ($symbolsLength === 0) {
                throw new ArgumentsRequired($this->id . ' watchTradesForSymbols() requires a non-empty array of symbols');
            }
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                list($instType, $instId) = $this->get_public_inst_type_and_id($market);
                $args = array(
                    'instType' => $instType,
                    'channel' => 'trade',
                    'instId' => $instId,
                );
                $topics[] = $args;
                $messageHashes[] = 'trade:' . $symbol;
            }
            $trades = Async\await($this->watch_public_multiple($messageHashes, $messageHashes, $topics, $params));
            if ($this->newUpdates) {
                $first = $this->safe_dict($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(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unsubscribe from the trades channel
             *
             * @see https://coincatch.github.io/github.io/en/spot/#trades-channel
             *
             * @param {string} $symbol unified $symbol of the market to unwatch the trades for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {any} status of the unwatch request
             */
            Async\await($this->load_markets());
            return Async\await($this->un_watch_channel($symbol, 'trade', 'trade', $params));
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         action => 'update',
        //         $arg => array( instType => 'sp', channel => 'trade', instId => 'ETHUSDT' ),
        //         $data => array( array( '1728341807469', '2421.41', '0.478', 'sell' ) ),
        //         ts => 1728341807482
        //     }
        //
        $arg = $this->safe_dict($message, 'arg', array());
        $market = $this->get_market_from_arg($arg);
        $marketId = $market['id'];
        $hash = 'trade:';
        if (mb_strpos($marketId, '_DMCBL') !== false) {
            $market = $this->handle_dmcbl_market_by_message_hashes($market, $hash, $client);
        }
        $symbol = $market['symbol'];
        if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->trades[$symbol] = new ArrayCache ($limit);
        }
        $stored = $this->trades[$symbol];
        $data = $this->safe_list($message, 'data', array());
        if ($data !== null) {
            $data = $this->sort_by($data, 0);
            for ($i = 0; $i < count($data); $i++) {
                $trade = $this->safe_list($data, $i);
                $parsed = $this->parse_ws_trade($trade, $market);
                $stored->append ($parsed);
            }
        }
        $messageHash = $hash . $symbol;
        $client->resolve ($stored, $messageHash);
    }

    public function parse_ws_trade($trade, $market = null): array {
        //
        //     array(
        //         '1728341807469',
        //         '2421.41',
        //         '0.478',
        //         'sell'
        //     )
        //
        $timestamp = $this->safe_integer($trade, 0);
        return $this->safe_trade(array(
            'id' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $market['symbol'],
            'side' => $this->safe_string_lower($trade, 3),
            'price' => $this->safe_string($trade, 1),
            'amount' => $this->safe_string($trade, 2),
            'cost' => null,
            'takerOrMaker' => null,
            'type' => null,
            'order' => null,
            'fee' => null,
            'info' => $trade,
        ), $market);
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watch balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://coincatch.github.io/github.io/en/spot/#account-$channel
             * @see https://coincatch.github.io/github.io/en/mix/#account-$channel
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {str} [$params->type] 'spot' or 'swap' (default is 'spot')
             * @param {string} [$params->instType] *swap only* 'umcbl' or 'dmcbl' (default is 'umcbl')
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            $type = null;
            list($type, $params) = $this->handle_market_type_and_params('watchBalance', null, $params);
            $instType = 'spbl'; // must be lower case for spot
            if ($type === 'swap') {
                $instType = 'umcbl';
            }
            $channel = 'account';
            list($instType, $params) = $this->handle_option_and_params($params, 'watchBalance', 'instType', $instType);
            $args = array(
                'instType' => $instType,
                'channel' => $channel,
                'instId' => 'default',
            );
            $messageHash = 'balance:' . strtolower($instType);
            return Async\await($this->watch_private($messageHash, $messageHash, $args, $params));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //  spot
        //     {
        //         action => 'snapshot',
        //         $arg => array( $instType => 'spbl', channel => 'account', instId => 'default' ),
        //         $data => array(
        //             {
        //                 coinId => '3',
        //                 coinName => 'ETH',
        //                 available => '0.0000832',
        //                 frozen => '0',
        //                 lock => '0'
        //             }
        //         ),
        //         ts => 1728464548725
        //     }
        //
        // //  swap
        //     {
        //         action => 'snapshot',
        //         $arg => array( $instType => 'dmcbl', channel => 'account', instId => 'default' ),
        //         $data => array(
        //             {
        //                 marginCoin => 'ETH',
        //                 locked => '0.00000000',
        //                 available => '0.00001203',
        //                 maxOpenPosAvailable => '0.00001203',
        //                 maxTransferOut => '0.00001203',
        //                 equity => '0.00001203',
        //                 usdtEquity => '0.029092328738',
        //                 coinDisplayName => 'ETH'
        //             }
        //         ),
        //         ts => 1728650777643
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $rawBalance = $data[$i];
            $currencyId = $this->safe_string_2($rawBalance, 'coinName', 'marginCoin');
            $code = $this->safe_currency_code($currencyId);
            $account = (is_array($this->balance) && array_key_exists($code, $this->balance)) ? $this->balance[$code] : $this->account();
            $freeQuery = (is_array($rawBalance) && array_key_exists('maxTransferOut', $rawBalance)) ? 'maxTransferOut' : 'available';
            $account['free'] = $this->safe_string($rawBalance, $freeQuery);
            $account['total'] = $this->safe_string($rawBalance, 'equity');
            $account['used'] = $this->safe_string($rawBalance, 'frozen');
            $this->balance[$code] = $account;
        }
        $this->balance = $this->safe_balance($this->balance);
        $arg = $this->safe_dict($message, 'arg');
        $instType = $this->safe_string_lower($arg, 'instType');
        $messageHash = 'balance:' . $instType;
        $client->resolve ($this->balance, $messageHash);
    }

    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://coincatch.github.io/github.io/en/spot/#order-$channel
             * @see https://coincatch.github.io/github.io/en/mix/#order-$channel
             * @see https://coincatch.github.io/github.io/en/mix/#plan-order-$channel
             *
             * @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
             * @param {string} [$params->type] 'spot' or 'swap'
             * @param {string} [$params->instType] *swap only* 'umcbl' or 'dmcbl' (default is 'umcbl')
             * @param {bool} [$params->trigger] *swap only* whether to watch trigger $orders (default is false)
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            $methodName = 'watchOrders';
            Async\await($this->load_markets());
            $market = null;
            $marketId = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $marketId = $market['id'];
            }
            $marketType = null;
            list($marketType, $params) = $this->handle_market_type_and_params($methodName, $market, $params);
            $instType = 'spbl';
            $instId = $marketId;
            if ($marketType === 'spot') {
                if ($symbol === null) {
                    throw new ArgumentsRequired($this->id . ' ' . $methodName . '() requires a $symbol argument for ' . $marketType . ' markets.');
                }
            } else {
                $instId = 'default';
                $instType = 'umcbl';
                if ($symbol === null) {
                    list($instType, $params) = $this->handle_option_and_params($params, $methodName, 'instType', $instType);
                } else {
                    if (mb_strpos($marketId, '_DMCBL') !== false) {
                        $instType = 'dmcbl';
                    }
                }
            }
            $channel = 'orders';
            $isTrigger = $this->safe_bool($params, 'trigger');
            if ($isTrigger) {
                $channel = 'ordersAlgo'; // $channel does not return any data
                $params = $this->omit($params, 'trigger');
            }
            $args = array(
                'instType' => $instType,
                'channel' => $channel,
                'instId' => $instId,
            );
            $messageHash = 'orders';
            if ($symbol !== null) {
                $messageHash .= ':' . $symbol;
            }
            $orders = Async\await($this->watch_private($messageHash, $messageHash, $args, $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        // spot
        //
        //     {
        //         action => 'snapshot',
        //         $arg => array( $instType => 'spbl', channel => 'orders', instId => 'ETHUSDT_SPBL' ),
        //         $data => array(
        //             {
        //                 instId => 'ETHUSDT_SPBL',
        //                 ordId => '1228627925964996608',
        //                 clOrdId => 'f0cccf74-c535-4523-a53d-dbe3b9958559',
        //                 px => '2000',
        //                 sz => '0.001',
        //                 notional => '2',
        //                 ordType => 'limit',
        //                 force => 'normal',
        //                 side => 'buy',
        //                 accFillSz => '0',
        //                 avgPx => '0',
        //                 status => 'new',
        //                 cTime => 1728653645030,
        //                 uTime => 1728653645030,
        //                 orderFee => array(),
        //                 eps => 'API'
        //             }
        //         ),
        //         ts => 1728653645046
        //     }
        //
        // swap
        //
        //     {
        //         action => 'snapshot',
        //         $arg => array( $instType => 'umcbl', channel => 'orders', instId => 'default' ),
        //         $data => array(
        //             {
        //                 accFillSz => '0',
        //                 cTime => 1728653796976,
        //                 clOrdId => '1228628563272753152',
        //                 eps => 'API',
        //                 force => 'normal',
        //                 hM => 'single_hold',
        //                 instId => 'ETHUSDT_UMCBL',
        //                 lever => '5',
        //                 low => false,
        //                 notionalUsd => '20',
        //                 ordId => '1228628563188867072',
        //                 ordType => 'limit',
        //                 orderFee => array(),
        //                 posSide => 'net',
        //                 px => '2000',
        //                 side => 'buy',
        //                 status => 'new',
        //                 sz => '0.01',
        //                 tS => 'buy_single',
        //                 tdMode => 'cross',
        //                 tgtCcy => 'USDT',
        //                 uTime => 1728653796976
        //             }
        //         ),
        //         ts => 1728653797002
        //     }
        //
        //
        $arg = $this->safe_dict($message, 'arg', array());
        $instType = $this->safe_string($arg, 'instType');
        $argInstId = $this->safe_string($arg, 'instId');
        $marketType = null;
        if ($instType === 'spbl') {
            $marketType = 'spot';
        } else {
            $marketType = 'swap';
        }
        $data = $this->safe_list($message, 'data', array());
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $hash = 'orders';
        $stored = $this->orders;
        $symbol = null;
        for ($i = 0; $i < count($data); $i++) {
            $order = $data[$i];
            $marketId = $this->safe_string($order, 'instId', $argInstId);
            $market = $this->safe_market($marketId, null, null, $marketType);
            $parsed = $this->parse_ws_order($order, $market);
            $stored->append ($parsed);
            $symbol = $parsed['symbol'];
            $messageHash = 'orders:' . $symbol;
            $client->resolve ($stored, $messageHash);
        }
        $client->resolve ($stored, $hash);
    }

    public function parse_ws_order(array $order, ?array $market = null): array {
        //
        // spot
        //     {
        //         instId => 'ETHUSDT_SPBL',
        //         ordId => '1228627925964996608',
        //         clOrdId => 'f0cccf74-c535-4523-a53d-dbe3b9958559',
        //         px => '2000',
        //         sz => '0.001',
        //         notional => '2',
        //         ordType => 'limit',
        //         force => 'normal',
        //         side => 'buy',
        //         accFillSz => '0',
        //         avgPx => '0',
        //         status => 'new',
        //         cTime => 1728653645030,
        //         uTime => 1728653645030,
        //         $orderFee => $orderFee => array( array( $fee => '0', feeCcy => 'USDT' ) ),
        //         eps => 'API'
        //     }
        //
        // swap
        //     {
        //         accFillSz => '0',
        //         cTime => 1728653796976,
        //         clOrdId => '1228628563272753152',
        //         eps => 'API',
        //         force => 'normal',
        //         hM => 'single_hold',
        //         instId => 'ETHUSDT_UMCBL',
        //         lever => '5',
        //         low => false,
        //         notionalUsd => '20',
        //         ordId => '1228628563188867072',
        //         ordType => 'limit',
        //         $orderFee => array( array( $fee => '0', feeCcy => 'USDT' ) ),
        //         posSide => 'net',
        //         px => '2000',
        //         side => 'buy',
        //         status => 'new',
        //         sz => '0.01',
        //         tS => 'buy_single',
        //         tdMode => 'cross',
        //         tgtCcy => 'USDT',
        //         uTime => 1728653796976
        //     }
        //
        $marketId = $this->safe_string($order, 'instId');
        $settleId = $this->safe_string($order, 'tgtCcy');
        $market = $this->safeMarketCustom ($marketId, $market, $settleId);
        $timestamp = $this->safe_integer($order, 'cTime');
        $symbol = $market['symbol'];
        $rawStatus = $this->safe_string($order, 'status');
        $orderFee = $this->safe_list($order, 'orderFee', array());
        $fee = $this->safe_dict($orderFee, 0);
        $feeCost = Precise::string_mul($this->safe_string($fee, 'fee'), '-1');
        $feeCurrency = $this->safe_string($fee, 'feeCcy');
        $price = $this->omit_zero($this->safe_string($order, 'px'));
        $priceAvg = $this->omit_zero($this->safe_string($order, 'avgPx'));
        if ($price === null) {
            $price = $priceAvg;
        }
        $type = $this->safe_string_lower($order, 'ordType');
        return $this->safe_order(array(
            'id' => $this->safe_string($order, 'ordId'),
            'clientOrderId' => $this->safe_string($order, 'clOrdId'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'lastUpdateTimestamp' => $this->safe_integer($order, 'uTime'),
            'status' => $this->parse_order_status($rawStatus),
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => $this->parseOrderTimeInForce ($this->safe_string_lower($order, 'force')),
            'side' => $this->safe_string_lower($order, 'side'),
            'price' => $price,
            'average' => $this->safe_string($order, 'avgPx'),
            'amount' => $this->safe_string($order, 'sz'),
            'filled' => $this->safe_string($order, 'accFillSz'),
            'remaining' => null,
            'triggerPrice' => null,
            'takeProfitPrice' => null,
            'stopLossPrice' => null,
            'cost' => $this->safe_string($order, 'notional'),
            'trades' => null,
            'fee' => array(
                'currency' => $feeCurrency,
                'cost' => $feeCost,
            ),
            'reduceOnly' => $this->safe_bool($order, 'low'),
            'postOnly' => null,
            'info' => $order,
        ), $market);
    }

    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://coincatch.github.io/github.io/en/mix/#positions-channel
             *
             * @param {string[]|null} $symbols list of unified $market $symbols
             * @param $since
             * @param $limit
             * @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, 'swap');
            $messageHashes = array();
            $hash = 'positions';
            $instTypes = array();
            if ($symbols !== null) {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $market = $this->market($symbol);
                    $instType = $this->get_private_inst_type($market);
                    if (!$this->in_array($instType, $instTypes)) {
                        $instTypes[] = $instType;
                    }
                    $messageHashes[] = $hash . '::' . $symbol;
                }
            } else {
                $instTypes = array( 'umcbl', 'dmcbl' );
                $messageHashes[] = $hash;
            }
            $args = array();
            $subscribeHashes = array();
            for ($i = 0; $i < count($instTypes); $i++) {
                $instType = $instTypes[$i];
                $arg = array(
                    'instType' => $instType,
                    'channel' => $hash,
                    'instId' => 'default',
                );
                $subscribeHashes[] = $hash . '::' . $instType;
                $args[] = $arg;
            }
            $newPositions = Async\await($this->watch_private_multiple($messageHashes, $subscribeHashes, $args, $params));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($newPositions, $symbols, $since, $limit, true);
        }) ();
    }

    public function get_private_inst_type(array $market) {
        $marketId = $market['id'];
        if (mb_strpos($marketId, '_DMCBL') !== false) {
            return 'dmcbl';
        }
        return 'umcbl';
    }

    public function handle_positions(Client $client, $message) {
        //
        //     {
        //         action => 'snapshot',
        //         arg => array( instType => 'umcbl', channel => 'positions', instId => 'default' ),
        //         data => array(
        //             {
        //                 posId => '1221355728745619456',
        //                 instId => 'ETHUSDT_UMCBL',
        //                 instName => 'ETHUSDT',
        //                 marginCoin => 'USDT',
        //                 margin => '5.27182',
        //                 marginMode => 'crossed',
        //                 holdSide => 'long',
        //                 holdMode => 'single_hold',
        //                 total => '0.01',
        //                 available => '0.01',
        //                 locked => '0',
        //                 averageOpenPrice => '2635.91',
        //                 leverage => 5,
        //                 achievedProfits => '0',
        //                 upl => '-0.0267',
        //                 uplRate => '-0.005064664576',
        //                 liqPx => '-3110.66866033',
        //                 keepMarginRate => '0.0033',
        //                 marginRate => '0.002460827254',
        //                 cTime => '1726919818102',
        //                 uTime => '1728919604312',
        //                 markPrice => '2633.24',
        //                 autoMargin => 'off'
        //             }
        //         ),
        //         ts => 1728919604329
        //     }
        //
        if ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions;
        $rawPositions = $this->safe_value($message, 'data', array());
        $dataLength = count($rawPositions);
        if ($dataLength === 0) {
            return;
        }
        $newPositions = array();
        $symbols = array();
        for ($i = 0; $i < count($rawPositions); $i++) {
            $rawPosition = $rawPositions[$i];
            $position = $this->parse_ws_position($rawPosition);
            $symbols[] = $position['symbol'];
            $newPositions[] = $position;
            $cache->append ($position);
        }
        $hash = 'positions';
        $messageHashes = $this->find_message_hashes($client, $hash);
        for ($i = 0; $i < count($messageHashes); $i++) {
            $messageHash = $messageHashes[$i];
            $parts = explode('::', $messageHash);
            $symbol = $parts[1];
            if ($this->in_array($symbol, $symbols)) {
                $positionsForSymbol = array();
                for ($j = 0; $j < count($newPositions); $j++) {
                    $position = $newPositions[$j];
                    if ($position['symbol'] === $symbol) {
                        $positionsForSymbol[] = $position;
                    }
                }
                $client->resolve ($positionsForSymbol, $messageHash);
            }
        }
        $client->resolve ($newPositions, $hash);
    }

    public function parse_ws_position($position, $market = null) {
        //
        //     {
        //         posId => '1221355728745619456',
        //         instId => 'ETHUSDT_UMCBL',
        //         instName => 'ETHUSDT',
        //         marginCoin => 'USDT',
        //         $margin => '5.27182',
        //         $marginMode => 'crossed',
        //         holdSide => 'long',
        //         $holdMode => 'single_hold',
        //         total => '0.01',
        //         available => '0.01',
        //         locked => '0',
        //         averageOpenPrice => '2635.91',
        //         leverage => 5,
        //         achievedProfits => '0',
        //         upl => '-0.0267',
        //         uplRate => '-0.005064664576',
        //         liqPx => '-3110.66866033',
        //         keepMarginRate => '0.0033',
        //         marginRate => '0.002460827254',
        //         cTime => '1726919818102',
        //         uTime => '1728919604312',
        //         markPrice => '2633.24',
        //         autoMargin => 'off'
        //     }
        //
        $marketId = $this->safe_string($position, 'symbol');
        $settleId = $this->safe_string($position, 'marginCoin');
        $market = $this->safeMarketCustom ($marketId, $market, $settleId);
        $timestamp = $this->safe_integer($position, 'cTime');
        $marginModeId = $this->safe_string($position, 'marginMode');
        $marginMode = $this->get_supported_mapping($marginModeId, array(
            'crossed' => 'cross',
            'isolated' => 'isolated',
        ));
        $isHedged = null;
        $holdMode = $this->safe_string($position, 'holdMode');
        if ($holdMode === 'double_hold') {
            $isHedged = true;
        } elseif ($holdMode === 'single_hold') {
            $isHedged = false;
        }
        $percentageDecimal = $this->safe_string($position, 'uplRate');
        $percentage = Precise::string_mul($percentageDecimal, '100');
        $margin = $this->safe_number($position, 'margin');
        return $this->safe_position(array(
            'symbol' => $market['symbol'],
            'id' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'contracts' => $this->safe_number($position, 'total'),
            'contractSize' => null,
            'side' => $this->safe_string_lower($position, 'holdSide'),
            'notional' => $margin, // todo check
            'leverage' => $this->safe_integer($position, 'leverage'),
            'unrealizedPnl' => $this->safe_number($position, 'upl'),
            'realizedPnl' => $this->safe_number($position, 'achievedProfits'),
            'collateral' => null, // todo check
            'entryPrice' => $this->safe_number($position, 'averageOpenPrice'),
            'markPrice' => $this->safe_number($position, 'markPrice'),
            'liquidationPrice' => $this->safe_number($position, 'liqPx'),
            'marginMode' => $marginMode,
            'hedged' => $isHedged,
            'maintenanceMargin' => null, // todo check
            'maintenanceMarginPercentage' => $this->safe_number($position, 'keepMarginRate'),
            'initialMargin' => $margin, // todo check
            'initialMarginPercentage' => null,
            'marginRatio' => $this->safe_number($position, 'marginRate'),
            'lastUpdateTimestamp' => $this->safe_integer($position, 'uTime'),
            'lastPrice' => null,
            'stopLossPrice' => null,
            'takeProfitPrice' => null,
            'percentage' => $percentage,
            'info' => $position,
        ));
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    array( $event => "error", $code => 30001, $msg => "Channel does not exist" )
        //
        $event = $this->safe_string($message, 'event');
        try {
            if ($event === 'error') {
                $code = $this->safe_string($message, 'code');
                $feedback = $this->id . ' ' . $this->json($message);
                $this->throw_exactly_matched_exception($this->exceptions['ws']['exact'], $code, $feedback);
                $msg = $this->safe_string($message, 'msg', '');
                $this->throw_broadly_matched_exception($this->exceptions['ws']['broad'], $msg, $feedback);
                throw new ExchangeError($feedback);
            }
            return false;
        } catch (Exception $e) {
            if ($e instanceof AuthenticationError) {
                $messageHash = 'authenticated';
                $client->reject ($e, $messageHash);
                if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                    unset($client->subscriptions[$messageHash]);
                }
            } else {
                $client->reject ($e);
            }
            return true;
        }
    }

    public function handle_message(Client $client, $message) {
        // todo handle with subscribe and unsubscribe
        if ($this->handle_error_message($client, $message)) {
            return;
        }
        $content = $this->safe_string($message, 'message');
        if ($content === 'pong') {
            $this->handle_pong($client, $message);
            return;
        }
        if ($message === 'pong') {
            $this->handle_pong($client, $message);
            return;
        }
        $event = $this->safe_string($message, 'event');
        if ($event === 'login') {
            $this->handle_authenticate($client, $message);
            return;
        }
        if ($event === 'subscribe') {
            $this->handle_subscription_status($client, $message);
            return;
        }
        if ($event === 'unsubscribe') {
            $this->handle_un_subscription_status($client, $message);
            return;
        }
        $data = $this->safe_dict($message, 'arg', array());
        $channel = $this->safe_string($data, 'channel');
        if ($channel === 'ticker') {
            $this->handle_ticker($client, $message);
        }
        if (mb_strpos($channel, 'candle') !== false) {
            $this->handle_ohlcv($client, $message);
        }
        if (mb_strpos($channel, 'books') !== false) {
            $this->handle_order_book($client, $message);
        }
        if ($channel === 'trade') {
            $this->handle_trades($client, $message);
        }
        if ($channel === 'account') {
            $this->handle_balance($client, $message);
        }
        if (($channel === 'orders') || ($channel === 'ordersAlgo')) {
            $this->handle_order($client, $message);
        }
        if ($channel === 'positions') {
            $this->handle_positions($client, $message);
        }
    }

    public function ping(Client $client) {
        return 'ping';
    }

    public function handle_pong(Client $client, $message) {
        $client->lastPong = $this->milliseconds();
        return $message;
    }

    public function handle_subscription_status(Client $client, $message) {
        return $message;
    }

    public function handle_un_subscription_status(Client $client, $message) {
        $argsList = $this->safe_list($message, 'args');
        if ($argsList === null) {
            $argsList = array( $this->safe_dict($message, 'arg', array()) );
        }
        for ($i = 0; $i < count($argsList); $i++) {
            $arg = $argsList[$i];
            $channel = $this->safe_string($arg, 'channel');
            if ($channel === 'books') {
                $this->handle_order_book_un_subscription($client, $message);
            } elseif ($channel === 'trade') {
                $this->handle_trades_un_subscription($client, $message);
            } elseif ($channel === 'ticker') {
                $this->handle_ticker_un_subscription($client, $message);
            } elseif (str_starts_with($channel, 'candle')) {
                $this->handle_ohlcv_un_subscription($client, $message);
            }
        }
        return $message;
    }

    public function handle_order_book_un_subscription(Client $client, $message) {
        $arg = $this->safe_dict($message, 'arg', array());
        $instType = $this->safe_string_lower($arg, 'instType');
        $type = ($instType === 'sp') ? 'spot' : 'swap';
        $instId = $this->safe_string($arg, 'instId');
        $market = $this->safe_market($instId, null, null, $type);
        $symbol = $market['symbol'];
        $messageHash = 'unsubscribe:orderbook:' . $market['symbol'];
        $subMessageHash = 'orderbook:' . $symbol;
        if (is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks)) {
            unset($this->orderbooks[$symbol]);
        }
        if (is_array($client->subscriptions) && array_key_exists($subMessageHash, $client->subscriptions)) {
            unset($client->subscriptions[$subMessageHash]);
        }
        if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
            unset($client->subscriptions[$messageHash]);
        }
        $error = new UnsubscribeError ($this->id . ' orderbook ' . $symbol);
        $client->reject ($error, $subMessageHash);
        $client->resolve (true, $messageHash);
    }

    public function handle_trades_un_subscription(Client $client, $message) {
        $arg = $this->safe_dict($message, 'arg', array());
        $instType = $this->safe_string_lower($arg, 'instType');
        $type = ($instType === 'sp') ? 'spot' : 'swap';
        $instId = $this->safe_string($arg, 'instId');
        $market = $this->safe_market($instId, null, null, $type);
        $symbol = $market['symbol'];
        $messageHash = 'unsubscribe:trade:' . $market['symbol'];
        $subMessageHash = 'trade:' . $symbol;
        if (is_array($this->trades) && array_key_exists($symbol, $this->trades)) {
            unset($this->trades[$symbol]);
        }
        if (is_array($client->subscriptions) && array_key_exists($subMessageHash, $client->subscriptions)) {
            unset($client->subscriptions[$subMessageHash]);
        }
        if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
            unset($client->subscriptions[$messageHash]);
        }
        $error = new UnsubscribeError ($this->id . ' trades ' . $symbol);
        $client->reject ($error, $subMessageHash);
        $client->resolve (true, $messageHash);
    }

    public function handle_ticker_un_subscription(Client $client, $message) {
        $arg = $this->safe_dict($message, 'arg', array());
        $instType = $this->safe_string_lower($arg, 'instType');
        $type = ($instType === 'sp') ? 'spot' : 'swap';
        $instId = $this->safe_string($arg, 'instId');
        $market = $this->safe_market($instId, null, null, $type);
        $symbol = $market['symbol'];
        $messageHash = 'unsubscribe:ticker:' . $market['symbol'];
        $subMessageHash = 'ticker:' . $symbol;
        if (is_array($this->tickers) && array_key_exists($symbol, $this->tickers)) {
            unset($this->tickers[$symbol]);
        }
        if (is_array($client->subscriptions) && array_key_exists($subMessageHash, $client->subscriptions)) {
            unset($client->subscriptions[$subMessageHash]);
        }
        if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
            unset($client->subscriptions[$messageHash]);
        }
        $error = new UnsubscribeError ($this->id . ' ticker ' . $symbol);
        $client->reject ($error, $subMessageHash);
        $client->resolve (true, $messageHash);
    }

    public function handle_ohlcv_un_subscription(Client $client, $message) {
        $arg = $this->safe_dict($message, 'arg', array());
        $instType = $this->safe_string_lower($arg, 'instType');
        $type = ($instType === 'sp') ? 'spot' : 'swap';
        $instId = $this->safe_string($arg, 'instId');
        $channel = $this->safe_string($arg, 'channel');
        $interval = str_replace('candle', '', $channel);
        $timeframes = $this->safe_dict($this->options, 'timeframesForWs');
        $timeframe = $this->find_timeframe($interval, $timeframes);
        $market = $this->safe_market($instId, null, null, $type);
        $symbol = $market['symbol'];
        $messageHash = 'unsubscribe:ohlcv:' . $timeframe . ':' . $market['symbol'];
        $subMessageHash = 'ohlcv:' . $symbol . ':' . $timeframe;
        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]);
            }
        }
        $this->clean_unsubscription($client, $subMessageHash, $messageHash);
    }
}
