mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-10 10:21:59 +00:00
Merge pull request #9659 from stash86/bt-metrics2
Add MarketCapPairList based on CoinGecko
This commit is contained in:
commit
fcedc7de0d
|
@ -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).
|
||||
|
|
|
@ -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']
|
||||
|
|
157
freqtrade/plugins/pairlist/MarketCapPairList.py
Normal file
157
freqtrade/plugins/pairlist/MarketCapPairList.py
Normal 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
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue
Block a user