diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 32b97605c..32e669065 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -389,6 +389,8 @@ The `refresh_period` setting defines the interval (in seconds) at which the mark The `categories` setting specifies the [coingecko categories](https://www.coingecko.com/en/categories) from which to select coins from. The default is an empty list `[]`, meaning no category filtering is applied. If an incorrect category string is chosen, the plugin will print the available categories from CoinGecko and fail. The category should be the ID of the category, for example, for `https://www.coingecko.com/en/categories/layer-1`, the category ID would be `layer-1`. You can pass multiple categories such as `["layer-1", "meme-token"]` to select from several categories. +Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis, with the prefixes `1000` and `K` being used to identify them. + !!! Warning "Many categories" Each added category corresponds to one API call to CoinGecko. The more categories you add, the longer the pairlist generation will take, potentially causing rate limit issues. diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index c6b469969..79112b32e 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -117,6 +117,16 @@ class MarketCapPairList(IPairList): }, } + def get_markets_exchange(self): + markets = [ + k + for k in self._exchange.get_markets( + quote_currencies=[self._stake_currency], tradable_only=True, active_only=True + ).keys() + ] + + return markets + def gen_pairlist(self, tickers: Tickers) -> list[str]: """ Generate the pairlist @@ -132,12 +142,8 @@ class MarketCapPairList(IPairList): else: # Use fresh pairlist # Check if pair quote currency equals to the stake currency. - _pairlist = [ - k - for k in self._exchange.get_markets( - quote_currencies=[self._stake_currency], tradable_only=True, active_only=True - ).keys() - ] + _pairlist = self.get_markets_exchange() + # No point in testing for blacklisted pairs... _pairlist = self.verify_blacklist(_pairlist, logger.info) @@ -146,6 +152,31 @@ class MarketCapPairList(IPairList): return pairlist + # Prefixes to test to discover coins like 1000PEPE/USDDT:USDT or KPEPE/USDC (hyperliquid) + prefixes = ("1000", "K") + + def resolve_marketcap_pair( + self, + pair: str, + pairlist: list[str], + markets: list[str], + filtered_pairlist: list[str], + ) -> str | None: + if pair in filtered_pairlist: + return None + + if pair in pairlist: + return pair + + if pair not in markets: + for prefix in self.prefixes: + test_prefix = f"{prefix}{pair}" + + if test_prefix in pairlist: + return test_prefix + + return None + def filter_pairlist(self, pairlist: list[str], tickers: dict) -> list[str]: """ Filters and sorts pairlist and returns the whitelist again. @@ -188,7 +219,7 @@ class MarketCapPairList(IPairList): self._marketcap_cache["marketcap"] = marketcap_list if marketcap_list: - filtered_pairlist = [] + filtered_pairlist: list[str] = [] market = self._exchange._config["trading_mode"] pair_format = f"{self._stake_currency.upper()}" @@ -196,13 +227,17 @@ class MarketCapPairList(IPairList): pair_format += f":{self._stake_currency.upper()}" top_marketcap = marketcap_list[: self._max_rank :] + markets = self.get_markets_exchange() for mc_pair in top_marketcap: - test_pair = f"{mc_pair.upper()}/{pair_format}" - if test_pair in pairlist and test_pair not in filtered_pairlist: - filtered_pairlist.append(test_pair) - if len(filtered_pairlist) == self._number_assets: - break + pair = f"{mc_pair.upper()}/{pair_format}" + resolved = self.resolve_marketcap_pair(pair, pairlist, markets, filtered_pairlist) + + if resolved: + filtered_pairlist.append(resolved) + + if len(filtered_pairlist) == self._number_assets: + break if len(filtered_pairlist) > 0: return filtered_pairlist diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 2a3087da0..f2c3c69e3 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -2409,7 +2409,7 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi pm = PairListManager(exchange, default_conf_usdt) markets_mock.reset_mock() pm.refresh_pairlist() - assert markets_mock.call_count == 3 + assert markets_mock.call_count == 4 markets_mock.reset_mock() time_machine.move_to(start_dt + timedelta(hours=20)) @@ -2421,7 +2421,52 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi time_machine.move_to(start_dt + timedelta(days=2)) pm.refresh_pairlist() # No longer cached pairlist ... - assert markets_mock.call_count == 3 + assert markets_mock.call_count == 4 + + +def test_MarketCapPairList_1000_K_fillup(mocker, default_conf_usdt, markets, time_machine): + test_value = [ + {"symbol": "btc"}, + {"symbol": "eth"}, + {"symbol": "usdt"}, + {"symbol": "bnb"}, + {"symbol": "sol"}, + {"symbol": "xrp"}, + {"symbol": "usdc"}, + {"symbol": "steth"}, + {"symbol": "ada"}, + {"symbol": "avax"}, + ] + + default_conf_usdt["trading_mode"] = "spot" + default_conf_usdt["exchange"]["pair_whitelist"] = [] + default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList", "number_assets": 3}] + markets["1000ETH/USDT"] = markets["ETH/USDT"] + markets["KXRP/USDT"] = markets["XRP/USDT"] + del markets["ETH/USDT"] + del markets["XRP/USDT"] + + markets_mock = MagicMock(return_value=markets) + mocker.patch.multiple( + EXMS, + get_markets=markets_mock, + exchange_has=MagicMock(return_value=True), + ) + + mocker.patch( + "freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets", + return_value=test_value, + ) + + start_dt = dt_now() + + exchange = get_patched_exchange(mocker, default_conf_usdt) + time_machine.move_to(start_dt) + + pm = PairListManager(exchange, default_conf_usdt) + markets_mock.reset_mock() + pm.refresh_pairlist() + assert pm.whitelist == ["BTC/USDT", "1000ETH/USDT", "KXRP/USDT"] def test_MarketCapPairList_filter_special_no_pair_from_coingecko(