diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 379643857..37e5b369c 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -304,6 +304,8 @@ The optional `mode` option specifies if the pairlist should be used as a `blackl The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter". +The optional `number_assets` option in the RemotePairList configuration determines how many pairs will be returned if used in whitelist `mode`. By default, all pairs will be returned. In blacklist `mode`, this option will be ignored. + In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out. In "append" mode, the retrieved pairlist is added to the original pairlist. All pairs from both lists are included in the final pairlist without any filtering. diff --git a/freqtrade/plugins/pairlist/RemotePairList.py b/freqtrade/plugins/pairlist/RemotePairList.py index 0966663a3..6b4150770 100644 --- a/freqtrade/plugins/pairlist/RemotePairList.py +++ b/freqtrade/plugins/pairlist/RemotePairList.py @@ -31,12 +31,6 @@ class RemotePairList(IPairList): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - if "number_assets" not in self._pairlistconfig: - raise OperationalException( - "`number_assets` not specified. Please check your configuration " - 'for "pairlist.config.number_assets"' - ) - if "pairlist_url" not in self._pairlistconfig: raise OperationalException( "`pairlist_url` not specified. Please check your configuration " @@ -45,7 +39,7 @@ class RemotePairList(IPairList): self._mode = self._pairlistconfig.get("mode", "whitelist") self._processing_mode = self._pairlistconfig.get("processing_mode", "filter") - self._number_pairs = self._pairlistconfig["number_assets"] + self._number_pairs: int | None = self._pairlistconfig.get("number_assets", None) self._refresh_period: int = self._pairlistconfig.get("refresh_period", 1800) self._keep_pairlist_on_failure = self._pairlistconfig.get("keep_pairlist_on_failure", True) self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period) @@ -248,7 +242,8 @@ class RemotePairList(IPairList): pairlist = expand_pairlist(pairlist, list(self._exchange.get_markets().keys())) pairlist = self._whitelist_for_active_markets(pairlist) - pairlist = pairlist[: self._number_pairs] + if self._number_pairs and (self._mode == "whitelist"): + pairlist = pairlist[: self._number_pairs] if pairlist: self._pair_cache["pairlist"] = pairlist.copy() @@ -305,5 +300,6 @@ class RemotePairList(IPairList): if filtered: self.log_once(f"Blacklist - Filtered out pairs: {filtered}", logger.info) - merged_list = merged_list[: self._number_pairs] + if self._number_pairs and (self._mode == "whitelist"): + merged_list = merged_list[: self._number_pairs] return merged_list diff --git a/tests/plugins/test_remotepairlist.py b/tests/plugins/test_remotepairlist.py index 2c1e80bf4..9f4459904 100644 --- a/tests/plugins/test_remotepairlist.py +++ b/tests/plugins/test_remotepairlist.py @@ -137,25 +137,6 @@ def test_remote_pairlist_init_no_pairlist_url(mocker, rpl_config): get_patched_freqtradebot(mocker, rpl_config) -def test_remote_pairlist_init_no_number_assets(mocker, rpl_config): - rpl_config["pairlists"] = [ - { - "method": "RemotePairList", - "pairlist_url": "http://example.com/pairlist", - "keep_pairlist_on_failure": True, - } - ] - - get_patched_exchange(mocker, rpl_config) - - with pytest.raises( - OperationalException, - match=r"`number_assets` not specified. " - 'Please check your configuration for "pairlist.config.number_assets"', - ): - get_patched_freqtradebot(mocker, rpl_config) - - def test_fetch_pairlist_mock_response_valid(mocker, rpl_config): rpl_config["pairlists"] = [ { @@ -341,3 +322,60 @@ def test_remote_pairlist_whitelist(mocker, rpl_config, processing_mode, markets, whitelist = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {}) assert whitelist == (["XRP/USDT"] if processing_mode == "filter" else ["ETH/USDT", "XRP/USDT"]) + + +@pytest.mark.parametrize( + "number_assets, result", + [ + (1, ["ETH/USDT"]), + (2, ["ETH/USDT", "XRP/USDT"]), + (500, ["ETH/USDT", "XRP/USDT"]), + (None, ["ETH/USDT", "XRP/USDT"]), + ], +) +def test_remote_pairlist_whitelist_number_assets( + mocker, rpl_config, number_assets, result, markets, tickers +): + mock_response = MagicMock() + + mock_response.json.return_value = { + "pairs": ["ETH/USDT", "XRP/USDT", "TKN/USDT"], + "refresh_period": 60, + } + + mock_response.headers = {"content-type": "application/json"} + + rpl_config["pairlists"] = [ + { + "method": "RemotePairList", + "mode": "whitelist", + "pairlist_url": "http://example.com/pairlist", + "number_assets": number_assets, + }, + ] + + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) + + mocker.patch( + "freqtrade.plugins.pairlist.RemotePairList.requests.get", return_value=mock_response + ) + + exchange = get_patched_exchange(mocker, rpl_config) + + pairlistmanager = PairListManager(exchange, rpl_config) + + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0 + ) + + pairs, _ = remote_pairlist.fetch_pairlist() + + assert pairs == ["ETH/USDT", "XRP/USDT", "TKN/USDT"] + + whitelist = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {}) + assert whitelist == result