Repository: ccxt/ccxt
Pull request: feat(yobit) - fetchTickers all
fix #5519
this exchange was exceptional and was a burden to get all tickers, so like we have `paginateAll` for convenience. takes some time to fetch all of them, but is still a helpful for users whoever use this exchange

(can't add static tests because of multiple url requested)

Problem statement:
YoBit fetchTickers() & fetchOrderBooks() returning "empty pair list"
- CCXT version: 1.18.941
- Exchange: YoBit
- Method: fetchOrderBooks

YoBit accepts multiple pairs when accessing the depth endpoint, but the number of pairs accepted seems undefined. When calling fetchOrderBooks with up to 40 pairs, YoBit returns the orderbook with no problems. If requesting the orderbook for more than 40 pairs, the return is {"success":0,"error":"Empty pair list"} -- I've had success responses with up to 48 pairs, but 40 seems to be a safe number.
The URL length plays no role, i.e. the error fires up with a URL length far below the max URL length.

This is probably a YoBit issue, nit a ccxt one

Hints from issue discussion:
I have the same problem with the `fetch_tickers()` method.
I'm using custom `MyYoBit` class and this approach should work for `fetchOrderBooks()` too.

Maybe it can be implemented in ccxt library to improve unification.

```
from ccxt import yobit

class MyYoBit(yobit):

    def __init__(self):
        yobit.__init__(self)

    def fetch_tickers(self, all_pairs):
        tickers = dict()
        print("Fetching tickers ", end="")
        for i in range(0, int(math.ceil(len(all_pairs) / 40))):
            pairs = all_pairs[i * 40:(i + 1) * 40]
            tickers.update(super().fetch_tickers(pairs))
            print(".", end="")  # Yobit has a lot of pairs, so I use it to check it's working
            # time.sleep(1)  # Use it to avoid DDOS ban from YoBit
        print(" Done")
        return tickers

exchange = MyYoBit()
all_tickers = exchange.fetch_tickers(any_number_of_pairs)

```

Commit message: rename to avoid a clash in php

Patch to review:
diff --git a/ts/src/test/static/request/yobit.json b/ts/src/test/static/request/yobit.json
index 0c7ffbb220654..26420308a156e 100644
--- a/ts/src/test/static/request/yobit.json
+++ b/ts/src/test/static/request/yobit.json
@@ -127,6 +127,18 @@
                         "ETH/USDT"
                     ]
                 ]
+            },
+            {
+                "disabled": true,
+                "description": "all tickers",
+                "method": "fetchTickers",
+                "url": "https://yobit.net/api/3/ticker/zmc_waves-zne_btc-zne_doge-zne_eth-zne_rur-zne_usd-zne_waves-zonto_btc-zonto_doge-zonto_eth-zonto_rur-zonto_usd-zonto_waves-zoom_btc-zoom_doge-zoom_eth-zoom_rur-zoom_usd-zoom_waves-zrc_btc-zrc_doge-zrc_eth-zrc_rur-zrc_usd-zrc_waves-zrx_btc-zrx_doge-zrx_eth-zrx_rur-zrx_usd-zrx_waves-zur_btc-zur_doge-zur_eth-zur_rur-zur_usd-zur_waves-zxz_btc-zxz_doge-zxz_eth-zxz_rur-zxz_usd-zxz_yo-zyd_btc-zyd_doge-zyd_eth-zyd_rur-zyd_usd-zyd_waves",
+                "input": [
+                  null,
+                  {
+                    "all": true
+                  }
+                ]
             }
         ]
     }
diff --git a/ts/src/yobit.ts b/ts/src/yobit.ts
index 34b956d1d6a29..111b8ad51bb3e 100644
--- a/ts/src/yobit.ts
+++ b/ts/src/yobit.ts
@@ -555,6 +555,23 @@ export default class yobit extends Exchange {
         }, market);
     }
 
+    async fetchTickersHelper (idsString: string, params = {}): Promise<Tickers> {
+        const request: Dict = {
+            'pair': idsString,
+        };
+        const tickers = await this.publicGetTickerPair (this.extend (request, params));
+        const result: Dict = {};
+        const keys = Object.keys (tickers);
+        for (let k = 0; k < keys.length; k++) {
+            const id = keys[k];
+            const ticker = tickers[id];
+            const market = this.safeMarket (id);
+            const symbol = market['symbol'];
+            result[symbol] = this.parseTicker (ticker, market);
+        }
+        return result;
+    }
+
     async fetchTickers (symbols: Strings = undefined, params = {}): Promise<Tickers> {
         /**
          * @method
@@ -563,42 +580,52 @@ export default class yobit extends Exchange {
          * @description fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
          * @param {string[]|undefined} symbols unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
          * @param {object} [params] extra parameters specific to the exchange API endpoint
+         * @param {object} [params.all] you can set to `true` for convenience to fetch all tickers from this exchange by sending multiple requests
          * @returns {object} a dictionary of [ticker structures]{@link https://docs.ccxt.com/#/?id=ticker-structure}
          */
-        if (symbols === undefined) {
-            throw new ArgumentsRequired (this.id + ' fetchTickers() requires "symbols" argument');
+        let allSymbols = undefined;
+        [ allSymbols, params ] = this.handleParamBool (params, 'all', false);
+        if (symbols === undefined && !allSymbols) {
+            throw new ArgumentsRequired (this.id + ' fetchTickers() requires "symbols" argument or use `params["all"] = true` to send multiple requests for all markets');
         }
         await this.loadMarkets ();
-        symbols = this.marketSymbols (symbols);
-        let ids = undefined;
-        if (symbols === undefined) {
-            ids = this.ids;
-        } else {
-            ids = this.marketIds (symbols);
-        }
-        const idsLength: number = ids.length;
-        const idsString = ids.join ('-');
+        const promises = [];
         const maxLength = this.safeInteger (this.options, 'maxUrlLength', 2048);
         // max URL length is 2048 symbols, including http schema, hostname, tld, etc...
-        const lenghtOfBaseUrl = 30; // the url including api-base and endpoint dir is 30 chars
-        const actualLength = idsString.length + lenghtOfBaseUrl;
-        if (actualLength > maxLength) {
-            throw new ArgumentsRequired (this.id + ' fetchTickers() is being requested for ' + idsLength.toString () + ' markets (which has an URL length of ' + actualLength.toString () + ' characters), but it exceedes max URL length (' + maxLength.toString () + '), please pass limisted symbols array to fetchTickers to fit in one request');
+        const lenghtOfBaseUrl = 40; // safe space for the url including api-base and endpoint dir is 30 chars
+        if (allSymbols) {
+            symbols = this.symbols;
+            let ids = '';
+            for (let i = 0; i < this.ids.length; i++) {
+                const id = this.ids[i];
+                const prefix = (ids === '') ? '' : '-';
+                ids += prefix + id;
+                if (ids.length > maxLength) {
+                    promises.push (this.fetchTickersHelper (ids, params));
+                    ids = '';
+                }
+            }
+            if (ids !== '') {
+                promises.push (this.fetchTickersHelper (ids, params));
+            }
+        } else {
+            symbols = this.marketSymbols (symbols);
+            const ids = this.marketIds (symbols);
+            const idsLength: number = ids.length;
+            const idsString = ids.join ('-');
+            const actualLength = idsString.length + lenghtOfBaseUrl;
+            if (actualLength > maxLength) {
+                throw new ArgumentsRequired (this.id + ' fetchTickers() is being requested for ' + idsLength.toString () + ' markets (which has an URL length of ' + actualLength.toString () + ' characters), but it exceedes max URL length (' + maxLength.toString () + '), please pass limisted symbols array to fetchTickers to fit in one request');
+            }
+            promises.push (this.fetchTickersHelper (idsString, params));
         }
-        const request: Dict = {
-            'pair': idsString,
-        };
-        const tickers = await this.publicGetTickerPair (this.extend (request, params));
-        const result: Dict = {};
-        const keys = Object.keys (tickers);
-        for (let k = 0; k < keys.length; k++) {
-            const id = keys[k];
-            const ticker = tickers[id];
-            const market = this.safeMarket (id);
-            const symbol = market['symbol'];
-            result[symbol] = this.parseTicker (ticker, market);
+        const resultAll = await Promise.all (promises);
+        let finalResult = {};
+        for (let i = 0; i < resultAll.length; i++) {
+            const result = this.filterByArrayTickers (resultAll[i], 'symbol', symbols);
+            finalResult = this.extend (finalResult, result);
         }
-        return this.filterByArrayTickers (result, 'symbol', symbols);
+        return finalResult;
     }
 
     async fetchTicker (symbol: string, params = {}): Promise<Ticker> {


Code review comments:
diff_hunk: @@ -127,6 +127,18 @@
                         "ETH/USDT"
                     ]
                 ]
+            },
+            {
+                "disabled": true,
+                "description": "all tickers",
+                "method": "fetchTickers",
path: ts/src/test/static/request/yobit.json
text: @user1:
@author static tests do not support multiple request at the moment so this test is not very useful

@user1:
and relies on the order of the first request which is not guaranteed