From 8411844d7ef930687b4c738f797c3813bcd748b6 Mon Sep 17 00:00:00 2001 From: jblestang Date: Sat, 30 Dec 2017 14:15:07 +0100 Subject: [PATCH] Implement pair_blacklist functionality (#257) * Adding an optional black_list of pairs not to be traded * applying the blacklist also when not using --dynamic-whitelist * fix error retrieving pair in conf * Refactoring the handling of whitelist among the various functions * unit test to verify that black listed pairs are being removed from the pair_whitelist * Fixing newly added unit tests in develop * fixing flake8 code review * fix code review from @garcq --- freqtrade/main.py | 38 ++++++++++++++------------------ freqtrade/misc.py | 8 +++++++ freqtrade/tests/test_acl_pair.py | 11 ++++----- freqtrade/tests/test_main.py | 17 ++++++++++++++ 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index ad7cc7289..18bc04e6f 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -26,19 +26,17 @@ logger = logging.getLogger('freqtrade') _CONF = {} -def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None: +def refresh_whitelist(whitelist: List[str]) -> List[str]: """ Check wallet health and remove pair from whitelist if necessary - :param whitelist: a new whitelist (optional) - :return: None + :param whitelist: the pair the user might want to trade + :return: the list of pairs the user wants to trade without the one unavailable or black_listed """ - whitelist = whitelist or _CONF['exchange']['pair_whitelist'] - sanitized_whitelist = [] health = exchange.get_wallet_health() for status in health: pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency']) - if pair not in whitelist: + if pair not in whitelist or pair in _CONF['exchange'].get('pair_blacklist', []): continue if status['IsActive']: sanitized_whitelist.append(pair) @@ -47,27 +45,29 @@ def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None: 'Ignoring %s from whitelist (reason: %s).', pair, status.get('Notice') or 'wallet is not active' ) - if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist: - logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist) - _CONF['exchange']['pair_whitelist'] = sanitized_whitelist + return sanitized_whitelist -def _process(dynamic_whitelist: Optional[int] = 0) -> bool: +def _process(nb_assets: Optional[int] = 0) -> bool: """ Queries the persistence layer for open trades and handles them, otherwise a new trade is created. - :param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional) + :param: nb_assets: the maximum number of pairs to be traded at the same time :return: True if a trade has been created or closed, False otherwise """ state_changed = False try: # Refresh whitelist based on wallet maintenance - refresh_whitelist( + sanitized_list = refresh_whitelist( gen_pair_whitelist( - _CONF['stake_currency'], - topn=dynamic_whitelist - ) if dynamic_whitelist else None + _CONF['stake_currency'] + ) if nb_assets else _CONF['exchange']['pair_whitelist'] ) + + # Keep only the subsets of pairs wanted (up to nb_assets) + final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list + _CONF['exchange']['pair_whitelist'] = final_list + # Query trades from persistence layer trades = Trade.query.filter(Trade.is_open.is_(True)).all() if len(trades) < _CONF['max_open_trades']: @@ -295,11 +295,10 @@ def init(config: dict, db_url: Optional[str] = None) -> None: @cached(TTLCache(maxsize=1, ttl=1800)) -def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]: +def gen_pair_whitelist(base_currency: str, key: str = 'BaseVolume') -> List[str]: """ Updates the whitelist with with a dynamically generated list :param base_currency: base currency as str - :param topn: maximum number of returned results, must be greater than 0 :param key: sort key (defaults to 'BaseVolume') :return: List of pairs """ @@ -309,10 +308,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum reverse=True ) - if topn <= 0: - topn = 20 - - return [s['MarketName'].replace('-', '_') for s in summaries[:topn]] + return [s['MarketName'].replace('-', '_') for s in summaries] def cleanup() -> None: diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 57e7c6735..553cd0253 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -268,6 +268,14 @@ CONF_SCHEMA = { 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' }, 'uniqueItems': True + }, + 'pair_blacklist': { + 'type': 'array', + 'items': { + 'type': 'string', + 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' + }, + 'uniqueItems': True } }, 'required': ['name', 'key', 'secret', 'pair_whitelist'] diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index 3cbc9cfa0..0067eb302 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -41,12 +41,10 @@ def test_refresh_whitelist(mocker): mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.multiple('freqtrade.main.exchange', get_wallet_health=get_health) - # no argument: use the whitelist provided by config - refresh_whitelist() + refreshedwhitelist = refresh_whitelist(conf['exchange']['pair_whitelist']) whitelist = ['BTC_ETH', 'BTC_TKN'] - pairslist = conf['exchange']['pair_whitelist'] # Ensure all except those in whitelist are removed - assert set(whitelist) == set(pairslist) + assert set(whitelist) == set(refreshedwhitelist) def test_refresh_whitelist_dynamic(mocker): @@ -56,9 +54,8 @@ def test_refresh_whitelist_dynamic(mocker): get_wallet_health=get_health) # argument: use the whitelist dynamically by exchange-volume whitelist = ['BTC_ETH', 'BTC_TKN'] - refresh_whitelist(whitelist) - pairslist = conf['exchange']['pair_whitelist'] - assert set(whitelist) == set(pairslist) + refreshedwhitelist = refresh_whitelist(whitelist) + assert set(whitelist) == set(refreshedwhitelist) def test_refresh_whitelist_dynamic_empty(mocker): diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 3d6335572..605ac084b 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -180,6 +180,23 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker): create_trade(default_conf['stake_amount']) +def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock()) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker, + buy=MagicMock(return_value='mocked_limit_buy')) + + with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'): + conf = copy.deepcopy(default_conf) + conf['exchange']['pair_whitelist'] = ["BTC_ETH"] + conf['exchange']['pair_blacklist'] = ["BTC_ETH"] + mocker.patch.dict('freqtrade.main._CONF', conf) + create_trade(default_conf['stake_amount']) + + def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)