Merge pull request #12077 from mrpabloyeah/allow-pairs-with-prefix-in-marketcap-pairList

Allow pairs with prefix in MarketCapPairList
This commit is contained in:
Matthias
2025-08-26 19:12:46 +02:00
committed by GitHub
3 changed files with 96 additions and 14 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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(