diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index dba9772a7..61bafbf94 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -33,50 +33,54 @@ 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']] +TESTABLE_PAIRLISTS = [p for p in AVAILABLE_PAIRLISTS if p not in ["RemotePairList"]] @pytest.fixture(scope="function") def whitelist_conf(default_conf): - default_conf['stake_currency'] = 'BTC' - default_conf['exchange']['pair_whitelist'] = [ - 'ETH/BTC', - 'TKN/BTC', - 'TRST/BTC', - 'SWT/BTC', - 'BCC/BTC', - 'HOT/BTC', + default_conf["stake_currency"] = "BTC" + default_conf["exchange"]["pair_whitelist"] = [ + "ETH/BTC", + "TKN/BTC", + "TRST/BTC", + "SWT/BTC", + "BCC/BTC", + "HOT/BTC", ] - default_conf['exchange']['pair_blacklist'] = [ - 'BLK/BTC' - ] - default_conf['pairlists'] = [ + default_conf["exchange"]["pair_blacklist"] = ["BLK/BTC"] + default_conf["pairlists"] = [ { "method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", }, ] - default_conf.update({ - "external_message_consumer": { - "enabled": True, - "producers": [], + default_conf.update( + { + "external_message_consumer": { + "enabled": True, + "producers": [], + } } - }) + ) return default_conf @pytest.fixture(scope="function") def whitelist_conf_2(default_conf): - default_conf['stake_currency'] = 'BTC' - default_conf['exchange']['pair_whitelist'] = [ - 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', - 'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC' + default_conf["stake_currency"] = "BTC" + default_conf["exchange"]["pair_whitelist"] = [ + "ETH/BTC", + "TKN/BTC", + "BLK/BTC", + "LTC/BTC", + "BTT/BTC", + "HOT/BTC", + "FUEL/BTC", + "XRP/BTC", ] - default_conf['exchange']['pair_blacklist'] = [ - 'BLK/BTC' - ] - default_conf['pairlists'] = [ + default_conf["exchange"]["pair_blacklist"] = ["BLK/BTC"] + default_conf["pairlists"] = [ # { "method": "StaticPairList"}, { "method": "VolumePairList", @@ -90,33 +94,33 @@ def whitelist_conf_2(default_conf): @pytest.fixture(scope="function") def whitelist_conf_agefilter(default_conf): - default_conf['stake_currency'] = 'BTC' - default_conf['exchange']['pair_whitelist'] = [ - 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', - 'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC' + default_conf["stake_currency"] = "BTC" + default_conf["exchange"]["pair_whitelist"] = [ + "ETH/BTC", + "TKN/BTC", + "BLK/BTC", + "LTC/BTC", + "BTT/BTC", + "HOT/BTC", + "FUEL/BTC", + "XRP/BTC", ] - default_conf['exchange']['pair_blacklist'] = [ - 'BLK/BTC' - ] - default_conf['pairlists'] = [ + default_conf["exchange"]["pair_blacklist"] = ["BLK/BTC"] + default_conf["pairlists"] = [ { "method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "refresh_period": -1, }, - { - "method": "AgeFilter", - "min_days_listed": 2, - "max_days_listed": 100 - } + {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": 100}, ] return default_conf @pytest.fixture(scope="function") def static_pl_conf(whitelist_conf): - whitelist_conf['pairlists'] = [ + whitelist_conf["pairlists"] = [ { "method": "StaticPairList", }, @@ -125,65 +129,70 @@ def static_pl_conf(whitelist_conf): def test_log_cached(mocker, static_pl_conf, markets, tickers): - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) logmock = MagicMock() # Assign starting whitelist pl = freqtrade.pairlists._pairlist_handlers[0] - pl.log_once('Hello world', logmock) + pl.log_once("Hello world", logmock) assert logmock.call_count == 1 - pl.log_once('Hello world', logmock) + pl.log_once("Hello world", logmock) assert logmock.call_count == 1 assert pl._log_cache.currsize == 1 - assert ('Hello world',) in pl._log_cache._Cache__data + assert ("Hello world",) in pl._log_cache._Cache__data - pl.log_once('Hello world2', logmock) + pl.log_once("Hello world2", logmock) assert logmock.call_count == 2 assert pl._log_cache.currsize == 2 def test_load_pairlist_noexist(mocker, markets, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) + mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets)) plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) - with pytest.raises(OperationalException, - match=r"Impossible to load Pairlist 'NonexistingPairList'. " - r"This class does not exist or contains Python code errors."): - PairListResolver.load_pairlist('NonexistingPairList', freqtrade.exchange, plm, - default_conf, {}, 1) + with pytest.raises( + OperationalException, + match=r"Impossible to load Pairlist 'NonexistingPairList'. " + r"This class does not exist or contains Python code errors.", + ): + PairListResolver.load_pairlist( + "NonexistingPairList", freqtrade.exchange, plm, default_conf, {}, 1 + ) def test_load_pairlist_verify_multi(mocker, markets_static, default_conf): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_static)) + mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets_static)) plm = PairListManager(freqtrade.exchange, default_conf, MagicMock()) # Call different versions one after the other, should always consider what was passed in # and have no side-effects (therefore the same check multiple times) - assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] - assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', 'BUUU/BTC'], print) == ['ETH/BTC', 'XRP/BTC'] - assert plm.verify_whitelist(['XRP/BTC', 'BUUU/BTC'], print) == ['XRP/BTC'] - assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] - assert plm.verify_whitelist(['ETH/USDT', 'XRP/USDT', ], print) == ['ETH/USDT', ] - assert plm.verify_whitelist(['ETH/BTC', 'XRP/BTC', ], print) == ['ETH/BTC', 'XRP/BTC'] + assert plm.verify_whitelist(["ETH/BTC", "XRP/BTC"], print) == ["ETH/BTC", "XRP/BTC"] + assert plm.verify_whitelist(["ETH/BTC", "XRP/BTC", "BUUU/BTC"], print) == ["ETH/BTC", "XRP/BTC"] + assert plm.verify_whitelist(["XRP/BTC", "BUUU/BTC"], print) == ["XRP/BTC"] + assert plm.verify_whitelist(["ETH/BTC", "XRP/BTC"], print) == ["ETH/BTC", "XRP/BTC"] + assert plm.verify_whitelist(["ETH/USDT", "XRP/USDT"], print) == ["ETH/USDT"] + assert plm.verify_whitelist(["ETH/BTC", "XRP/BTC"], print) == ["ETH/BTC", "XRP/BTC"] def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): - freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) - mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets)) + mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets)) freqtrade.pairlists.refresh_pairlist() # List ordered by BaseVolume - whitelist = ['ETH/BTC', 'TKN/BTC'] + whitelist = ["ETH/BTC", "TKN/BTC"] # Ensure all except those in whitelist are removed assert set(whitelist) == set(freqtrade.pairlists.whitelist) # Ensure config dict hasn't been changed - assert (static_pl_conf['exchange']['pair_whitelist'] == - freqtrade.config['exchange']['pair_whitelist']) + assert ( + static_pl_conf["exchange"]["pair_whitelist"] + == freqtrade.config["exchange"]["pair_whitelist"] + ) def test_refresh_static_pairlist(mocker, markets, static_pl_conf): @@ -195,22 +204,28 @@ def test_refresh_static_pairlist(mocker, markets, static_pl_conf): ) freqtrade.pairlists.refresh_pairlist() # List ordered by BaseVolume - whitelist = ['ETH/BTC', 'TKN/BTC'] + whitelist = ["ETH/BTC", "TKN/BTC"] # Ensure all except those in whitelist are removed assert set(whitelist) == set(freqtrade.pairlists.whitelist) - assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist + assert static_pl_conf["exchange"]["pair_blacklist"] == freqtrade.pairlists.blacklist -@pytest.mark.parametrize('pairs,expected', [ - (['NOEXIST/BTC', r'\+WHAT/BTC'], - ['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'NOEXIST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']), - (['NOEXIST/BTC', r'*/BTC'], # This is an invalid regex - []), -]) +@pytest.mark.parametrize( + "pairs,expected", + [ + ( + ["NOEXIST/BTC", r"\+WHAT/BTC"], + ["ETH/BTC", "TKN/BTC", "TRST/BTC", "NOEXIST/BTC", "SWT/BTC", "BCC/BTC", "HOT/BTC"], + ), + ( + ["NOEXIST/BTC", r"*/BTC"], # This is an invalid regex + [], + ), + ], +) def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, expected, caplog): - - static_pl_conf['pairlists'][0]['allow_inactive'] = True - static_pl_conf['exchange']['pair_whitelist'] += pairs + static_pl_conf["pairlists"][0]["allow_inactive"] = True + static_pl_conf["exchange"]["pair_whitelist"] += pairs freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( EXMS, @@ -221,13 +236,13 @@ def test_refresh_static_pairlist_noexist(mocker, markets, static_pl_conf, pairs, # Ensure all except those in whitelist are removed assert set(expected) == set(freqtrade.pairlists.whitelist) - assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist + assert static_pl_conf["exchange"]["pair_blacklist"] == freqtrade.pairlists.blacklist if not expected: - assert log_has_re(r'Pair whitelist contains an invalid Wildcard: Wildcard error.*', caplog) + assert log_has_re(r"Pair whitelist contains an invalid Wildcard: Wildcard error.*", caplog) def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): - static_pl_conf['exchange']['pair_blacklist'] = ['*/BTC'] + static_pl_conf["exchange"]["pair_blacklist"] = ["*/BTC"] freqtrade = get_patched_freqtradebot(mocker, static_pl_conf) mocker.patch.multiple( EXMS, @@ -238,7 +253,7 @@ def test_invalid_blacklist(mocker, markets, static_pl_conf, caplog): whitelist = [] # Ensure all except those in whitelist are removed assert set(whitelist) == set(freqtrade.pairlists.whitelist) - assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist + assert static_pl_conf["exchange"]["pair_blacklist"] == freqtrade.pairlists.blacklist log_has_re(r"Pair blacklist contains an invalid Wildcard.*", caplog) @@ -251,27 +266,26 @@ def test_remove_logs_for_pairs_already_in_blacklist(mocker, markets, static_pl_c markets=PropertyMock(return_value=markets), ) freqtrade.pairlists.refresh_pairlist() - whitelist = ['ETH/BTC', 'TKN/BTC'] + whitelist = ["ETH/BTC", "TKN/BTC"] caplog.clear() caplog.set_level(logging.INFO) # Ensure all except those in whitelist are removed. assert set(whitelist) == set(freqtrade.pairlists.whitelist) - assert static_pl_conf['exchange']['pair_blacklist'] == freqtrade.pairlists.blacklist + assert static_pl_conf["exchange"]["pair_blacklist"] == freqtrade.pairlists.blacklist # Ensure that log message wasn't generated. - assert not log_has('Pair BLK/BTC in your blacklist. Removing it from whitelist...', caplog) + assert not log_has("Pair BLK/BTC in your blacklist. Removing it from whitelist...", caplog) for _ in range(3): new_whitelist = freqtrade.pairlists.verify_blacklist( - whitelist + ['BLK/BTC'], logger.warning) + whitelist + ["BLK/BTC"], logger.warning + ) # Ensure that the pair is removed from the white list, and properly logged. assert set(whitelist) == set(new_whitelist) - assert num_log_has('Pair BLK/BTC in your blacklist. Removing it from whitelist...', - caplog) == 1 + assert num_log_has("Pair BLK/BTC in your blacklist. Removing it from whitelist...", caplog) == 1 def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_conf): - mocker.patch.multiple( EXMS, get_tickers=tickers, @@ -284,19 +298,20 @@ def test_refresh_pairlist_dynamic(mocker, shitcoinmarkets, tickers, whitelist_co markets=PropertyMock(return_value=shitcoinmarkets), ) # argument: use the whitelist dynamically by exchange-volume - whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC'] + whitelist = ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC", "HOT/BTC"] freqtrade.pairlists.refresh_pairlist() assert whitelist == freqtrade.pairlists.whitelist - whitelist_conf['pairlists'] = [{'method': 'VolumePairList'}] - with pytest.raises(OperationalException, - match=r'`number_assets` not specified. Please check your configuration ' - r'for "pairlist.config.number_assets"'): + whitelist_conf["pairlists"] = [{"method": "VolumePairList"}] + with pytest.raises( + OperationalException, + match=r"`number_assets` not specified. Please check your configuration " + r'for "pairlist.config.number_assets"', + ): PairListManager(freqtrade.exchange, whitelist_conf, MagicMock()) def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_conf_2): - tickers_dict = tickers() mocker.patch.multiple( @@ -305,7 +320,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ ) # Remove caching of ticker data to emulate changing volume by the time of second call mocker.patch.multiple( - 'freqtrade.plugins.pairlistmanager.PairListManager', + "freqtrade.plugins.pairlistmanager.PairListManager", _get_cached_tickers=MagicMock(return_value=tickers_dict), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2) @@ -315,14 +330,14 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ markets=PropertyMock(return_value=shitcoinmarkets), ) - whitelist = ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC'] + whitelist = ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC", "HOT/BTC"] freqtrade.pairlists.refresh_pairlist() assert whitelist == freqtrade.pairlists.whitelist # Delay to allow 0 TTL cache to expire... time.sleep(1) - whitelist = ['FUEL/BTC', 'ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC'] - tickers_dict['FUEL/BTC']['quoteVolume'] = 10000.0 + whitelist = ["FUEL/BTC", "ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC"] + tickers_dict["FUEL/BTC"]["quoteVolume"] = 10000.0 freqtrade.pairlists.refresh_pairlist() assert whitelist == freqtrade.pairlists.whitelist @@ -333,235 +348,468 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): exchange_has=MagicMock(return_value=True), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch(f'{EXMS}.markets', PropertyMock(return_value=markets_empty)) + mocker.patch(f"{EXMS}.markets", PropertyMock(return_value=markets_empty)) # argument: use the whitelist dynamically by exchange-volume whitelist = [] - whitelist_conf['exchange']['pair_whitelist'] = [] + whitelist_conf["exchange"]["pair_whitelist"] = [] freqtrade.pairlists.refresh_pairlist() - pairslist = whitelist_conf['exchange']['pair_whitelist'] + pairslist = whitelist_conf["exchange"]["pair_whitelist"] assert set(whitelist) == set(pairslist) -@pytest.mark.parametrize("pairlists,base_currency,whitelist_result", [ - # VolumePairList only - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), - # No pair for ETH, VolumePairList - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "ETH", []), - # No pair for ETH, StaticPairList - ([{"method": "StaticPairList"}], - "ETH", []), - # No pair for ETH, all handlers - ([{"method": "StaticPairList"}, - {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": None}, - {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.03}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005}, - {"method": "ShuffleFilter"}, {"method": "PerformanceFilter"}], - "ETH", []), - # AgeFilter and VolumePairList (require 2 days only, all should pass age test) - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": 100}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), - # AgeFilter and VolumePairList (require 10 days, all should fail age test) - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "AgeFilter", "min_days_listed": 10, "max_days_listed": None}], - "BTC", []), - # AgeFilter and VolumePairList (all pair listed > 2, all should fail age test) - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "AgeFilter", "min_days_listed": 1, "max_days_listed": 2}], - "BTC", []), - # AgeFilter and VolumePairList LTC/BTC has 6 candles - removes all - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 5}], - "BTC", []), - # AgeFilter and VolumePairList LTC/BTC has 6 candles - passes - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 10}], - "BTC", ["LTC/BTC"]), - # Precisionfilter and quote volume - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PrecisionFilter"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PrecisionFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT']), - # PriceFilter and VolumePairList - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PriceFilter", "low_price_ratio": 0.03}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), - # PriceFilter and VolumePairList - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PriceFilter", "low_price_ratio": 0.03}], - "USDT", ['ETH/USDT', 'NANO/USDT']), - # Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price. - ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, - {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), - # Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price. - ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, - {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}], - "BTC", ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), - # HOT and XRP are removed because below 1250 quoteVolume - ([{"method": "VolumePairList", "number_assets": 5, - "sort_key": "quoteVolume", "min_value": 1250}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), - # HOT, XRP and FUEL whitelisted because they are below 1300 quoteVolume. - ([{"method": "VolumePairList", "number_assets": 5, - "sort_key": "quoteVolume", "max_value": 1300}], - "BTC", ['XRP/BTC', 'HOT/BTC', 'FUEL/BTC']), - # HOT, XRP whitelisted because they are between 100 and 1300 quoteVolume. - ([{"method": "VolumePairList", "number_assets": 5, - "sort_key": "quoteVolume", "min_value": 100, "max_value": 1300}], - "BTC", ['XRP/BTC', 'HOT/BTC']), - # StaticPairlist only - ([{"method": "StaticPairList"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), - # Static Pairlist before VolumePairList - sorting changes - # SpreadFilter - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005}], - "USDT", ['ETH/USDT']), - # ShuffleFilter - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "ShuffleFilter", "seed": 77}], - "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), - # ShuffleFilter, other seed - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "ShuffleFilter", "seed": 42}], - "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), - # ShuffleFilter, no seed - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "ShuffleFilter"}], - "USDT", 4), # whitelist_result is integer -- check only length of randomized pairlist - # AgeFilter only - ([{"method": "AgeFilter", "min_days_listed": 2}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PrecisionFilter after StaticPairList - ([{"method": "StaticPairList"}, - {"method": "PrecisionFilter"}], - "BTC", ['ETH/BTC', 'TKN/BTC']), - # PrecisionFilter only - ([{"method": "PrecisionFilter"}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PriceFilter after StaticPairList - ([{"method": "StaticPairList"}, - {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}], - "BTC", ['ETH/BTC', 'TKN/BTC']), - # PriceFilter only - ([{"method": "PriceFilter", "low_price_ratio": 0.02}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected - # ShuffleFilter after StaticPairList - ([{"method": "StaticPairList"}, - {"method": "ShuffleFilter", "seed": 42}], - "BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']), - # ShuffleFilter only - ([{"method": "ShuffleFilter", "seed": 42}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PerformanceFilter after StaticPairList - ([{"method": "StaticPairList"}, - {"method": "PerformanceFilter"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), - # PerformanceFilter only - ([{"method": "PerformanceFilter"}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected - # SpreadFilter after StaticPairList - ([{"method": "StaticPairList"}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005}], - "BTC", ['ETH/BTC', 'TKN/BTC']), - # SpreadFilter only - ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected - # Static Pairlist after VolumePairList, on a non-first position (appends pairs) - ([{"method": "VolumePairList", "number_assets": 2, "sort_key": "quoteVolume"}, - {"method": "StaticPairList"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']), - ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], - "USDT", ['ETH/USDT', 'NANO/USDT']), - ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, - {"method": "PriceFilter", "max_value": 0.000001}], - "USDT", ['NANO/USDT']), - ([{"method": "StaticPairList"}, - {"method": "RangeStabilityFilter", "lookback_days": 10, - "min_rate_of_change": 0.01, "refresh_period": 1440}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), - ([{"method": "StaticPairList"}, - {"method": "RangeStabilityFilter", "lookback_days": 10, - "max_rate_of_change": 0.01, "refresh_period": 1440}], - "BTC", []), # All removed because of max_rate_of_change being 0.017 - ([{"method": "StaticPairList"}, - {"method": "RangeStabilityFilter", "lookback_days": 10, - "min_rate_of_change": 0.018, "max_rate_of_change": 0.02, "refresh_period": 1440}], - "BTC", []), # All removed - limits are above the highest change_rate - ([{"method": "StaticPairList"}, - {"method": "VolatilityFilter", "lookback_days": 3, - "min_volatility": 0.002, "max_volatility": 0.004, "refresh_period": 1440}], - "BTC", ['ETH/BTC', 'TKN/BTC']), - # VolumePairList with no offset = unchanged pairlist - ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, - {"method": "OffsetFilter", "offset": 0, "number_assets": 0}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), - # VolumePairList with offset = 2 - ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, - {"method": "OffsetFilter", "offset": 2}], - "USDT", ['ADAHALF/USDT', 'ADADOUBLE/USDT']), - # VolumePairList with offset and limit - ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, - {"method": "OffsetFilter", "offset": 1, "number_assets": 2}], - "USDT", ['NANO/USDT', 'ADAHALF/USDT']), - # VolumePairList with higher offset, than total pairlist - ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, - {"method": "OffsetFilter", "offset": 100}], - "USDT", []) -]) -def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - ohlcv_history, pairlists, base_currency, - whitelist_result, caplog) -> None: - whitelist_conf['pairlists'] = pairlists - whitelist_conf['stake_currency'] = base_currency +@pytest.mark.parametrize( + "pairlists,base_currency,whitelist_result", + [ + # VolumePairList only + ( + [{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], + "BTC", + ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC", "HOT/BTC"], + ), + ( + [{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], + "USDT", + ["ETH/USDT", "NANO/USDT", "ADAHALF/USDT", "ADADOUBLE/USDT"], + ), + # No pair for ETH, VolumePairList + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "ETH", []), + # No pair for ETH, StaticPairList + ([{"method": "StaticPairList"}], "ETH", []), + # No pair for ETH, all handlers + ( + [ + {"method": "StaticPairList"}, + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": None}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.03}, + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + {"method": "ShuffleFilter"}, + {"method": "PerformanceFilter"}, + ], + "ETH", + [], + ), + # AgeFilter and VolumePairList (require 2 days only, all should pass age test) + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 2, "max_days_listed": 100}, + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC", "HOT/BTC"], + ), + # AgeFilter and VolumePairList (require 10 days, all should fail age test) + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 10, "max_days_listed": None}, + ], + "BTC", + [], + ), + # AgeFilter and VolumePairList (all pair listed > 2, all should fail age test) + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 1, "max_days_listed": 2}, + ], + "BTC", + [], + ), + # AgeFilter and VolumePairList LTC/BTC has 6 candles - removes all + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 5}, + ], + "BTC", + [], + ), + # AgeFilter and VolumePairList LTC/BTC has 6 candles - passes + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 4, "max_days_listed": 10}, + ], + "BTC", + ["LTC/BTC"], + ), + # Precisionfilter and quote volume + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC"], + ), + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + ], + "USDT", + ["ETH/USDT", "NANO/USDT"], + ), + # PriceFilter and VolumePairList + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "low_price_ratio": 0.03}, + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "LTC/BTC", "XRP/BTC"], + ), + # PriceFilter and VolumePairList + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "low_price_ratio": 0.03}, + ], + "USDT", + ["ETH/USDT", "NANO/USDT"], + ), + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price. + ( + [ + {"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}, + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + ), + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price. + ( + [ + {"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}, + ], + "BTC", + ["TKN/BTC", "LTC/BTC", "XRP/BTC"], + ), + # HOT and XRP are removed because below 1250 quoteVolume + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "min_value": 1250, + } + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + ), + # HOT, XRP and FUEL whitelisted because they are below 1300 quoteVolume. + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "max_value": 1300, + } + ], + "BTC", + ["XRP/BTC", "HOT/BTC", "FUEL/BTC"], + ), + # HOT, XRP whitelisted because they are between 100 and 1300 quoteVolume. + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "min_value": 100, + "max_value": 1300, + } + ], + "BTC", + ["XRP/BTC", "HOT/BTC"], + ), + # StaticPairlist only + ([{"method": "StaticPairList"}], "BTC", ["ETH/BTC", "TKN/BTC", "HOT/BTC"]), + # Static Pairlist before VolumePairList - sorting changes + # SpreadFilter + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + ], + "USDT", + ["ETH/USDT"], + ), + # ShuffleFilter + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "ShuffleFilter", "seed": 77}, + ], + "USDT", + ["ADADOUBLE/USDT", "ETH/USDT", "NANO/USDT", "ADAHALF/USDT"], + ), + # ShuffleFilter, other seed + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "ShuffleFilter", "seed": 42}, + ], + "USDT", + ["ADAHALF/USDT", "NANO/USDT", "ADADOUBLE/USDT", "ETH/USDT"], + ), + # ShuffleFilter, no seed + ( + [ + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "ShuffleFilter"}, + ], + "USDT", + 4, + ), # whitelist_result is integer -- check only length of randomized pairlist + # AgeFilter only + ( + [{"method": "AgeFilter", "min_days_listed": 2}], + "BTC", + "filter_at_the_beginning", + ), # OperationalException expected + # PrecisionFilter after StaticPairList + ( + [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}], + "BTC", + ["ETH/BTC", "TKN/BTC"], + ), + # PrecisionFilter only + ( + [{"method": "PrecisionFilter"}], + "BTC", + "filter_at_the_beginning", + ), # OperationalException expected + # PriceFilter after StaticPairList + ( + [ + {"method": "StaticPairList"}, + { + "method": "PriceFilter", + "low_price_ratio": 0.02, + "min_price": 0.000001, + "max_price": 0.1, + }, + ], + "BTC", + ["ETH/BTC", "TKN/BTC"], + ), + # PriceFilter only + ( + [{"method": "PriceFilter", "low_price_ratio": 0.02}], + "BTC", + "filter_at_the_beginning", + ), # OperationalException expected + # ShuffleFilter after StaticPairList + ( + [{"method": "StaticPairList"}, {"method": "ShuffleFilter", "seed": 42}], + "BTC", + ["TKN/BTC", "ETH/BTC", "HOT/BTC"], + ), + # ShuffleFilter only + ( + [{"method": "ShuffleFilter", "seed": 42}], + "BTC", + "filter_at_the_beginning", + ), # OperationalException expected + # PerformanceFilter after StaticPairList + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + "BTC", + ["ETH/BTC", "TKN/BTC", "HOT/BTC"], + ), + # PerformanceFilter only + ( + [{"method": "PerformanceFilter"}], + "BTC", + "filter_at_the_beginning", + ), # OperationalException expected + # SpreadFilter after StaticPairList + ( + [{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], + "BTC", + ["ETH/BTC", "TKN/BTC"], + ), + # SpreadFilter only + ( + [{"method": "SpreadFilter", "max_spread_ratio": 0.005}], + "BTC", + "filter_at_the_beginning", + ), # OperationalException expected + # Static Pairlist after VolumePairList, on a non-first position (appends pairs) + ( + [ + {"method": "VolumePairList", "number_assets": 2, "sort_key": "quoteVolume"}, + {"method": "StaticPairList"}, + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "TRST/BTC", "SWT/BTC", "BCC/BTC", "HOT/BTC"], + ), + ( + [ + {"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "low_price_ratio": 0.02}, + ], + "USDT", + ["ETH/USDT", "NANO/USDT"], + ), + ( + [ + {"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "max_value": 0.000001}, + ], + "USDT", + ["NANO/USDT"], + ), + ( + [ + {"method": "StaticPairList"}, + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, + "refresh_period": 1440, + }, + ], + "BTC", + ["ETH/BTC", "TKN/BTC", "HOT/BTC"], + ), + ( + [ + {"method": "StaticPairList"}, + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "max_rate_of_change": 0.01, + "refresh_period": 1440, + }, + ], + "BTC", + [], + ), # All removed because of max_rate_of_change being 0.017 + ( + [ + {"method": "StaticPairList"}, + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.018, + "max_rate_of_change": 0.02, + "refresh_period": 1440, + }, + ], + "BTC", + [], + ), # All removed - limits are above the highest change_rate + ( + [ + {"method": "StaticPairList"}, + { + "method": "VolatilityFilter", + "lookback_days": 3, + "min_volatility": 0.002, + "max_volatility": 0.004, + "refresh_period": 1440, + }, + ], + "BTC", + ["ETH/BTC", "TKN/BTC"], + ), + # VolumePairList with no offset = unchanged pairlist + ( + [ + {"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "OffsetFilter", "offset": 0, "number_assets": 0}, + ], + "USDT", + ["ETH/USDT", "NANO/USDT", "ADAHALF/USDT", "ADADOUBLE/USDT"], + ), + # VolumePairList with offset = 2 + ( + [ + {"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "OffsetFilter", "offset": 2}, + ], + "USDT", + ["ADAHALF/USDT", "ADADOUBLE/USDT"], + ), + # VolumePairList with offset and limit + ( + [ + {"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "OffsetFilter", "offset": 1, "number_assets": 2}, + ], + "USDT", + ["NANO/USDT", "ADAHALF/USDT"], + ), + # VolumePairList with higher offset, than total pairlist + ( + [ + {"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "OffsetFilter", "offset": 100}, + ], + "USDT", + [], + ), + ], +) +def test_VolumePairList_whitelist_gen( + mocker, + whitelist_conf, + shitcoinmarkets, + tickers, + ohlcv_history, + pairlists, + base_currency, + whitelist_result, + caplog, +) -> None: + whitelist_conf["pairlists"] = pairlists + whitelist_conf["stake_currency"] = base_currency ohlcv_history_high_vola = ohlcv_history.copy() - ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090 + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, "close"] = 0.00090 ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('LTC/BTC', '1d', CandleType.SPOT): pd.concat([ohlcv_history, ohlcv_history]), - ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_vola, + ("ETH/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("TKN/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("LTC/BTC", "1d", CandleType.SPOT): pd.concat([ohlcv_history, ohlcv_history]), + ("XRP/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("HOT/BTC", "1d", CandleType.SPOT): ohlcv_history_high_vola, } - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple(EXMS, - get_tickers=tickers, - markets=PropertyMock(return_value=shitcoinmarkets) - ) + mocker.patch.multiple( + EXMS, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) + ) mocker.patch.multiple( EXMS, refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) # Provide for PerformanceFilter's dependency - mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=[]) - ) + mocker.patch.multiple( + "freqtrade.persistence.Trade", get_overall_performance=MagicMock(return_value=[]) + ) # Set whitelist_result to None if pairlist is invalid and should produce exception - if whitelist_result == 'filter_at_the_beginning': - with pytest.raises(OperationalException, - match=r"This Pairlist Handler should not be used at the first position " - r"in the list of Pairlist Handlers."): + if whitelist_result == "filter_at_the_beginning": + with pytest.raises( + OperationalException, + match=r"This Pairlist Handler should not be used at the first position " + r"in the list of Pairlist Handlers.", + ): freqtrade.pairlists.refresh_pairlist() else: freqtrade.pairlists.refresh_pairlist() @@ -576,145 +824,276 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert len(whitelist) == whitelist_result for pairlist in pairlists: - if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ - len(ohlcv_history) < pairlist['min_days_listed']: - assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' - r'.* day.*', caplog) - if pairlist['method'] == 'AgeFilter' and pairlist['max_days_listed'] and \ - len(ohlcv_history) > pairlist['max_days_listed']: - assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' - r'.* day.* or more than .* day', caplog) - if pairlist['method'] == 'PrecisionFilter' and whitelist_result: - assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' - r'would be <= stop limit.*', caplog) - if pairlist['method'] == 'PriceFilter' and whitelist_result: - assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or - log_has_re(r'^Removed .* from whitelist, ' - r'because last price < .*%$', caplog) or - log_has_re(r'^Removed .* from whitelist, ' - r'because last price > .*%$', caplog) or - log_has_re(r'^Removed .* from whitelist, ' - r'because min value change of .*', caplog) or - log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " - r"is empty.*", caplog)) - if pairlist['method'] == 'VolumePairList': - logmsg = ("DEPRECATED: using any key other than quoteVolume for " - "VolumePairList is deprecated.") - if pairlist['sort_key'] != 'quoteVolume': + if ( + pairlist["method"] == "AgeFilter" + and pairlist["min_days_listed"] + and len(ohlcv_history) < pairlist["min_days_listed"] + ): + assert log_has_re( + r"^Removed .* from whitelist, because age .* is less than " r".* day.*", caplog + ) + if ( + pairlist["method"] == "AgeFilter" + and pairlist["max_days_listed"] + and len(ohlcv_history) > pairlist["max_days_listed"] + ): + assert log_has_re( + r"^Removed .* from whitelist, because age .* is less than " + r".* day.* or more than .* day", + caplog, + ) + if pairlist["method"] == "PrecisionFilter" and whitelist_result: + assert log_has_re( + r"^Removed .* from whitelist, because stop price .* " + r"would be <= stop limit.*", + caplog, + ) + if pairlist["method"] == "PriceFilter" and whitelist_result: + assert ( + log_has_re(r"^Removed .* from whitelist, because 1 unit is .*%$", caplog) + or log_has_re( + r"^Removed .* from whitelist, " r"because last price < .*%$", caplog + ) + or log_has_re( + r"^Removed .* from whitelist, " r"because last price > .*%$", caplog + ) + or log_has_re( + r"^Removed .* from whitelist, " r"because min value change of .*", caplog + ) + or log_has_re( + r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", + caplog, + ) + ) + if pairlist["method"] == "VolumePairList": + logmsg = ( + "DEPRECATED: using any key other than quoteVolume for " + "VolumePairList is deprecated." + ) + if pairlist["sort_key"] != "quoteVolume": assert log_has(logmsg, caplog) else: assert not log_has(logmsg, caplog) - if pairlist["method"] == 'VolatilityFilter': - assert log_has_re(r'^Removed .* from whitelist, because volatility.*$', caplog) + if pairlist["method"] == "VolatilityFilter": + assert log_has_re(r"^Removed .* from whitelist, because volatility.*$", caplog) -@pytest.mark.parametrize("pairlists,base_currency,exchange,volumefilter_result", [ - # default refresh of 1800 to small for daily candle lookback - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_days": 1}], - "BTC", "binance", "default_refresh_too_short"), # OperationalException expected - # ambiguous configuration with lookback days and period - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_days": 1, "lookback_period": 1}], - "BTC", "binance", "lookback_days_and_period"), # OperationalException expected - # negative lookback period - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_timeframe": "1d", "lookback_period": -1}], - "BTC", "binance", "lookback_period_negative"), # OperationalException expected - # lookback range exceedes exchange limit - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_timeframe": "1m", "lookback_period": 2000, "refresh_period": 3600}], - "BTC", "binance", "lookback_exceeds_exchange_request_size"), # OperationalException expected - # expecting pairs as given - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], - "BTC", "binance", ['LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC', 'HOT/BTC']), - # expecting pairs as input, because 1h candles are not available - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_timeframe": "1h", "lookback_period": 2, "refresh_period": 3600}], - "BTC", "binance", ['ETH/BTC', 'LTC/BTC', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), - # TKN/BTC is removed because it doesn't have enough candles - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_timeframe": "1d", "lookback_period": 6, "refresh_period": 86400}], - "BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'HOT/BTC', 'NEO/BTC']), - # VolumePairlist in range mode as filter. - # TKN/BTC is removed because it doesn't have enough candles - ([{"method": "VolumePairList", "number_assets": 5}, - {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "lookback_timeframe": "1d", "lookback_period": 2, "refresh_period": 86400}], - "BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'TKN/BTC', 'HOT/BTC']), - # ftx data is already in Quote currency, therefore won't require conversion - # ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - # "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], - # "BTC", "ftx", ['HOT/BTC', 'LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC']), -]) +@pytest.mark.parametrize( + "pairlists,base_currency,exchange,volumefilter_result", + [ + # default refresh of 1800 to small for daily candle lookback + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_days": 1, + } + ], + "BTC", + "binance", + "default_refresh_too_short", + ), # OperationalException expected + # ambiguous configuration with lookback days and period + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_days": 1, + "lookback_period": 1, + } + ], + "BTC", + "binance", + "lookback_days_and_period", + ), # OperationalException expected + # negative lookback period + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_timeframe": "1d", + "lookback_period": -1, + } + ], + "BTC", + "binance", + "lookback_period_negative", + ), # OperationalException expected + # lookback range exceedes exchange limit + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_timeframe": "1m", + "lookback_period": 2000, + "refresh_period": 3600, + } + ], + "BTC", + "binance", + "lookback_exceeds_exchange_request_size", + ), # OperationalException expected + # expecting pairs as given + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_timeframe": "1d", + "lookback_period": 1, + "refresh_period": 86400, + } + ], + "BTC", + "binance", + ["LTC/BTC", "ETH/BTC", "TKN/BTC", "XRP/BTC", "HOT/BTC"], + ), + # expecting pairs as input, because 1h candles are not available + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_timeframe": "1h", + "lookback_period": 2, + "refresh_period": 3600, + } + ], + "BTC", + "binance", + ["ETH/BTC", "LTC/BTC", "NEO/BTC", "TKN/BTC", "XRP/BTC"], + ), + # TKN/BTC is removed because it doesn't have enough candles + ( + [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_timeframe": "1d", + "lookback_period": 6, + "refresh_period": 86400, + } + ], + "BTC", + "binance", + ["LTC/BTC", "XRP/BTC", "ETH/BTC", "HOT/BTC", "NEO/BTC"], + ), + # VolumePairlist in range mode as filter. + # TKN/BTC is removed because it doesn't have enough candles + ( + [ + {"method": "VolumePairList", "number_assets": 5}, + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "lookback_timeframe": "1d", + "lookback_period": 2, + "refresh_period": 86400, + }, + ], + "BTC", + "binance", + ["LTC/BTC", "XRP/BTC", "ETH/BTC", "TKN/BTC", "HOT/BTC"], + ), + # ftx data is already in Quote currency, therefore won't require conversion + # ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", + # "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], + # "BTC", "ftx", ['HOT/BTC', 'LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC']), + ], +) def test_VolumePairList_range( - mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history, - pairlists, base_currency, exchange, volumefilter_result, time_machine) -> None: - whitelist_conf['pairlists'] = pairlists - whitelist_conf['stake_currency'] = base_currency - whitelist_conf['exchange']['name'] = exchange + mocker, + whitelist_conf, + shitcoinmarkets, + tickers, + ohlcv_history, + pairlists, + base_currency, + exchange, + volumefilter_result, + time_machine, +) -> None: + whitelist_conf["pairlists"] = pairlists + whitelist_conf["stake_currency"] = base_currency + whitelist_conf["exchange"]["name"] = exchange # Ensure we have 6 candles ohlcv_history_long = pd.concat([ohlcv_history, ohlcv_history]) ohlcv_history_high_vola = ohlcv_history_long.copy() - ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, 'close'] = 0.00090 + ohlcv_history_high_vola.loc[ohlcv_history_high_vola.index == 1, "close"] = 0.00090 # create candles for medium overall volume with last candle high volume ohlcv_history_medium_volume = ohlcv_history_long.copy() - ohlcv_history_medium_volume.loc[ohlcv_history_medium_volume.index == 2, 'volume'] = 5 + ohlcv_history_medium_volume.loc[ohlcv_history_medium_volume.index == 2, "volume"] = 5 # create candles for high volume with all candles high volume, but very low price. ohlcv_history_high_volume = ohlcv_history_long.copy() - ohlcv_history_high_volume['volume'] = 10 - ohlcv_history_high_volume['low'] = ohlcv_history_high_volume.loc[:, 'low'] * 0.01 - ohlcv_history_high_volume['high'] = ohlcv_history_high_volume.loc[:, 'high'] * 0.01 - ohlcv_history_high_volume['close'] = ohlcv_history_high_volume.loc[:, 'close'] * 0.01 + ohlcv_history_high_volume["volume"] = 10 + ohlcv_history_high_volume["low"] = ohlcv_history_high_volume.loc[:, "low"] * 0.01 + ohlcv_history_high_volume["high"] = ohlcv_history_high_volume.loc[:, "high"] * 0.01 + ohlcv_history_high_volume["close"] = ohlcv_history_high_volume.loc[:, "close"] * 0.01 ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history_long, - ('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history_medium_volume, - ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history_high_vola, - ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history_high_volume, + ("ETH/BTC", "1d", CandleType.SPOT): ohlcv_history_long, + ("TKN/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("LTC/BTC", "1d", CandleType.SPOT): ohlcv_history_medium_volume, + ("XRP/BTC", "1d", CandleType.SPOT): ohlcv_history_high_vola, + ("HOT/BTC", "1d", CandleType.SPOT): ohlcv_history_high_volume, } - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) - if volumefilter_result == 'default_refresh_too_short': - with pytest.raises(OperationalException, - match=r'Refresh period of [0-9]+ seconds is smaller than one timeframe ' - r'of [0-9]+.*\. Please adjust refresh_period to at least [0-9]+ ' - r'and restart the bot\.'): + if volumefilter_result == "default_refresh_too_short": + with pytest.raises( + OperationalException, + match=r"Refresh period of [0-9]+ seconds is smaller than one timeframe " + r"of [0-9]+.*\. Please adjust refresh_period to at least [0-9]+ " + r"and restart the bot\.", + ): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) return - elif volumefilter_result == 'lookback_days_and_period': - with pytest.raises(OperationalException, - match=r'Ambigous configuration: lookback_days and lookback_period both ' - r'set in pairlist config\..*'): + elif volumefilter_result == "lookback_days_and_period": + with pytest.raises( + OperationalException, + match=r"Ambigous configuration: lookback_days and lookback_period both " + r"set in pairlist config\..*", + ): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - elif volumefilter_result == 'lookback_period_negative': - with pytest.raises(OperationalException, - match=r'VolumeFilter requires lookback_period to be >= 0'): + elif volumefilter_result == "lookback_period_negative": + with pytest.raises( + OperationalException, match=r"VolumeFilter requires lookback_period to be >= 0" + ): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - elif volumefilter_result == 'lookback_exceeds_exchange_request_size': - with pytest.raises(OperationalException, - match=r'VolumeFilter requires lookback_period to not exceed ' - r'exchange max request size \([0-9]+\)'): + elif volumefilter_result == "lookback_exceeds_exchange_request_size": + with pytest.raises( + OperationalException, + match=r"VolumeFilter requires lookback_period to not exceed " + r"exchange max request size \([0-9]+\)", + ): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) else: freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple( - EXMS, - get_tickers=tickers, - markets=PropertyMock(return_value=shitcoinmarkets) + EXMS, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) ) start_dt = dt_now() time_machine.move_to(start_dt) # remove ohlcv when looback_timeframe != 1d # to enforce fallback to ticker data - if 'lookback_timeframe' in pairlists[0]: - if pairlists[0]['lookback_timeframe'] != '1d': + if "lookback_timeframe" in pairlists[0]: + if pairlists[0]["lookback_timeframe"] != "1d": ohlcv_data = {} ohclv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) @@ -742,21 +1121,22 @@ def test_VolumePairList_range( def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: - whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] - del whitelist_conf['stoploss'] + whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] + del whitelist_conf["stoploss"] - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) - with pytest.raises(OperationalException, - match=r"PrecisionFilter can only work with stoploss defined\..*"): + with pytest.raises( + OperationalException, match=r"PrecisionFilter can only work with stoploss defined\..*" + ): PairListManager(MagicMock, whitelist_conf, MagicMock()) def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: - whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] - if hasattr(Trade, 'session'): + whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] + if hasattr(Trade, "session"): del Trade.session - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, whitelist_conf) pm = PairListManager(exchange, whitelist_conf, MagicMock()) pm.refresh_pairlist() @@ -766,85 +1146,108 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: def test_VolatilityFilter_error(mocker, whitelist_conf) -> None: volatility_filter = {"method": "VolatilityFilter", "lookback_days": -1} - whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter] - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) exchange_mock = MagicMock() exchange_mock.ohlcv_candle_limit = MagicMock(return_value=1000) - with pytest.raises(OperationalException, - match=r"VolatilityFilter requires lookback_days to be >= 1*"): + with pytest.raises( + OperationalException, match=r"VolatilityFilter requires lookback_days to be >= 1*" + ): PairListManager(exchange_mock, whitelist_conf, MagicMock()) volatility_filter = {"method": "VolatilityFilter", "lookback_days": 2000} - whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] - with pytest.raises(OperationalException, - match=r"VolatilityFilter requires lookback_days to not exceed exchange max"): + whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter] + with pytest.raises( + OperationalException, + match=r"VolatilityFilter requires lookback_days to not exceed exchange max", + ): PairListManager(exchange_mock, whitelist_conf, MagicMock()) volatility_filter = {"method": "VolatilityFilter", "sort_direction": "Random"} - whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] - with pytest.raises(OperationalException, - match=r"VolatilityFilter requires sort_direction to be either " - r"None .*'asc'.*'desc'"): + whitelist_conf["pairlists"] = [{"method": "StaticPairList"}, volatility_filter] + with pytest.raises( + OperationalException, + match=r"VolatilityFilter requires sort_direction to be either " r"None .*'asc'.*'desc'", + ): PairListManager(exchange_mock, whitelist_conf, MagicMock()) -@pytest.mark.parametrize('pairlist,expected_pairlist', [ - ({"method": "VolatilityFilter", "sort_direction": "asc"}, - ['XRP/BTC', 'ETH/BTC', 'LTC/BTC', 'TKN/BTC']), - ({"method": "VolatilityFilter", "sort_direction": "desc"}, - ['TKN/BTC', 'LTC/BTC', 'ETH/BTC', 'XRP/BTC']), - ({"method": "VolatilityFilter", "sort_direction": "desc", 'min_volatility': 0.4}, - ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), - ({"method": "VolatilityFilter", "sort_direction": "asc", 'min_volatility': 0.4}, - ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), - ({"method": "VolatilityFilter", "sort_direction": "desc", 'max_volatility': 0.5}, - ['LTC/BTC', 'ETH/BTC', 'XRP/BTC']), - ({"method": "VolatilityFilter", "sort_direction": "asc", 'max_volatility': 0.5}, - ['XRP/BTC', 'ETH/BTC', 'LTC/BTC']), - ({"method": "RangeStabilityFilter", "sort_direction": "asc"}, - ['ETH/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), - ({"method": "RangeStabilityFilter", "sort_direction": "desc"}, - ['TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'ETH/BTC']), - ({"method": "RangeStabilityFilter", "sort_direction": "asc", 'min_rate_of_change': 0.4}, - ['XRP/BTC', 'LTC/BTC', 'TKN/BTC']), - ({"method": "RangeStabilityFilter", "sort_direction": "desc", 'min_rate_of_change': 0.4}, - ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), -]) +@pytest.mark.parametrize( + "pairlist,expected_pairlist", + [ + ( + {"method": "VolatilityFilter", "sort_direction": "asc"}, + ["XRP/BTC", "ETH/BTC", "LTC/BTC", "TKN/BTC"], + ), + ( + {"method": "VolatilityFilter", "sort_direction": "desc"}, + ["TKN/BTC", "LTC/BTC", "ETH/BTC", "XRP/BTC"], + ), + ( + {"method": "VolatilityFilter", "sort_direction": "desc", "min_volatility": 0.4}, + ["TKN/BTC", "LTC/BTC", "ETH/BTC"], + ), + ( + {"method": "VolatilityFilter", "sort_direction": "asc", "min_volatility": 0.4}, + ["ETH/BTC", "LTC/BTC", "TKN/BTC"], + ), + ( + {"method": "VolatilityFilter", "sort_direction": "desc", "max_volatility": 0.5}, + ["LTC/BTC", "ETH/BTC", "XRP/BTC"], + ), + ( + {"method": "VolatilityFilter", "sort_direction": "asc", "max_volatility": 0.5}, + ["XRP/BTC", "ETH/BTC", "LTC/BTC"], + ), + ( + {"method": "RangeStabilityFilter", "sort_direction": "asc"}, + ["ETH/BTC", "XRP/BTC", "LTC/BTC", "TKN/BTC"], + ), + ( + {"method": "RangeStabilityFilter", "sort_direction": "desc"}, + ["TKN/BTC", "LTC/BTC", "XRP/BTC", "ETH/BTC"], + ), + ( + {"method": "RangeStabilityFilter", "sort_direction": "asc", "min_rate_of_change": 0.4}, + ["XRP/BTC", "LTC/BTC", "TKN/BTC"], + ), + ( + {"method": "RangeStabilityFilter", "sort_direction": "desc", "min_rate_of_change": 0.4}, + ["TKN/BTC", "LTC/BTC", "XRP/BTC"], + ), + ], +) def test_VolatilityFilter_RangeStabilityFilter_sort( - mocker, whitelist_conf, tickers, time_machine, pairlist, expected_pairlist) -> None: - whitelist_conf['pairlists'] = [ - {'method': 'VolumePairList', 'number_assets': 10}, - pairlist - ] + mocker, whitelist_conf, tickers, time_machine, pairlist, expected_pairlist +) -> None: + whitelist_conf["pairlists"] = [{"method": "VolumePairList", "number_assets": 10}, pairlist] - df1 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=42) - df2 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=2) - df3 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=3) - df4 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=4) - df5 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=5) - df6 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=6) + df1 = generate_test_data("1d", 10, "2022-01-05 00:00:00+00:00", random_seed=42) + df2 = generate_test_data("1d", 10, "2022-01-05 00:00:00+00:00", random_seed=2) + df3 = generate_test_data("1d", 10, "2022-01-05 00:00:00+00:00", random_seed=3) + df4 = generate_test_data("1d", 10, "2022-01-05 00:00:00+00:00", random_seed=4) + df5 = generate_test_data("1d", 10, "2022-01-05 00:00:00+00:00", random_seed=5) + df6 = generate_test_data("1d", 10, "2022-01-05 00:00:00+00:00", random_seed=6) assert not df1.equals(df2) - time_machine.move_to('2022-01-15 00:00:00+00:00') + time_machine.move_to("2022-01-15 00:00:00+00:00") ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): df1, - ('TKN/BTC', '1d', CandleType.SPOT): df2, - ('LTC/BTC', '1d', CandleType.SPOT): df3, - ('XRP/BTC', '1d', CandleType.SPOT): df4, - ('HOT/BTC', '1d', CandleType.SPOT): df5, - ('BLK/BTC', '1d', CandleType.SPOT): df6, - + ("ETH/BTC", "1d", CandleType.SPOT): df1, + ("TKN/BTC", "1d", CandleType.SPOT): df2, + ("LTC/BTC", "1d", CandleType.SPOT): df3, + ("XRP/BTC", "1d", CandleType.SPOT): df4, + ("HOT/BTC", "1d", CandleType.SPOT): df5, + ("BLK/BTC", "1d", CandleType.SPOT): df6, } ohlcv_mock = MagicMock(return_value=ohlcv_data) mocker.patch.multiple( EXMS, exchange_has=MagicMock(return_value=True), refresh_latest_ohlcv=ohlcv_mock, - get_tickers=tickers - + get_tickers=tickers, ) exchange = get_patched_exchange(mocker, whitelist_conf) @@ -863,9 +1266,9 @@ def test_VolatilityFilter_RangeStabilityFilter_sort( def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: - whitelist_conf['pairlists'] = [ + whitelist_conf["pairlists"] = [ {"method": "StaticPairList"}, - {"method": "ShuffleFilter", "seed": 43} + {"method": "ShuffleFilter", "seed": 43}, ] exchange = get_patched_exchange(mocker, whitelist_conf) @@ -883,7 +1286,7 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: assert plm.whitelist != pl1 caplog.clear() - whitelist_conf['runmode'] = RunMode.DRY_RUN + whitelist_conf["runmode"] = RunMode.DRY_RUN plm = PairListManager(exchange, whitelist_conf) assert not log_has("Backtesting mode detected, applying seed value: 42", caplog) assert log_has("Live mode detected, not applying seed.", caplog) @@ -891,98 +1294,122 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: @pytest.mark.usefixtures("init_persistence") def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> None: - default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'XRP/USDT', 'ETC/USDT']) - default_conf_usdt['pairlists'] = [ + default_conf_usdt["exchange"]["pair_whitelist"].extend(["ADA/USDT", "XRP/USDT", "ETC/USDT"]) + default_conf_usdt["pairlists"] = [ {"method": "StaticPairList"}, - {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01} + {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01}, ] - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] + assert pm.whitelist == ["ETH/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT"] with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: create_mock_trades_usdt(fee) pm.refresh_pairlist() - assert pm.whitelist == ['XRP/USDT', 'NEO/USDT'] - assert log_has_re(r'Removing pair .* since .* is below .*', caplog) + assert pm.whitelist == ["XRP/USDT", "NEO/USDT"] + assert log_has_re(r"Removing pair .* since .* is below .*", caplog) # Move to "outside" of lookback window, so original sorting is restored. t.move_to("2021-09-01 07:00:00 +00:00") pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] + assert pm.whitelist == ["ETH/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT"] @pytest.mark.usefixtures("init_persistence") def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog) -> None: - default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'ETC/USDT']) - default_conf_usdt['pairlists'] = [ + default_conf_usdt["exchange"]["pair_whitelist"].extend(["ADA/USDT", "ETC/USDT"]) + default_conf_usdt["pairlists"] = [ {"method": "StaticPairList", "allow_inactive": True}, - {"method": "PerformanceFilter", "minutes": 60, } + { + "method": "PerformanceFilter", + "minutes": 60, + }, ] - mocker.patch(f'{EXMS}.exchange_has', return_value=True) + mocker.patch(f"{EXMS}.exchange_has", return_value=True) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'LTC/USDT', 'XRP/USDT', - 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'ETC/USDT'] + assert pm.whitelist == [ + "ETH/USDT", + "LTC/USDT", + "XRP/USDT", + "NEO/USDT", + "TKN/USDT", + "ADA/USDT", + "ETC/USDT", + ] with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: create_mock_trades_usdt(fee) pm.refresh_pairlist() - assert pm.whitelist == ['XRP/USDT', 'NEO/USDT', 'ETH/USDT', 'LTC/USDT', - 'TKN/USDT', 'ADA/USDT', 'ETC/USDT', ] + assert pm.whitelist == [ + "XRP/USDT", + "NEO/USDT", + "ETH/USDT", + "LTC/USDT", + "TKN/USDT", + "ADA/USDT", + "ETC/USDT", + ] # assert log_has_re(r'Removing pair .* since .* is below .*', caplog) # Move to "outside" of lookback window, so original sorting is restored. t.move_to("2021-09-01 07:00:00 +00:00") pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'LTC/USDT', 'XRP/USDT', - 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'ETC/USDT'] + assert pm.whitelist == [ + "ETH/USDT", + "LTC/USDT", + "XRP/USDT", + "NEO/USDT", + "TKN/USDT", + "ADA/USDT", + "ETC/USDT", + ] def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] + default_conf["pairlists"] = [{"method": "VolumePairList", "number_assets": 10}] - mocker.patch.multiple(EXMS, - get_tickers=tickers, - exchange_has=MagicMock(return_value=False), - ) + mocker.patch.multiple( + EXMS, + get_tickers=tickers, + exchange_has=MagicMock(return_value=False), + ) - with pytest.raises(OperationalException, - match=r'Exchange does not support dynamic whitelist.*'): + with pytest.raises( + OperationalException, match=r"Exchange does not support dynamic whitelist.*" + ): get_patched_freqtradebot(mocker, default_conf) def test_pair_whitelist_not_supported_Spread(mocker, default_conf, tickers) -> None: - default_conf['pairlists'] = [{'method': 'StaticPairList'}, {'method': 'SpreadFilter'}] + default_conf["pairlists"] = [{"method": "StaticPairList"}, {"method": "SpreadFilter"}] - mocker.patch.multiple(EXMS, - get_tickers=tickers, - exchange_has=MagicMock(return_value=False), - ) + mocker.patch.multiple( + EXMS, + get_tickers=tickers, + exchange_has=MagicMock(return_value=False), + ) - with pytest.raises(OperationalException, - match=r'Exchange does not support fetchTickers, .*'): + with pytest.raises(OperationalException, match=r"Exchange does not support fetchTickers, .*"): get_patched_freqtradebot(mocker, default_conf) - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) - mocker.patch(f'{EXMS}.get_option', MagicMock(return_value=False)) - with pytest.raises(OperationalException, - match=r'.*requires exchange to have bid/ask data'): + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.get_option", MagicMock(return_value=False)) + with pytest.raises(OperationalException, match=r".*requires exchange to have bid/ask data"): get_patched_freqtradebot(mocker, default_conf) @pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS) def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): - whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True) - ) + whitelist_conf["pairlists"][0]["method"] = pairlist + mocker.patch.multiple( + EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) + ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) assert freqtrade.pairlists.name_list == [pairlist] @@ -992,27 +1419,32 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): @pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS) -@pytest.mark.parametrize("whitelist,log_message", [ - (['ETH/BTC', 'TKN/BTC'], ""), - # TRX/ETH not in markets - (['ETH/BTC', 'TKN/BTC', 'TRX/ETH'], "is not compatible with exchange"), - # wrong stake - (['ETH/BTC', 'TKN/BTC', 'ETH/USDT'], "is not compatible with your stake currency"), - # BCH/BTC not available - (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), - # BTT/BTC is inactive - (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active"), - # XLTCUSDT is not a valid pair - (['ETH/BTC', 'TKN/BTC', 'XLTCUSDT'], "is not tradable with Freqtrade"), -]) -def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, - log_message, tickers): - whitelist_conf['pairlists'][0]['method'] = pairlist - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) +@pytest.mark.parametrize( + "whitelist,log_message", + [ + (["ETH/BTC", "TKN/BTC"], ""), + # TRX/ETH not in markets + (["ETH/BTC", "TKN/BTC", "TRX/ETH"], "is not compatible with exchange"), + # wrong stake + (["ETH/BTC", "TKN/BTC", "ETH/USDT"], "is not compatible with your stake currency"), + # BCH/BTC not available + (["ETH/BTC", "TKN/BTC", "BCH/BTC"], "is not compatible with exchange"), + # BTT/BTC is inactive + (["ETH/BTC", "TKN/BTC", "BTT/BTC"], "Market is not active"), + # XLTCUSDT is not a valid pair + (["ETH/BTC", "TKN/BTC", "XLTCUSDT"], "is not tradable with Freqtrade"), + ], +) +def test__whitelist_for_active_markets( + mocker, whitelist_conf, markets, pairlist, whitelist, caplog, log_message, tickers +): + whitelist_conf["pairlists"][0]["method"] = pairlist + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) caplog.clear() @@ -1020,43 +1452,39 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist pairlist_handler = freqtrade.pairlists._pairlist_handlers[0] new_whitelist = pairlist_handler._whitelist_for_active_markets(whitelist) - assert set(new_whitelist) == set(['ETH/BTC', 'TKN/BTC']) + assert set(new_whitelist) == set(["ETH/BTC", "TKN/BTC"]) assert log_message in caplog.text @pytest.mark.parametrize("pairlist", TESTABLE_PAIRLISTS) def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers): - whitelist_conf['pairlists'][0]['method'] = pairlist + whitelist_conf["pairlists"][0]["method"] = pairlist - mocker.patch(f'{EXMS}.exchange_has', return_value=True) + mocker.patch(f"{EXMS}.exchange_has", return_value=True) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=None), - get_tickers=tickers - ) + mocker.patch.multiple(EXMS, markets=PropertyMock(return_value=None), get_tickers=tickers) # Assign starting whitelist pairlist_handler = freqtrade.pairlists._pairlist_handlers[0] - with pytest.raises(OperationalException, match=r'Markets not loaded.*'): - pairlist_handler._whitelist_for_active_markets(['ETH/BTC']) + with pytest.raises(OperationalException, match=r"Markets not loaded.*"): + pairlist_handler._whitelist_for_active_markets(["ETH/BTC"]) def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): - whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) + whitelist_conf["pairlists"][0].update({"sort_key": "asdf"}) - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) - with pytest.raises(OperationalException, - match=r"key asdf not in .*"): + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) + with pytest.raises(OperationalException, match=r"key asdf not in .*"): get_patched_freqtradebot(mocker, whitelist_conf) def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): - - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) assert len(freqtrade.pairlists._pairlist_handlers[0]._pair_cache) == 0 assert tickers.call_count == 0 @@ -1069,58 +1497,70 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'AgeFilter', 'min_days_listed': -1}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "AgeFilter", "min_days_listed": -1}, + ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) - with pytest.raises(OperationalException, - match=r'AgeFilter requires min_days_listed to be >= 1'): + with pytest.raises( + OperationalException, match=r"AgeFilter requires min_days_listed to be >= 1" + ): get_patched_freqtradebot(mocker, default_conf) def test_agefilter_max_days_lower_than_min_days(mocker, default_conf, markets, tickers): - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'AgeFilter', 'min_days_listed': 3, - "max_days_listed": 2}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "AgeFilter", "min_days_listed": 3, "max_days_listed": 2}, + ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) - with pytest.raises(OperationalException, - match=r'AgeFilter max_days_listed <= min_days_listed not permitted'): + with pytest.raises( + OperationalException, match=r"AgeFilter max_days_listed <= min_days_listed not permitted" + ): get_patched_freqtradebot(mocker, default_conf) def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'AgeFilter', 'min_days_listed': 99999}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "AgeFilter", "min_days_listed": 99999}, + ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) - with pytest.raises(OperationalException, - match=r'AgeFilter requires min_days_listed to not exceed ' - r'exchange max request size \([0-9]+\)'): + with pytest.raises( + OperationalException, + match=r"AgeFilter requires min_days_listed to not exceed " + r"exchange max request size \([0-9]+\)", + ): get_patched_freqtradebot(mocker, default_conf) def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history): with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, + ("ETH/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("TKN/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("LTC/BTC", "1d", CandleType.SPOT): ohlcv_history, } mocker.patch.multiple( EXMS, @@ -1142,19 +1582,19 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 2 ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history.iloc[[0]], + ("ETH/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("TKN/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("LTC/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("XRP/BTC", "1d", CandleType.SPOT): ohlcv_history.iloc[[0]], } - mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 # Move to next day t.move_to("2021-09-02 01:00:00 +00:00") - mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 @@ -1163,12 +1603,12 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o t.move_to("2021-09-03 01:00:00 +00:00") # Called once for XRP/BTC ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history, + ("ETH/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("TKN/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("LTC/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("XRP/BTC", "1d", CandleType.SPOT): ohlcv_history, } - mocker.patch(f'{EXMS}.refresh_latest_ohlcv', return_value=ohlcv_data) + mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 4 # Called once (only for XRP/BTC) @@ -1176,70 +1616,99 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o def test_OffsetFilter_error(mocker, whitelist_conf) -> None: - whitelist_conf['pairlists'] = ( - [{"method": "StaticPairList"}, {"method": "OffsetFilter", "offset": -1}] - ) + whitelist_conf["pairlists"] = [ + {"method": "StaticPairList"}, + {"method": "OffsetFilter", "offset": -1}, + ] - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) - with pytest.raises(OperationalException, - match=r'OffsetFilter requires offset to be >= 0'): + with pytest.raises(OperationalException, match=r"OffsetFilter requires offset to be >= 0"): PairListManager(MagicMock, whitelist_conf) def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'RangeStabilityFilter', 'lookback_days': 99999}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "RangeStabilityFilter", "lookback_days": 99999}, + ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) - with pytest.raises(OperationalException, - match=r'RangeStabilityFilter requires lookback_days to not exceed ' - r'exchange max request size \([0-9]+\)'): + with pytest.raises( + OperationalException, + match=r"RangeStabilityFilter requires lookback_days to not exceed " + r"exchange max request size \([0-9]+\)", + ): get_patched_freqtradebot(mocker, default_conf) - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'RangeStabilityFilter', 'lookback_days': 0}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "RangeStabilityFilter", "lookback_days": 0}, + ] - with pytest.raises(OperationalException, - match='RangeStabilityFilter requires lookback_days to be >= 1'): + with pytest.raises( + OperationalException, match="RangeStabilityFilter requires lookback_days to be >= 1" + ): get_patched_freqtradebot(mocker, default_conf) - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'RangeStabilityFilter', 'sort_direction': 'something'}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "RangeStabilityFilter", "sort_direction": "something"}, + ] - with pytest.raises(OperationalException, - match='RangeStabilityFilter requires sort_direction to be either None.*'): + with pytest.raises( + OperationalException, + match="RangeStabilityFilter requires sort_direction to be either None.*", + ): get_patched_freqtradebot(mocker, default_conf) -@pytest.mark.parametrize('min_rate_of_change,max_rate_of_change,expected_length', [ - (0.01, 0.99, 5), - (0.05, 0.0, 0), # Setting min rate_of_change to 5% removes all pairs from the whitelist. -]) -def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history, - min_rate_of_change, max_rate_of_change, expected_length): - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'RangeStabilityFilter', 'lookback_days': 2, - 'min_rate_of_change': min_rate_of_change, - "max_rate_of_change": max_rate_of_change}] +@pytest.mark.parametrize( + "min_rate_of_change,max_rate_of_change,expected_length", + [ + (0.01, 0.99, 5), + (0.05, 0.0, 0), # Setting min rate_of_change to 5% removes all pairs from the whitelist. + ], +) +def test_rangestabilityfilter_caching( + mocker, + markets, + default_conf, + tickers, + ohlcv_history, + min_rate_of_change, + max_rate_of_change, + expected_length, +): + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + { + "method": "RangeStabilityFilter", + "lookback_days": 2, + "min_rate_of_change": min_rate_of_change, + "max_rate_of_change": max_rate_of_change, + }, + ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) ohlcv_data = { - ('ETH/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('TKN/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('LTC/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('XRP/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('HOT/BTC', '1d', CandleType.SPOT): ohlcv_history, - ('BLK/BTC', '1d', CandleType.SPOT): ohlcv_history, + ("ETH/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("TKN/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("LTC/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("XRP/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("HOT/BTC", "1d", CandleType.SPOT): ohlcv_history, + ("BLK/BTC", "1d", CandleType.SPOT): ohlcv_history, } mocker.patch.multiple( EXMS, @@ -1260,369 +1729,447 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplog): - default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'SpreadFilter', 'max_spread_ratio': 0.1}] + default_conf["pairlists"] = [ + {"method": "VolumePairList", "number_assets": 10}, + {"method": "SpreadFilter", "max_spread_ratio": 0.1}, + ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + ) ftbot = get_patched_freqtradebot(mocker, default_conf) ftbot.pairlists.refresh_pairlist() assert len(ftbot.pairlists.whitelist) == 5 - tickers.return_value['ETH/BTC']['ask'] = 0.0 - del tickers.return_value['TKN/BTC'] - del tickers.return_value['LTC/BTC'] + tickers.return_value["ETH/BTC"]["ask"] = 0.0 + del tickers.return_value["TKN/BTC"] + del tickers.return_value["LTC/BTC"] mocker.patch.multiple(EXMS, get_tickers=tickers) ftbot.pairlists.refresh_pairlist() - assert log_has_re(r'Removed .* invalid ticker data.*', caplog) + assert log_has_re(r"Removed .* invalid ticker data.*", caplog) assert len(ftbot.pairlists.whitelist) == 2 -@pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ - ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, - "max_price": 1.0}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " - "0.1% or below 0.00000010 or above 1.00000000.'}]", - None - ), - ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% " - "or below 0.00000010.'}]", - None - ), - ({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% " - "or above 1.00010000.'}]", - None - ), - ({"method": "PriceFilter", "min_price": 0.00002000}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", - None - ), - ({"method": "PriceFilter", "max_value": 0.00002000}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]", - None - ), - ({"method": "PriceFilter"}, - "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", - None - ), - ({"method": "PriceFilter", "low_price_ratio": -0.001}, - None, - "PriceFilter requires low_price_ratio to be >= 0" - ), # OperationalException expected - ({"method": "PriceFilter", "min_price": -0.00000010}, - None, - "PriceFilter requires min_price to be >= 0" - ), # OperationalException expected - ({"method": "PriceFilter", "max_price": -1.00010000}, - None, - "PriceFilter requires max_price to be >= 0" - ), # OperationalException expected - ({"method": "PriceFilter", "max_value": -1.00010000}, - None, - "PriceFilter requires max_value to be >= 0" - ), # OperationalException expected - ({"method": "RangeStabilityFilter", "lookback_days": 10, - "min_rate_of_change": 0.01}, - "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate " - "of change below 0.01 over the last days.'}]", - None - ), - ({"method": "RangeStabilityFilter", "lookback_days": 10, - "min_rate_of_change": 0.01, "max_rate_of_change": 0.99}, - "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate " - "of change below 0.01 and above 0.99 over the last days.'}]", - None - ), - ({"method": "OffsetFilter", "offset": 5, "number_assets": 10}, - "[{'OffsetFilter': 'OffsetFilter - Taking 10 Pairs, starting from 5.'}]", - None - ), - ({"method": "ProducerPairList"}, - "[{'ProducerPairList': 'ProducerPairList - default'}]", - None - ), - ({"method": "RemotePairList", "number_assets": 10, "pairlist_url": "https://example.com"}, - "[{'RemotePairList': 'RemotePairList - 10 pairs from RemotePairlist.'}]", - None - ), -]) -def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, - desc_expected, exception_expected): - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True) - ) - whitelist_conf['pairlists'] = [pairlistconfig] +@pytest.mark.parametrize( + "pairlistconfig,desc_expected,exception_expected", + [ + ( + { + "method": "PriceFilter", + "low_price_ratio": 0.001, + "min_price": 0.00000010, + "max_price": 1.0, + }, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " + "0.1% or below 0.00000010 or above 1.00000000.'}]", + None, + ), + ( + {"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% " + "or below 0.00000010.'}]", + None, + ), + ( + {"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% " + "or above 1.00010000.'}]", + None, + ), + ( + {"method": "PriceFilter", "min_price": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", + None, + ), + ( + {"method": "PriceFilter", "max_value": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced Value above 0.00002000.'}]", + None, + ), + ( + {"method": "PriceFilter"}, + "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", + None, + ), + ( + {"method": "PriceFilter", "low_price_ratio": -0.001}, + None, + "PriceFilter requires low_price_ratio to be >= 0", + ), # OperationalException expected + ( + {"method": "PriceFilter", "min_price": -0.00000010}, + None, + "PriceFilter requires min_price to be >= 0", + ), # OperationalException expected + ( + {"method": "PriceFilter", "max_price": -1.00010000}, + None, + "PriceFilter requires max_price to be >= 0", + ), # OperationalException expected + ( + {"method": "PriceFilter", "max_value": -1.00010000}, + None, + "PriceFilter requires max_value to be >= 0", + ), # OperationalException expected + ( + {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, + "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate " + "of change below 0.01 over the last days.'}]", + None, + ), + ( + { + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, + "max_rate_of_change": 0.99, + }, + "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate " + "of change below 0.01 and above 0.99 over the last days.'}]", + None, + ), + ( + {"method": "OffsetFilter", "offset": 5, "number_assets": 10}, + "[{'OffsetFilter': 'OffsetFilter - Taking 10 Pairs, starting from 5.'}]", + None, + ), + ( + {"method": "ProducerPairList"}, + "[{'ProducerPairList': 'ProducerPairList - default'}]", + None, + ), + ( + { + "method": "RemotePairList", + "number_assets": 10, + "pairlist_url": "https://example.com", + }, + "[{'RemotePairList': 'RemotePairList - 10 pairs from RemotePairlist.'}]", + None, + ), + ], +) +def test_pricefilter_desc( + mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected +): + mocker.patch.multiple( + EXMS, markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) + ) + whitelist_conf["pairlists"] = [pairlistconfig] if desc_expected is not None: freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) short_desc = str(freqtrade.pairlists.short_desc()) assert short_desc == desc_expected else: # OperationalException expected - with pytest.raises(OperationalException, - match=exception_expected): + with pytest.raises(OperationalException, match=exception_expected): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) - whitelist_conf['pairlists'] = [] + whitelist_conf["pairlists"] = [] - with pytest.raises(OperationalException, - match=r"No Pairlist Handlers defined"): + with pytest.raises(OperationalException, match=r"No Pairlist Handlers defined"): get_patched_freqtradebot(mocker, whitelist_conf) -@pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ - # No trades yet - ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], [], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), - # Happy path: Descending order, all values filled - ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - ['ETH/BTC', 'TKN/BTC'], - [{'pair': 'TKN/BTC', 'profit_ratio': 0.05, 'count': 3}, - {'pair': 'ETH/BTC', 'profit_ratio': 0.04, 'count': 2}], - ['TKN/BTC', 'ETH/BTC']), - # Performance data outside allow list ignored - ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - ['ETH/BTC', 'TKN/BTC'], - [{'pair': 'OTHER/BTC', 'profit_ratio': 0.05, 'count': 3}, - {'pair': 'ETH/BTC', 'profit_ratio': 0.04, 'count': 2}], - ['ETH/BTC', 'TKN/BTC']), - # Partial performance data missing and sorted between positive and negative profit - ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], - [{'pair': 'ETH/BTC', 'profit_ratio': -0.05, 'count': 100}, - {'pair': 'TKN/BTC', 'profit_ratio': 0.04, 'count': 2}], - ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), - # Tie in performance data broken by count (ascending) - ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], - [{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 101}, - {'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 2}, - {'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 100}], - ['TKN/BTC', 'ETH/BTC', 'LTC/BTC']), - # Tie in performance and count, broken by prior sorting sort - ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], - [{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 1}, - {'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 1}, - {'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 1}], - ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), -]) -def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, - allowlist_result, tickers, markets, ohlcv_history_list): +@pytest.mark.parametrize( + "pairlists,pair_allowlist,overall_performance,allowlist_result", + [ + # No trades yet + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + [], + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + ), + # Happy path: Descending order, all values filled + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ["ETH/BTC", "TKN/BTC"], + [ + {"pair": "TKN/BTC", "profit_ratio": 0.05, "count": 3}, + {"pair": "ETH/BTC", "profit_ratio": 0.04, "count": 2}, + ], + ["TKN/BTC", "ETH/BTC"], + ), + # Performance data outside allow list ignored + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ["ETH/BTC", "TKN/BTC"], + [ + {"pair": "OTHER/BTC", "profit_ratio": 0.05, "count": 3}, + {"pair": "ETH/BTC", "profit_ratio": 0.04, "count": 2}, + ], + ["ETH/BTC", "TKN/BTC"], + ), + # Partial performance data missing and sorted between positive and negative profit + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + [ + {"pair": "ETH/BTC", "profit_ratio": -0.05, "count": 100}, + {"pair": "TKN/BTC", "profit_ratio": 0.04, "count": 2}, + ], + ["TKN/BTC", "LTC/BTC", "ETH/BTC"], + ), + # Tie in performance data broken by count (ascending) + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + [ + {"pair": "LTC/BTC", "profit_ratio": -0.0501, "count": 101}, + {"pair": "TKN/BTC", "profit_ratio": -0.0501, "count": 2}, + {"pair": "ETH/BTC", "profit_ratio": -0.0501, "count": 100}, + ], + ["TKN/BTC", "ETH/BTC", "LTC/BTC"], + ), + # Tie in performance and count, broken by prior sorting sort + ( + [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + [ + {"pair": "LTC/BTC", "profit_ratio": -0.0501, "count": 1}, + {"pair": "TKN/BTC", "profit_ratio": -0.0501, "count": 1}, + {"pair": "ETH/BTC", "profit_ratio": -0.0501, "count": 1}, + ], + ["ETH/BTC", "TKN/BTC", "LTC/BTC"], + ), + ], +) +def test_performance_filter( + mocker, + whitelist_conf, + pairlists, + pair_allowlist, + overall_performance, + allowlist_result, + tickers, + markets, + ohlcv_history_list, +): allowlist_conf = whitelist_conf - allowlist_conf['pairlists'] = pairlists - allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist + allowlist_conf["pairlists"] = pairlists + allowlist_conf["exchange"]["pair_whitelist"] = pair_allowlist - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) - mocker.patch.multiple(EXMS, - get_tickers=tickers, - markets=PropertyMock(return_value=markets) - ) - mocker.patch.multiple(EXMS, - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) - mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=overall_performance), - ) + mocker.patch.multiple(EXMS, get_tickers=tickers, markets=PropertyMock(return_value=markets)) + mocker.patch.multiple( + EXMS, + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + mocker.patch.multiple( + "freqtrade.persistence.Trade", + get_overall_performance=MagicMock(return_value=overall_performance), + ) freqtrade.pairlists.refresh_pairlist() allowlist = freqtrade.pairlists.whitelist assert allowlist == allowlist_result -@pytest.mark.parametrize('wildcardlist,pairs,expected', [ - (['BTC/USDT'], - ['BTC/USDT'], - ['BTC/USDT']), - (['BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETH/USDT']), - (['BTC/USDT', 'ETH/USDT'], - ['BTC/USDT'], ['BTC/USDT']), # Test one too many - (['.*/USDT'], - ['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple - (['.*C/USDT'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one - (['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], - ['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one - (['BTC/.*', 'ETH/.*'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'], - ['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one - (['*UP/USDT', 'BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], - None), - (['BTC/USD'], - ['BTC/USD', 'BTC/USDT'], - ['BTC/USD']), -]) +@pytest.mark.parametrize( + "wildcardlist,pairs,expected", + [ + (["BTC/USDT"], ["BTC/USDT"], ["BTC/USDT"]), + (["BTC/USDT", "ETH/USDT"], ["BTC/USDT", "ETH/USDT"], ["BTC/USDT", "ETH/USDT"]), + (["BTC/USDT", "ETH/USDT"], ["BTC/USDT"], ["BTC/USDT"]), # Test one too many + ([".*/USDT"], ["BTC/USDT", "ETH/USDT"], ["BTC/USDT", "ETH/USDT"]), # Wildcard simple + ( + [".*C/USDT"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT"], + ["BTC/USDT", "ETC/USDT"], + ), # Wildcard exclude one + ( + [".*UP/USDT", "BTC/USDT", "ETH/USDT"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT", "BTCUP/USDT", "XRPUP/USDT", "XRPDOWN/USDT"], + ["BTC/USDT", "ETH/USDT", "BTCUP/USDT", "XRPUP/USDT"], + ), # Wildcard exclude one + ( + ["BTC/.*", "ETH/.*"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT", "BTC/USD", "ETH/EUR", "BTC/GBP"], + ["BTC/USDT", "ETH/USDT", "BTC/USD", "ETH/EUR", "BTC/GBP"], + ), # Wildcard exclude one + ( + ["*UP/USDT", "BTC/USDT", "ETH/USDT"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT", "BTCUP/USDT", "XRPUP/USDT", "XRPDOWN/USDT"], + None, + ), + (["BTC/USD"], ["BTC/USD", "BTC/USDT"], ["BTC/USD"]), + ], +) def test_expand_pairlist(wildcardlist, pairs, expected): if expected is None: - with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'): + with pytest.raises(ValueError, match=r"Wildcard error in \*UP/USDT,"): expand_pairlist(wildcardlist, pairs) else: assert sorted(expand_pairlist(wildcardlist, pairs)) == sorted(expected) conf = { - 'pairs': wildcardlist, - 'freqai': { + "pairs": wildcardlist, + "freqai": { "enabled": True, "feature_parameters": { "include_corr_pairlist": [ "BTC/USDT:USDT", "XRP/BUSD", ] - } - } + }, + }, } - assert sorted(dynamic_expand_pairlist(conf, pairs)) == sorted(expected + [ - "BTC/USDT:USDT", - "XRP/BUSD", - ]) + assert sorted(dynamic_expand_pairlist(conf, pairs)) == sorted( + expected + + [ + "BTC/USDT:USDT", + "XRP/BUSD", + ] + ) -@pytest.mark.parametrize('wildcardlist,pairs,expected', [ - (['BTC/USDT'], - ['BTC/USDT'], - ['BTC/USDT']), - (['BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETH/USDT']), - (['BTC/USDT', 'ETH/USDT'], - ['BTC/USDT'], ['BTC/USDT', 'ETH/USDT']), # Test one too many - (['.*/USDT'], - ['BTC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETH/USDT']), # Wildcard simple - (['.*C/USDT'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT'], ['BTC/USDT', 'ETC/USDT']), # Wildcard exclude one - (['.*UP/USDT', 'BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], - ['BTC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT']), # Wildcard exclude one - (['BTC/.*', 'ETH/.*'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP'], - ['BTC/USDT', 'ETH/USDT', 'BTC/USD', 'ETH/EUR', 'BTC/GBP']), # Wildcard exclude one - (['*UP/USDT', 'BTC/USDT', 'ETH/USDT'], - ['BTC/USDT', 'ETC/USDT', 'ETH/USDT', 'BTCUP/USDT', 'XRPUP/USDT', 'XRPDOWN/USDT'], - None), - (['HELLO/WORLD'], [], ['HELLO/WORLD']), # Invalid pair kept - (['BTC/USD'], - ['BTC/USD', 'BTC/USDT'], - ['BTC/USD']), - (['BTC/USDT:USDT'], - ['BTC/USDT:USDT', 'BTC/USDT'], - ['BTC/USDT:USDT']), - (['BB_BTC/USDT', 'CC_BTC/USDT', 'AA_ETH/USDT', 'XRP/USDT', 'ETH/USDT', 'XX_BTC/USDT'], - ['BTC/USDT', 'ETH/USDT'], - ['XRP/USDT', 'ETH/USDT']), -]) +@pytest.mark.parametrize( + "wildcardlist,pairs,expected", + [ + (["BTC/USDT"], ["BTC/USDT"], ["BTC/USDT"]), + (["BTC/USDT", "ETH/USDT"], ["BTC/USDT", "ETH/USDT"], ["BTC/USDT", "ETH/USDT"]), + (["BTC/USDT", "ETH/USDT"], ["BTC/USDT"], ["BTC/USDT", "ETH/USDT"]), # Test one too many + ([".*/USDT"], ["BTC/USDT", "ETH/USDT"], ["BTC/USDT", "ETH/USDT"]), # Wildcard simple + ( + [".*C/USDT"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT"], + ["BTC/USDT", "ETC/USDT"], + ), # Wildcard exclude one + ( + [".*UP/USDT", "BTC/USDT", "ETH/USDT"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT", "BTCUP/USDT", "XRPUP/USDT", "XRPDOWN/USDT"], + ["BTC/USDT", "ETH/USDT", "BTCUP/USDT", "XRPUP/USDT"], + ), # Wildcard exclude one + ( + ["BTC/.*", "ETH/.*"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT", "BTC/USD", "ETH/EUR", "BTC/GBP"], + ["BTC/USDT", "ETH/USDT", "BTC/USD", "ETH/EUR", "BTC/GBP"], + ), # Wildcard exclude one + ( + ["*UP/USDT", "BTC/USDT", "ETH/USDT"], + ["BTC/USDT", "ETC/USDT", "ETH/USDT", "BTCUP/USDT", "XRPUP/USDT", "XRPDOWN/USDT"], + None, + ), + (["HELLO/WORLD"], [], ["HELLO/WORLD"]), # Invalid pair kept + (["BTC/USD"], ["BTC/USD", "BTC/USDT"], ["BTC/USD"]), + (["BTC/USDT:USDT"], ["BTC/USDT:USDT", "BTC/USDT"], ["BTC/USDT:USDT"]), + ( + ["BB_BTC/USDT", "CC_BTC/USDT", "AA_ETH/USDT", "XRP/USDT", "ETH/USDT", "XX_BTC/USDT"], + ["BTC/USDT", "ETH/USDT"], + ["XRP/USDT", "ETH/USDT"], + ), + ], +) def test_expand_pairlist_keep_invalid(wildcardlist, pairs, expected): if expected is None: - with pytest.raises(ValueError, match=r'Wildcard error in \*UP/USDT,'): + with pytest.raises(ValueError, match=r"Wildcard error in \*UP/USDT,"): expand_pairlist(wildcardlist, pairs, keep_invalid=True) else: assert sorted(expand_pairlist(wildcardlist, pairs, keep_invalid=True)) == sorted(expected) def test_ProducerPairlist_no_emc(mocker, whitelist_conf): - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) - whitelist_conf['pairlists'] = [ + whitelist_conf["pairlists"] = [ { "method": "ProducerPairList", "number_assets": 10, "producer_name": "hello_world", } ] - del whitelist_conf['external_message_consumer'] + del whitelist_conf["external_message_consumer"] - with pytest.raises(OperationalException, - match=r"ProducerPairList requires external_message_consumer to be enabled."): + with pytest.raises( + OperationalException, + match=r"ProducerPairList requires external_message_consumer to be enabled.", + ): get_patched_freqtradebot(mocker, whitelist_conf) def test_ProducerPairlist(mocker, whitelist_conf, markets): - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - ) - whitelist_conf['pairlists'] = [ + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) + mocker.patch.multiple( + EXMS, + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + ) + whitelist_conf["pairlists"] = [ { "method": "ProducerPairList", "number_assets": 2, "producer_name": "hello_world", } ] - whitelist_conf.update({ - "external_message_consumer": { - "enabled": True, - "producers": [ - { - "name": "hello_world", - "host": "null", - "port": 9891, - "ws_token": "dummy", - } - ] + whitelist_conf.update( + { + "external_message_consumer": { + "enabled": True, + "producers": [ + { + "name": "hello_world", + "host": "null", + "port": 9891, + "ws_token": "dummy", + } + ], + } } - }) + ) exchange = get_patched_exchange(mocker, whitelist_conf) dp = DataProvider(whitelist_conf, exchange, None) - pairs = ['ETH/BTC', 'LTC/BTC', 'XRP/BTC'] + pairs = ["ETH/BTC", "LTC/BTC", "XRP/BTC"] # different producer - dp._set_producer_pairs(pairs + ['MEEP/USDT'], 'default') + dp._set_producer_pairs(pairs + ["MEEP/USDT"], "default") pm = PairListManager(exchange, whitelist_conf, dp) pm.refresh_pairlist() assert pm.whitelist == [] # proper producer - dp._set_producer_pairs(pairs, 'hello_world') + dp._set_producer_pairs(pairs, "hello_world") pm.refresh_pairlist() # Pairlist reduced to 2 assert pm.whitelist == pairs[:2] assert len(pm.whitelist) == 2 - whitelist_conf['exchange']['pair_whitelist'] = ['TKN/BTC'] + whitelist_conf["exchange"]["pair_whitelist"] = ["TKN/BTC"] - whitelist_conf['pairlists'] = [ + whitelist_conf["pairlists"] = [ {"method": "StaticPairList"}, { "method": "ProducerPairList", "producer_name": "hello_world", - } + }, ] pm = PairListManager(exchange, whitelist_conf, dp) pm.refresh_pairlist() assert len(pm.whitelist) == 4 - assert pm.whitelist == ['TKN/BTC'] + pairs + assert pm.whitelist == ["TKN/BTC"] + pairs @pytest.mark.usefixtures("init_persistence") def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None: - default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'XRP/USDT', 'ETC/USDT']) - default_conf_usdt['pairlists'] = [ - {"method": "StaticPairList"}, - {"method": "FullTradesFilter"} - ] - default_conf_usdt['max_open_trades'] = -1 - mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + default_conf_usdt["exchange"]["pair_whitelist"].extend(["ADA/USDT", "XRP/USDT", "ETC/USDT"]) + default_conf_usdt["pairlists"] = [{"method": "StaticPairList"}, {"method": "FullTradesFilter"}] + default_conf_usdt["max_open_trades"] = -1 + mocker.patch(f"{EXMS}.exchange_has", MagicMock(return_value=True)) exchange = get_patched_exchange(mocker, default_conf_usdt) pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] + assert pm.whitelist == ["ETH/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT"] with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: create_mock_trades_usdt(fee) @@ -1630,13 +2177,13 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None: # Unlimited max open trades, so no change to whitelist pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] + assert pm.whitelist == ["ETH/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT"] # Set max_open_trades to 4, the filter should empty the whitelist - default_conf_usdt['max_open_trades'] = 4 + default_conf_usdt["max_open_trades"] = 4 pm.refresh_pairlist() assert pm.whitelist == [] - assert log_has_re(r'Whitelist with 0 pairs: \[]', caplog) + assert log_has_re(r"Whitelist with 0 pairs: \[]", caplog) list_trades = LocalTrade.get_open_trades() assert len(list_trades) == 4 @@ -1650,56 +2197,90 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None: list_trades = LocalTrade.get_open_trades() assert len(list_trades) == 3 pm.refresh_pairlist() - assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] + assert pm.whitelist == ["ETH/USDT", "XRP/USDT", "NEO/USDT", "TKN/USDT"] # Set max_open_trades to 3, the filter should empty the whitelist - default_conf_usdt['max_open_trades'] = 3 + default_conf_usdt["max_open_trades"] = 3 pm.refresh_pairlist() assert pm.whitelist == [] - assert log_has_re(r'Whitelist with 0 pairs: \[]', caplog) + 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']), -]) +@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 + mocker, default_conf_usdt, trade_mode, markets, pairlists, result ): test_value = [ {"symbol": "btc"}, @@ -1714,17 +2295,20 @@ def test_MarketCapPairList_filter( {"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), - ) + 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) + mocker.patch( + "freqtrade.plugins.pairlist.MarketCapPairList.CoinGeckoAPI.get_coins_markets", + return_value=test_value, + ) exchange = get_patched_exchange(mocker, default_conf_usdt) @@ -1748,18 +2332,21 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi {"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}] + 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.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) + mocker.patch( + "freqtrade.plugins.pairlist.MarketCapPairList.CoinGeckoAPI.get_coins_markets", + return_value=test_value, + ) start_dt = dt_now() @@ -1785,16 +2372,16 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi def test_MarketCapPairList_exceptions(mocker, default_conf_usdt): - exchange = get_patched_exchange(mocker, default_conf_usdt) - default_conf_usdt['pairlists'] = [{"method": "MarketCapPairList"}] + 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."): + 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) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index 6e209df60..0102079fe 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -7,10 +7,10 @@ from freqtrade.persistence.models import PairLock from freqtrade.util import dt_now -@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.parametrize("use_db", (False, True)) @pytest.mark.usefixtures("init_persistence") def test_PairLocks(use_db): - PairLocks.timeframe = '5m' + PairLocks.timeframe = "5m" PairLocks.use_db = use_db # No lock should be present if use_db: @@ -18,28 +18,28 @@ def test_PairLocks(use_db): assert PairLocks.use_db == use_db - pair = 'ETH/BTC' + pair = "ETH/BTC" assert not PairLocks.is_pair_locked(pair) PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4)) # ETH/BTC locked for 4 minutes (on both sides) assert PairLocks.is_pair_locked(pair) - assert PairLocks.is_pair_locked(pair, side='long') - assert PairLocks.is_pair_locked(pair, side='short') + assert PairLocks.is_pair_locked(pair, side="long") + assert PairLocks.is_pair_locked(pair, side="short") - pair = 'BNB/BTC' - PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4), side='long') + pair = "BNB/BTC" + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4), side="long") assert not PairLocks.is_pair_locked(pair) - assert PairLocks.is_pair_locked(pair, side='long') - assert not PairLocks.is_pair_locked(pair, side='short') + assert PairLocks.is_pair_locked(pair, side="long") + assert not PairLocks.is_pair_locked(pair, side="short") - pair = 'BNB/USDT' - PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4), side='short') + pair = "BNB/USDT" + PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4), side="short") assert not PairLocks.is_pair_locked(pair) - assert not PairLocks.is_pair_locked(pair, side='long') - assert PairLocks.is_pair_locked(pair, side='short') + assert not PairLocks.is_pair_locked(pair, side="long") + assert PairLocks.is_pair_locked(pair, side="short") # XRP/BTC should not be locked now - pair = 'XRP/BTC' + pair = "XRP/BTC" assert not PairLocks.is_pair_locked(pair) # Unlocking a pair that's not locked should not raise an error PairLocks.unlock_pair(pair) @@ -52,12 +52,12 @@ def test_PairLocks(use_db): assert len(locks) == 2 # Unlock original pair - pair = 'ETH/BTC' + pair = "ETH/BTC" PairLocks.unlock_pair(pair) assert not PairLocks.is_pair_locked(pair) assert not PairLocks.is_global_lock() - pair = 'BTC/USDT' + pair = "BTC/USDT" # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) PairLocks.lock_pair(pair, lock_time) @@ -73,18 +73,18 @@ def test_PairLocks(use_db): locks = PairLocks.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) assert len(locks) == 1 - assert 'PairLock' in str(locks[0]) + assert "PairLock" in str(locks[0]) # Unlock all PairLocks.unlock_pair(pair, lock_time + timedelta(minutes=-2)) assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) # Global lock - PairLocks.lock_pair('*', lock_time) + PairLocks.lock_pair("*", lock_time) assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) # Global lock also locks every pair separately assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) - assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) + assert PairLocks.is_pair_locked("XRP/USDT", lock_time + timedelta(minutes=-50)) if use_db: locks = PairLocks.get_all_locks() @@ -100,10 +100,10 @@ def test_PairLocks(use_db): PairLocks.use_db = True -@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.parametrize("use_db", (False, True)) @pytest.mark.usefixtures("init_persistence") def test_PairLocks_getlongestlock(use_db): - PairLocks.timeframe = '5m' + PairLocks.timeframe = "5m" # No lock should be present PairLocks.use_db = use_db if use_db: @@ -111,7 +111,7 @@ def test_PairLocks_getlongestlock(use_db): assert PairLocks.use_db == use_db - pair = 'ETH/BTC' + pair = "ETH/BTC" assert not PairLocks.is_pair_locked(pair) PairLocks.lock_pair(pair, dt_now() + timedelta(minutes=4)) # ETH/BTC locked for 4 minutes @@ -132,10 +132,10 @@ def test_PairLocks_getlongestlock(use_db): PairLocks.use_db = True -@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.parametrize("use_db", (False, True)) @pytest.mark.usefixtures("init_persistence") def test_PairLocks_reason(use_db): - PairLocks.timeframe = '5m' + PairLocks.timeframe = "5m" PairLocks.use_db = use_db # No lock should be present if use_db: @@ -143,15 +143,15 @@ def test_PairLocks_reason(use_db): assert PairLocks.use_db == use_db - PairLocks.lock_pair('XRP/USDT', dt_now() + timedelta(minutes=4), 'TestLock1') - PairLocks.lock_pair('ETH/USDT', dt_now() + timedelta(minutes=4), 'TestLock2') + PairLocks.lock_pair("XRP/USDT", dt_now() + timedelta(minutes=4), "TestLock1") + PairLocks.lock_pair("ETH/USDT", dt_now() + timedelta(minutes=4), "TestLock2") - assert PairLocks.is_pair_locked('XRP/USDT') - assert PairLocks.is_pair_locked('ETH/USDT') + assert PairLocks.is_pair_locked("XRP/USDT") + assert PairLocks.is_pair_locked("ETH/USDT") - PairLocks.unlock_reason('TestLock1') - assert not PairLocks.is_pair_locked('XRP/USDT') - assert PairLocks.is_pair_locked('ETH/USDT') + PairLocks.unlock_reason("TestLock1") + assert not PairLocks.is_pair_locked("XRP/USDT") + assert PairLocks.is_pair_locked("ETH/USDT") PairLocks.reset_locks() PairLocks.use_db = True diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 53963b58e..c8a8fdf20 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -11,12 +11,16 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from tests.conftest import get_patched_freqtradebot, log_has_re -def generate_mock_trade(pair: str, fee: float, is_open: bool, - exit_reason: str = ExitType.EXIT_SIGNAL, - min_ago_open: int = None, min_ago_close: int = None, - profit_rate: float = 0.9, - is_short: bool = False, - ): +def generate_mock_trade( + pair: str, + fee: float, + is_open: bool, + exit_reason: str = ExitType.EXIT_SIGNAL, + min_ago_open: int = None, + min_ago_close: int = None, + profit_rate: float = 0.9, + is_short: bool = False, +): open_rate = random.random() trade = Trade( @@ -29,32 +33,15 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, - exchange='binance', + exchange="binance", is_short=is_short, leverage=1, ) - trade.orders.append(Order( - ft_order_side=trade.entry_side, - order_id=f'{pair}-{trade.entry_side}-{trade.open_date}', - ft_is_open=False, - ft_pair=pair, - ft_amount=trade.amount, - ft_price=trade.open_rate, - amount=trade.amount, - filled=trade.amount, - remaining=0, - price=open_rate, - average=open_rate, - status="closed", - order_type="market", - side=trade.entry_side, - )) - if not is_open: - close_price = open_rate * (2 - profit_rate if is_short else profit_rate) - trade.orders.append(Order( - ft_order_side=trade.exit_side, - order_id=f'{pair}-{trade.exit_side}-{trade.close_date}', + trade.orders.append( + Order( + ft_order_side=trade.entry_side, + order_id=f"{pair}-{trade.entry_side}-{trade.open_date}", ft_is_open=False, ft_pair=pair, ft_amount=trade.amount, @@ -62,12 +49,33 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, amount=trade.amount, filled=trade.amount, remaining=0, - price=close_price, - average=close_price, + price=open_rate, + average=open_rate, status="closed", order_type="market", - side=trade.exit_side, - )) + side=trade.entry_side, + ) + ) + if not is_open: + close_price = open_rate * (2 - profit_rate if is_short else profit_rate) + trade.orders.append( + Order( + ft_order_side=trade.exit_side, + order_id=f"{pair}-{trade.exit_side}-{trade.close_date}", + ft_is_open=False, + ft_pair=pair, + ft_amount=trade.amount, + ft_price=trade.open_rate, + amount=trade.amount, + filled=trade.amount, + remaining=0, + price=close_price, + average=close_price, + status="closed", + order_type="market", + side=trade.exit_side, + ) + ) trade.recalc_open_trade_value() if not is_open: @@ -80,54 +88,79 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, def test_protectionmanager(mocker, default_conf): - default_conf['protections'] = [{'method': protection} - for protection in constants.AVAILABLE_PROTECTIONS] + default_conf["protections"] = [ + {"method": protection} for protection in constants.AVAILABLE_PROTECTIONS + ] freqtrade = get_patched_freqtradebot(mocker, default_conf) for handler in freqtrade.protections._protection_handlers: assert handler.name in constants.AVAILABLE_PROTECTIONS if not handler.has_global_stop: - assert handler.global_stop(datetime.now(timezone.utc), '*') is None + assert handler.global_stop(datetime.now(timezone.utc), "*") is None if not handler.has_local_stop: - assert handler.stop_per_pair('XRP/BTC', datetime.now(timezone.utc), '*') is None + assert handler.stop_per_pair("XRP/BTC", datetime.now(timezone.utc), "*") is None -@pytest.mark.parametrize('timeframe,expected,protconf', [ - ('1m', [20, 10], - [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}]), - ('5m', [100, 15], - [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 15}]), - ('1h', [1200, 40], - [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 40}]), - ('1d', [1440, 5], - [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration": 5}]), - ('1m', [20, 5], - [{"method": "StoplossGuard", "lookback_period": 20, "stop_duration_candles": 5}]), - ('5m', [15, 25], - [{"method": "StoplossGuard", "lookback_period": 15, "stop_duration_candles": 5}]), - ('1h', [50, 600], - [{"method": "StoplossGuard", "lookback_period": 50, "stop_duration_candles": 10}]), - ('1h', [60, 540], - [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration_candles": 9}]), -]) +@pytest.mark.parametrize( + "timeframe,expected,protconf", + [ + ( + "1m", + [20, 10], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], + ), + ( + "5m", + [100, 15], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 15}], + ), + ( + "1h", + [1200, 40], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 40}], + ), + ( + "1d", + [1440, 5], + [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration": 5}], + ), + ( + "1m", + [20, 5], + [{"method": "StoplossGuard", "lookback_period": 20, "stop_duration_candles": 5}], + ), + ( + "5m", + [15, 25], + [{"method": "StoplossGuard", "lookback_period": 15, "stop_duration_candles": 5}], + ), + ( + "1h", + [50, 600], + [{"method": "StoplossGuard", "lookback_period": 50, "stop_duration_candles": 10}], + ), + ( + "1h", + [60, 540], + [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration_candles": 9}], + ), + ], +) def test_protections_init(default_conf, timeframe, expected, protconf): - default_conf['timeframe'] = timeframe + default_conf["timeframe"] = timeframe man = ProtectionManager(default_conf, protconf) assert len(man._protection_handlers) == len(protconf) assert man._protection_handlers[0]._lookback_period == expected[0] assert man._protection_handlers[0]._stop_duration == expected[1] -@pytest.mark.parametrize('is_short', [False, True]) +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.usefixtures("init_persistence") def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short): # Active for both sides (long and short) - default_conf['protections'] = [{ - "method": "StoplossGuard", - "lookback_period": 60, - "stop_duration": 40, - "trade_limit": 3 - }] + default_conf["protections"] = [ + {"method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, "trade_limit": 3} + ] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" assert not freqtrade.protections.global_stop() @@ -135,8 +168,13 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short): caplog.clear() generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=200, min_ago_close=30, is_short=is_short, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=200, + min_ago_close=30, + is_short=is_short, ) assert not freqtrade.protections.global_stop() @@ -144,13 +182,23 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short): caplog.clear() # This trade does not count, as it's closed too long ago generate_mock_trade( - 'BCH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=250, min_ago_close=100, is_short=is_short, + "BCH/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=250, + min_ago_close=100, + is_short=is_short, ) generate_mock_trade( - 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=240, min_ago_close=30, is_short=is_short, + "ETH/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=240, + min_ago_close=30, + is_short=is_short, ) # 3 Trades closed - but the 2nd has been closed too long ago. assert not freqtrade.protections.global_stop() @@ -158,8 +206,13 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short): caplog.clear() generate_mock_trade( - 'LTC/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=180, min_ago_close=30, is_short=is_short, + "LTC/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=180, + min_ago_close=30, + is_short=is_short, ) assert freqtrade.protections.global_stop() @@ -168,36 +221,44 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short): # Test 5m after lock-period - this should try and relock the pair, but end-time # should be the previous end-time - end_time = PairLocks.get_pair_longest_lock('*').lock_end_time + timedelta(minutes=5) + end_time = PairLocks.get_pair_longest_lock("*").lock_end_time + timedelta(minutes=5) freqtrade.protections.global_stop(end_time) assert not PairLocks.is_global_lock(end_time) -@pytest.mark.parametrize('only_per_pair', [False, True]) -@pytest.mark.parametrize('only_per_side', [False, True]) +@pytest.mark.parametrize("only_per_pair", [False, True]) +@pytest.mark.parametrize("only_per_side", [False, True]) @pytest.mark.usefixtures("init_persistence") def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair, only_per_side): - default_conf['protections'] = [{ - "method": "StoplossGuard", - "lookback_period": 60, - "trade_limit": 2, - "stop_duration": 60, - "only_per_pair": only_per_pair, - "only_per_side": only_per_side, - }] - check_side = 'long' if only_per_side else '*' + default_conf["protections"] = [ + { + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 2, + "stop_duration": 60, + "only_per_pair": only_per_pair, + "only_per_side": only_per_side, + } + ] + check_side = "long" if only_per_side else "*" is_short = False freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" - pair = 'XRP/BTC' + pair = "XRP/BTC" assert not freqtrade.protections.stop_per_pair(pair) assert not freqtrade.protections.global_stop() assert not log_has_re(message, caplog) caplog.clear() generate_mock_trade( - pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=200, min_ago_close=30, profit_rate=0.9, is_short=is_short + pair, + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=200, + min_ago_close=30, + profit_rate=0.9, + is_short=is_short, ) assert not freqtrade.protections.stop_per_pair(pair) @@ -206,13 +267,25 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair caplog.clear() # This trade does not count, as it's closed too long ago generate_mock_trade( - pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=250, min_ago_close=100, profit_rate=0.9, is_short=is_short + pair, + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=250, + min_ago_close=100, + profit_rate=0.9, + is_short=is_short, ) # Trade does not count for per pair stop as it's the wrong pair. generate_mock_trade( - 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=240, min_ago_close=30, profit_rate=0.9, is_short=is_short + "ETH/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=240, + min_ago_close=30, + profit_rate=0.9, + is_short=is_short, ) # 3 Trades closed - but the 2nd has been closed too long ago. assert not freqtrade.protections.stop_per_pair(pair) @@ -226,23 +299,35 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair # Trade does not count potentially, as it's in the wrong direction generate_mock_trade( - pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=150, min_ago_close=25, profit_rate=0.9, is_short=not is_short + pair, + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=150, + min_ago_close=25, + profit_rate=0.9, + is_short=not is_short, ) freqtrade.protections.stop_per_pair(pair) assert freqtrade.protections.global_stop() != only_per_pair assert PairLocks.is_pair_locked(pair, side=check_side) != (only_per_side and only_per_pair) assert PairLocks.is_global_lock(side=check_side) != only_per_pair if only_per_side: - assert not PairLocks.is_pair_locked(pair, side='*') - assert not PairLocks.is_global_lock(side='*') + assert not PairLocks.is_pair_locked(pair, side="*") + assert not PairLocks.is_global_lock(side="*") caplog.clear() # 2nd Trade that counts with correct pair generate_mock_trade( - pair, fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=180, min_ago_close=31, profit_rate=0.9, is_short=is_short + pair, + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=180, + min_ago_close=31, + profit_rate=0.9, + is_short=is_short, ) freqtrade.protections.stop_per_pair(pair) @@ -250,174 +335,239 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair assert PairLocks.is_pair_locked(pair, side=check_side) assert PairLocks.is_global_lock(side=check_side) != only_per_pair if only_per_side: - assert not PairLocks.is_pair_locked(pair, side='*') - assert not PairLocks.is_global_lock(side='*') + assert not PairLocks.is_pair_locked(pair, side="*") + assert not PairLocks.is_global_lock(side="*") @pytest.mark.usefixtures("init_persistence") def test_CooldownPeriod(mocker, default_conf, fee, caplog): - default_conf['protections'] = [{ - "method": "CooldownPeriod", - "stop_duration": 60, - }] + default_conf["protections"] = [ + { + "method": "CooldownPeriod", + "stop_duration": 60, + } + ] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") assert not log_has_re(message, caplog) caplog.clear() generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=200, min_ago_close=30, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=200, + min_ago_close=30, ) assert not freqtrade.protections.global_stop() - assert freqtrade.protections.stop_per_pair('XRP/BTC') - assert PairLocks.is_pair_locked('XRP/BTC') + assert freqtrade.protections.stop_per_pair("XRP/BTC") + assert PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() generate_mock_trade( - 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, - min_ago_open=205, min_ago_close=35, + "ETH/BTC", + fee.return_value, + False, + exit_reason=ExitType.ROI.value, + min_ago_open=205, + min_ago_close=35, ) assert not freqtrade.protections.global_stop() - assert not PairLocks.is_pair_locked('ETH/BTC') - assert freqtrade.protections.stop_per_pair('ETH/BTC') - assert PairLocks.is_pair_locked('ETH/BTC') + assert not PairLocks.is_pair_locked("ETH/BTC") + assert freqtrade.protections.stop_per_pair("ETH/BTC") + assert PairLocks.is_pair_locked("ETH/BTC") assert not PairLocks.is_global_lock() -@pytest.mark.parametrize('only_per_side', [False, True]) +@pytest.mark.parametrize("only_per_side", [False, True]) @pytest.mark.usefixtures("init_persistence") def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side): - default_conf['protections'] = [{ - "method": "LowProfitPairs", - "lookback_period": 400, - "stop_duration": 60, - "trade_limit": 2, - "required_profit": 0.0, - "only_per_side": only_per_side, - }] + default_conf["protections"] = [ + { + "method": "LowProfitPairs", + "lookback_period": 400, + "stop_duration": 60, + "trade_limit": 2, + "required_profit": 0.0, + "only_per_side": only_per_side, + } + ] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") assert not log_has_re(message, caplog) caplog.clear() generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=800, min_ago_close=450, profit_rate=0.9, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=800, + min_ago_close=450, + profit_rate=0.9, ) Trade.commit() # Not locked with 1 trade assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') - assert not PairLocks.is_pair_locked('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=200, min_ago_close=120, profit_rate=0.9, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=200, + min_ago_close=120, + profit_rate=0.9, ) Trade.commit() # Not locked with 1 trade (first trade is outside of lookback_period) assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') - assert not PairLocks.is_pair_locked('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() # Add positive trade generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, - min_ago_open=20, min_ago_close=10, profit_rate=1.15, is_short=True + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.ROI.value, + min_ago_open=20, + min_ago_close=10, + profit_rate=1.15, + is_short=True, ) Trade.commit() - assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side - assert not PairLocks.is_pair_locked('XRP/BTC', side='*') - assert PairLocks.is_pair_locked('XRP/BTC', side='long') == only_per_side + assert freqtrade.protections.stop_per_pair("XRP/BTC") != only_per_side + assert not PairLocks.is_pair_locked("XRP/BTC", side="*") + assert PairLocks.is_pair_locked("XRP/BTC", side="long") == only_per_side generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=110, min_ago_close=21, profit_rate=0.8, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=110, + min_ago_close=21, + profit_rate=0.8, ) Trade.commit() # Locks due to 2nd trade assert freqtrade.protections.global_stop() != only_per_side - assert freqtrade.protections.stop_per_pair('XRP/BTC') != only_per_side - assert PairLocks.is_pair_locked('XRP/BTC', side='long') - assert PairLocks.is_pair_locked('XRP/BTC', side='*') != only_per_side + assert freqtrade.protections.stop_per_pair("XRP/BTC") != only_per_side + assert PairLocks.is_pair_locked("XRP/BTC", side="long") + assert PairLocks.is_pair_locked("XRP/BTC", side="*") != only_per_side assert not PairLocks.is_global_lock() Trade.commit() @pytest.mark.usefixtures("init_persistence") def test_MaxDrawdown(mocker, default_conf, fee, caplog): - default_conf['protections'] = [{ - "method": "MaxDrawdown", - "lookback_period": 1000, - "stop_duration": 60, - "trade_limit": 3, - "max_allowed_drawdown": 0.15 - }] + default_conf["protections"] = [ + { + "method": "MaxDrawdown", + "lookback_period": 1000, + "stop_duration": 60, + "trade_limit": 3, + "max_allowed_drawdown": 0.15, + } + ] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to Max.*" assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") caplog.clear() generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=1000, + min_ago_close=900, + profit_rate=1.1, ) generate_mock_trade( - 'ETH/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + "ETH/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=1000, + min_ago_close=900, + profit_rate=1.1, ) generate_mock_trade( - 'NEO/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + "NEO/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=1000, + min_ago_close=900, + profit_rate=1.1, ) Trade.commit() # No losing trade yet ... so max_drawdown will raise exception assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=500, min_ago_close=400, profit_rate=0.9, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=500, + min_ago_close=400, + profit_rate=0.9, ) # Not locked with one trade assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') - assert not PairLocks.is_pair_locked('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.STOP_LOSS.value, - min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.STOP_LOSS.value, + min_ago_open=1200, + min_ago_close=1100, + profit_rate=0.5, ) Trade.commit() # Not locked with 1 trade (2nd trade is outside of lookback_period) assert not freqtrade.protections.global_stop() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') - assert not PairLocks.is_pair_locked('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") + assert not PairLocks.is_pair_locked("XRP/BTC") assert not PairLocks.is_global_lock() assert not log_has_re(message, caplog) # Winning trade ... (should not lock, does not change drawdown!) generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, - min_ago_open=320, min_ago_close=410, profit_rate=1.5, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.ROI.value, + min_ago_open=320, + min_ago_close=410, + profit_rate=1.5, ) Trade.commit() assert not freqtrade.protections.global_stop() @@ -427,63 +577,89 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): # Add additional negative trade, causing a loss of > 15% generate_mock_trade( - 'XRP/BTC', fee.return_value, False, exit_reason=ExitType.ROI.value, - min_ago_open=20, min_ago_close=10, profit_rate=0.8, + "XRP/BTC", + fee.return_value, + False, + exit_reason=ExitType.ROI.value, + min_ago_open=20, + min_ago_close=10, + profit_rate=0.8, ) Trade.commit() - assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not freqtrade.protections.stop_per_pair("XRP/BTC") # local lock not supported - assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_pair_locked("XRP/BTC") assert freqtrade.protections.global_stop() assert PairLocks.is_global_lock() assert log_has_re(message, caplog) -@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ - ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}, - "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " - "2 stoplosses with profit < 0.00% within 60 minutes.'}]", - None - ), - ({"method": "CooldownPeriod", "stop_duration": 60}, - "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 minutes.'}]", - None - ), - ({"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, - "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " - "profit < 0.0 within 60 minutes.'}]", - None - ), - ({"method": "MaxDrawdown", "lookback_period": 60, "stop_duration": 60}, - "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading " - "if drawdown is > 0.0 within 60 minutes.'}]", - None - ), - ({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2, - "required_profit": -0.05, "stop_duration": 60}, - "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " - "2 stoplosses with profit < -5.00% within 12 candles.'}]", - None - ), - ({"method": "CooldownPeriod", "stop_duration_candles": 5}, - "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 5 candles.'}]", - None - ), - ({"method": "LowProfitPairs", "lookback_period_candles": 11, "stop_duration": 60}, - "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " - "profit < 0.0 within 11 candles.'}]", - None - ), - ({"method": "MaxDrawdown", "lookback_period_candles": 20, "stop_duration": 60}, - "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading " - "if drawdown is > 0.0 within 20 candles.'}]", - None - ), -]) -def test_protection_manager_desc(mocker, default_conf, protectionconf, - desc_expected, exception_expected): - - default_conf['protections'] = [protectionconf] +@pytest.mark.parametrize( + "protectionconf,desc_expected,exception_expected", + [ + ( + { + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 2, + "stop_duration": 60, + }, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses with profit < 0.00% within 60 minutes.'}]", + None, + ), + ( + {"method": "CooldownPeriod", "stop_duration": 60}, + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 minutes.'}]", + None, + ), + ( + {"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, + "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " + "profit < 0.0 within 60 minutes.'}]", + None, + ), + ( + {"method": "MaxDrawdown", "lookback_period": 60, "stop_duration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading " + "if drawdown is > 0.0 within 60 minutes.'}]", + None, + ), + ( + { + "method": "StoplossGuard", + "lookback_period_candles": 12, + "trade_limit": 2, + "required_profit": -0.05, + "stop_duration": 60, + }, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses with profit < -5.00% within 12 candles.'}]", + None, + ), + ( + {"method": "CooldownPeriod", "stop_duration_candles": 5}, + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 5 candles.'}]", + None, + ), + ( + {"method": "LowProfitPairs", "lookback_period_candles": 11, "stop_duration": 60}, + "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " + "profit < 0.0 within 11 candles.'}]", + None, + ), + ( + {"method": "MaxDrawdown", "lookback_period_candles": 20, "stop_duration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading " + "if drawdown is > 0.0 within 20 candles.'}]", + None, + ), + ], +) +def test_protection_manager_desc( + mocker, default_conf, protectionconf, desc_expected, exception_expected +): + default_conf["protections"] = [protectionconf] freqtrade = get_patched_freqtradebot(mocker, default_conf) short_desc = str(freqtrade.protections.short_desc()) diff --git a/tests/plugins/test_remotepairlist.py b/tests/plugins/test_remotepairlist.py index 9d407de9f..96fc0e336 100644 --- a/tests/plugins/test_remotepairlist.py +++ b/tests/plugins/test_remotepairlist.py @@ -12,59 +12,57 @@ from tests.conftest import EXMS, get_patched_exchange, get_patched_freqtradebot, @pytest.fixture(scope="function") def rpl_config(default_conf): - default_conf['stake_currency'] = 'USDT' + default_conf["stake_currency"] = "USDT" - default_conf['exchange']['pair_whitelist'] = [ - 'ETH/USDT', - 'XRP/USDT', - ] - default_conf['exchange']['pair_blacklist'] = [ - 'BLK/USDT' + default_conf["exchange"]["pair_whitelist"] = [ + "ETH/USDT", + "XRP/USDT", ] + default_conf["exchange"]["pair_blacklist"] = ["BLK/USDT"] return default_conf def test_gen_pairlist_with_local_file(mocker, rpl_config): - mock_file = MagicMock() mock_file.read.return_value = '{"pairs": ["TKN/USDT","ETH/USDT","NANO/USDT"]}' - mocker.patch('freqtrade.plugins.pairlist.RemotePairList.open', return_value=mock_file) + mocker.patch("freqtrade.plugins.pairlist.RemotePairList.open", return_value=mock_file) - mock_file_path = mocker.patch('freqtrade.plugins.pairlist.RemotePairList.Path') + mock_file_path = mocker.patch("freqtrade.plugins.pairlist.RemotePairList.Path") mock_file_path.exists.return_value = True jsonparse = json.loads(mock_file.read.return_value) - mocker.patch('freqtrade.plugins.pairlist.RemotePairList.rapidjson.load', return_value=jsonparse) + mocker.patch("freqtrade.plugins.pairlist.RemotePairList.rapidjson.load", return_value=jsonparse) - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", - 'number_assets': 2, - 'refresh_period': 1800, - 'keep_pairlist_on_failure': True, - 'pairlist_url': 'file:///pairlist.json', - 'bearer_token': '', - 'read_timeout': 60 + "number_assets": 2, + "refresh_period": 1800, + "keep_pairlist_on_failure": True, + "pairlist_url": "file:///pairlist.json", + "bearer_token": "", + "read_timeout": 60, } ] exchange = get_patched_exchange(mocker, rpl_config) pairlistmanager = PairListManager(exchange, rpl_config) - remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config, - rpl_config['pairlists'][0], 0) + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0 + ) result = remote_pairlist.gen_pairlist([]) - assert result == ['TKN/USDT', 'ETH/USDT'] + assert result == ["TKN/USDT", "ETH/USDT"] def test_fetch_pairlist_mock_response_html(mocker, rpl_config): mock_response = MagicMock() - mock_response.headers = {'content-type': 'text/html'} + mock_response.headers = {"content-type": "text/html"} - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "pairlist_url": "http://example.com/pairlist", @@ -77,17 +75,19 @@ def test_fetch_pairlist_mock_response_html(mocker, rpl_config): exchange = get_patched_exchange(mocker, rpl_config) pairlistmanager = PairListManager(exchange, rpl_config) - mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get", - return_value=mock_response) - remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config, - rpl_config['pairlists'][0], 0) + mocker.patch( + "freqtrade.plugins.pairlist.RemotePairList.requests.get", return_value=mock_response + ) + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0 + ) - with pytest.raises(OperationalException, match='RemotePairList is not of type JSON.'): + with pytest.raises(OperationalException, match="RemotePairList is not of type JSON."): remote_pairlist.fetch_pairlist() def test_fetch_pairlist_timeout_keep_last_pairlist(mocker, rpl_config, caplog): - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "pairlist_url": "http://example.com/pairlist", @@ -100,25 +100,27 @@ def test_fetch_pairlist_timeout_keep_last_pairlist(mocker, rpl_config, caplog): exchange = get_patched_exchange(mocker, rpl_config) pairlistmanager = PairListManager(exchange, rpl_config) - mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get", - side_effect=requests.exceptions.RequestException) + mocker.patch( + "freqtrade.plugins.pairlist.RemotePairList.requests.get", + side_effect=requests.exceptions.RequestException, + ) - remote_pairlist = RemotePairList(exchange, pairlistmanager, rpl_config, - rpl_config['pairlists'][0], 0) + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0 + ) remote_pairlist._last_pairlist = ["BTC/USDT", "ETH/USDT", "LTC/USDT"] remote_pairlist._init_done = True - pairlist_url = rpl_config['pairlists'][0]['pairlist_url'] + pairlist_url = rpl_config["pairlists"][0]["pairlist_url"] pairs, _time_elapsed = remote_pairlist.fetch_pairlist() - assert log_has(f'Error: Was not able to fetch pairlist from: ' f'{pairlist_url}', caplog) + assert log_has(f"Error: Was not able to fetch pairlist from: " f"{pairlist_url}", caplog) assert log_has("Keeping last fetched pairlist", caplog) assert pairs == ["BTC/USDT", "ETH/USDT", "LTC/USDT"] def test_remote_pairlist_init_no_pairlist_url(mocker, rpl_config): - - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "number_assets": 10, @@ -127,14 +129,16 @@ def test_remote_pairlist_init_no_pairlist_url(mocker, rpl_config): ] get_patched_exchange(mocker, rpl_config) - with pytest.raises(OperationalException, match=r'`pairlist_url` not specified.' - r' Please check your configuration for "pairlist.config.pairlist_url"'): + with pytest.raises( + OperationalException, + match=r"`pairlist_url` not specified." + r' Please check your configuration for "pairlist.config.pairlist_url"', + ): get_patched_freqtradebot(mocker, rpl_config) def test_remote_pairlist_init_no_number_assets(mocker, rpl_config): - - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "pairlist_url": "http://example.com/pairlist", @@ -144,14 +148,16 @@ def test_remote_pairlist_init_no_number_assets(mocker, rpl_config): 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"'): + 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'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "pairlist_url": "http://example.com/pairlist", @@ -166,21 +172,21 @@ def test_fetch_pairlist_mock_response_valid(mocker, rpl_config): mock_response.json.return_value = { "pairs": ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"], - "refresh_period": 60 + "refresh_period": 60, } - mock_response.headers = { - "content-type": "application/json" - } + mock_response.headers = {"content-type": "application/json"} mock_response.elapsed.total_seconds.return_value = 0.4 - mocker.patch("freqtrade.plugins.pairlist.RemotePairList.requests.get", - return_value=mock_response) + 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) + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][0], 0 + ) pairs, time_elapsed = remote_pairlist.fetch_pairlist() assert pairs == ["ETH/USDT", "XRP/USDT", "LTC/USDT", "EOS/USDT"] @@ -189,7 +195,7 @@ def test_fetch_pairlist_mock_response_valid(mocker, rpl_config): def test_remote_pairlist_init_wrong_mode(mocker, rpl_config): - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "mode": "blacklis", @@ -201,11 +207,11 @@ def test_remote_pairlist_init_wrong_mode(mocker, rpl_config): with pytest.raises( OperationalException, - match=r'`mode` not configured correctly. Supported Modes are "whitelist","blacklist"' + match=r'`mode` not configured correctly. Supported Modes are "whitelist","blacklist"', ): get_patched_freqtradebot(mocker, rpl_config) - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "mode": "blacklist", @@ -216,14 +222,13 @@ def test_remote_pairlist_init_wrong_mode(mocker, rpl_config): ] with pytest.raises( - OperationalException, - match=r'A `blacklist` mode RemotePairList can not be.*first.*' + OperationalException, match=r"A `blacklist` mode RemotePairList can not be.*first.*" ): get_patched_freqtradebot(mocker, rpl_config) def test_remote_pairlist_init_wrong_proc_mode(mocker, rpl_config): - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "RemotePairList", "processing_mode": "filler", @@ -237,25 +242,19 @@ def test_remote_pairlist_init_wrong_proc_mode(mocker, rpl_config): get_patched_exchange(mocker, rpl_config) with pytest.raises( OperationalException, - match=r'`processing_mode` not configured correctly. Supported Modes are "filter","append"' + match=r'`processing_mode` not configured correctly. Supported Modes are "filter","append"', ): get_patched_freqtradebot(mocker, rpl_config) def test_remote_pairlist_blacklist(mocker, rpl_config, caplog, markets, tickers): - mock_response = MagicMock() - mock_response.json.return_value = { - "pairs": ["XRP/USDT"], - "refresh_period": 60 - } + mock_response.json.return_value = {"pairs": ["XRP/USDT"], "refresh_period": 60} - mock_response.headers = { - "content-type": "application/json" - } + mock_response.headers = {"content-type": "application/json"} - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "StaticPairList", }, @@ -263,31 +262,34 @@ def test_remote_pairlist_blacklist(mocker, rpl_config, caplog, markets, tickers) "method": "RemotePairList", "mode": "blacklist", "pairlist_url": "http://example.com/pairlist", - "number_assets": 3 - } + "number_assets": 3, + }, ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + 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) + 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"][1], 1) + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][1], 1 + ) pairs, _time_elapsed = remote_pairlist.fetch_pairlist() assert pairs == ["XRP/USDT"] - whitelist = remote_pairlist.filter_pairlist(rpl_config['exchange']['pair_whitelist'], {}) + whitelist = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {}) assert whitelist == ["ETH/USDT"] assert log_has(f"Blacklist - Filtered out pairs: {pairs}", caplog) @@ -295,19 +297,13 @@ def test_remote_pairlist_blacklist(mocker, rpl_config, caplog, markets, tickers) @pytest.mark.parametrize("processing_mode", ["filter", "append"]) def test_remote_pairlist_whitelist(mocker, rpl_config, processing_mode, markets, tickers): - mock_response = MagicMock() - mock_response.json.return_value = { - "pairs": ["XRP/USDT"], - "refresh_period": 60 - } + mock_response.json.return_value = {"pairs": ["XRP/USDT"], "refresh_period": 60} - mock_response.headers = { - "content-type": "application/json" - } + mock_response.headers = {"content-type": "application/json"} - rpl_config['pairlists'] = [ + rpl_config["pairlists"] = [ { "method": "StaticPairList", }, @@ -316,29 +312,32 @@ def test_remote_pairlist_whitelist(mocker, rpl_config, processing_mode, markets, "mode": "whitelist", "processing_mode": processing_mode, "pairlist_url": "http://example.com/pairlist", - "number_assets": 3 - } + "number_assets": 3, + }, ] - mocker.patch.multiple(EXMS, - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) + 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) + 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"][1], 1) + remote_pairlist = RemotePairList( + exchange, pairlistmanager, rpl_config, rpl_config["pairlists"][1], 1 + ) pairs, _time_elapsed = remote_pairlist.fetch_pairlist() assert pairs == ["XRP/USDT"] - whitelist = remote_pairlist.filter_pairlist(rpl_config['exchange']['pair_whitelist'], {}) - assert whitelist == (["XRP/USDT"] if processing_mode == "filter" else ['ETH/USDT', 'XRP/USDT']) + whitelist = remote_pairlist.filter_pairlist(rpl_config["exchange"]["pair_whitelist"], {}) + assert whitelist == (["XRP/USDT"] if processing_mode == "filter" else ["ETH/USDT", "XRP/USDT"])