From 1ea6a99c4873a1c3e6c9a953613f139dd7048ff0 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Mon, 4 Aug 2025 11:54:03 +0200 Subject: [PATCH 1/9] Allow pairs with prefix in MarketCapPairList --- .../plugins/pairlist/MarketCapPairList.py | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index b95cec50f..0d3289906 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -118,6 +118,19 @@ class MarketCapPairList(IPairList): }, } + def get_markets_cache(self): + markets = self._marketcap_cache.get("markets") + if not markets: + markets = [ + k + for k in self._exchange.get_markets( + quote_currencies=[self._stake_currency], tradable_only=True, active_only=True + ).keys() + ] + self._marketcap_cache["markets"] = markets.copy() + + return markets.copy() + def gen_pairlist(self, tickers: Tickers) -> list[str]: """ Generate the pairlist @@ -133,12 +146,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_cache() + # No point in testing for blacklisted pairs... _pairlist = self.verify_blacklist(_pairlist, logger.info) @@ -197,13 +206,24 @@ class MarketCapPairList(IPairList): pair_format += f":{self._stake_currency.upper()}" top_marketcap = marketcap_list[: self._max_rank :] + markets = self.get_markets_cache() 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 + test_prefix_1000 = f"1000{test_pair}" # Binance + test_prefix_k = f"k{test_pair}" # Hyperliquid + + if not any(p.startswith(test_pair) for p in filtered_pairlist): + if any(p.startswith(test_pair) for p in pairlist): + filtered_pairlist.append(test_pair) + elif not any(p.startswith(test_pair) for p in markets): + if any(p.startswith(test_prefix_1000) for p in pairlist): + filtered_pairlist.append(test_prefix_1000) + elif any(p.startswith(test_prefix_k) for p in pairlist): + filtered_pairlist.append(test_prefix_k) + + if len(filtered_pairlist) == self._number_assets: + break if len(filtered_pairlist) > 0: return filtered_pairlist From c0143b04c8dde6bd035504c5ebfc10bab910a842 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 5 Aug 2025 11:14:02 +0200 Subject: [PATCH 2/9] Refactor pair resolution logic for improved clarity and modularity --- .../plugins/pairlist/MarketCapPairList.py | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index 0d3289906..276b16261 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -156,6 +156,29 @@ class MarketCapPairList(IPairList): return pairlist + def resolve_marketcap_pair( + self, + test_pair: str, + prefixes: list[str], + pairlist: list[str], + markets: list[str], + filtered_pairlist: list[str], + ) -> str | None: + if any(p.startswith(test_pair) for p in filtered_pairlist): + return None + + if any(p.startswith(test_pair) for p in pairlist): + return test_pair + + if not any(p.startswith(test_pair) for p in markets): + for prefix in prefixes: + test_prefix = f"{prefix}{test_pair}" + + if any(p.startswith(test_prefix) for p 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. @@ -198,7 +221,7 @@ class MarketCapPairList(IPairList): self._marketcap_cache["marketcap"] = marketcap_list if marketcap_list: - filtered_pairlist = [] + filtered_pairlist: list[str] = [] market = self._config["trading_mode"] pair_format = f"{self._stake_currency.upper()}" @@ -210,17 +233,13 @@ class MarketCapPairList(IPairList): for mc_pair in top_marketcap: test_pair = f"{mc_pair.upper()}/{pair_format}" - test_prefix_1000 = f"1000{test_pair}" # Binance - test_prefix_k = f"k{test_pair}" # Hyperliquid + prefixes = ["1000", "k"] + resolved = self.resolve_marketcap_pair( + test_pair, prefixes, pairlist, markets, filtered_pairlist + ) - if not any(p.startswith(test_pair) for p in filtered_pairlist): - if any(p.startswith(test_pair) for p in pairlist): - filtered_pairlist.append(test_pair) - elif not any(p.startswith(test_pair) for p in markets): - if any(p.startswith(test_prefix_1000) for p in pairlist): - filtered_pairlist.append(test_prefix_1000) - elif any(p.startswith(test_prefix_k) for p in pairlist): - filtered_pairlist.append(test_prefix_k) + if resolved: + filtered_pairlist.append(resolved) if len(filtered_pairlist) == self._number_assets: break From 761d3f216ed03b5008051561ecfee1ca46ec3e5b Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Mon, 11 Aug 2025 22:40:49 +0200 Subject: [PATCH 3/9] Simplify resolve_marketcap_pair() --- freqtrade/plugins/pairlist/MarketCapPairList.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index 276b16261..c22d67dfb 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -164,17 +164,17 @@ class MarketCapPairList(IPairList): markets: list[str], filtered_pairlist: list[str], ) -> str | None: - if any(p.startswith(test_pair) for p in filtered_pairlist): + if test_pair in filtered_pairlist: return None - if any(p.startswith(test_pair) for p in pairlist): + if test_pair in pairlist: return test_pair - if not any(p.startswith(test_pair) for p in markets): + if not test_pair in markets: for prefix in prefixes: test_prefix = f"{prefix}{test_pair}" - if any(p.startswith(test_prefix) for p in pairlist): + if test_prefix in pairlist: return test_prefix return None From eaf76047e36d008fb4d778366014b19a858d341d Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Mon, 11 Aug 2025 22:51:35 +0200 Subject: [PATCH 4/9] Correct syntax --- freqtrade/plugins/pairlist/MarketCapPairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index c22d67dfb..bcd97eb45 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -170,7 +170,7 @@ class MarketCapPairList(IPairList): if test_pair in pairlist: return test_pair - if not test_pair in markets: + if test_pair not in markets: for prefix in prefixes: test_prefix = f"{prefix}{test_pair}" From ecfa1fd5b5879d6e9cd464a0e7ae034210f9751e Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Mon, 11 Aug 2025 23:54:50 +0200 Subject: [PATCH 5/9] Adjust expectation for get_markets calls in test_MarketCapPairList_timing --- tests/plugins/test_pairlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 0f133242b..de72bea40 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -2404,7 +2404,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)) @@ -2416,7 +2416,7 @@ 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_filter_special_no_pair_from_coingecko( From ae676d8e03482a05c13705a0d0f38eb1e0394a8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Aug 2025 07:09:37 +0200 Subject: [PATCH 6/9] chore: small code refactoring, simplifying the code --- .../plugins/pairlist/MarketCapPairList.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index bcd97eb45..4c7605f43 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -156,23 +156,25 @@ 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, - test_pair: str, - prefixes: list[str], + pair: str, pairlist: list[str], markets: list[str], filtered_pairlist: list[str], ) -> str | None: - if test_pair in filtered_pairlist: + if pair in filtered_pairlist: return None - if test_pair in pairlist: - return test_pair + if pair in pairlist: + return pair - if test_pair not in markets: - for prefix in prefixes: - test_prefix = f"{prefix}{test_pair}" + if pair not in markets: + for prefix in self.prefixes: + test_prefix = f"{prefix}{pair}" if test_prefix in pairlist: return test_prefix @@ -232,11 +234,8 @@ class MarketCapPairList(IPairList): markets = self.get_markets_cache() for mc_pair in top_marketcap: - test_pair = f"{mc_pair.upper()}/{pair_format}" - prefixes = ["1000", "k"] - resolved = self.resolve_marketcap_pair( - test_pair, prefixes, pairlist, markets, filtered_pairlist - ) + pair = f"{mc_pair.upper()}/{pair_format}" + resolved = self.resolve_marketcap_pair(pair, pairlist, markets, filtered_pairlist) if resolved: filtered_pairlist.append(resolved) From 48ab5ed9905abf579ab82aff296615ba6f186c58 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Mon, 25 Aug 2025 14:24:38 +0200 Subject: [PATCH 7/9] Replace get_markets_cache() with get_markets_exchange() --- .../plugins/pairlist/MarketCapPairList.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/freqtrade/plugins/pairlist/MarketCapPairList.py b/freqtrade/plugins/pairlist/MarketCapPairList.py index f5dce0f99..79112b32e 100644 --- a/freqtrade/plugins/pairlist/MarketCapPairList.py +++ b/freqtrade/plugins/pairlist/MarketCapPairList.py @@ -117,18 +117,15 @@ class MarketCapPairList(IPairList): }, } - def get_markets_cache(self): - markets = self._marketcap_cache.get("markets") - if not markets: - markets = [ - k - for k in self._exchange.get_markets( - quote_currencies=[self._stake_currency], tradable_only=True, active_only=True - ).keys() - ] - self._marketcap_cache["markets"] = markets.copy() + 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.copy() + return markets def gen_pairlist(self, tickers: Tickers) -> list[str]: """ @@ -145,7 +142,7 @@ class MarketCapPairList(IPairList): else: # Use fresh pairlist # Check if pair quote currency equals to the stake currency. - _pairlist = self.get_markets_cache() + _pairlist = self.get_markets_exchange() # No point in testing for blacklisted pairs... _pairlist = self.verify_blacklist(_pairlist, logger.info) @@ -230,7 +227,7 @@ class MarketCapPairList(IPairList): pair_format += f":{self._stake_currency.upper()}" top_marketcap = marketcap_list[: self._max_rank :] - markets = self.get_markets_cache() + markets = self.get_markets_exchange() for mc_pair in top_marketcap: pair = f"{mc_pair.upper()}/{pair_format}" From b40848373da902e4645b30493cb065147ef26880 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Aug 2025 07:13:17 +0200 Subject: [PATCH 8/9] docs: add line about 1000 / K testing logic --- docs/includes/pairlists.md | 2 ++ 1 file changed, 2 insertions(+) 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. From 8f6d64f5e167066eac1552ae88c3d70734785aaf Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 26 Aug 2025 07:21:09 +0200 Subject: [PATCH 9/9] test: add test for 1000 / k logic --- tests/plugins/test_pairlist.py | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index b687e56d1..f2c3c69e3 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -2424,6 +2424,51 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi 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( mocker, default_conf_usdt,