diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index e8f393dc3..0cafbf396 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -17,15 +17,17 @@ logger = logging.getLogger(__name__) CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS +ENV_PREFIX = 'FREQTRADE_' + class SubstitutionException(Exception): """ Indicates that a variable within the configuration couldn't be substituted. """ - def __init__(self, key: str, offset: int): + def __init__(self, err: str, offset: int): self.offset = offset - self.err = f'Environment variable {key} was requested for substitution, but is not set.' + self.err = err super().__init__(self.err) @@ -52,8 +54,17 @@ def substitute_environment_variable(match: re.Match) -> str: Substitutes a matched environment variable with its value """ key = match.group(1).strip() + if not key.startswith(ENV_PREFIX): + raise SubstitutionException( + f'Environment variable {key} must be prefixed with {ENV_PREFIX} .', + match.start(0) + ) + if key not in environ: - raise SubstitutionException(key, match.start(0)) + raise SubstitutionException( + f'Environment variable {key} was requested for substitution, but is not set.', + match.start(0) + ) return environ[key] diff --git a/tests/config_test_environment.json b/tests/config_test_environment.json index ac3a64390..534326c55 100644 --- a/tests/config_test_environment.json +++ b/tests/config_test_environment.json @@ -106,8 +106,8 @@ // We can now comment out some settings // "enabled": true, "enabled": false, - "token": "${TELEGRAM_TOKEN}", - "chat_id": "${TELEGRAM_CHAT}" + "token": "${FREQTRADE_TELEGRAM_TOKEN}", + "chat_id": "${FREQTRADE_TELEGRAM_CHAT}" }, "api_server": { "enabled": false, diff --git a/tests/config_test_environment_invalid.json b/tests/config_test_environment_invalid.json new file mode 100644 index 000000000..ac3a64390 --- /dev/null +++ b/tests/config_test_environment_invalid.json @@ -0,0 +1,127 @@ +{ + /* Single-line C-style comment */ + "max_open_trades": 3, + /* + * Multi-line C-style comment + */ + "stake_currency": "BTC", + "stake_amount": 0.05, + "fiat_display_currency": "USD", // C++-style comment + "amount_reserve_percent" : 0.05, // And more, tabs before this comment + "dry_run": false, + "timeframe": "5m", + "trailing_stop": false, + "trailing_stop_positive": 0.005, + "trailing_stop_positive_offset": 0.0051, + "trailing_only_offset_is_reached": false, + "minimal_roi": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, + "stoploss": -0.10, + "unfilledtimeout": { + "buy": 10, + "sell": 30, // Trailing comma should also be accepted now + }, + "bid_strategy": { + "use_order_book": false, + "ask_last_balance": 0.0, + "order_book_top": 1, + "check_depth_of_market": { + "enabled": false, + "bids_to_ask_delta": 1 + } + }, + "ask_strategy":{ + "use_order_book": false, + "order_book_min": 1, + "order_book_max": 9 + }, + "order_types": { + "buy": "limit", + "sell": "limit", + "stoploss": "market", + "stoploss_on_exchange": false, + "stoploss_on_exchange_interval": 60 + }, + "order_time_in_force": { + "buy": "gtc", + "sell": "gtc" + }, + "pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume", + "precision_filter": false + } + }, + "exchange": { + "name": "bittrex", + "sandbox": false, + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": false, + "rateLimit": 500, + "aiohttp_trust_env": false + }, + "pair_whitelist": [ + "ETH/BTC", + "LTC/BTC", + "ETC/BTC", + "DASH/BTC", + "ZEC/BTC", + "XLM/BTC", + "NXT/BTC", + "TRX/BTC", + "ADA/BTC", + "XMR/BTC" + ], + "pair_blacklist": [ + "DOGE/BTC" + ], + "outdated_offset": 5, + "markets_refresh_interval": 60 + }, + "edge": { + "enabled": false, + "process_throttle_secs": 3600, + "calculate_since_number_of_days": 7, + "allowed_risk": 0.01, + "stoploss_range_min": -0.01, + "stoploss_range_max": -0.1, + "stoploss_range_step": -0.01, + "minimum_winrate": 0.60, + "minimum_expectancy": 0.20, + "min_trade_number": 10, + "max_trade_duration_minute": 1440, + "remove_pumps": false + }, + "telegram": { +// We can now comment out some settings +// "enabled": true, + "enabled": false, + "token": "${TELEGRAM_TOKEN}", + "chat_id": "${TELEGRAM_CHAT}" + }, + "api_server": { + "enabled": false, + "listen_ip_address": "127.0.0.1", + "listen_port": 8080, + "username": "freqtrader", + "password": "SuperSecurePassword" + }, + "db_url": "sqlite:///tradesv3.sqlite", + "initial_state": "running", + "forcebuy_enable": false, + "internals": { + "process_throttle_secs": 5 + }, + "strategy": "DefaultStrategy", + "strategy_path": "user_data/strategies/" +} diff --git a/tests/test_configuration.py b/tests/test_configuration.py index b25067bd5..2d60229ae 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -919,7 +919,10 @@ def test_load_config_test_env_variables(mocker) -> None: token = "17264728:eW91dHUuYmUvMDAwYWw3cnUzbXMg" chat_id = "17263827" - mocker.patch.dict(os.environ, {'TELEGRAM_TOKEN': token, 'TELEGRAM_CHAT': chat_id}) + mocker.patch.dict(os.environ, { + 'FREQTRADE_TELEGRAM_TOKEN': token, + 'FREQTRADE_TELEGRAM_CHAT': chat_id + }) config_file = Path(__file__).parents[0] / "config_test_environment.json" conf = load_config_file(str(config_file)) @@ -932,9 +935,18 @@ def test_load_config_test_substitution_error() -> None: """ Load config with environment variables without setting them """ - config_file = Path(__file__).parents[0] / "config_test_environment.json" - with pytest.raises(OperationalException, match=r'.*Environment variable TELEGRAM_TOKEN*'): + with pytest.raises(OperationalException, match=r'.*variable FREQTRADE_TELEGRAM_TOKEN*'): + load_config_file(str(config_file)) + + +def test_load_config_test_prefix_error() -> None: + """ + Load config with environment variables without setting them + """ + + config_file = Path(__file__).parents[0] / "config_test_environment_invalid.json" + with pytest.raises(OperationalException, match=r'.*must be prefixed*'): load_config_file(str(config_file))