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

class kraken extends \ccxt\async\kraken {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOrders' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchBidsAsks' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'createOrderWs' => true,
                'editOrderWs' => true,
                'cancelOrderWs' => true,
                'cancelOrdersWs' => true,
                'cancelAllOrdersWs' => true,
                // 'watchHeartbeat' => true,
                // 'watchStatus' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://ws.kraken.com',
                        'private' => 'wss://ws-auth.kraken.com',
                        'privateV2' => 'wss://ws-auth.kraken.com/v2',
                        'publicV2' => 'wss://ws.kraken.com/v2',
                        'beta' => 'wss://beta-ws.kraken.com',
                        'beta-private' => 'wss://beta-ws-auth.kraken.com',
                    ),
                ),
            ),
            // 'versions' => array(
            //     'ws' => '0.2.0',
            // ),
            'options' => array(
                'tradesLimit' => 1000,
                'OHLCVLimit' => 1000,
                'ordersLimit' => 1000,
                'symbolsByOrderId' => array(),
                'watchOrderBook' => array(
                    'checksum' => false,
                ),
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
                'keepAlive' => 6000,
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                        'Event(s) not found' => '\\ccxt\\BadRequest',
                    ),
                    'broad' => array(
                        'Already subscribed' => '\\ccxt\\BadRequest',
                        'Currency pair not in ISO 4217-A3 format' => '\\ccxt\\BadSymbol',
                        'Currency pair not supported' => '\\ccxt\\BadSymbol',
                        'Malformed request' => '\\ccxt\\BadRequest',
                        'Pair field must be an array' => '\\ccxt\\BadRequest',
                        'Pair field unsupported for this subscription type' => '\\ccxt\\BadRequest',
                        'Pair(s) not found' => '\\ccxt\\BadSymbol',
                        'Subscription book depth must be an integer' => '\\ccxt\\BadRequest',
                        'Subscription depth not supported' => '\\ccxt\\BadRequest',
                        'Subscription field must be an object' => '\\ccxt\\BadRequest',
                        'Subscription name invalid' => '\\ccxt\\BadRequest',
                        'Subscription object unsupported field' => '\\ccxt\\BadRequest',
                        'Subscription ohlc interval must be an integer' => '\\ccxt\\BadRequest',
                        'Subscription ohlc interval not supported' => '\\ccxt\\BadRequest',
                        'Subscription ohlc requires interval' => '\\ccxt\\BadRequest',
                        'EAccount:Invalid permissions' => '\\ccxt\\PermissionDenied',
                        'EAuth:Account temporary disabled' => '\\ccxt\\AccountSuspended',
                        'EAuth:Account unconfirmed' => '\\ccxt\\AuthenticationError',
                        'EAuth:Rate limit exceeded' => '\\ccxt\\RateLimitExceeded',
                        'EAuth:Too many requests' => '\\ccxt\\RateLimitExceeded',
                        'EDatabase => Internal error (to be deprecated)' => '\\ccxt\\ExchangeError',
                        'EGeneral:Internal error[:<code>]' => '\\ccxt\\ExchangeError',
                        'EGeneral:Invalid arguments' => '\\ccxt\\BadRequest',
                        'EOrder:Cannot open opposing position' => '\\ccxt\\InvalidOrder',
                        'EOrder:Cannot open position' => '\\ccxt\\InvalidOrder',
                        'EOrder:Insufficient funds (insufficient user funds)' => '\\ccxt\\InsufficientFunds',
                        'EOrder:Insufficient margin (exchange does not have sufficient funds to allow margin trading)' => '\\ccxt\\InsufficientFunds',
                        'EOrder:Invalid price' => '\\ccxt\\InvalidOrder',
                        'EOrder:Margin allowance exceeded' => '\\ccxt\\InvalidOrder',
                        'EOrder:Margin level too low' => '\\ccxt\\InvalidOrder',
                        'EOrder:Margin position size exceeded (client would exceed the maximum position size for this pair)' => '\\ccxt\\InvalidOrder',
                        'EOrder:Order minimum not met (volume too low)' => '\\ccxt\\InvalidOrder',
                        'EOrder:Orders limit exceeded' => '\\ccxt\\InvalidOrder',
                        'EOrder:Positions limit exceeded' => '\\ccxt\\InvalidOrder',
                        'EOrder:Rate limit exceeded' => '\\ccxt\\RateLimitExceeded',
                        'EOrder:Scheduled orders limit exceeded' => '\\ccxt\\InvalidOrder',
                        'EOrder:Unknown position' => '\\ccxt\\OrderNotFound',
                        'EOrder:Unknown order' => '\\ccxt\\OrderNotFound',
                        'EOrder:Invalid order' => '\\ccxt\\InvalidOrder',
                        'EService:Deadline elapsed' => '\\ccxt\\ExchangeNotAvailable',
                        'EService:Market in cancel_only mode' => '\\ccxt\\NotSupported',
                        'EService:Market in limit_only mode' => '\\ccxt\\NotSupported',
                        'EService:Market in post_only mode' => '\\ccxt\\NotSupported',
                        'EService:Unavailable' => '\\ccxt\\ExchangeNotAvailable',
                        'ETrade:Invalid request' => '\\ccxt\\BadRequest',
                        'ESession:Invalid session' => '\\ccxt\\AuthenticationError',
                    ),
                ),
            ),
        ));
    }

    public function order_request_ws(string $method, string $symbol, string $type, array $request, ?float $amount, ?float $price = null, $params = array ()) {
        $isLimitOrder = str_ends_with($type, 'limit'); // supporting limit, stop-loss-limit, take-profit-limit, etc
        if ($isLimitOrder) {
            if ($price === null) {
                throw new ArgumentsRequired($this->id . ' limit orders require a $price argument');
            }
            $request['params']['limit_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $price));
        }
        $isMarket = ($type === 'market');
        $postOnly = null;
        list($postOnly, $params) = $this->handle_post_only($isMarket, false, $params);
        if ($postOnly) {
            $request['params']['post_only'] = true;
        }
        $clientOrderId = $this->safe_string($params, 'clientOrderId');
        if ($clientOrderId !== null) {
            $request['params']['cl_ord_id'] = $clientOrderId;
        }
        $cost = $this->safe_string($params, 'cost');
        if ($cost !== null) {
            $request['params']['order_qty'] = $this->parse_to_numeric($this->cost_to_precision($symbol, $cost));
        }
        $stopLoss = $this->safe_dict($params, 'stopLoss', array());
        $takeProfit = $this->safe_dict($params, 'takeProfit', array());
        $presetStopLoss = $this->safe_string($stopLoss, 'triggerPrice');
        $presetTakeProfit = $this->safe_string($takeProfit, 'triggerPrice');
        $presetStopLossLimit = $this->safe_string($stopLoss, 'price');
        $presetTakeProfitLimit = $this->safe_string($takeProfit, 'price');
        $isPresetStopLoss = $presetStopLoss !== null;
        $isPresetTakeProfit = $presetTakeProfit !== null;
        $stopLossPrice = $this->safe_string($params, 'stopLossPrice');
        $takeProfitPrice = $this->safe_string($params, 'takeProfitPrice');
        $isStopLossPriceOrder = $stopLossPrice !== null;
        $isTakeProfitPriceOrder = $takeProfitPrice !== null;
        $trailingAmount = $this->safe_string($params, 'trailingAmount');
        $trailingPercent = $this->safe_string($params, 'trailingPercent');
        $trailingLimitAmount = $this->safe_string($params, 'trailingLimitAmount');
        $trailingLimitPercent = $this->safe_string($params, 'trailingLimitPercent');
        $isTrailingAmountOrder = $trailingAmount !== null;
        $isTrailingPercentOrder = $trailingPercent !== null;
        $isTrailingLimitAmountOrder = $trailingLimitAmount !== null;
        $isTrailingLimitPercentOrder = $trailingLimitPercent !== null;
        $offset = $this->safe_string($params, 'offset', ''); // can set this to - for minus
        $trailingAmountString = ($trailingAmount !== null) ? $offset . $this->number_to_string($trailingAmount) : null;
        $trailingPercentString = ($trailingPercent !== null) ? $offset . $this->number_to_string($trailingPercent) : null;
        $trailingLimitAmountString = ($trailingLimitAmount !== null) ? $offset . $this->number_to_string($trailingLimitAmount) : null;
        $trailingLimitPercentString = ($trailingLimitPercent !== null) ? $offset . $this->number_to_string($trailingLimitPercent) : null;
        $priceType = ($isTrailingPercentOrder || $isTrailingLimitPercentOrder) ? 'pct' : 'quote';
        if ($method === 'createOrderWs') {
            $reduceOnly = $this->safe_bool($params, 'reduceOnly');
            if ($reduceOnly) {
                $request['params']['reduce_only'] = true;
            }
            $timeInForce = $this->safe_string_lower($params, 'timeInForce');
            if ($timeInForce !== null) {
                $request['params']['time_in_force'] = $timeInForce;
            }
            $params = $this->omit($params, array( 'reduceOnly', 'timeInForce' ));
            if ($isStopLossPriceOrder || $isTakeProfitPriceOrder || $isTrailingAmountOrder || $isTrailingPercentOrder || $isTrailingLimitAmountOrder || $isTrailingLimitPercentOrder) {
                $request['params']['triggers'] = array();
            }
            if ($isPresetStopLoss || $isPresetTakeProfit) {
                $request['params']['conditional'] = array();
                if ($isPresetStopLoss) {
                    $request['params']['conditional']['order_type'] = 'stop-loss';
                    $request['params']['conditional']['trigger_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $presetStopLoss));
                } elseif ($isPresetTakeProfit) {
                    $request['params']['conditional']['order_type'] = 'take-profit';
                    $request['params']['conditional']['trigger_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $presetTakeProfit));
                }
                if ($presetStopLossLimit !== null) {
                    $request['params']['conditional']['order_type'] = 'stop-loss-limit';
                    $request['params']['conditional']['limit_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $presetStopLossLimit));
                } elseif ($presetTakeProfitLimit !== null) {
                    $request['params']['conditional']['order_type'] = 'take-profit-limit';
                    $request['params']['conditional']['limit_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $presetTakeProfitLimit));
                }
                $params = $this->omit($params, array( 'stopLoss', 'takeProfit' ));
            } elseif ($isStopLossPriceOrder || $isTakeProfitPriceOrder) {
                if ($isStopLossPriceOrder) {
                    $request['params']['triggers']['price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $stopLossPrice));
                    if ($isLimitOrder) {
                        $request['params']['order_type'] = 'stop-loss-limit';
                    } else {
                        $request['params']['order_type'] = 'stop-loss';
                    }
                } else {
                    $request['params']['triggers']['price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $takeProfitPrice));
                    if ($isLimitOrder) {
                        $request['params']['order_type'] = 'take-profit-limit';
                    } else {
                        $request['params']['order_type'] = 'take-profit';
                    }
                }
            } elseif ($isTrailingAmountOrder || $isTrailingPercentOrder || $isTrailingLimitAmountOrder || $isTrailingLimitPercentOrder) {
                $request['params']['triggers']['price_type'] = $priceType;
                if (!$isLimitOrder && ($isTrailingAmountOrder || $isTrailingPercentOrder)) {
                    $request['params']['order_type'] = 'trailing-stop';
                    if ($isTrailingAmountOrder) {
                        $request['params']['triggers']['price'] = $this->parse_to_numeric($trailingAmountString);
                    } else {
                        $request['params']['triggers']['price'] = $this->parse_to_numeric($trailingPercentString);
                    }
                } else {
                    // trailing limit orders are not conventionally supported because the static limit_price_type param is not available for trailing-stop-limit orders
                    $request['params']['limit_price_type'] = $priceType;
                    $request['params']['order_type'] = 'trailing-stop-limit';
                    if ($isTrailingLimitAmountOrder) {
                        $request['params']['triggers']['price'] = $this->parse_to_numeric($trailingLimitAmountString);
                    } else {
                        $request['params']['triggers']['price'] = $this->parse_to_numeric($trailingLimitPercentString);
                    }
                }
            }
        } elseif ($method === 'editOrderWs') {
            if ($isPresetStopLoss || $isPresetTakeProfit) {
                throw new NotSupported($this->id . ' editing the $stopLoss and $takeProfit on existing orders is currently not supported');
            }
            if ($isStopLossPriceOrder || $isTakeProfitPriceOrder) {
                if ($isStopLossPriceOrder) {
                    $request['params']['trigger_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $stopLossPrice));
                } else {
                    $request['params']['trigger_price'] = $this->parse_to_numeric($this->price_to_precision($symbol, $takeProfitPrice));
                }
            } elseif ($isTrailingAmountOrder || $isTrailingPercentOrder || $isTrailingLimitAmountOrder || $isTrailingLimitPercentOrder) {
                $request['params']['trigger_price_type'] = $priceType;
                if (!$isLimitOrder && ($isTrailingAmountOrder || $isTrailingPercentOrder)) {
                    if ($isTrailingAmountOrder) {
                        $request['params']['trigger_price'] = $this->parse_to_numeric($trailingAmountString);
                    } else {
                        $request['params']['trigger_price'] = $this->parse_to_numeric($trailingPercentString);
                    }
                } else {
                    $request['params']['limit_price_type'] = $priceType;
                    if ($isTrailingLimitAmountOrder) {
                        $request['params']['trigger_price'] = $this->parse_to_numeric($trailingLimitAmountString);
                    } else {
                        $request['params']['trigger_price'] = $this->parse_to_numeric($trailingLimitPercentString);
                    }
                }
            }
        }
        $params = $this->omit($params, array( 'clientOrderId', 'cost', 'offset', 'stopLossPrice', 'takeProfitPrice', 'trailingAmount', 'trailingPercent', 'trailingLimitAmount', 'trailingLimitPercent' ));
        return array( $request, $params );
    }

    public function create_order_ws(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             * create a trade order
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/add_order
             *
             * @param {string} $symbol unified $symbol of the $market to create an order in
             * @param {string} $type 'market' or 'limit'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of currency you want to trade in units of base currency
             * @param {float} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in $market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $market = $this->market($symbol);
            $url = $this->urls['api']['ws']['privateV2'];
            $requestId = $this->request_id();
            $messageHash = $this->number_to_string($requestId);
            $request = array(
                'method' => 'add_order',
                'params' => array(
                    'order_type' => $type,
                    'side' => $side,
                    'order_qty' => $this->parse_to_numeric($this->amount_to_precision($symbol, $amount)),
                    'symbol' => $market['symbol'],
                    'token' => $token,
                ),
                'req_id' => $requestId,
            );
            list($request, $params) = $this->order_request_ws('createOrderWs', $symbol, $type, $request, $amount, $price, $params);
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function handle_create_edit_order($client, $message) {
        //
        //  createOrder
        //     {
        //         "method" => "add_order",
        //         "req_id" => 1,
        //         "result" => array(
        //             "order_id" => "OXM2QD-EALR2-YBAVEU"
        //         ),
        //         "success" => true,
        //         "time_in" => "2025-05-13T10:12:13.876173Z",
        //         "time_out" => "2025-05-13T10:12:13.890137Z"
        //     }
        //
        //  editOrder
        //     {
        //         "method" => "amend_order",
        //         "req_id" => 1,
        //         "result" => array(
        //             "amend_id" => "TYDLSQ-OYNYU-3MNRER",
        //             "order_id" => "OGL7HR-SWFO4-NRQTHO"
        //         ),
        //         "success" => true,
        //         "time_in" => "2025-05-14T13:54:10.840342Z",
        //         "time_out" => "2025-05-14T13:54:10.855046Z"
        //     }
        //
        $result = $this->safe_dict($message, 'result', array());
        $order = $this->parse_order($result);
        $messageHash = $this->safe_string_2($message, 'reqid', 'req_id');
        $client->resolve ($order, $messageHash);
    }

    public function edit_order_ws(string $id, string $symbol, string $type, string $side, ?float $amount = null, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $type, $side, $amount, $price, $params) {
            /**
             * edit a trade order
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/amend_order
             *
             * @param {string} $id order $id
             * @param {string} $symbol unified $symbol of the market to create an order in
             * @param {string} $type 'market' or 'limit'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of the currency you want to trade in units of the base currency
             * @param {float} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['privateV2'];
            $requestId = $this->request_id();
            $messageHash = $this->number_to_string($requestId);
            $request = array(
                'method' => 'amend_order',
                'params' => array(
                    'order_id' => $id,
                    'order_qty' => $this->parse_to_numeric($this->amount_to_precision($symbol, $amount)),
                    'token' => $token,
                ),
                'req_id' => $requestId,
            );
            list($request, $params) = $this->order_request_ws('editOrderWs', $symbol, $type, $request, $amount, $price, $params);
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function cancel_orders_ws(array $ids, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($ids, $symbol, $params) {
            /**
             * cancel multiple orders
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/cancel_order
             *
             * @param {string[]} $ids order $ids
             * @param {string} [$symbol] unified market $symbol, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            if ($symbol !== null) {
                throw new NotSupported($this->id . ' cancelOrdersWs () does not support cancelling orders for a specific $symbol->');
            }
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['privateV2'];
            $requestId = $this->request_id();
            $messageHash = $this->number_to_string($requestId);
            $request = array(
                'method' => 'cancel_order',
                'params' => array(
                    'order_id' => $ids,
                    'token' => $token,
                ),
                'req_id' => $requestId,
            );
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function cancel_order_ws(string $id, ?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * cancels an open order
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/cancel_order
             *
             * @param {string} $id order $id
             * @param {string} [$symbol] unified $symbol of the market the order was made in
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            if ($symbol !== null) {
                throw new NotSupported($this->id . ' cancelOrderWs () does not support cancelling orders for a specific $symbol->');
            }
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['privateV2'];
            $requestId = $this->request_id();
            $messageHash = $this->number_to_string($requestId);
            $request = array(
                'method' => 'cancel_order',
                'params' => array(
                    'order_id' => array( $id ),
                    'token' => $token,
                ),
                'req_id' => $requestId,
            );
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function handle_cancel_order($client, $message) {
        //
        //     {
        //         "method" => "cancel_order",
        //         "req_id" => 123456789,
        //         "result" => array(
        //             "order_id" => "OKAGJC-YHIWK-WIOZWG"
        //         ),
        //         "success" => true,
        //         "time_in" => "2023-09-21T14:36:57.428972Z",
        //         "time_out" => "2023-09-21T14:36:57.437952Z"
        //     }
        //
        $reqId = $this->safe_string($message, 'req_id');
        $client->resolve ($message, $reqId);
    }

    public function cancel_all_orders_ws(?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * cancel all open orders
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/cancel_all
             *
             * @param {string} [$symbol] unified market $symbol, only orders in the market of this $symbol are cancelled when $symbol is not null
             * @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~
             */
            if ($symbol !== null) {
                throw new NotSupported($this->id . ' cancelAllOrdersWs () does not support cancelling orders in a specific market.');
            }
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['privateV2'];
            $requestId = $this->request_id();
            $messageHash = $this->number_to_string($requestId);
            $request = array(
                'method' => 'cancel_all',
                'params' => array(
                    'token' => $token,
                ),
                'req_id' => $requestId,
            );
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function handle_cancel_all_orders($client, $message) {
        //
        //     {
        //         "method" => "cancel_all",
        //         "req_id" => 123456789,
        //         "result" => array(
        //             "count" => 1
        //         ),
        //         "success" => true,
        //         "time_in" => "2023-09-21T14:36:57.428972Z",
        //         "time_out" => "2023-09-21T14:36:57.437952Z"
        //     }
        //
        $reqId = $this->safe_string($message, 'req_id');
        $client->resolve ($message, $reqId);
    }

    public function handle_ticker($client, $message) {
        //
        //     {
        //         "channel" => "ticker",
        //         "type" => "snapshot",
        //         "data" => array(
        //             {
        //                 "symbol" => "BTC/USD",
        //                 "bid" => 108359.8,
        //                 "bid_qty" => 0.01362603,
        //                 "ask" => 108359.9,
        //                 "ask_qty" => 17.17988863,
        //                 "last" => 108359.8,
        //                 "volume" => 2158.32346723,
        //                 "vwap" => 108894.5,
        //                 "low" => 106824,
        //                 "high" => 111300,
        //                 "change" => -2679.9,
        //                 "change_pct" => -2.41
        //             }
        //         )
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        $ticker = $data[0];
        $symbol = $this->safe_string($ticker, 'symbol');
        $messageHash = $this->get_message_hash('ticker', null, $symbol);
        $vwap = $this->safe_string($ticker, 'vwap');
        $quoteVolume = null;
        $baseVolume = $this->safe_string($ticker, 'volume');
        if ($baseVolume !== null && $vwap !== null) {
            $quoteVolume = Precise::string_mul($baseVolume, $vwap);
        }
        $last = $this->safe_string($ticker, 'last');
        $result = $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => null,
            'datetime' => null,
            'high' => $this->safe_string($ticker, 'high'),
            'low' => $this->safe_string($ticker, 'low'),
            'bid' => $this->safe_string($ticker, 'bid'),
            'bidVolume' => $this->safe_string($ticker, 'bid_qty'),
            'ask' => $this->safe_string($ticker, 'ask'),
            'askVolume' => $this->safe_string($ticker, 'ask_qty'),
            'vwap' => $vwap,
            'open' => null,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => $this->safe_string($ticker, 'change'),
            'percentage' => $this->safe_string($ticker, 'change_pct'),
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'info' => $ticker,
        ));
        $this->tickers[$symbol] = $result;
        $client->resolve ($result, $messageHash);
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         "channel" => "trade",
        //         "type" => "update",
        //         "data" => array(
        //             {
        //                 "symbol" => "MATIC/USD",
        //                 "side" => "sell",
        //                 "price" => 0.5117,
        //                 "qty" => 40.0,
        //                 "ord_type" => "market",
        //                 "trade_id" => 4665906,
        //                 "timestamp" => "2023-09-25T07:49:37.708706Z"
        //             }
        //         )
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        $trade = $data[0];
        $symbol = $this->safe_string($trade, 'symbol');
        $messageHash = $this->get_message_hash('trade', null, $symbol);
        $stored = $this->safe_value($this->trades, $symbol);
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $market = $this->market($symbol);
        $parsed = $this->parse_trades($data, $market);
        for ($i = 0; $i < count($parsed); $i++) {
            $stored->append ($parsed[$i]);
        }
        $client->resolve ($stored, $messageHash);
    }

    public function handle_ohlcv(Client $client, $message, $subscription) {
        //
        //     array(
        //         216, // channelID
        //         array(
        //             "1574454214.962096", // Time, seconds since epoch
        //             "1574454240.000000", // End $timestamp of the $interval
        //             "0.020970", // Open price at midnight UTC
        //             "0.020970", // Intraday high price
        //             "0.020970", // Intraday low price
        //             "0.020970", // Closing price at midnight UTC
        //             "0.020970", // Volume weighted average price
        //             "0.08636138", // Accumulated volume today
        //             1, // Number of trades today
        //         ),
        //         "ohlc-1", // Channel Name of $subscription
        //         "ETH/XBT", // Asset pair
        //     )
        //
        $info = $this->safe_value($subscription, 'subscription', array());
        $interval = $this->safe_integer($info, 'interval');
        $name = $this->safe_string($info, 'name');
        $wsName = $this->safe_string($message, 3);
        $market = $this->safe_value($this->options['marketsByWsName'], $wsName);
        $symbol = $market['symbol'];
        $timeframe = $this->find_timeframe($interval);
        $duration = $this->parse_timeframe($timeframe);
        if ($timeframe !== null) {
            $candle = $this->safe_value($message, 1);
            $messageHash = $name . ':' . $timeframe . ':' . $wsName;
            $timestamp = $this->safe_float($candle, 1);
            $timestamp -= $duration;
            $ts = $this->parse_to_int($timestamp * 1000);
            $result = array(
                $ts,
                $this->safe_float($candle, 2),
                $this->safe_float($candle, 3),
                $this->safe_float($candle, 4),
                $this->safe_float($candle, 5),
                $this->safe_float($candle, 7),
            );
            $this->ohlcvs[$symbol] = $this->safe_value($this->ohlcvs, $symbol, array());
            $stored = $this->safe_value($this->ohlcvs[$symbol], $timeframe);
            if ($stored === null) {
                $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
                $stored = new ArrayCacheByTimestamp ($limit);
                $this->ohlcvs[$symbol][$timeframe] = $stored;
            }
            $stored->append ($result);
            $client->resolve ($stored, $messageHash);
        }
    }

    public function request_id() {
        // their support said that $reqid must be an int32, not documented
        $reqid = $this->sum($this->safe_integer($this->options, 'reqid', 0), 1);
        $this->options['reqid'] = $reqid;
        return $reqid;
    }

    public function watch_public($name, $symbol, $params = array ()) {
        return Async\async(function () use ($name, $symbol, $params) {
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $wsName = $this->safe_value($market['info'], 'wsname');
            $messageHash = $name . ':' . $wsName;
            $url = $this->urls['api']['ws']['public'];
            $requestId = $this->request_id();
            $subscribe = array(
                'event' => 'subscribe',
                'reqid' => $requestId,
                'pair' => array(
                    $wsName,
                ),
                'subscription' => array(
                    'name' => $name,
                ),
            );
            $request = $this->deep_extend($subscribe, $params);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    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.kraken.com/api/docs/websocket-v2/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());
            $symbol = $this->symbol($symbol);
            $tickers = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $tickers[$symbol];
        }) ();
    }

    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 a specific market
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/ticker
             *
             * @param {string[]} $symbols
             * @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);
            $ticker = Async\await($this->watch_multi_helper('ticker', 'ticker', $symbols, null, $params));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    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.kraken.com/api/docs/websocket-v2/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);
            $params['event_trigger'] = 'bbo';
            $ticker = Async\await($this->watch_multi_helper('bidask', 'ticker', $symbols, null, $params));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->bidsasks, 'symbol', $symbols);
        }) ();
    }

    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://docs.kraken.com/api/docs/websocket-v2/trade
             *
             * @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) {
            /**
             * get the list of most recent $trades for a list of $symbols
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/trade
             *
             * @param {string[]} $symbols unified symbol of the market to fetch $trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of $trades to fetch
             * @param {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~
             */
            $trades = Async\await($this->watch_multi_helper('trade', 'trade', $symbols, null, $params));
            if ($this->newUpdates) {
                $first = $this->safe_list($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 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.kraken.com/api/docs/websocket-v2/book
             *
             * @param {string} $symbol unified $symbol of the market to fetch the order book for
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market symbols
             */
            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) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.kraken.com/api/docs/websocket-v2/book
             *
             * @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
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market $symbols
             */
            $requiredParams = array();
            if ($limit !== null) {
                if ($this->in_array($limit, array( 10, 25, 100, 500, 1000 ))) {
                    $requiredParams['depth'] = $limit; // default 10, valid options 10, 25, 100, 500, 1000
                } else {
                    throw new NotSupported($this->id . ' watchOrderBook accepts $limit values of 10, 25, 100, 500 and 1000 only');
                }
            }
            $orderbook = Async\await($this->watch_multi_helper('orderbook', 'book', $symbols, array( 'limit' => $limit ), $this->extend($requiredParams, $params)));
            return $orderbook->limit ();
        }) ();
    }

    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://docs.kraken.com/api/docs/websocket-v1/ohlc
             *
             * @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
             */
            Async\await($this->load_markets());
            $name = 'ohlc';
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $wsName = $this->safe_value($market['info'], 'wsname');
            $messageHash = $name . ':' . $timeframe . ':' . $wsName;
            $url = $this->urls['api']['ws']['public'];
            $requestId = $this->request_id();
            $subscribe = array(
                'event' => 'subscribe',
                'reqid' => $requestId,
                'pair' => array(
                    $wsName,
                ),
                'subscription' => array(
                    'name' => $name,
                    'interval' => $this->safe_value($this->timeframes, $timeframe, $timeframe),
                ),
            );
            $request = $this->deep_extend($subscribe, $params);
            $ohlcv = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function load_markets($reload = false, $params = array ()) {
        return Async\async(function () use ($reload, $params) {
            $markets = Async\await(parent::load_markets($reload, $params));
            $marketsByWsName = $this->safe_value($this->options, 'marketsByWsName');
            if (($marketsByWsName === null) || $reload) {
                $marketsByWsName = array();
                for ($i = 0; $i < count($this->symbols); $i++) {
                    $symbol = $this->symbols[$i];
                    $market = $this->markets[$symbol];
                    $info = $this->safe_value($market, 'info', array());
                    $wsName = $this->safe_string($info, 'wsname');
                    $marketsByWsName[$wsName] = $market;
                }
                $this->options['marketsByWsName'] = $marketsByWsName;
            }
            return $markets;
        }) ();
    }

    public function ping(Client $client) {
        $url = $client->url;
        $request = array();
        if (mb_strpos($url, 'v2') !== false) {
            $request['method'] = 'ping';
        } else {
            $request['event'] = 'ping';
        }
        return $request;
    }

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

    public function watch_heartbeat($params = array ()) {
        return Async\async(function () use ($params) {
            Async\await($this->load_markets());
            $event = 'heartbeat';
            $url = $this->urls['api']['ws']['public'];
            return Async\await($this->watch($url, $event));
        }) ();
    }

    public function handle_heartbeat(Client $client, $message) {
        //
        // every second (approx) if no other updates are sent
        //
        //     array( "event" => "heartbeat" )
        //
        $event = $this->safe_string($message, 'event');
        $client->resolve ($message, $event);
    }

    public function handle_order_book(Client $client, $message) {
        //
        // $first $message (snapshot)
        //
        //     {
        //         "channel" => "book",
        //         "type" => "snapshot",
        //         "data" => array(
        //             {
        //                 "symbol" => "MATIC/USD",
        //                 "bids" => array(
        //                     array(
        //                         "price" => 0.5666,
        //                         "qty" => 4831.75496356
        //                     ),
        //                     {
        //                         "price" => 0.5665,
        //                         "qty" => 6658.22734739
        //                     }
        //                 ),
        //                 "asks" => array(
        //                     array(
        //                         "price" => 0.5668,
        //                         "qty" => 4410.79769741
        //                     ),
        //                     {
        //                         "price" => 0.5669,
        //                         "qty" => 4655.40412487
        //                     }
        //                 ),
        //                 "checksum" => 2439117997
        //             }
        //         )
        //     }
        //
        // subsequent updates
        //
        //     {
        //         "channel" => "book",
        //         "type" => "update",
        //         "data" => array(
        //             {
        //                 "symbol" => "MATIC/USD",
        //                 "bids" => array(
        //                     {
        //                         "price" => 0.5657,
        //                         "qty" => 1098.3947558
        //                     }
        //                 ),
        //                 "asks" => array(),
        //                 "checksum" => 2114181697,
        //                 "timestamp" => "2023-10-06T17:35:55.440295Z"
        //             }
        //         )
        //     }
        //
        $type = $this->safe_string($message, 'type');
        $data = $this->safe_list($message, 'data', array());
        $first = $this->safe_dict($data, 0, array());
        $symbol = $this->safe_string($first, 'symbol');
        $a = $this->safe_value($first, 'asks', array());
        $b = $this->safe_value($first, 'bids', array());
        $c = $this->safe_integer($first, 'checksum');
        $messageHash = $this->get_message_hash('orderbook', null, $symbol);
        $orderbook = null;
        if ($type === 'update') {
            $orderbook = $this->orderbooks[$symbol];
            $storedAsks = $orderbook['asks'];
            $storedBids = $orderbook['bids'];
            if ($a !== null) {
                $this->custom_handle_deltas($storedAsks, $a);
            }
            if ($b !== null) {
                $this->custom_handle_deltas($storedBids, $b);
            }
            $datetime = $this->safe_string($first, 'timestamp');
            $orderbook['symbol'] = $symbol;
            $orderbook['timestamp'] = $this->parse8601($datetime);
            $orderbook['datetime'] = $datetime;
        } else {
            // snapshot
            $depth = count($a);
            $this->orderbooks[$symbol] = $this->order_book(array(), $depth);
            $orderbook = $this->orderbooks[$symbol];
            $keys = array( 'asks', 'bids' );
            for ($i = 0; $i < count($keys); $i++) {
                $key = $keys[$i];
                $bookside = $orderbook[$key];
                $deltas = $this->safe_value($first, $key, array());
                if (strlen($deltas) > 0) {
                    $this->custom_handle_deltas($bookside, $deltas);
                }
            }
            $orderbook['symbol'] = $symbol;
        }
        $orderbook->limit ();
        // $checksum temporarily disabled because the exchange $checksum was not reliable
        $checksum = $this->handle_option('watchOrderBook', 'checksum', false);
        if ($checksum) {
            $payloadArray = array();
            if ($c !== null) {
                $checkAsks = $orderbook['asks'];
                $checkBids = $orderbook['bids'];
                // $checkAsks = asks.map ((elem) => [ elem['price'], elem['qty'] ]);
                // $checkBids = bids.map ((elem) => [ elem['price'], elem['qty'] ]);
                for ($i = 0; $i < 10; $i++) {
                    $currentAsk = $this->safe_value($checkAsks, $i, array());
                    $formattedAsk = $this->format_number($currentAsk[0]) . $this->format_number($currentAsk[1]);
                    $payloadArray[] = $formattedAsk;
                }
                for ($i = 0; $i < 10; $i++) {
                    $currentBid = $this->safe_value($checkBids, $i, array());
                    $formattedBid = $this->format_number($currentBid[0]) . $this->format_number($currentBid[1]);
                    $payloadArray[] = $formattedBid;
                }
            }
            $payload = implode('', $payloadArray);
            $localChecksum = $this->crc32($payload, false);
            if ($localChecksum !== $c) {
                $error = new ChecksumError ($this->id . ' ' . $this->orderbook_checksum_message($symbol));
                unset($client->subscriptions[$messageHash]);
                unset($this->orderbooks[$symbol]);
                $client->reject ($error, $messageHash);
                return;
            }
        }
        $client->resolve ($orderbook, $messageHash);
    }

    public function custom_handle_deltas($bookside, $deltas) {
        // $sortOrder = (key === 'bids') ? true : false;
        for ($j = 0; $j < count($deltas); $j++) {
            $delta = $deltas[$j];
            $price = $this->safe_number($delta, 'price');
            $amount = $this->safe_number($delta, 'qty');
            $bookside->store ($price, $amount);
            // if ($amount === 0) {
            //     $index = $bookside->findIndex ((x => Int) => x[0] === $price);
            //     $bookside->splice ($index, 1);
            // } else {
            //     $bookside->store ($price, $amount);
            // }
            // $bookside = $this->sort_by($bookside, 0, $sortOrder);
            // mb_substr($bookside, 0, 9 - 0);
        }
    }

    public function format_number($data) {
        $parts = explode('.', $data);
        $integer = $this->safe_string($parts, 0);
        $decimals = $this->safe_string($parts, 1, '');
        $joinedResult = $integer . $decimals;
        $i = 0;
        while ($joinedResult[$i] === '0') {
            $i += 1;
        }
        if ($i > 0) {
            $joinedResult = mb_substr($joinedResult, $i);
        }
        return $joinedResult;
    }

    public function handle_system_status(Client $client, $message) {
        //
        // todo => answer the question whether handleSystemStatus should be renamed
        // and unified for any usage pattern that
        // involves system status and maintenance updates
        //
        //     {
        //         "connectionID" => 15527282728335292000,
        //         "event" => "systemStatus",
        //         "status" => "online", // online|maintenance|(custom status tbd)
        //         "version" => "0.2.0"
        //     }
        //
        // v2
        //     {
        //         channel => 'status',
        //         type => 'update',
        //         data => array(
        //             {
        //                 version => '2.0.10',
        //                 system => 'online',
        //                 api_version => 'v2',
        //                 connection_id => 6447481662169813000
        //             }
        //         )
        //     }
        //
        return $message;
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $url = $this->urls['api']['ws']['private'];
            $client = $this->client($url);
            $authenticated = 'authenticated';
            $subscription = $this->safe_value($client->subscriptions, $authenticated);
            $now = $this->seconds();
            $start = $this->safe_integer($subscription, 'start');
            $expires = $this->safe_integer($subscription, 'expires');
            if (($subscription === null) || (($subscription !== null) && ($start . $expires) <= $now)) {
                // https://docs.kraken.com/api/docs/rest-api/get-websockets-token
                $response = Async\await($this->privatePostGetWebSocketsToken ($params));
                //
                //     {
                //         "error":array(),
                //         "result":{
                //             "token":"xeAQ\/RCChBYNVh53sTv1yZ5H4wIbwDF20PiHtTF+4UI",
                //             "expires":900
                //         }
                //     }
                //
                $subscription = $this->safe_dict($response, 'result');
                $subscription['start'] = $now;
                $client->subscriptions[$authenticated] = $subscription;
            }
            return $this->safe_string($subscription, 'token');
        }) ();
    }

    public function watch_private($name, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($name, $symbol, $since, $limit, $params) {
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $subscriptionHash = $name;
            $messageHash = $name;
            if ($symbol !== null) {
                $symbol = $this->symbol($symbol);
                $messageHash .= ':' . $symbol;
            }
            $url = $this->urls['api']['ws']['private'];
            $requestId = $this->request_id();
            $subscribe = array(
                'event' => 'subscribe',
                'reqid' => $requestId,
                'subscription' => array(
                    'name' => $name,
                    'token' => $token,
                ),
            );
            $request = $this->deep_extend($subscribe, $params);
            $result = Async\await($this->watch($url, $messageHash, $request, $subscriptionHash));
            if ($this->newUpdates) {
                $limit = $result->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($result, $symbol, $since, $limit);
        }) ();
    }

    public function watch_my_trades(?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 trades made by the user
             *
             * @see https://docs.kraken.com/api/docs/websocket-v1/owntrades
             *
             * @param {string} $symbol unified market $symbol of the market trades were made in
             * @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 list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            return Async\await($this->watch_private('ownTrades', $symbol, $since, $limit, $params));
        }) ();
    }

    public function handle_my_trades(Client $client, $message, $subscription = null) {
        //
        //     array(
        //         array(
        //             {
        //                 "TT5UC3-GOIRW-6AZZ6R" => array(
        //                     "cost" => "1493.90107",
        //                     "fee" => "3.88415",
        //                     "margin" => "0.00000",
        //                     "ordertxid" => "OTLAS3-RRHUF-NDWH5A",
        //                     "ordertype" => "market",
        //                     "pair" => "XBT/USDT",
        //                     "postxid" => "TKH2SE-M7IF5-CFI7LT",
        //                     "price" => "6851.50005",
        //                     "time" => "1586822919.335498",
        //                     "type" => "sell",
        //                     "vol" => "0.21804000"
        //                 }
        //             ),
        //             {
        //                 "TIY6G4-LKLAI-Y3GD4A" => array(
        //                     "cost" => "22.17134",
        //                     "fee" => "0.05765",
        //                     "margin" => "0.00000",
        //                     "ordertxid" => "ODQXS7-MOLK6-ICXKAA",
        //                     "ordertype" => "market",
        //                     "pair" => "ETH/USD",
        //                     "postxid" => "TKH2SE-M7IF5-CFI7LT",
        //                     "price" => "169.97999",
        //                     "time" => "1586340530.895739",
        //                     "type" => "buy",
        //                     "vol" => "0.13043500"
        //                 }
        //             ),
        //         ),
        //         "ownTrades",
        //         array( sequence => 1 )
        //     )
        //
        $allTrades = $this->safe_value($message, 0, array());
        $allTradesLength = count($allTrades);
        if ($allTradesLength > 0) {
            if ($this->myTrades === null) {
                $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
                $this->myTrades = new ArrayCache ($limit);
            }
            $stored = $this->myTrades;
            $symbols = array();
            for ($i = 0; $i < count($allTrades); $i++) {
                $trades = $this->safe_value($allTrades, $i, array());
                $ids = is_array($trades) ? array_keys($trades) : array();
                for ($j = 0; $j < count($ids); $j++) {
                    $id = $ids[$j];
                    $trade = $trades[$id];
                    $parsed = $this->parse_ws_trade($this->extend(array( 'id' => $id ), $trade));
                    $stored->append ($parsed);
                    $symbol = $parsed['symbol'];
                    $symbols[$symbol] = true;
                }
            }
            $name = 'ownTrades';
            $client->resolve ($this->myTrades, $name);
            $keys = is_array($symbols) ? array_keys($symbols) : array();
            for ($i = 0; $i < count($keys); $i++) {
                $messageHash = $name . ':' . $keys[$i];
                $client->resolve ($this->myTrades, $messageHash);
            }
        }
    }

    public function parse_ws_trade($trade, $market = null) {
        //
        //     {
        //         "id" => "TIMIRG-WUNNE-RRJ6GT", // injected from outside
        //         "ordertxid" => "OQRPN2-LRHFY-HIFA7D",
        //         "postxid" => "TKH2SE-M7IF5-CFI7LT",
        //         "pair" => "USDCUSDT",
        //         "time" => 1586340086.457,
        //         "type" => "sell",
        //         "ordertype" => "market",
        //         "price" => "0.99860000",
        //         "cost" => "22.16892001",
        //         "fee" => "0.04433784",
        //         "vol" => "22.20000000",
        //         "margin" => "0.00000000",
        //         "misc" => ''
        //     }
        //
        //     {
        //         "id" => "TIY6G4-LKLAI-Y3GD4A",
        //         "cost" => "22.17134",
        //         "fee" => "0.05765",
        //         "margin" => "0.00000",
        //         "ordertxid" => "ODQXS7-MOLK6-ICXKAA",
        //         "ordertype" => "market",
        //         "pair" => "ETH/USD",
        //         "postxid" => "TKH2SE-M7IF5-CFI7LT",
        //         "price" => "169.97999",
        //         "time" => "1586340530.895739",
        //         "type" => "buy",
        //         "vol" => "0.13043500"
        //     }
        //
        $wsName = $this->safe_string($trade, 'pair');
        $market = $this->safe_value($this->options['marketsByWsName'], $wsName, $market);
        $symbol = null;
        $orderId = $this->safe_string($trade, 'ordertxid');
        $id = $this->safe_string_2($trade, 'id', 'postxid');
        $timestamp = $this->safe_timestamp($trade, 'time');
        $side = $this->safe_string($trade, 'type');
        $type = $this->safe_string($trade, 'ordertype');
        $price = $this->safe_float($trade, 'price');
        $amount = $this->safe_float($trade, 'vol');
        $cost = null;
        $fee = null;
        if (is_array($trade) && array_key_exists('fee', $trade)) {
            $currency = null;
            if ($market !== null) {
                $currency = $market['quote'];
            }
            $fee = array(
                'cost' => $this->safe_float($trade, 'fee'),
                'currency' => $currency,
            );
        }
        if ($market !== null) {
            $symbol = $market['symbol'];
        }
        if ($price !== null) {
            if ($amount !== null) {
                $cost = $price * $amount;
            }
        }
        return array(
            'id' => $id,
            'order' => $orderId,
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'type' => $type,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $price,
            'amount' => $amount,
            'cost' => $cost,
            'fee' => $fee,
        );
    }

    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) {
            /**
             *
             * @see https://docs.kraken.com/api/docs/websocket-v1/openorders
             *
             * watches information on multiple orders made by the user
             * @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  orde structures to retrieve
             * @param {array} [$params] maximum number of orderic to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            return Async\await($this->watch_private('openOrders', $symbol, $since, $limit, $params));
        }) ();
    }

    public function handle_orders(Client $client, $message, $subscription = null) {
        //
        //     array(
        //         array(
        //             {
        //                 "OGTT3Y-C6I3P-XRI6HX" => array(
        //                     "cost" => "0.00000",
        //                     "descr" => array(
        //                         "close" => "",
        //                         "leverage" => "0:1",
        //                         "order" => "sell 10.00345345 XBT/EUR @ $limit 34.50000 with 0:1 leverage",
        //                         "ordertype" => "limit",
        //                         "pair" => "XBT/EUR",
        //                         "price" => "34.50000",
        //                         "price2" => "0.00000",
        //                         "type" => "sell"
        //                     ),
        //                     "expiretm" => "0.000000",
        //                     "fee" => "0.00000",
        //                     "limitprice" => "34.50000",
        //                     "misc" => "",
        //                     "oflags" => "fcib",
        //                     "opentm" => "0.000000",
        //                     "price" => "34.50000",
        //                     "refid" => "OKIVMP-5GVZN-Z2D2UA",
        //                     "starttm" => "0.000000",
        //                     "status" => "open",
        //                     "stopprice" => "0.000000",
        //                     "userref" => 0,
        //                     "vol" => "10.00345345",
        //                     "vol_exec" => "0.00000000"
        //                 }
        //             ),
        //             {
        //                 "OGTT3Y-C6I3P-XRI6HX" => array(
        //                     "cost" => "0.00000",
        //                     "descr" => array(
        //                         "close" => "",
        //                         "leverage" => "0:1",
        //                         "order" => "sell 0.00000010 XBT/EUR @ $limit 5334.60000 with 0:1 leverage",
        //                         "ordertype" => "limit",
        //                         "pair" => "XBT/EUR",
        //                         "price" => "5334.60000",
        //                         "price2" => "0.00000",
        //                         "type" => "sell"
        //                     ),
        //                     "expiretm" => "0.000000",
        //                     "fee" => "0.00000",
        //                     "limitprice" => "5334.60000",
        //                     "misc" => "",
        //                     "oflags" => "fcib",
        //                     "opentm" => "0.000000",
        //                     "price" => "5334.60000",
        //                     "refid" => "OKIVMP-5GVZN-Z2D2UA",
        //                     "starttm" => "0.000000",
        //                     "status" => "open",
        //                     "stopprice" => "0.000000",
        //                     "userref" => 0,
        //                     "vol" => "0.00000010",
        //                     "vol_exec" => "0.00000000"
        //                 }
        //             ),
        //         ),
        //         "openOrders",
        //         array( "sequence" => 234 )
        //     )
        //
        // status-change
        //
        //     array(
        //         array(
        //             array( "OGTT3Y-C6I3P-XRI6HX" => array( "status" => "closed" )),
        //             array( "OGTT3Y-C6I3P-XRI6HX" => array( "status" => "closed" )),
        //         ),
        //         "openOrders",
        //         array( "sequence" => 59342 )
        //     )
        //
        $allOrders = $this->safe_value($message, 0, array());
        $allOrdersLength = count($allOrders);
        if ($allOrdersLength > 0) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            if ($this->orders === null) {
                $this->orders = new ArrayCacheBySymbolById ($limit);
            }
            $stored = $this->orders;
            $symbols = array();
            for ($i = 0; $i < count($allOrders); $i++) {
                $orders = $this->safe_value($allOrders, $i, array());
                $ids = is_array($orders) ? array_keys($orders) : array();
                for ($j = 0; $j < count($ids); $j++) {
                    $id = $ids[$j];
                    $order = $orders[$id];
                    $parsed = $this->parse_ws_order($order);
                    $parsed['id'] = $id;
                    $symbol = null;
                    $symbolsByOrderId = $this->safe_value($this->options, 'symbolsByOrderId', array());
                    if ($parsed['symbol'] !== null) {
                        $symbol = $parsed['symbol'];
                        $symbolsByOrderId[$id] = $symbol;
                        $this->options['symbolsByOrderId'] = $symbolsByOrderId;
                    } else {
                        $symbol = $this->safe_string($symbolsByOrderId, $id);
                    }
                    $previousOrders = $this->safe_value($stored->hashmap, $symbol);
                    $previousOrder = $this->safe_value($previousOrders, $id);
                    $newOrder = $parsed;
                    if ($previousOrder !== null) {
                        $newRawOrder = $this->extend($previousOrder['info'], $newOrder['info']);
                        $newOrder = $this->parse_ws_order($newRawOrder);
                        $newOrder['id'] = $id;
                    }
                    $length = count($stored);
                    if ($length === $limit && ($previousOrder === null)) {
                        $first = $stored[0];
                        if (is_array($symbolsByOrderId) && array_key_exists($first['id'], $symbolsByOrderId)) {
                            unset($symbolsByOrderId[$first['id']]);
                        }
                    }
                    $stored->append ($newOrder);
                    $symbols[$symbol] = true;
                }
            }
            $name = 'openOrders';
            $client->resolve ($this->orders, $name);
            $keys = is_array($symbols) ? array_keys($symbols) : array();
            for ($i = 0; $i < count($keys); $i++) {
                $messageHash = $name . ':' . $keys[$i];
                $client->resolve ($this->orders, $messageHash);
            }
        }
    }

    public function parse_ws_order($order, $market = null) {
        //
        // createOrder
        //    {
        //        "avg_price" => "0.00000",
        //        "cost" => "0.00000",
        //        "descr" => array(
        //            "close" => null,
        //            "leverage" => null,
        //            "order" => "sell 0.01000000 ETH/USDT @ limit 1900.00000",
        //            "ordertype" => "limit",
        //            "pair" => "ETH/USDT",
        //            "price" => "1900.00000",
        //            "price2" => "0.00000",
        //            "type" => "sell"
        //        ),
        //        "expiretm" => null,
        //        "fee" => "0.00000",
        //        "limitprice" => "0.00000",
        //        "misc" => '',
        //        "oflags" => "fciq",
        //        "opentm" => "1667522705.757622",
        //        "refid" => null,
        //        "starttm" => null,
        //        "status" => "open",
        //        "stopprice" => "0.00000",
        //        "timeinforce" => "GTC",
        //        "userref" => 0,
        //        "vol" => "0.01000000",
        //        "vol_exec" => "0.00000000"
        //    }
        //
        $description = $this->safe_value($order, 'descr', array());
        $orderDescription = $this->safe_string($description, 'order');
        $side = null;
        $type = null;
        $wsName = null;
        $price = null;
        $amount = null;
        if ($orderDescription !== null) {
            $parts = explode(' ', $orderDescription);
            $side = $this->safe_string($parts, 0);
            $amount = $this->safe_string($parts, 1);
            $wsName = $this->safe_string($parts, 2);
            $type = $this->safe_string($parts, 4);
            $price = $this->safe_string($parts, 5);
        }
        $side = $this->safe_string($description, 'type', $side);
        $type = $this->safe_string($description, 'ordertype', $type);
        $wsName = $this->safe_string($description, 'pair', $wsName);
        $market = $this->safe_value($this->options['marketsByWsName'], $wsName, $market);
        $symbol = null;
        $timestamp = $this->safe_timestamp($order, 'opentm');
        $amount = $this->safe_string($order, 'vol', $amount);
        $filled = $this->safe_string($order, 'vol_exec');
        $fee = null;
        $cost = $this->safe_string($order, 'cost');
        $price = $this->safe_string($description, 'price', $price);
        if (($price === null) || (Precise::string_eq($price, '0.0'))) {
            $price = $this->safe_string($description, 'price2');
        }
        if (($price === null) || (Precise::string_eq($price, '0.0'))) {
            $price = $this->safe_string($order, 'price', $price);
        }
        $average = $this->safe_string_2($order, 'avg_price', 'price');
        if ($market !== null) {
            $symbol = $market['symbol'];
            if (is_array($order) && array_key_exists('fee', $order)) {
                $flags = $order['oflags'];
                $feeCost = $this->safe_string($order, 'fee');
                $fee = array(
                    'cost' => $feeCost,
                    'rate' => null,
                );
                if (mb_strpos($flags, 'fciq') !== false) {
                    $fee['currency'] = $market['quote'];
                } elseif (mb_strpos($flags, 'fcib') !== false) {
                    $fee['currency'] = $market['base'];
                }
            }
        }
        $status = $this->parse_order_status($this->safe_string($order, 'status'));
        $id = $this->safe_string($order, 'id');
        if ($id === null) {
            $txid = $this->safe_value($order, 'txid');
            $id = $this->safe_string($txid, 0);
        }
        $clientOrderId = $this->safe_string($order, 'userref');
        $rawTrades = $this->safe_value($order, 'trades');
        $trades = null;
        if ($rawTrades !== null) {
            $trades = $this->parse_trades($rawTrades, $market, null, null, array( 'order' => $id ));
        }
        $stopPrice = $this->safe_number($order, 'stopprice');
        return $this->safe_order(array(
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'info' => $order,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'status' => $status,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'stopPrice' => $stopPrice,
            'triggerPrice' => $stopPrice,
            'cost' => $cost,
            'amount' => $amount,
            'filled' => $filled,
            'average' => $average,
            'remaining' => null,
            'fee' => $fee,
            'trades' => $trades,
        ));
    }

    public function watch_multi_helper(string $unifiedName, string $channelName, ?array $symbols = null, $subscriptionArgs = null, $params = array ()) {
        return Async\async(function () use ($unifiedName, $channelName, $symbols, $subscriptionArgs, $params) {
            Async\await($this->load_markets());
            // $symbols are required
            $symbols = $this->market_symbols($symbols, null, false, true, false);
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $eventTrigger = $this->safe_string($params, 'event_trigger');
                if ($eventTrigger !== null) {
                    $messageHashes[] = $this->get_message_hash($channelName, null, $this->symbol($symbols[$i]));
                } else {
                    $messageHashes[] = $this->get_message_hash($unifiedName, null, $this->symbol($symbols[$i]));
                }
            }
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channel' => $channelName,
                    'symbol' => $symbols,
                ),
                'req_id' => $this->request_id(),
            );
            $request['params'] = $this->deep_extend($request['params'], $params);
            $url = $this->urls['api']['ws']['publicV2'];
            return Async\await($this->watch_multiple($url, $messageHashes, $request, $messageHashes, $subscriptionArgs));
        }) ();
    }

    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://docs.kraken.com/api/docs/websocket-v2/balances
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $token = Async\await($this->authenticate());
            $messageHash = 'balances';
            $url = $this->urls['api']['ws']['privateV2'];
            $requestId = $this->request_id();
            $subscribe = array(
                'method' => 'subscribe',
                'req_id' => $requestId,
                'params' => array(
                    'channel' => 'balances',
                    'token' => $token,
                ),
            );
            $request = $this->deep_extend($subscribe, $params);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //     {
        //         "channel" => "balances",
        //         "data" => array(
        //             {
        //                 "asset" => "BTC",
        //                 "asset_class" => "currency",
        //                 "balance" => 1.2,
        //                 "wallets" => array(
        //                     {
        //                         "type" => "spot",
        //                         "id" => "main",
        //                         "balance" => 1.2
        //                     }
        //                 )
        //             }
        //         ),
        //         "type" => "snapshot",
        //         "sequence" => 1
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        $result = array( 'info' => $message );
        for ($i = 0; $i < count($data); $i++) {
            $currencyId = $this->safe_string($data[$i], 'asset');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $eq = $this->safe_string($data[$i], 'balance');
            $account['total'] = $eq;
            $result[$code] = $account;
        }
        $type = 'spot';
        $balance = $this->safe_balance($result);
        $oldBalance = $this->safe_value($this->balance, $type, array());
        $newBalance = $this->deep_extend($oldBalance, $balance);
        $this->balance[$type] = $this->safe_balance($newBalance);
        $channel = $this->safe_string($message, 'channel');
        $client->resolve ($this->balance[$type], $channel);
    }

    public function get_message_hash(string $unifiedElementName, ?string $subChannelName = null, ?string $symbol = null) {
        // $unifiedElementName can be : orderbook, trade, ticker, bidask ...
        // $subChannelName only applies to channel that needs specific variation (i.e. depth_50, depth_100..) to be selected
        $withSymbol = $symbol !== null;
        $messageHash = $unifiedElementName;
        if (!$withSymbol) {
            $messageHash .= 's';
        } else {
            $messageHash .= '@' . $symbol;
        }
        if ($subChannelName !== null) {
            $messageHash .= '#' . $subChannelName;
        }
        return $messageHash;
    }

    public function handle_subscription_status(Client $client, $message) {
        //
        // public
        //
        //     {
        //         "channelID" => 210,
        //         "channelName" => "book-10",
        //         "event" => "subscriptionStatus",
        //         "reqid" => 1574146735269,
        //         "pair" => "ETH/XBT",
        //         "status" => "subscribed",
        //         "subscription" => array( depth => 10, name => "book" )
        //     }
        //
        // private
        //
        //     {
        //         "channelName" => "openOrders",
        //         "event" => "subscriptionStatus",
        //         "reqid" => 1,
        //         "status" => "subscribed",
        //         "subscription" => array( maxratecount => 125, name => "openOrders" )
        //     }
        //
        $channelId = $this->safe_string($message, 'channelID');
        if ($channelId !== null) {
            $client->subscriptions[$channelId] = $message;
        }
        // $requestId = $this->safe_string($message, "reqid");
        // if (is_array($client->futures) && array_key_exists($requestId, $client->futures)) {
        //     unset($client->futures[$requestId]);
        // }
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //     {
        //         "errorMessage" => "Currency pair not in ISO 4217-A3 format foobar",
        //         "event" => "subscriptionStatus",
        //         "pair" => "foobar",
        //         "reqid" => 1574146735269,
        //         "status" => "error",
        //         "subscription" => array( name => "ticker" )
        //     }
        //
        // v2
        //     {
        //         "error" => "Unsupported field => 'price' for the given msg type => add order",
        //         "method" => "add_order",
        //         "success" => false,
        //         "time_in" => "2025-05-13T08:59:44.803511Z",
        //         "time_out" => "2025-05-13T08:59:44.803542Z'
        //     }
        //
        $errorMessage = $this->safe_string_2($message, 'errorMessage', 'error');
        if ($errorMessage !== null) {
            $requestId = $this->safe_string_2($message, 'reqid', 'req_id');
            $broad = $this->exceptions['ws']['broad'];
            $broadKey = $this->find_broadly_matched_key($broad, $errorMessage);
            $exception = null;
            if ($broadKey === null) {
                $exception = new ExchangeError ($errorMessage); // c# requirement to convert the $errorMessage to string
            } else {
                $exception = new $broad[$broadKey] ($errorMessage);
            }
            if ($requestId !== null) {
                $client->reject ($exception, $requestId);
            }
            return false;
        }
        return true;
    }

    public function handle_message(Client $client, $message) {
        if (gettype($message) === 'array' && array_keys($message) === array_keys(array_keys($message))) {
            $channelId = $this->safe_string($message, 0);
            $subscription = $this->safe_value($client->subscriptions, $channelId, array());
            $info = $this->safe_value($subscription, 'subscription', array());
            $messageLength = count($message);
            $channelName = $this->safe_string($message, $messageLength - 2);
            $name = $this->safe_string($info, 'name');
            $methods = array(
                // public
                'ohlc' => array($this, 'handle_ohlcv'),
                // private
                'openOrders' => array($this, 'handle_orders'),
                'ownTrades' => array($this, 'handle_my_trades'),
            );
            $method = $this->safe_value_2($methods, $name, $channelName);
            if ($method !== null) {
                $method($client, $message, $subscription);
            }
        } else {
            $channel = $this->safe_string($message, 'channel');
            if ($channel !== null) {
                $methods = array(
                    'balances' => array($this, 'handle_balance'),
                    'book' => array($this, 'handle_order_book'),
                    'ticker' => array($this, 'handle_ticker'),
                    'trade' => array($this, 'handle_trades'),
                );
                $method = $this->safe_value($methods, $channel);
                if ($method !== null) {
                    $method($client, $message);
                }
            }
            if ($this->handle_error_message($client, $message)) {
                $event = $this->safe_string_2($message, 'event', 'method');
                $methods = array(
                    'heartbeat' => array($this, 'handle_heartbeat'),
                    'systemStatus' => array($this, 'handle_system_status'),
                    'subscriptionStatus' => array($this, 'handle_subscription_status'),
                    'add_order' => array($this, 'handle_create_edit_order'),
                    'amend_order' => array($this, 'handle_create_edit_order'),
                    'cancel_order' => array($this, 'handle_cancel_order'),
                    'cancel_all' => array($this, 'handle_cancel_all_orders'),
                    'pong' => array($this, 'handle_pong'),
                );
                $method = $this->safe_value($methods, $event);
                if ($method !== null) {
                    $method($client, $message);
                }
            }
        }
    }
}
