Merge pull request #13149 from alisalama/develop

Add PairInformationFilter Pairlist
pull/13201/head
Matthias 1 week ago committed by GitHub
commit 779645f846
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -658,6 +658,7 @@
"DelistFilter",
"FullTradesFilter",
"OffsetFilter",
"PairInformationFilter",
"PerformanceFilter",
"PrecisionFilter",
"PriceFilter",

@ -31,6 +31,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`DelistFilter`](#delistfilter)
* [`FullTradesFilter`](#fulltradesfilter)
* [`OffsetFilter`](#offsetfilter)
* [`PairInformationFilter`](#pairinformationfilter)
* [`PerformanceFilter`](#performancefilter)
* [`PrecisionFilter`](#precisionfilter)
* [`PriceFilter`](#pricefilter)
@ -469,6 +470,65 @@ Example to remove the first 10 pairs from the pairlist, and takes the next 20 (t
!!! Note
An offset larger than the total length of the incoming pairlist will result in an empty pairlist.
#### PairInformationFilter
Filters pairs based on the presence of certain information in the ccxt markets argument.
To get the correct field - you can use the following code snippet to print the market information for a given pair, and find the correct field and value to filter for:
``` python
import ccxt
from pprint import pprint
exchange = ccxt.binance({
'options': {'defaultType': 'swap'}
})
lm = exchange.load_markets()
pprint(lm['XAU/USDT:USDT'])
```
``` json hl_lines="13 16"
{
"id": "XAUUSDT",
"lowercaseId": "xauusdt",
"symbol": "XAU/USDT:USDT",
"base": "XAU",
"quote": "USDT",
"settle": "USDT",
"baseId": "XAU",
"quoteId": "USDT",
"settleId": "USDT",
"type": "swap",
// ...
"info": {
"symbol": "XAUUSDT",
"pair": "XAUUSDT",
"contractType": "TRADIFI_PERPETUAL",
"deliveryDate": 4133404800000,
"onboardDate": 1765440300000,
"status": "TRADING",
// ...
}
}
```
As the highlighted lines show, the `contractType` field in the `info` section of the market data contains the value `TRADIFI_PERPETUAL`, which can be used to filter for TradeFi perpetual contracts.
Nested dictionaries can be accessed by using dot notation in the `info_key` - so the `contractType` field can be accessed with `info.contractType`.
In this example, the resulting `PairInformationFilter` configuration will include pairs that have `TRADIFI_PERPETUAL` as their `contractType` in the `info` section of the market data.
``` json
[
// ...
{
"method": "PairInformationFilter",
"selection_mode": "whitelist", // can be whitelist or blacklist
"info_key" : "info.contractType", // can be any key in market data
"info_compare_value": "TRADIFI_PERPETUAL", // can be any matching value
}
]
```
#### PerformanceFilter
Sorts pairs by past trade performance, as follows:

@ -66,6 +66,7 @@ AVAILABLE_PAIRLISTS = [
"DelistFilter",
"FullTradesFilter",
"OffsetFilter",
"PairInformationFilter",
"PerformanceFilter",
"PrecisionFilter",
"PriceFilter",

@ -0,0 +1,91 @@
"""Pair Information filter"""
import logging
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange_types import Tickers
from freqtrade.misc import safe_value_nested
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
logger = logging.getLogger(__name__)
class PairInformationFilter(IPairList):
supports_backtesting = SupportsBacktesting.BIASED
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._selection_mode: str = self._pairlistconfig.get("selection_mode", "whitelist")
self._info_key: str = self._pairlistconfig.get("info_key", "")
self._info_compare_value: str = self._pairlistconfig.get("info_compare_value", "")
if not self._info_key:
raise OperationalException(
"`info_key` not specified. Please check your configuration "
'for "pairlist.config.info_key"'
)
if not self._info_compare_value:
raise OperationalException(
"`info_compare_value` not specified. Please check your configuration "
'for "pairlist.config.info_compare_value"'
)
if self._selection_mode not in ["whitelist", "blacklist"]:
raise OperationalException(
"`selection_mode` not configured correctly. "
"Supported Modes are `whitelist` and `blacklist`"
)
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
return (
f"{self.name} - Returns {self._selection_mode} pairs by comparing "
f"{self._info_key} matches {self._info_compare_value}."
)
@staticmethod
def description() -> str:
return "Filter pairs based upon any information in their market data."
@staticmethod
def available_parameters() -> dict[str, PairlistParameter]:
return {
"selection_mode": {
"type": "option",
"default": "whitelist",
"options": ["whitelist", "blacklist"],
"description": "Whether to use filter as whitelist or blacklist",
"help": "Whether to use filter as whitelist or blacklist",
},
"info_key": {
"type": "string",
"default": "",
"description": "The key in the market data to compare against",
"help": "The key in the market data to compare against",
},
"info_compare_value": {
"type": "string",
"default": "",
"description": "The value to compare the key against",
"help": "The value to compare the key against",
},
}
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
whitelist_or_blacklist = self._selection_mode == "whitelist"
whitelist_pairlist: list[str] = []
blacklist_pairlist: list[str] = []
# loop through and add them to either list based on the market info check
for pair in pairlist:
market = self._exchange.markets[pair]
if safe_value_nested(market, self._info_key, "") == self._info_compare_value:
whitelist_pairlist.append(pair)
else:
blacklist_pairlist.append(pair)
return whitelist_pairlist if whitelist_or_blacklist else blacklist_pairlist

@ -0,0 +1,171 @@
from copy import deepcopy
import pytest
from freqtrade.exceptions import OperationalException
from freqtrade.plugins.pairlistmanager import PairListManager
from tests.conftest import get_markets, get_patched_exchange
@pytest.fixture(scope="function")
def pif_config(default_conf_usdt):
default_conf_usdt["exchange"]["pair_whitelist"] = [
"ETH/USDT",
"XRP/USDT",
"BTC/USDT",
]
default_conf_usdt["exchange"]["pair_blacklist"] = ["BLK/USDT"]
return default_conf_usdt
@pytest.mark.parametrize(
"missing_key,error_msg",
[
("info_key", "`info_key` not specified"),
("info_compare_value", "`info_compare_value` not specified"),
("selection_mode", "`selection_mode` not configured correctly"),
],
)
def test_PairInformationFilter_validation(mocker, pif_config, missing_key, error_msg):
pif_config["pairlists"] = [
{
"method": "PairInformationFilter",
"selection_mode": "whitelist",
"info_key": "info.contractType",
"info_compare_value": "TRADIFI_PERPETUAL",
"refresh_period": 1800,
}
]
exchange = get_patched_exchange(mocker, pif_config)
with pytest.raises(OperationalException, match=error_msg):
if missing_key == "selection_mode":
pif_config["pairlists"][0]["selection_mode"] = "invalid_mode"
else:
pif_config["pairlists"][0].pop(missing_key)
PairListManager(exchange, pif_config)
def test_PairInformationFilter_filter_float(mocker, pif_config):
pif_config["pairlists"] = [
{
"method": "StaticPairList",
},
{
"method": "PairInformationFilter",
"selection_mode": "whitelist",
"info_key": "limits.amount.min",
"info_compare_value": 0.01,
"refresh_period": 1800,
},
]
exchange = get_patched_exchange(mocker, pif_config)
pairlist_manager = PairListManager(exchange, pif_config)
pairlist_manager.refresh_pairlist()
pairlist = pairlist_manager.whitelist
assert set(pairlist) == {"XRP/USDT"}
def _get_pairinformation_test_markets() -> dict:
markets = get_markets()
custom_markets = {
pair: deepcopy(markets[pair]) for pair in ["ETH/USDT", "XRP/USDT", "BTC/USDT"]
}
custom_markets["ETH/USDT"]["info"] = {"contractType": "TRADIFI_PERPETUAL"}
custom_markets["XRP/USDT"]["info"] = {}
custom_markets["BTC/USDT"]["info"] = {"contractType": "CURRENT_QUARTER"}
return custom_markets
def test_PairInformationFilter_filter_nested_info_string_whitelist(mocker, pif_config):
markets = _get_pairinformation_test_markets()
pif_config["pairlists"] = [
{
"method": "StaticPairList",
},
{
"method": "PairInformationFilter",
"selection_mode": "whitelist",
"info_key": "info.contractType",
"info_compare_value": "TRADIFI_PERPETUAL",
"refresh_period": 1800,
},
]
exchange = get_patched_exchange(mocker, pif_config, mock_markets=markets)
pairlist_manager = PairListManager(exchange, pif_config)
pairlist_manager.refresh_pairlist()
pairlist = pairlist_manager.whitelist
assert pairlist == ["ETH/USDT"]
def test_PairInformationFilter_filter_nested_info_string_blacklist(mocker, pif_config):
markets = _get_pairinformation_test_markets()
pif_config["pairlists"] = [
{
"method": "StaticPairList",
},
{
"method": "PairInformationFilter",
"selection_mode": "blacklist",
"info_key": "info.contractType",
"info_compare_value": "TRADIFI_PERPETUAL",
"refresh_period": 1800,
},
]
exchange = get_patched_exchange(mocker, pif_config, mock_markets=markets)
pairlist_manager = PairListManager(exchange, pif_config)
pairlist_manager.refresh_pairlist()
pairlist = pairlist_manager.whitelist
assert pairlist == ["XRP/USDT", "BTC/USDT"]
def test_PairInformationFilter_filter_nested_info_combi(mocker, pif_config):
markets = _get_pairinformation_test_markets()
pif_config["pairlists"] = [
{
"method": "StaticPairList",
},
{
"method": "PairInformationFilter",
"selection_mode": "blacklist",
"info_key": "info.contractType",
"info_compare_value": "TRADIFI_PERPETUAL",
"refresh_period": 1800,
},
{
"method": "PairInformationFilter",
"selection_mode": "whitelist",
"info_key": "info.contractType",
"info_compare_value": "CURRENT_QUARTER",
"refresh_period": 1800,
},
]
exchange = get_patched_exchange(mocker, pif_config, mock_markets=markets)
pairlist_manager = PairListManager(exchange, pif_config)
pairlist_manager.refresh_pairlist()
pairlist = pairlist_manager.whitelist
assert pairlist == ["BTC/USDT"]
desc = pairlist_manager._pairlist_handlers[1].description()
assert desc == "Filter pairs based upon any information in their market data."
short_desc = pairlist_manager._pairlist_handlers[1].short_desc()
assert short_desc == (
"PairInformationFilter - Returns blacklist pairs by comparing "
"info.contractType matches TRADIFI_PERPETUAL."
)
short_desc2 = pairlist_manager._pairlist_handlers[2].short_desc()
assert short_desc2 == (
"PairInformationFilter - Returns whitelist pairs by comparing "
"info.contractType matches CURRENT_QUARTER."
)

@ -31,9 +31,11 @@ from tests.conftest import (
)
# Exclude RemotePairList from tests.
# It has a mandatory parameter, and requires special handling, which happens in test_remotepairlist.
TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ["RemotePairList"]]
# Exclude RemotePairList and PairInformationFilter from tests.
# They have mandatory parameters, and require special handling, which happens in explicit tests.
TESTABLE_PAIRLISTS = [
p for p in AVAILABLE_PAIRLISTS if p not in ["RemotePairList", "PairInformationFilter"]
]
@pytest.fixture(scope="function")

Loading…
Cancel
Save