From ca94bbe9941a0ff3b3bf8cfbe197dc60573f1864 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 06:39:34 +0100 Subject: [PATCH 01/12] Add configuration error to Exception hierarchy --- docs/developer.md | 2 ++ freqtrade/exceptions.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index 9c549012d..572d5f58b 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -129,6 +129,8 @@ Below is an outline of exception inheritance hierarchy: + FreqtradeException | +---+ OperationalException +| | +| +---+ ConfigurationError | +---+ DependencyException | | diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 6b0039a3f..8b2c9c993 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -12,6 +12,12 @@ class OperationalException(FreqtradeException): """ +class ConfigurationError(OperationalException): + """ + Configuration error. Usually caused by invalid configuration. + """ + + class DependencyException(FreqtradeException): """ Indicates that an assumed dependency is not met. From 3a6474967883d2b6995991571fa06c4a9d459f6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 06:41:23 +0100 Subject: [PATCH 02/12] Raise ConfigurationError on config validation --- freqtrade/configuration/config_validation.py | 52 ++++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 4afed60cd..419af8347 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -9,7 +9,7 @@ from jsonschema.exceptions import ValidationError, best_match from freqtrade import constants from freqtrade.configuration.deprecated_settings import process_deprecated_setting from freqtrade.enums import RunMode, TradingMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError logger = logging.getLogger(__name__) @@ -73,7 +73,7 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal Should be ran after loading both configuration and strategy, since strategies can set certain configuration settings too. :param conf: Config in JSON format - :return: Returns None if everything is ok, otherwise throw an OperationalException + :return: Returns None if everything is ok, otherwise throw an ConfigurationError """ # validating trailing stoploss @@ -98,12 +98,12 @@ def validate_config_consistency(conf: Dict[str, Any], *, preliminary: bool = Fal def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: """ If edge is disabled, either max_open_trades or stake_amount need to be set. - :raise: OperationalException if config validation failed + :raise: ConfigurationError if config validation failed """ if (not conf.get('edge', {}).get('enabled') and conf.get('max_open_trades') == float('inf') and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT): - raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") + raise ConfigurationError("`max_open_trades` and `stake_amount` cannot both be unlimited.") def _validate_price_config(conf: Dict[str, Any]) -> None: @@ -113,18 +113,18 @@ def _validate_price_config(conf: Dict[str, Any]) -> None: # TODO: The below could be an enforced setting when using market orders if (conf.get('order_types', {}).get('entry') == 'market' and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')): - raise OperationalException( + raise ConfigurationError( 'Market entry orders require entry_pricing.price_side = "other".') if (conf.get('order_types', {}).get('exit') == 'market' and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')): - raise OperationalException('Market exit orders require exit_pricing.price_side = "other".') + raise ConfigurationError('Market exit orders require exit_pricing.price_side = "other".') def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: - raise OperationalException( + raise ConfigurationError( 'The config stoploss needs to be different from 0 to avoid problems with sell orders.' ) # Skip if trailing stoploss is not activated @@ -137,17 +137,17 @@ def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if tsl_only_offset: if tsl_positive == 0.0: - raise OperationalException( + raise ConfigurationError( 'The config trailing_only_offset_is_reached needs ' 'trailing_stop_positive_offset to be more than 0 in your config.') if tsl_positive > 0 and 0 < tsl_offset <= tsl_positive: - raise OperationalException( + raise ConfigurationError( 'The config trailing_stop_positive_offset needs ' 'to be greater than trailing_stop_positive in your config.') # Fetch again without default if 'trailing_stop_positive' in conf and float(conf['trailing_stop_positive']) == 0.0: - raise OperationalException( + raise ConfigurationError( 'The config trailing_stop_positive needs to be different from 0 ' 'to avoid problems with sell orders.' ) @@ -162,7 +162,7 @@ def _validate_edge(conf: Dict[str, Any]) -> None: return if not conf.get('use_exit_signal', True): - raise OperationalException( + raise ConfigurationError( "Edge requires `use_exit_signal` to be True, otherwise no sells will happen." ) @@ -178,7 +178,7 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: for pl in conf.get('pairlists', [{'method': 'StaticPairList'}]): if (isinstance(pl, dict) and pl.get('method') == 'StaticPairList' and not conf.get('exchange', {}).get('pair_whitelist')): - raise OperationalException("StaticPairList requires pair_whitelist to be set.") + raise ConfigurationError("StaticPairList requires pair_whitelist to be set.") def _validate_protections(conf: Dict[str, Any]) -> None: @@ -188,13 +188,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None: for prot in conf.get('protections', []): if ('stop_duration' in prot and 'stop_duration_candles' in prot): - raise OperationalException( + raise ConfigurationError( "Protections must specify either `stop_duration` or `stop_duration_candles`.\n" f"Please fix the protection {prot.get('method')}" ) if ('lookback_period' in prot and 'lookback_period_candles' in prot): - raise OperationalException( + raise ConfigurationError( "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" f"Please fix the protection {prot.get('method')}" ) @@ -206,7 +206,7 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None: ob_max = ask_strategy.get('order_book_max') if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'): if ob_min != ob_max: - raise OperationalException( + raise ConfigurationError( "Using order_book_max != order_book_min in exit_pricing is no longer supported." "Please pick one value and use `order_book_top` in the future." ) @@ -234,7 +234,7 @@ def _validate_time_in_force(conf: Dict[str, Any]) -> None: time_in_force = conf.get('order_time_in_force', {}) if 'buy' in time_in_force or 'sell' in time_in_force: if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your time_in_force settings to use 'entry' and 'exit'.") else: logger.warning( @@ -255,7 +255,7 @@ def _validate_order_types(conf: Dict[str, Any]) -> None: 'forcesell', 'emergencyexit', 'forceexit', 'forceentry'] if any(x in order_types for x in old_order_types): if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your order_types settings to use the new wording.") else: logger.warning( @@ -280,7 +280,7 @@ def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None: unfilledtimeout = conf.get('unfilledtimeout', {}) if any(x in unfilledtimeout for x in ['buy', 'sell']): if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your unfilledtimeout settings to use the new wording.") else: @@ -300,7 +300,7 @@ def _validate_pricing_rules(conf: Dict[str, Any]) -> None: if conf.get('ask_strategy') or conf.get('bid_strategy'): if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT: - raise OperationalException( + raise ConfigurationError( "Please migrate your pricing settings to use the new wording.") else: @@ -331,7 +331,7 @@ def _validate_freqai_hyperopt(conf: Dict[str, Any]) -> None: freqai_enabled = conf.get('freqai', {}).get('enabled', False) analyze_per_epoch = conf.get('analyze_per_epoch', False) if analyze_per_epoch and freqai_enabled: - raise OperationalException( + raise ConfigurationError( 'Using analyze-per-epoch parameter is not supported with a FreqAI strategy.') @@ -350,7 +350,7 @@ def _validate_freqai_include_timeframes(conf: Dict[str, Any], preliminary: bool) if tf_s < main_tf_s: offending_lines.append(tf) if offending_lines: - raise OperationalException( + raise ConfigurationError( f"Main timeframe of {main_tf} must be smaller or equal to FreqAI " f"`include_timeframes`.Offending include-timeframes: {', '.join(offending_lines)}") @@ -368,17 +368,17 @@ def _validate_freqai_backtest(conf: Dict[str, Any]) -> None: timerange = conf.get('timerange') freqai_backtest_live_models = conf.get('freqai_backtest_live_models', False) if freqai_backtest_live_models and freqai_enabled and timerange: - raise OperationalException( + raise ConfigurationError( 'Using timerange parameter is not supported with ' '--freqai-backtest-live-models parameter.') if freqai_backtest_live_models and not freqai_enabled: - raise OperationalException( + raise ConfigurationError( 'Using --freqai-backtest-live-models parameter is only ' 'supported with a FreqAI strategy.') if freqai_enabled and not freqai_backtest_live_models and not timerange: - raise OperationalException( + raise ConfigurationError( 'Please pass --timerange if you intend to use FreqAI for backtesting.') @@ -386,12 +386,12 @@ def _validate_consumers(conf: Dict[str, Any]) -> None: emc_conf = conf.get('external_message_consumer', {}) if emc_conf.get('enabled', False): if len(emc_conf.get('producers', [])) < 1: - raise OperationalException("You must specify at least 1 Producer to connect to.") + raise ConfigurationError("You must specify at least 1 Producer to connect to.") producer_names = [p['name'] for p in emc_conf.get('producers', [])] duplicates = [item for item, count in Counter(producer_names).items() if count > 1] if duplicates: - raise OperationalException( + raise ConfigurationError( f"Producer names must be unique. Duplicate: {', '.join(duplicates)}") if conf.get('process_only_new_candles', True): # Warning here or require it? From b5548dbee093d5b7e7a9502000b362d8ace38df8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 06:58:29 +0100 Subject: [PATCH 03/12] Add proper handler for config-error --- freqtrade/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 05e5409ad..dd3e497d8 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -16,7 +16,7 @@ if sys.version_info < (3, 9): # pragma: no cover from freqtrade import __version__ from freqtrade.commands import Arguments -from freqtrade.exceptions import FreqtradeException, OperationalException +from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException from freqtrade.loggers import setup_logging_pre @@ -56,6 +56,8 @@ def main(sysargv: Optional[List[str]] = None) -> None: except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') return_code = 0 + except ConfigurationError as e: + logger.error(f"Configuration error: {e}") except FreqtradeException as e: logger.error(str(e)) return_code = 2 From ebd516cadba63cf07e8230bcf1602af6708f20bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:02:48 +0100 Subject: [PATCH 04/12] Use combined exception handler at startup --- freqtrade/commands/trade_commands.py | 5 ----- tests/commands/test_commands.py | 7 ++++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 0707cc803..c7f7e524b 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -23,11 +23,6 @@ def start_trading(args: Dict[str, Any]) -> int: signal.signal(signal.SIGTERM, term_handler) worker = Worker(args) worker.run() - except Exception as e: - logger.error(str(e)) - logger.exception("Fatal exception!") - except (KeyboardInterrupt): - logger.info('SIGINT received, aborting ...') finally: if worker: logger.info("worker found ... calling exit") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 2252ff9f4..90bd1c270 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -51,15 +51,16 @@ def test_start_trading_fail(mocker, caplog): 'trade', '-c', 'tests/testdata/testconfigs/main_test_config.json' ] - start_trading(get_args(args)) + with pytest.raises(OperationalException): + start_trading(get_args(args)) assert exitmock.call_count == 1 exitmock.reset_mock() caplog.clear() mocker.patch("freqtrade.worker.Worker.__init__", MagicMock(side_effect=OperationalException)) - start_trading(get_args(args)) + with pytest.raises(OperationalException): + start_trading(get_args(args)) assert exitmock.call_count == 0 - assert log_has('Fatal exception!', caplog) def test_start_webserver(mocker, caplog): From 17d052df3f522a100b0b03f29c2742b27de1d2ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:04:28 +0100 Subject: [PATCH 05/12] Configerror handling for bt api endpoint --- freqtrade/rpc/api_server/api_backtest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index f387a9ac8..814dcf8da 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -14,7 +14,7 @@ from freqtrade.data.btanalysis import (delete_backtest_result, get_backtest_resu get_backtest_resultlist, load_and_merge_backtest_result, update_backtest_metadata) from freqtrade.enums import BacktestState -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import ConfigurationError, DependencyException, OperationalException from freqtrade.exchange.common import remove_exchange_credentials from freqtrade.misc import deep_merge_dicts, is_file_in_dir from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestMetadataUpdate, @@ -98,10 +98,12 @@ def __run_backtest_bg(btconfig: Config): logger.info("Backtest finished.") + except ConfigurationError as e: + logger.error(f"Backtesting encountered a configuration Error: {e}") + except (Exception, OperationalException, DependencyException) as e: logger.exception(f"Backtesting caused an error: {e}") ApiBG.bt['bt_error'] = str(e) - pass finally: ApiBG.bgtask_running = False From 3621ba034bbb7d900dd3f77a8243b99166cc42a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:06:43 +0100 Subject: [PATCH 06/12] Use configError in more places --- freqtrade/commands/analyze_commands.py | 8 ++++---- freqtrade/commands/data_commands.py | 8 ++++---- freqtrade/commands/list_commands.py | 4 ++-- freqtrade/commands/optimize_commands.py | 4 ++-- freqtrade/commands/plot_commands.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py index e928ccad7..54f7b27e5 100644 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError logger = logging.getLogger(__name__) @@ -34,12 +34,12 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s btfile = Path(config['exportfilename']) signals_file = f"{btfile.parent}/{btfile.stem}_signals.pkl" else: - raise OperationalException(f"{config['exportfilename']} does not exist.") + raise ConfigurationError(f"{config['exportfilename']} does not exist.") else: - raise OperationalException('exportfilename not in config.') + raise ConfigurationError('exportfilename not in config.') if (not Path(signals_file).exists()): - raise OperationalException( + raise ConfigurationError( f"Cannot find latest backtest signals file: {signals_file}." "Run backtesting with `--export signals`." ) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 2279cdc39..6762a83da 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -9,7 +9,7 @@ from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_forma convert_trades_to_ohlcv) from freqtrade.data.history import download_data_main from freqtrade.enums import CandleType, RunMode, TradingMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError from freqtrade.exchange import timeframe_to_minutes from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.resolvers import ExchangeResolver @@ -21,11 +21,11 @@ logger = logging.getLogger(__name__) def _check_data_config_download_sanity(config: Config) -> None: if 'days' in config and 'timerange' in config: - raise OperationalException("--days and --timerange are mutually exclusive. " - "You can only specify one or the other.") + raise ConfigurationError("--days and --timerange are mutually exclusive. " + "You can only specify one or the other.") if 'pairs' not in config: - raise OperationalException( + raise ConfigurationError( "Downloading data requires a list of pairs. " "Please check the documentation on how to configure this.") diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 84f237f77..550c29f69 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.exchange import list_available_exchanges, market_is_active from freqtrade.misc import parse_db_uri_for_logging, plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -246,7 +246,7 @@ def start_show_trades(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if 'db_url' not in config: - raise OperationalException("--db-url is required for this command.") + raise ConfigurationError("--db-url is required for this command.") logger.info(f'Using DB: "{parse_db_uri_for_logging(config["db_url"])}"') init_db(config['db_url']) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 837a5e4f3..f010a3cee 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.util import fmt_coin @@ -31,7 +31,7 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ and config['stake_amount'] > wallet_size): wallet = fmt_coin(wallet_size, config['stake_currency']) stake = fmt_coin(config['stake_amount'], config['stake_currency']) - raise OperationalException( + raise ConfigurationError( f"Starting balance ({wallet}) is smaller than stake_amount {stake}. " f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`." ) diff --git a/freqtrade/commands/plot_commands.py b/freqtrade/commands/plot_commands.py index 73a429a28..95ad3cdce 100644 --- a/freqtrade/commands/plot_commands.py +++ b/freqtrade/commands/plot_commands.py @@ -2,12 +2,12 @@ from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError def validate_plot_args(args: Dict[str, Any]) -> None: if not args.get('datadir') and not args.get('config'): - raise OperationalException( + raise ConfigurationError( "You need to specify either `--datadir` or `--config` " "for plot-profit and plot-dataframe.") From 64019e0e6c64b79c0182d4e2dabb19501cefdf0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:08:05 +0100 Subject: [PATCH 07/12] More configError usages --- freqtrade/configuration/deprecated_settings.py | 8 ++++---- freqtrade/configuration/load_config.py | 6 +++--- freqtrade/configuration/timerange.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 46c19a5b2..6a2d365a3 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -6,7 +6,7 @@ import logging from typing import Optional from freqtrade.constants import Config -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def process_removed_setting(config: Config, section1_config = config.get(section1, {}) if name1 in section1_config: section_2 = f"{section2}.{name2}" if section2 else f"{name2}" - raise OperationalException( + raise ConfigurationError( f"Setting `{section1}.{name1}` has been moved to `{section_2}. " f"Please delete it from your configuration and use the `{section_2}` " "setting instead." @@ -122,7 +122,7 @@ def process_temporary_deprecated_settings(config: Config) -> None: None, 'ignore_roi_if_entry_signal') if (config.get('edge', {}).get('enabled', False) and 'capital_available_percentage' in config.get('edge', {})): - raise OperationalException( + raise ConfigurationError( "DEPRECATED: " "Using 'edge.capital_available_percentage' has been deprecated in favor of " "'tradable_balance_ratio'. Please migrate your configuration to " @@ -131,7 +131,7 @@ def process_temporary_deprecated_settings(config: Config) -> None: ) if 'ticker_interval' in config: - raise OperationalException( + raise ConfigurationError( "DEPRECATED: 'ticker_interval' detected. " "Please use 'timeframe' instead of 'ticker_interval." ) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 5df2f85de..22eeeca55 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional import rapidjson from freqtrade.constants import MINIMAL_CONFIG, Config -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.misc import deep_merge_dicts @@ -66,7 +66,7 @@ def load_config_file(path: str) -> Dict[str, Any]: ' Please create a config file or check whether it exists.') from None except rapidjson.JSONDecodeError as e: err_range = log_config_error_range(path, str(e)) - raise OperationalException( + raise ConfigurationError( f'{e}\n' f'Please verify the following segment of your configuration:\n{err_range}' if err_range else 'Please verify your configuration file for syntax errors.' @@ -83,7 +83,7 @@ def load_from_files( """ config: Config = {} if level > 5: - raise OperationalException("Config loop detected.") + raise ConfigurationError("Config loop detected.") if not files: return deepcopy(MINIMAL_CONFIG) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 23e3a6b60..b82b13b10 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -9,7 +9,7 @@ from typing import Optional from typing_extensions import Self from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError logger = logging.getLogger(__name__) @@ -156,7 +156,7 @@ class TimeRange: else: stop = int(stops) if start > stop > 0: - raise OperationalException( + raise ConfigurationError( f'Start date is after stop date for timerange "{text}"') return cls(stype[0], stype[1], start, stop) - raise OperationalException(f'Incorrect syntax for timerange "{text}"') + raise ConfigurationError(f'Incorrect syntax for timerange "{text}"') From 4a097bd64449a8debc04613f46d0f8c2481ea5c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:13:17 +0100 Subject: [PATCH 08/12] More usages for ConfigurationError --- freqtrade/commands/deploy_commands.py | 4 ++-- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/exchange/exchange.py | 30 ++++++++++++++------------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 75da2552e..c87f55e43 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -9,7 +9,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.enums import RunMode -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.util import render_template, render_template_with_fallback @@ -89,7 +89,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None: deploy_new_strategy(args['strategy'], new_path, args['template']) else: - raise OperationalException("`new-strategy` requires --strategy to be set.") + raise ConfigurationError("`new-strategy` requires --strategy to be set.") def clean_ui_subdir(directory: Path): diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 9d9b17552..ef92d4db6 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -11,7 +11,7 @@ import numpy as np import pandas as pd from freqtrade.constants import LAST_BT_RESULT_FN, IntOrInf -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import ConfigurationError, OperationalException from freqtrade.misc import file_dump_json, json_load from freqtrade.optimize.backtest_caching import get_backtest_metadata_filename from freqtrade.persistence import LocalTrade, Trade, init_db @@ -106,7 +106,7 @@ def get_latest_hyperopt_file( directory = Path(directory) if predef_filename: if Path(predef_filename).is_absolute(): - raise OperationalException( + raise ConfigurationError( "--hyperopt-filename expects only the filename, not an absolute path.") return directory / predef_filename return directory / get_latest_hyperopt_filename(directory) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 84b4212bd..07c1c2809 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -24,9 +24,10 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode -from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, - InvalidOrderException, OperationalException, PricingError, - RetryableOrderError, TemporaryError) +from freqtrade.exceptions import (ConfigurationError, DDosProtection, ExchangeError, + InsufficientFundsError, InvalidOrderException, + OperationalException, PricingError, RetryableOrderError, + TemporaryError) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, remove_exchange_credentials, retrier, retrier_async) from freqtrade.exchange.exchange_utils import (ROUND, ROUND_DOWN, ROUND_UP, CcxtModuleType, @@ -527,7 +528,7 @@ class Exchange: ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: - raise OperationalException( + raise ConfigurationError( f"{stake_currency} is not available as stake on {self.name}. " f"Available currencies are: {', '.join(quote_currencies)}") @@ -595,7 +596,7 @@ class Exchange: f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") if timeframe and (timeframe not in self.timeframes): - raise OperationalException( + raise ConfigurationError( f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") if ( @@ -603,7 +604,7 @@ class Exchange: and self._config['runmode'] != RunMode.UTIL_EXCHANGE and timeframe_to_minutes(timeframe) < 1 ): - raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.") + raise ConfigurationError("Timeframes < 1m are currently not supported by Freqtrade.") def validate_ordertypes(self, order_types: Dict) -> None: """ @@ -611,7 +612,7 @@ class Exchange: """ if any(v == 'market' for k, v in order_types.items()): if not self.exchange_has('createMarketOrder'): - raise OperationalException( + raise ConfigurationError( f'Exchange {self.name} does not support market orders.') self.validate_stop_ordertypes(order_types) @@ -621,7 +622,7 @@ class Exchange: """ if (order_types.get("stoploss_on_exchange") and not self._ft_has.get("stoploss_on_exchange", False)): - raise OperationalException( + raise ConfigurationError( f'On exchange stoploss is not supported for {self.name}.' ) if self.trading_mode == TradingMode.FUTURES: @@ -631,17 +632,17 @@ class Exchange: and 'stoploss_price_type' in order_types and order_types['stoploss_price_type'] not in price_mapping ): - raise OperationalException( + raise ConfigurationError( f'On exchange stoploss price type is not supported for {self.name}.' ) def validate_pricing(self, pricing: Dict) -> None: if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'): - raise OperationalException(f'Orderbook not available for {self.name}.') + raise ConfigurationError(f'Orderbook not available for {self.name}.') if (not pricing.get('use_order_book', False) and ( not self.exchange_has('fetchTicker') or not self._ft_has['tickers_have_price'])): - raise OperationalException(f'Ticker pricing not available for {self.name}.') + raise ConfigurationError(f'Ticker pricing not available for {self.name}.') def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ @@ -649,7 +650,7 @@ class Exchange: """ if any(v.upper() not in self._ft_has["order_time_in_force"] for k, v in order_time_in_force.items()): - raise OperationalException( + raise ConfigurationError( f'Time in force policies are not supported for {self.name} yet.') def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int: @@ -671,12 +672,12 @@ class Exchange: if required_candle_call_count > 5: # Only allow 5 calls per pair to somewhat limit the impact - raise OperationalException( + raise ConfigurationError( f"This strategy requires {startup_candles} candles to start, " "which is more than 5x " f"the amount of candles {self.name} provides for {timeframe}.") elif required_candle_call_count > 1: - raise OperationalException( + raise ConfigurationError( f"This strategy requires {startup_candles} candles to start, which is more than " f"the amount of candles {self.name} provides for {timeframe}.") if required_candle_call_count > 1: @@ -3094,3 +3095,4 @@ class Exchange: # describes the min amt for a tier, and the lowest tier will always go down to 0 else: raise ExchangeError(f"Cannot get maintenance ratio using {self.name}") + raise ExchangeError(f"Cannot get maintenance ratio using {self.name}") From abd096dcd82dde35f339b1390579cab3a91e1219 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:13:28 +0100 Subject: [PATCH 09/12] Update exchange tests to config error in a few places --- tests/exchange/test_exchange.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 34d4ca4c6..ae224cc83 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,8 +11,8 @@ from numpy import NaN from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode -from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, - InsufficientFundsError, InvalidOrderException, +from freqtrade.exceptions import (ConfigurationError, DDosProtection, DependencyException, + ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import (Binance, Bybit, Exchange, Kraken, market_is_active, timeframe_to_prev_date) @@ -595,7 +595,7 @@ def test_validate_stakecurrency_error(default_conf, mocker, caplog): mocker.patch(f'{EXMS}.validate_pairs') mocker.patch(f'{EXMS}.validate_timeframes') mocker.patch(f'{EXMS}._load_async_markets') - with pytest.raises(OperationalException, + with pytest.raises(ConfigurationError, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): Exchange(default_conf) @@ -800,12 +800,12 @@ def test_validate_timeframes_failed(default_conf, mocker): mocker.patch(f'{EXMS}.validate_pairs') mocker.patch(f'{EXMS}.validate_stakecurrency') mocker.patch(f'{EXMS}.validate_pricing') - with pytest.raises(OperationalException, + with pytest.raises(ConfigurationError, match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) default_conf["timeframe"] = "15s" - with pytest.raises(OperationalException, + with pytest.raises(ConfigurationError, match=r"Timeframes < 1m are currently not supported by Freqtrade."): Exchange(default_conf) From 669076a29f511d07f4fe4e66869a8781d7ac0d25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:15:14 +0100 Subject: [PATCH 10/12] Add test to ensure configuration error is displayed as expected --- tests/test_main.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 442e6e1d7..4b28c094f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -8,7 +8,7 @@ import pytest from freqtrade.commands import Arguments from freqtrade.enums import State -from freqtrade.exceptions import FreqtradeException, OperationalException +from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.worker import Worker @@ -141,6 +141,22 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has_re(r'SIGINT.*', caplog) +def test_main_ConfigurationError(mocker, default_conf, caplog) -> None: + patch_exchange(mocker) + mocker.patch( + 'freqtrade.commands.list_commands.list_available_exchanges', + MagicMock(side_effect=ConfigurationError('Oh snap!')) + ) + patched_configuration_load_config_file(mocker, default_conf) + + args = ['list-exchanges'] + + # Test Main + the KeyboardInterrupt exception + with pytest.raises(SystemExit): + main(args) + assert log_has_re('Configuration error: Oh snap!', caplog) + + def test_main_reload_config(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) From 6219a25901f23208934b7c675ecc4ba5020fa1ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 19 Mar 2024 07:20:38 +0100 Subject: [PATCH 11/12] Add documentation link, re-align imports --- freqtrade/main.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index dd3e497d8..5eabe398f 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -7,8 +7,6 @@ import logging import sys from typing import Any, List, Optional -from freqtrade.util.gc_setup import gc_set_threshold - # check min. python version if sys.version_info < (3, 9): # pragma: no cover @@ -16,8 +14,10 @@ if sys.version_info < (3, 9): # pragma: no cover from freqtrade import __version__ from freqtrade.commands import Arguments +from freqtrade.constants import DOCS_LINK from freqtrade.exceptions import ConfigurationError, FreqtradeException, OperationalException from freqtrade.loggers import setup_logging_pre +from freqtrade.util.gc_setup import gc_set_threshold logger = logging.getLogger('freqtrade') @@ -57,7 +57,8 @@ def main(sysargv: Optional[List[str]] = None) -> None: logger.info('SIGINT received, aborting ...') return_code = 0 except ConfigurationError as e: - logger.error(f"Configuration error: {e}") + logger.error(f"Configuration error: {e}\n" + f"Please make sure to review the documentation at {DOCS_LINK}.") except FreqtradeException as e: logger.error(str(e)) return_code = 2 From d2a678137961be4bb4768704b7de445ac500473e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 21 Mar 2024 07:02:23 +0100 Subject: [PATCH 12/12] Revert one error to OperationalException --- freqtrade/commands/analyze_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/analyze_commands.py b/freqtrade/commands/analyze_commands.py index 54f7b27e5..d271a82e3 100644 --- a/freqtrade/commands/analyze_commands.py +++ b/freqtrade/commands/analyze_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode -from freqtrade.exceptions import ConfigurationError +from freqtrade.exceptions import ConfigurationError, OperationalException logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ def setup_analyze_configuration(args: Dict[str, Any], method: RunMode) -> Dict[s raise ConfigurationError('exportfilename not in config.') if (not Path(signals_file).exists()): - raise ConfigurationError( + raise OperationalException( f"Cannot find latest backtest signals file: {signals_file}." "Run backtesting with `--export signals`." )