# pragma pylint: disable=missing-docstring, protected-access, invalid-name import json import warnings from copy import deepcopy from pathlib import Path from unittest.mock import MagicMock import pytest from freqtrade.commands import Arguments from freqtrade.configuration import ( Configuration, remove_exchange_credentials, sanitize_config, validate_config_consistency, ) from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import ( check_conflicting_settings, process_deprecated_setting, process_removed_setting, process_temporary_deprecated_settings, ) from freqtrade.configuration.environment_vars import _flat_vars_to_nested_dict from freqtrade.configuration.load_config import ( load_config_file, load_file, load_from_files, log_config_error_range, ) from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX from freqtrade.enums import RunMode from freqtrade.exceptions import ConfigurationError, OperationalException from tests.conftest import ( CURRENT_TEST_STRATEGY, log_has, log_has_re, patched_configuration_load_config_file, ) @pytest.fixture(scope="function") def all_conf(): config_file = Path(__file__).parents[1] / "config_examples/config_full.example.json" conf = load_config_file(str(config_file)) return conf def test_load_config_missing_attributes(default_conf) -> None: conf = deepcopy(default_conf) conf.pop("exchange") with pytest.raises(ConfigurationError, match=r".*'exchange' is a required property.*"): validate_config_schema(conf) conf = deepcopy(default_conf) conf.pop("stake_currency") conf["runmode"] = RunMode.DRY_RUN with pytest.raises(ConfigurationError, match=r".*'stake_currency' is a required property.*"): validate_config_schema(conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: default_conf["stake_amount"] = "fake" with pytest.raises(ConfigurationError, match=r".*'fake' does not match 'unlimited'.*"): validate_config_schema(default_conf) def test_load_config_file(default_conf, mocker, caplog) -> None: del default_conf["user_data_dir"] default_conf["datadir"] = str(default_conf["datadir"]) file_mock = mocker.patch( "freqtrade.configuration.load_config.Path.open", mocker.mock_open(read_data=json.dumps(default_conf)), ) validated_conf = load_config_file("somefile") assert file_mock.call_count == 1 assert validated_conf.items() >= default_conf.items() def test_load_config_file_error(default_conf, mocker, caplog) -> None: del default_conf["user_data_dir"] default_conf["datadir"] = str(default_conf["datadir"]) filedata = json.dumps(default_conf).replace('"stake_amount": 0.001,', '"stake_amount": .001,') mocker.patch( "freqtrade.configuration.load_config.Path.open", mocker.mock_open(read_data=filedata) ) mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) with pytest.raises(OperationalException, match=r".*Please verify the following segment.*"): load_config_file("somefile") def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: del default_conf["user_data_dir"] default_conf["datadir"] = str(default_conf["datadir"]) filedata = json.dumps(default_conf).replace('"stake_amount": 0.001,', '"stake_amount": .001,') mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) x = log_config_error_range("somefile", "Parse error at offset 64: Invalid value.") assert isinstance(x, str) assert ( x == '{"max_open_trades": 1, "stake_currency": "BTC", ' '"stake_amount": .001, "fiat_display_currency": "USD", ' '"timeframe": "5m", "dry_run": true, "cance' ) filedata = json.dumps(default_conf, indent=2).replace( '"stake_amount": 0.001,', '"stake_amount": .001,' ) mocker.patch.object(Path, "read_text", MagicMock(return_value=filedata)) x = log_config_error_range("somefile", "Parse error at offset 4: Invalid value.") assert isinstance(x, str) assert x == ' "max_open_trades": 1,\n "stake_currency": "BTC",\n "stake_amount": .001,' x = log_config_error_range("-", "") assert x == "" def test_load_file_error(tmp_path): testpath = tmp_path / "config.json" with pytest.raises(OperationalException, match=r"File .* not found!"): load_file(testpath) def test__args_to_config(caplog): arg_list = ["trade", "--strategy-path", "TestTest"] args = Arguments(arg_list).get_parsed_arg() configuration = Configuration(args) config = {} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # No warnings ... configuration._args_to_config(config, argname="strategy_path", logstring="DeadBeef") assert len(w) == 0 assert log_has("DeadBeef", caplog) assert config["strategy_path"] == "TestTest" configuration = Configuration(args) config = {} with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") # Deprecation warnings! configuration._args_to_config( config, argname="strategy_path", logstring="DeadBeef", deprecated_msg="Going away soon!" ) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "DEPRECATED: Going away soon!" in str(w[-1].message) assert log_has("DeadBeef", caplog) assert config["strategy_path"] == "TestTest" def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: default_conf["max_open_trades"] = 0 patched_configuration_load_config_file(mocker, default_conf) args = Arguments(["trade"]).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf["max_open_trades"] == 0 assert "internals" in validated_conf def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None: conf1 = deepcopy(default_conf) conf2 = deepcopy(default_conf) del conf1["exchange"]["key"] del conf1["exchange"]["secret"] del conf2["exchange"]["name"] conf2["exchange"]["pair_whitelist"] += ["NANO/BTC"] config_files = [conf1, conf2] configsmock = MagicMock(side_effect=config_files) mocker.patch("freqtrade.configuration.load_config.load_config_file", configsmock) arg_list = [ "trade", "-c", "test_conf.json", "--config", "test2_conf.json", ] args = Arguments(arg_list).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() exchange_conf = default_conf["exchange"] assert validated_conf["exchange"]["name"] == exchange_conf["name"] assert validated_conf["exchange"]["key"] == exchange_conf["key"] assert validated_conf["exchange"]["secret"] == exchange_conf["secret"] assert validated_conf["exchange"]["pair_whitelist"] != conf1["exchange"]["pair_whitelist"] assert validated_conf["exchange"]["pair_whitelist"] == conf2["exchange"]["pair_whitelist"] assert "internals" in validated_conf def test_from_config(default_conf, mocker, caplog) -> None: conf1 = deepcopy(default_conf) conf2 = deepcopy(default_conf) del conf1["exchange"]["key"] del conf1["exchange"]["secret"] del conf2["exchange"]["name"] conf2["exchange"]["pair_whitelist"] += ["NANO/BTC"] conf2["fiat_display_currency"] = "EUR" config_files = [conf1, conf2] mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x) configsmock = MagicMock(side_effect=config_files) mocker.patch("freqtrade.configuration.load_config.load_config_file", configsmock) validated_conf = Configuration.from_files(["test_conf.json", "test2_conf.json"]) exchange_conf = default_conf["exchange"] assert validated_conf["exchange"]["name"] == exchange_conf["name"] assert validated_conf["exchange"]["key"] == exchange_conf["key"] assert validated_conf["exchange"]["secret"] == exchange_conf["secret"] assert validated_conf["exchange"]["pair_whitelist"] != conf1["exchange"]["pair_whitelist"] assert validated_conf["exchange"]["pair_whitelist"] == conf2["exchange"]["pair_whitelist"] assert validated_conf["fiat_display_currency"] == "EUR" assert "internals" in validated_conf assert isinstance(validated_conf["user_data_dir"], Path) def test_from_recursive_files(testdatadir) -> None: files = testdatadir / "testconfigs/testconfig.json" conf = Configuration.from_files([files]) assert conf # Exchange comes from "the first config" assert conf["exchange"] # Pricing comes from the 2nd config assert conf["entry_pricing"] assert conf["entry_pricing"]["price_side"] == "same" assert conf["exit_pricing"] # The other key comes from pricing2, which is imported by pricing.json. # pricing.json is a level higher, therefore wins. assert conf["exit_pricing"]["price_side"] == "same" assert len(conf["config_files"]) == 4 assert "testconfig.json" in conf["config_files"][0] assert "test_pricing_conf.json" in conf["config_files"][1] assert "test_base_config.json" in conf["config_files"][2] assert "test_pricing2_conf.json" in conf["config_files"][3] files = testdatadir / "testconfigs/recursive.json" with pytest.raises(OperationalException, match="Config loop detected."): load_from_files([files]) def test_print_config(default_conf, mocker, caplog) -> None: conf1 = deepcopy(default_conf) # Delete non-json elements from default_conf del conf1["user_data_dir"] conf1["datadir"] = str(conf1["datadir"]) config_files = [conf1] configsmock = MagicMock(side_effect=config_files) mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x) mocker.patch("freqtrade.configuration.configuration.load_from_files", configsmock) validated_conf = Configuration.from_files(["test_conf.json"]) assert isinstance(validated_conf["user_data_dir"], Path) assert "user_data_dir" in validated_conf assert "original_config" in validated_conf assert isinstance(json.dumps(validated_conf["original_config"]), str) def test_load_config_max_open_trades_minus_one(default_conf, mocker, caplog) -> None: default_conf["max_open_trades"] = -1 patched_configuration_load_config_file(mocker, default_conf) args = Arguments(["trade"]).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf["max_open_trades"] > 999999999 assert validated_conf["max_open_trades"] == float("inf") assert "runmode" in validated_conf assert validated_conf["runmode"] == RunMode.DRY_RUN def test_load_config_file_exception(mocker) -> None: mocker.patch( "freqtrade.configuration.configuration.Path.open", MagicMock(side_effect=FileNotFoundError("File not found")), ) with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'): load_config_file("somefile") def test_load_config(default_conf, mocker) -> None: del default_conf["strategy_path"] patched_configuration_load_config_file(mocker, default_conf) args = Arguments(["trade"]).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("strategy_path") is None assert "edge" not in validated_conf def test_load_config_with_params(default_conf, mocker) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ "trade", "--strategy", "TestStrategy", "--strategy-path", "/some/path", "--db-url", "sqlite:///someurl", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("strategy") == "TestStrategy" assert validated_conf.get("strategy_path") == "/some/path" assert validated_conf.get("db_url") == "sqlite:///someurl" # Test conf provided db_url prod conf = default_conf.copy() conf["dry_run"] = False conf["db_url"] = "sqlite:///path/to/db.sqlite" patched_configuration_load_config_file(mocker, conf) arglist = ["trade", "--strategy", "TestStrategy", "--strategy-path", "/some/path"] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("db_url") == "sqlite:///path/to/db.sqlite" # Test conf provided db_url dry_run conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = "sqlite:///path/to/db.sqlite" patched_configuration_load_config_file(mocker, conf) arglist = ["trade", "--strategy", "TestStrategy", "--strategy-path", "/some/path"] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("db_url") == "sqlite:///path/to/db.sqlite" # Test args provided db_url prod conf = default_conf.copy() conf["dry_run"] = False del conf["db_url"] patched_configuration_load_config_file(mocker, conf) arglist = ["trade", "--strategy", "TestStrategy", "--strategy-path", "/some/path"] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("db_url") == DEFAULT_DB_PROD_URL assert "runmode" in validated_conf assert validated_conf["runmode"] == RunMode.LIVE # Test args provided db_url dry_run conf = default_conf.copy() conf["dry_run"] = True conf["db_url"] = DEFAULT_DB_PROD_URL patched_configuration_load_config_file(mocker, conf) arglist = ["trade", "--strategy", "TestStrategy", "--strategy-path", "/some/path"] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("db_url") == DEFAULT_DB_DRYRUN_URL @pytest.mark.parametrize( "config_value,expected,arglist", [ (True, True, ["trade", "--dry-run"]), # Leave config untouched (False, True, ["trade", "--dry-run"]), # Override config untouched (False, False, ["trade"]), # Leave config untouched (True, True, ["trade"]), # Leave config untouched ], ) def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) -> None: default_conf["dry_run"] = config_value patched_configuration_load_config_file(mocker, default_conf) configuration = Configuration(Arguments(arglist).get_parsed_arg()) validated_conf = configuration.load_config() assert validated_conf["dry_run"] is expected assert validated_conf["runmode"] == (RunMode.DRY_RUN if expected else RunMode.LIVE) def test_load_custom_strategy(default_conf, mocker, tmp_path) -> None: default_conf.update( { "strategy": "CustomStrategy", "strategy_path": f"{tmp_path}/strategies", } ) patched_configuration_load_config_file(mocker, default_conf) args = Arguments(["trade"]).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("strategy") == "CustomStrategy" assert validated_conf.get("strategy_path") == f"{tmp_path}/strategies" def test_show_info(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ "trade", "--strategy", "TestStrategy", "--db-url", "sqlite:///tmp/testdb", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) configuration.get_config() assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog) assert log_has("Dry run is enabled", caplog) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ "backtesting", "--config", "config.json", "--strategy", CURRENT_TEST_STRATEGY, ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() assert "max_open_trades" in config assert "stake_currency" in config assert "stake_amount" in config assert "exchange" in config assert "pair_whitelist" in config["exchange"] assert "datadir" in config assert "user_data_dir" in config assert log_has("Using data directory: {} ...".format(config["datadir"]), caplog) assert "timeframe" in config assert not log_has("Parameter -i/--timeframe detected ...", caplog) assert "position_stacking" not in config assert not log_has("Parameter --enable-position-stacking detected ...", caplog) assert "timerange" not in config def test_setup_configuration_with_arguments(mocker, default_conf, caplog, tmp_path) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x) mocker.patch( "freqtrade.configuration.configuration.create_userdata_dir", lambda x, *args, **kwargs: Path(x), ) arglist = [ "backtesting", "--config", "config.json", "--strategy", CURRENT_TEST_STRATEGY, "--datadir", "/foo/bar", "--userdir", f"{tmp_path}/freqtrade", "--timeframe", "1m", "--enable-position-stacking", "--timerange", ":100", "--export", "trades", "--stake-amount", "unlimited", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() assert "max_open_trades" in config assert "stake_currency" in config assert "stake_amount" in config assert "exchange" in config assert "pair_whitelist" in config["exchange"] assert "datadir" in config assert log_has("Using data directory: {} ...".format("/foo/bar"), caplog) assert log_has(f"Using user-data directory: {tmp_path / 'freqtrade'} ...", caplog) assert "user_data_dir" in config assert "timeframe" in config assert log_has("Parameter -i/--timeframe detected ... Using timeframe: 1m ...", caplog) assert "position_stacking" in config assert log_has("Parameter --enable-position-stacking detected ...", caplog) assert "timerange" in config assert log_has("Parameter --timerange detected: {} ...".format(config["timerange"]), caplog) assert "export" in config assert log_has("Parameter --export detected: {} ...".format(config["export"]), caplog) assert "stake_amount" in config assert config["stake_amount"] == "unlimited" def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: """ Test setup_configuration() function """ patched_configuration_load_config_file(mocker, default_conf) arglist = [ "backtesting", "--config", "config.json", "--timeframe", "1m", "--export", "trades", "--strategy-list", CURRENT_TEST_STRATEGY, "TestStrategy", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args, RunMode.BACKTEST) config = configuration.get_config() assert config["runmode"] == RunMode.BACKTEST assert "max_open_trades" in config assert "stake_currency" in config assert "stake_amount" in config assert "exchange" in config assert "pair_whitelist" in config["exchange"] assert "datadir" in config assert log_has("Using data directory: {} ...".format(config["datadir"]), caplog) assert "timeframe" in config assert log_has("Parameter -i/--timeframe detected ... Using timeframe: 1m ...", caplog) assert "strategy_list" in config assert log_has("Using strategy list of 2 strategies", caplog) assert "position_stacking" not in config assert "timerange" not in config assert "export" in config assert log_has("Parameter --export detected: {} ...".format(config["export"]), caplog) def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) arglist = [ "hyperopt", "--epochs", "10", "--spaces", "all", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args, RunMode.HYPEROPT) config = configuration.get_config() assert "epochs" in config assert int(config["epochs"]) == 10 assert log_has( "Parameter --epochs detected ... Will run Hyperopt with for 10 epochs ...", caplog ) assert "spaces" in config assert config["spaces"] == ["all"] assert log_has("Parameter -s/--spaces detected: ['all']", caplog) assert "runmode" in config assert config["runmode"] == RunMode.HYPEROPT def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) # Prevent setting loggers mocker.patch("freqtrade.loggers.logging.config.dictConfig", MagicMock) arglist = ["trade", "-vvv"] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("verbosity") == 3 assert log_has("Verbosity set to 3", caplog) @pytest.mark.usefixtures("keep_log_config_loggers") def test_set_logfile(default_conf, mocker, tmp_path): default_conf["ft_tests_force_logging"] = True patched_configuration_load_config_file(mocker, default_conf) f = tmp_path / "test_file.log" assert not f.is_file() arglist = [ "trade", "--logfile", str(f), ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf["logfile"] == str(f) assert f.is_file() try: f.unlink() except Exception: pass def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf["force_entry_enable"] = True patched_configuration_load_config_file(mocker, default_conf) args = Arguments(["trade"]).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() assert validated_conf.get("force_entry_enable") assert log_has("`force_entry_enable` RPC message enabled.", caplog) def test_validate_default_conf(default_conf) -> None: # Validate via our validator - we allow setting defaults! validate_config_schema(default_conf) @pytest.mark.parametrize("fiat", ["EUR", "USD", "", None]) def test_validate_fiat_currency_options(default_conf, fiat) -> None: # Validate via our validator - we allow setting defaults! if fiat is not None: default_conf["fiat_display_currency"] = fiat else: del default_conf["fiat_display_currency"] validate_config_schema(default_conf) def test_validate_max_open_trades(default_conf): default_conf["max_open_trades"] = float("inf") default_conf["stake_amount"] = "unlimited" with pytest.raises( OperationalException, match="`max_open_trades` and `stake_amount` cannot both be unlimited.", ): validate_config_consistency(default_conf) def test_validate_price_side(default_conf): default_conf["order_types"] = { "entry": "limit", "exit": "limit", "stoploss": "limit", "stoploss_on_exchange": False, } # Default should pass validate_config_consistency(default_conf) conf = deepcopy(default_conf) conf["order_types"]["entry"] = "market" with pytest.raises( OperationalException, match='Market entry orders require entry_pricing.price_side = "other".', ): validate_config_consistency(conf) conf = deepcopy(default_conf) conf["order_types"]["exit"] = "market" with pytest.raises( OperationalException, match='Market exit orders require exit_pricing.price_side = "other".' ): validate_config_consistency(conf) # Validate inversed case conf = deepcopy(default_conf) conf["order_types"]["exit"] = "market" conf["order_types"]["entry"] = "market" conf["exit_pricing"]["price_side"] = "bid" conf["entry_pricing"]["price_side"] = "ask" validate_config_consistency(conf) def test_validate_tsl(default_conf): default_conf["stoploss"] = 0.0 with pytest.raises( OperationalException, match="The config stoploss needs to be different " "from 0 to avoid problems with sell orders.", ): validate_config_consistency(default_conf) default_conf["stoploss"] = -0.10 default_conf["trailing_stop"] = True default_conf["trailing_stop_positive"] = 0 default_conf["trailing_stop_positive_offset"] = 0 default_conf["trailing_only_offset_is_reached"] = True with pytest.raises( OperationalException, match=r"The config trailing_only_offset_is_reached needs " "trailing_stop_positive_offset to be more than 0 in your config.", ): validate_config_consistency(default_conf) default_conf["trailing_stop_positive_offset"] = 0.01 default_conf["trailing_stop_positive"] = 0.015 with pytest.raises( OperationalException, match=r"The config trailing_stop_positive_offset needs " "to be greater than trailing_stop_positive in your config.", ): validate_config_consistency(default_conf) default_conf["trailing_stop_positive"] = 0.01 default_conf["trailing_stop_positive_offset"] = 0.015 validate_config_consistency(default_conf) # 0 trailing stop positive - results in "Order would trigger immediately" default_conf["trailing_stop_positive"] = 0 default_conf["trailing_stop_positive_offset"] = 0.02 default_conf["trailing_only_offset_is_reached"] = False with pytest.raises( OperationalException, match="The config trailing_stop_positive needs to be different from 0 " "to avoid problems with sell orders", ): validate_config_consistency(default_conf) def test_validate_whitelist(default_conf): default_conf["runmode"] = RunMode.DRY_RUN # Test regular case - has whitelist and uses StaticPairlist validate_config_consistency(default_conf) conf = deepcopy(default_conf) del conf["exchange"]["pair_whitelist"] # Test error case with pytest.raises( OperationalException, match="StaticPairList requires pair_whitelist to be set." ): validate_config_consistency(conf) conf = deepcopy(default_conf) conf.update( { "pairlists": [ { "method": "VolumePairList", } ] } ) # Dynamic whitelist should not care about pair_whitelist validate_config_consistency(conf) del conf["exchange"]["pair_whitelist"] validate_config_consistency(conf) def test_validate_ask_orderbook(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf["exit_pricing"]["use_order_book"] = True conf["exit_pricing"]["order_book_min"] = 2 conf["exit_pricing"]["order_book_max"] = 2 validate_config_consistency(conf) assert log_has_re(r"DEPRECATED: Please use `order_book_top` instead of.*", caplog) assert conf["exit_pricing"]["order_book_top"] == 2 conf["exit_pricing"]["order_book_max"] = 5 with pytest.raises( OperationalException, match=r"Using order_book_max != order_book_min in exit_pricing.*" ): validate_config_consistency(conf) def test_validate_time_in_force(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf["order_time_in_force"] = { "buy": "gtc", "sell": "GTC", } validate_config_consistency(conf) assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for time_in_force is.*", caplog) assert conf["order_time_in_force"]["entry"] == "gtc" assert conf["order_time_in_force"]["exit"] == "GTC" conf = deepcopy(default_conf) conf["order_time_in_force"] = { "buy": "GTC", "sell": "GTC", } conf["trading_mode"] = "futures" with pytest.raises( OperationalException, match=r"Please migrate your time_in_force settings .* 'entry' and 'exit'\.", ): validate_config_consistency(conf) def test__validate_order_types(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf["order_types"] = { "buy": "limit", "sell": "market", "forcesell": "market", "forcebuy": "limit", "stoploss": "market", "stoploss_on_exchange": False, } validate_config_consistency(conf) assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for order_types is.*", caplog) assert conf["order_types"]["entry"] == "limit" assert conf["order_types"]["exit"] == "market" assert conf["order_types"]["force_entry"] == "limit" assert "buy" not in conf["order_types"] assert "sell" not in conf["order_types"] assert "forcebuy" not in conf["order_types"] assert "forcesell" not in conf["order_types"] conf = deepcopy(default_conf) conf["order_types"] = { "buy": "limit", "sell": "market", "forcesell": "market", "forcebuy": "limit", "stoploss": "market", "stoploss_on_exchange": False, } conf["trading_mode"] = "futures" with pytest.raises( OperationalException, match=r"Please migrate your order_types settings to use the new wording\.", ): validate_config_consistency(conf) def test__validate_unfilledtimeout(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf["unfilledtimeout"] = { "buy": 30, "sell": 35, } validate_config_consistency(conf) assert log_has_re(r"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is.*", caplog) assert conf["unfilledtimeout"]["entry"] == 30 assert conf["unfilledtimeout"]["exit"] == 35 assert "buy" not in conf["unfilledtimeout"] assert "sell" not in conf["unfilledtimeout"] conf = deepcopy(default_conf) conf["unfilledtimeout"] = { "buy": 30, "sell": 35, } conf["trading_mode"] = "futures" with pytest.raises( OperationalException, match=r"Please migrate your unfilledtimeout settings to use the new wording\.", ): validate_config_consistency(conf) def test__validate_pricing_rules(default_conf, caplog) -> None: def_conf = deepcopy(default_conf) del def_conf["entry_pricing"] del def_conf["exit_pricing"] def_conf["ask_strategy"] = { "price_side": "ask", "use_order_book": True, "bid_last_balance": 0.5, } def_conf["bid_strategy"] = { "price_side": "bid", "use_order_book": False, "ask_last_balance": 0.7, } conf = deepcopy(def_conf) validate_config_consistency(conf) assert log_has_re(r"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is.*", caplog) assert conf["exit_pricing"]["price_side"] == "ask" assert conf["exit_pricing"]["use_order_book"] is True assert conf["exit_pricing"]["price_last_balance"] == 0.5 assert conf["entry_pricing"]["price_side"] == "bid" assert conf["entry_pricing"]["use_order_book"] is False assert conf["entry_pricing"]["price_last_balance"] == 0.7 assert "ask_strategy" not in conf assert "bid_strategy" not in conf conf = deepcopy(def_conf) conf["trading_mode"] = "futures" with pytest.raises( OperationalException, match=r"Please migrate your pricing settings to use the new wording\." ): validate_config_consistency(conf) def test__validate_freqai_include_timeframes(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf.update( { "freqai": { "enabled": True, "feature_parameters": { "include_timeframes": ["1m", "5m"], "include_corr_pairlist": [], }, "data_split_parameters": {}, "model_training_parameters": {}, } } ) with pytest.raises(OperationalException, match=r"Main timeframe of .*"): validate_config_consistency(conf) # Validation pass conf.update({"timeframe": "1m"}) validate_config_consistency(conf) # Ensure base timeframe is in include_timeframes conf["freqai"]["feature_parameters"]["include_timeframes"] = ["5m", "15m"] validate_config_consistency(conf) assert conf["freqai"]["feature_parameters"]["include_timeframes"] == ["1m", "5m", "15m"] conf.update({"analyze_per_epoch": True}) with pytest.raises( OperationalException, match=r"Using analyze-per-epoch .* not supported with a FreqAI strategy.", ): validate_config_consistency(conf) def test__validate_consumers(default_conf, caplog) -> None: conf = deepcopy(default_conf) conf.update({"external_message_consumer": {"enabled": True, "producers": []}}) with pytest.raises( OperationalException, match="You must specify at least 1 Producer to connect to." ): validate_config_consistency(conf) conf = deepcopy(default_conf) conf.update( { "external_message_consumer": { "enabled": True, "producers": [ { "name": "default", "host": "127.0.0.1", "port": 8081, "ws_token": "secret_ws_t0ken.", }, { "name": "default", "host": "127.0.0.1", "port": 8080, "ws_token": "secret_ws_t0ken.", }, ], } } ) with pytest.raises( OperationalException, match="Producer names must be unique. Duplicate: default" ): validate_config_consistency(conf) conf = deepcopy(default_conf) conf.update( { "process_only_new_candles": True, "external_message_consumer": { "enabled": True, "producers": [ { "name": "default", "host": "127.0.0.1", "port": 8081, "ws_token": "secret_ws_t0ken.", } ], }, } ) validate_config_consistency(conf) assert log_has_re("To receive best performance with external data.*", caplog) def test__validate_orderflow(default_conf) -> None: conf = deepcopy(default_conf) conf["exchange"]["use_public_trades"] = True with pytest.raises( ConfigurationError, match="Orderflow is a required configuration key when using public trades.", ): validate_config_consistency(conf) conf.update( { "orderflow": { "scale": 0.5, "stacked_imbalance_range": 3, "imbalance_volume": 100, "imbalance_ratio": 3, } } ) # Should pass. validate_config_consistency(conf) def test_validate_edge_removal(default_conf): default_conf["edge"] = { "enabled": True, } with pytest.raises( ConfigurationError, match="Edge is no longer supported and has been removed from Freqtrade with 2025.6.", ): validate_config_consistency(default_conf) def test_load_config_test_comments() -> None: """ Load config with comments """ config_file = Path(__file__).parents[0] / "config_test_comments.json" conf = load_config_file(str(config_file)) assert conf def test_load_config_default_exchange(all_conf) -> None: """ config['exchange'] subtree has required options in it so it cannot be omitted in the config """ del all_conf["exchange"] assert "exchange" not in all_conf with pytest.raises(ConfigurationError, match=r"'exchange' is a required property"): validate_config_schema(all_conf) def test_load_config_default_exchange_name(all_conf) -> None: """ config['exchange']['name'] option is required so it cannot be omitted in the config """ del all_conf["exchange"]["name"] assert "name" not in all_conf["exchange"] with pytest.raises(ConfigurationError, match=r"'name' is a required property"): validate_config_schema(all_conf) def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None: all_conf["order_types"]["stoploss_on_exchange_limit_ratio"] = 1.15 with pytest.raises(ConfigurationError, match=r"1.15 is greater than the maximum"): validate_config_schema(all_conf) @pytest.mark.parametrize( "keys", [ ("exchange", "key", ""), ("exchange", "secret", ""), ("exchange", "password", ""), ], ) def test_load_config_default_subkeys(all_conf, keys) -> None: """ Test for parameters with default values in sub-paths so they can be omitted in the config and the default value should is added to the config. """ # Get first level key key = keys[0] # get second level key subkey = keys[1] del all_conf[key][subkey] assert subkey not in all_conf[key] validate_config_schema(all_conf) assert subkey in all_conf[key] assert all_conf[key][subkey] == keys[2] def test_pairlist_resolving(): arglist = ["download-data", "--pairs", "ETH/BTC", "XRP/BTC", "--exchange", "binance"] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() assert config["pairs"] == ["ETH/BTC", "XRP/BTC"] assert config["exchange"]["pair_whitelist"] == ["ETH/BTC", "XRP/BTC"] assert config["exchange"]["name"] == "binance" def test_pairlist_resolving_with_config(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) arglist = [ "download-data", "--config", "config.json", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() assert config["pairs"] == default_conf["exchange"]["pair_whitelist"] assert config["exchange"]["name"] == default_conf["exchange"]["name"] # Override pairs arglist = [ "download-data", "--config", "config.json", "--pairs", "ETH/BTC", "XRP/BTC", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() assert config["pairs"] == ["ETH/BTC", "XRP/BTC"] assert config["exchange"]["name"] == default_conf["exchange"]["name"] def test_pairlist_resolving_with_config_pl(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) arglist = [ "download-data", "--config", "config.json", "--pairs-file", "tests/testdata/pairs.json", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() assert len(config["pairs"]) == 23 assert "ETH/BTC" in config["pairs"] assert "XRP/BTC" in config["pairs"] assert config["exchange"]["name"] == default_conf["exchange"]["name"] def test_pairlist_resolving_with_config_pl_not_exists(mocker, default_conf): patched_configuration_load_config_file(mocker, default_conf) arglist = [ "download-data", "--config", "config.json", "--pairs-file", "tests/testdata/pairs_doesnotexist.json", ] args = Arguments(arglist).get_parsed_arg() with pytest.raises(OperationalException, match=r"No pairs file found with path.*"): configuration = Configuration(args) configuration.get_config() def test_pairlist_resolving_fallback(mocker, tmp_path): mocker.patch.object(Path, "exists", MagicMock(return_value=True)) mocker.patch.object(Path, "open", MagicMock(return_value=MagicMock())) mocker.patch( "freqtrade.configuration.configuration.load_file", MagicMock(return_value=["XRP/BTC", "ETH/BTC"]), ) arglist = ["download-data", "--exchange", "binance"] args = Arguments(arglist).get_parsed_arg() # Fix flaky tests if config.json exists args["config"] = None configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() assert config["pairs"] == ["ETH/BTC", "XRP/BTC"] assert config["exchange"]["name"] == "binance" assert config["datadir"] == tmp_path / "user_data/data/binance" @pytest.mark.parametrize( "setting", [ ("webhook", "webhookbuy", "testWEbhook", "webhook", "webhookentry", "testWEbhook"), ( "ask_strategy", "ignore_buying_expired_candle_after", 5, None, "ignore_buying_expired_candle_after", 6, ), ], ) def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings # (they may not exist in the config) default_conf[setting[0]] = {} default_conf[setting[3]] = {} # Assign deprecated setting default_conf[setting[0]][setting[1]] = setting[2] # Assign new setting if setting[3]: default_conf[setting[3]][setting[4]] = setting[5] else: default_conf[setting[4]] = setting[5] # New and deprecated settings are conflicting ones with pytest.raises(OperationalException, match=r"DEPRECATED"): process_temporary_deprecated_settings(default_conf) caplog.clear() # Delete new setting if setting[3]: del default_conf[setting[3]][setting[4]] else: del default_conf[setting[4]] process_temporary_deprecated_settings(default_conf) assert log_has_re("DEPRECATED", caplog) # The value of the new setting shall have been set to the # value of the deprecated one if setting[3]: assert default_conf[setting[3]][setting[4]] == setting[2] else: assert default_conf[setting[4]] == setting[2] @pytest.mark.parametrize( "setting", [ ("experimental", "use_sell_signal", False), ("experimental", "sell_profit_only", True), ("experimental", "ignore_roi_if_buy_signal", True), ], ) def test_process_removed_settings(mocker, default_conf, setting): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings # (they may not exist in the config) default_conf[setting[0]] = {} # Assign removed setting default_conf[setting[0]][setting[1]] = setting[2] # New and deprecated settings are conflicting ones with pytest.raises(OperationalException, match=r"Setting .* has been moved"): process_temporary_deprecated_settings(default_conf) def test_check_conflicting_settings(mocker, default_conf, caplog): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings # (they may not exist in the config) default_conf["sectionA"] = {} default_conf["sectionB"] = {} # Assign new setting default_conf["sectionA"]["new_setting"] = "valA" # Assign deprecated setting default_conf["sectionB"]["deprecated_setting"] = "valB" # New and deprecated settings are conflicting ones with pytest.raises(OperationalException, match=r"DEPRECATED"): check_conflicting_settings( default_conf, "sectionB", "deprecated_setting", "sectionA", "new_setting" ) caplog.clear() # Delete new setting (deprecated exists) del default_conf["sectionA"]["new_setting"] check_conflicting_settings( default_conf, "sectionB", "deprecated_setting", "sectionA", "new_setting" ) assert not log_has_re("DEPRECATED", caplog) assert "new_setting" not in default_conf["sectionA"] caplog.clear() # Assign new setting default_conf["sectionA"]["new_setting"] = "valA" # Delete deprecated setting del default_conf["sectionB"]["deprecated_setting"] check_conflicting_settings( default_conf, "sectionB", "deprecated_setting", "sectionA", "new_setting" ) assert not log_has_re("DEPRECATED", caplog) assert default_conf["sectionA"]["new_setting"] == "valA" def test_process_deprecated_setting(mocker, default_conf, caplog): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings # (they may not exist in the config) default_conf["sectionA"] = {} default_conf["sectionB"] = {} # Assign deprecated setting default_conf["sectionB"]["deprecated_setting"] = "valB" # Both new and deprecated settings exists process_deprecated_setting( default_conf, "sectionB", "deprecated_setting", "sectionA", "new_setting" ) assert log_has_re("DEPRECATED", caplog) # The value of the new setting shall have been set to the # value of the deprecated one assert default_conf["sectionA"]["new_setting"] == "valB" # Old setting is removed assert "deprecated_setting" not in default_conf["sectionB"] caplog.clear() # Delete new setting (deprecated exists) del default_conf["sectionA"]["new_setting"] default_conf["sectionB"]["deprecated_setting"] = "valB" process_deprecated_setting( default_conf, "sectionB", "deprecated_setting", "sectionA", "new_setting" ) assert log_has_re("DEPRECATED", caplog) # The value of the new setting shall have been set to the # value of the deprecated one assert default_conf["sectionA"]["new_setting"] == "valB" caplog.clear() # Assign new setting default_conf["sectionA"]["new_setting"] = "valA" # Delete deprecated setting default_conf["sectionB"].pop("deprecated_setting", None) process_deprecated_setting( default_conf, "sectionB", "deprecated_setting", "sectionA", "new_setting" ) assert not log_has_re("DEPRECATED", caplog) assert default_conf["sectionA"]["new_setting"] == "valA" caplog.clear() # Test moving to root default_conf["sectionB"]["deprecated_setting2"] = "DeadBeef" process_deprecated_setting(default_conf, "sectionB", "deprecated_setting2", None, "new_setting") assert log_has_re("DEPRECATED", caplog) assert default_conf["new_setting"] def test_process_removed_setting(mocker, default_conf, caplog): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings # (they may not exist in the config) default_conf["sectionA"] = {} default_conf["sectionB"] = {} # Assign new setting default_conf["sectionB"]["somesetting"] = "valA" # Only new setting exists (nothing should happen) process_removed_setting(default_conf, "sectionA", "somesetting", "sectionB", "somesetting") # Assign removed setting default_conf["sectionA"]["somesetting"] = "valB" with pytest.raises(OperationalException, match=r"Setting .* has been moved"): process_removed_setting(default_conf, "sectionA", "somesetting", "sectionB", "somesetting") def test_process_deprecated_ticker_interval(default_conf, caplog): message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval." config = deepcopy(default_conf) process_temporary_deprecated_settings(config) assert not log_has(message, caplog) del config["timeframe"] config["ticker_interval"] = "15m" with pytest.raises( OperationalException, match=r"DEPRECATED: 'ticker_interval' detected. Please use.*" ): process_temporary_deprecated_settings(config) def test_process_deprecated_protections(default_conf, caplog): message = "DEPRECATED: Setting 'protections' in the configuration is deprecated." config = deepcopy(default_conf) process_temporary_deprecated_settings(config) assert not log_has(message, caplog) config["protections"] = [] with pytest.raises(ConfigurationError, match=message): process_temporary_deprecated_settings(config) def test_flat_vars_to_nested_dict(caplog): test_args = { "FREQTRADE__EXCHANGE__SOME_SETTING": "true", "FREQTRADE__EXCHANGE__SOME_FALSE_SETTING": "false", "FREQTRADE__EXCHANGE__CONFIG__whatEver": "sometime", # Lowercased # Preserve case for ccxt_config "FREQTRADE__EXCHANGE__CCXT_CONFIG__httpsProxy": "something", "FREQTRADE__EXIT_PRICING__PRICE_SIDE": "bid", "FREQTRADE__EXIT_PRICING__cccc": "500", "FREQTRADE__STAKE_AMOUNT": "200.05", "FREQTRADE__TELEGRAM__CHAT_ID": "2151", "NOT_RELEVANT": "200.0", # Will be ignored "FREQTRADE__ARRAY": '[{"name":"default","host":"xxx"}]', "FREQTRADE__EXCHANGE__PAIR_WHITELIST": '["BTC/USDT", "ETH/USDT"]', # Fails due to trailing comma "FREQTRADE__ARRAY_TRAIL_COMMA": '[{"name":"default","host":"xxx",}]', # Object fails "FREQTRADE__OBJECT": '{"name":"default","host":"xxx"}', } expected = { "stake_amount": 200.05, "exit_pricing": { "price_side": "bid", "cccc": 500, }, "exchange": { "config": { "whatever": "sometime", }, "ccxt_config": { "httpsProxy": "something", }, "some_setting": True, "some_false_setting": False, "pair_whitelist": ["BTC/USDT", "ETH/USDT"], }, "telegram": {"chat_id": "2151"}, "array": [{"name": "default", "host": "xxx"}], "object": '{"name":"default","host":"xxx"}', "array_trail_comma": '[{"name":"default","host":"xxx",}]', } res = _flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX) assert res == expected assert log_has("Loading variable 'FREQTRADE__EXCHANGE__SOME_SETTING'", caplog) assert not log_has("Loading variable 'NOT_RELEVANT'", caplog) def test_setup_hyperopt_freqai(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x) mocker.patch( "freqtrade.configuration.configuration.create_userdata_dir", lambda x, *args, **kwargs: Path(x), ) arglist = [ "hyperopt", "--config", "config.json", "--strategy", CURRENT_TEST_STRATEGY, "--timerange", "20220801-20220805", "--freqaimodel", "LightGBMRegressorMultiTarget", "--analyze-per-epoch", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() config["freqai"] = {"enabled": True} with pytest.raises( OperationalException, match=r".*analyze-per-epoch parameter is not supported.*" ): validate_config_consistency(config) def test_setup_freqai_backtesting(mocker, default_conf) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch("freqtrade.configuration.configuration.create_datadir", lambda c, x: x) mocker.patch( "freqtrade.configuration.configuration.create_userdata_dir", lambda x, *args, **kwargs: Path(x), ) arglist = [ "backtesting", "--config", "config.json", "--strategy", CURRENT_TEST_STRATEGY, "--timerange", "20220801-20220805", "--freqaimodel", "LightGBMRegressorMultiTarget", "--freqai-backtest-live-models", ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) config = configuration.get_config() config["runmode"] = RunMode.BACKTEST with pytest.raises( OperationalException, match=r".*--freqai-backtest-live-models parameter is only.*" ): validate_config_consistency(config) conf = deepcopy(config) conf["freqai"] = {"enabled": True} with pytest.raises( OperationalException, match=r".* timerange parameter is not supported with .*" ): validate_config_consistency(conf) conf["timerange"] = None conf["freqai_backtest_live_models"] = False with pytest.raises( OperationalException, match=r".* pass --timerange if you intend to use FreqAI .*" ): validate_config_consistency(conf) def test_sanitize_config(default_conf_usdt): assert default_conf_usdt["exchange"]["key"] != "REDACTED" res = sanitize_config(default_conf_usdt) # Didn't modify original dict assert default_conf_usdt["exchange"]["key"] != "REDACTED" assert "accountId" not in default_conf_usdt["exchange"] assert res["exchange"]["key"] == "REDACTED" assert res["exchange"]["secret"] == "REDACTED" # Didn't add a non-existing key assert "accountId" not in res["exchange"] res = sanitize_config(default_conf_usdt, show_sensitive=True) assert res["exchange"]["key"] == default_conf_usdt["exchange"]["key"] assert res["exchange"]["secret"] == default_conf_usdt["exchange"]["secret"] def test_remove_exchange_credentials(default_conf) -> None: conf = deepcopy(default_conf) remove_exchange_credentials(conf["exchange"], False) assert conf["exchange"]["key"] != "" assert conf["exchange"]["secret"] != "" remove_exchange_credentials(conf["exchange"], True) assert conf["exchange"]["key"] == "" assert conf["exchange"]["secret"] == "" assert conf["exchange"].get("password", "") == "" assert conf["exchange"].get("uid", "") == ""