Merge pull request #9659 from stash86/bt-metrics2

Add MarketCapPairList based on CoinGecko
This commit is contained in:
Matthias 2024-01-30 07:00:07 +01:00 committed by GitHub
commit fcedc7de0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 324 additions and 4 deletions

View File

@ -6,7 +6,7 @@ In your configuration, you can use Static Pairlist (defined by the [`StaticPairL
Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler.
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList` or `MarketCapPairList` as the starting Pairlist Handler.
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
@ -24,6 +24,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
* [`VolumePairList`](#volume-pair-list)
* [`ProducerPairList`](#producerpairlist)
* [`RemotePairList`](#remotepairlist)
* [`MarketCapPairList`](#marketcappairlist)
* [`AgeFilter`](#agefilter)
* [`FullTradesFilter`](#fulltradesfilter)
* [`OffsetFilter`](#offsetfilter)
@ -264,6 +265,25 @@ The optional `bearer_token` will be included in the requests Authorization Heade
!!! Note
In case of a server error the last received pairlist will be kept if `keep_pairlist_on_failure` is set to true, when set to false a empty pairlist is returned.
#### MarketCapPairList
`MarketCapPairList` employs sorting/filtering of pairs by their marketcap rank based of CoinGecko. It will only recognize coins up to the coin placed at rank 250. The returned pairlist will be sorted based of their marketcap ranks.
```json
"pairlists": [
{
"method": "MarketCapPairList",
"number_assets": 20,
"max_rank": 50,
"refresh_period": 86400
}
]
```
`number_assets` defines the maximum number of pairs returned by the pairlist. `max_rank` will determine the maximum rank used in creating/filtering the pairlist. It's expected that some coins within the top `max_rank` marketcap will not be included in the resulting pairlist since not all pairs will have active trading pairs in your preferred market/stake/exchange combination.
`refresh_period` setting defines the period (in seconds) at which the marketcap rank data will be refreshed. Defaults to 86,400s (1 day). The pairlist cache (`refresh_period`) is applicable on both generating pairlists (first position in the list) and filtering instances (not the first position in the list).
#### AgeFilter
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).

View File

@ -33,9 +33,10 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss',
'ProfitDrawDownHyperOptLoss']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ProducerPairList', 'RemotePairList',
'AgeFilter', "FullTradesFilter", 'OffsetFilter', 'PerformanceFilter',
'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter',
'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter']
'MarketCapPairList', 'AgeFilter', "FullTradesFilter", 'OffsetFilter',
'PerformanceFilter', 'PrecisionFilter', 'PriceFilter',
'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter',
'VolatilityFilter']
AVAILABLE_PROTECTIONS = ['CooldownPeriod',
'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard']
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5', 'feather', 'parquet']

View File

@ -0,0 +1,157 @@
"""
Market Cap PairList provider
Provides dynamic pair list based on Market Cap
"""
import logging
from typing import Any, Dict, List
from cachetools import TTLCache
from pycoingecko import CoinGeckoAPI
from freqtrade.constants import Config
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.types import Tickers
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter
logger = logging.getLogger(__name__)
class MarketCapPairList(IPairList):
is_pairlist_generator = True
def __init__(self, exchange, pairlistmanager,
config: Config, pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
if 'number_assets' not in self._pairlistconfig:
raise OperationalException(
'`number_assets` not specified. Please check your configuration '
'for "pairlist.config.number_assets"')
self._stake_currency = config['stake_currency']
self._number_assets = self._pairlistconfig['number_assets']
self._max_rank = self._pairlistconfig.get('max_rank', 30)
self._refresh_period = self._pairlistconfig.get('refresh_period', 86400)
self._marketcap_cache: TTLCache = TTLCache(maxsize=1, ttl=self._refresh_period)
self._def_candletype = self._config['candle_type_def']
self._coingekko: CoinGeckoAPI = CoinGeckoAPI()
if self._max_rank > 250:
raise OperationalException(
"This filter only support marketcap rank up to 250."
)
@property
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
def short_desc(self) -> str:
"""
Short whitelist method description - used for startup-messages
"""
num = self._number_assets
rank = self._max_rank
msg = f"{self.name} - {num} pairs placed within top {rank} market cap."
return msg
@staticmethod
def description() -> str:
return "Provides pair list based on CoinGecko's market cap rank."
@staticmethod
def available_parameters() -> Dict[str, PairlistParameter]:
return {
"number_assets": {
"type": "number",
"default": 30,
"description": "Number of assets",
"help": "Number of assets to use from the pairlist",
},
"max_rank": {
"type": "number",
"default": 30,
"description": "Max rank of assets",
"help": "Maximum rank of assets to use from the pairlist",
},
"refresh_period": {
"type": "number",
"default": 86400,
"description": "Refresh period",
"help": "Refresh period in seconds",
}
}
def gen_pairlist(self, tickers: Tickers) -> List[str]:
"""
Generate the pairlist
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: List of pairs
"""
# Generate dynamic whitelist
# Must always run if this pairlist is the first in the list.
pairlist = self._marketcap_cache.get('pairlist_mc')
if pairlist:
# Item found - no refresh necessary
return pairlist.copy()
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()]
# No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info)
pairlist = self.filter_pairlist(_pairlist, tickers)
self._marketcap_cache['pairlist_mc'] = pairlist.copy()
return pairlist
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
"""
Filters and sorts pairlist and returns the whitelist again.
Called on each bot iteration - please use internal caching if necessary
:param pairlist: pairlist to filter or sort
:param tickers: Tickers (from exchange.get_tickers). May be cached.
:return: new whitelist
"""
marketcap_list = self._marketcap_cache.get('marketcap')
if marketcap_list is None:
data = self._coingekko.get_coins_markets(vs_currency='usd', order='market_cap_desc',
per_page='250', page='1', sparkline='false',
locale='en')
if data:
marketcap_list = [row['symbol'] for row in data]
self._marketcap_cache['marketcap'] = marketcap_list
if marketcap_list:
filtered_pairlist = []
market = self._config['trading_mode']
pair_format = f"{self._stake_currency.upper()}"
if (market == 'futures'):
pair_format += f":{self._stake_currency.upper()}"
top_marketcap = marketcap_list[:self._max_rank:]
for mc_pair in top_marketcap:
test_pair = f"{mc_pair.upper()}/{pair_format}"
if test_pair in pairlist:
filtered_pairlist.append(test_pair)
if len(filtered_pairlist) == self._number_assets:
break
if len(filtered_pairlist) > 0:
return filtered_pairlist
return pairlist

View File

@ -18,6 +18,7 @@ from freqtrade.persistence import LocalTrade, Trade
from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, expand_pairlist
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.resolvers import PairListResolver
from freqtrade.util.datetime_helpers import dt_now
from tests.conftest import (EXMS, create_mock_trades_usdt, get_patched_exchange,
get_patched_freqtradebot, log_has, log_has_re, num_log_has)
@ -1513,3 +1514,144 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
pm.refresh_pairlist()
assert pm.whitelist == []
assert log_has_re(r'Whitelist with 0 pairs: \[]', caplog)
@pytest.mark.parametrize('pairlists,trade_mode,result', [
([
# Get 2 pairs
{"method": "StaticPairList", "allow_inactive": True},
{"method": "MarketCapPairList", "number_assets": 2}
], 'spot', ['BTC/USDT', 'ETH/USDT']),
([
# Get 6 pairs
{"method": "StaticPairList", "allow_inactive": True},
{"method": "MarketCapPairList", "number_assets": 6}
], 'spot', ['BTC/USDT', 'ETH/USDT', 'XRP/USDT', 'ADA/USDT']),
([
# Get 3 pairs within top 6 ranks
{"method": "StaticPairList", "allow_inactive": True},
{"method": "MarketCapPairList", "max_rank": 6, "number_assets": 3}
], 'spot', ['BTC/USDT', 'ETH/USDT', 'XRP/USDT']),
([
# Get 4 pairs within top 8 ranks
{"method": "StaticPairList", "allow_inactive": True},
{"method": "MarketCapPairList", "max_rank": 8, "number_assets": 4}
], 'spot', ['BTC/USDT', 'ETH/USDT', 'XRP/USDT']),
([
# MarketCapPairList as generator
{"method": "MarketCapPairList", "number_assets": 5}
], 'spot', ['BTC/USDT', 'ETH/USDT', 'XRP/USDT']),
([
# MarketCapPairList as generator - low max_rank
{"method": "MarketCapPairList", "max_rank": 2, "number_assets": 5}
], 'spot', ['BTC/USDT', 'ETH/USDT']),
([
# MarketCapPairList as generator - futures - low max_rank
{"method": "MarketCapPairList", "max_rank": 2, "number_assets": 5}
], 'futures', ['ETH/USDT:USDT']),
([
# MarketCapPairList as generator - futures - low number_assets
{"method": "MarketCapPairList", "number_assets": 2}
], 'futures', ['ETH/USDT:USDT', 'ADA/USDT:USDT']),
])
def test_MarketCapPairList_filter(
mocker, default_conf_usdt, trade_mode, markets, pairlists, result
):
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'] = trade_mode
if trade_mode == 'spot':
default_conf_usdt['exchange']['pair_whitelist'].extend(['BTC/USDT', 'ETC/USDT', 'ADA/USDT'])
default_conf_usdt['pairlists'] = pairlists
mocker.patch.multiple(EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
mocker.patch("freqtrade.plugins.pairlist.MarketCapPairList.CoinGeckoAPI.get_coins_markets",
return_value=test_value)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pm.refresh_pairlist()
assert pm.whitelist == result
def test_MarketCapPairList_timing(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'].extend(['BTC/USDT', 'ETC/USDT', 'ADA/USDT'])
default_conf_usdt['pairlists'] = [{"method": "MarketCapPairList", "number_assets": 2}]
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.CoinGeckoAPI.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 markets_mock.call_count == 3
markets_mock.reset_mock()
time_machine.move_to(start_dt + timedelta(hours=20))
pm.refresh_pairlist()
# Cached pairlist ...
assert markets_mock.call_count == 1
markets_mock.reset_mock()
time_machine.move_to(start_dt + timedelta(days=2))
pm.refresh_pairlist()
# No longer cached pairlist ...
assert markets_mock.call_count == 3
def test_MarketCapPairList_exceptions(mocker, default_conf_usdt, markets, time_machine):
exchange = get_patched_exchange(mocker, default_conf_usdt)
default_conf_usdt['pairlists'] = [{"method": "MarketCapPairList"}]
with pytest.raises(OperationalException, match=r"`number_assets` not specified.*"):
# No number_assets
PairListManager(exchange, default_conf_usdt)
default_conf_usdt['pairlists'] = [{
"method": "MarketCapPairList", 'number_assets': 20, 'max_rank': 260
}]
with pytest.raises(OperationalException,
match="This filter only support marketcap rank up to 250."):
PairListManager(exchange, default_conf_usdt)