Merge pull request #2158 from freqtrade/config_consistency
Config consistency checking improvements
This commit is contained in:
		| @@ -1,3 +1,4 @@ | |||||||
| from freqtrade.configuration.arguments import Arguments  # noqa: F401 | from freqtrade.configuration.arguments import Arguments  # noqa: F401 | ||||||
| from freqtrade.configuration.timerange import TimeRange  # noqa: F401 | from freqtrade.configuration.timerange import TimeRange  # noqa: F401 | ||||||
| from freqtrade.configuration.configuration import Configuration  # noqa: F401 | from freqtrade.configuration.configuration import Configuration  # noqa: F401 | ||||||
|  | from freqtrade.configuration.config_validation import validate_config_consistency  # noqa: F401 | ||||||
|   | |||||||
							
								
								
									
										102
									
								
								freqtrade/configuration/config_validation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								freqtrade/configuration/config_validation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | |||||||
|  | import logging | ||||||
|  | from typing import Any, Dict | ||||||
|  |  | ||||||
|  | from jsonschema import Draft4Validator, validators | ||||||
|  | from jsonschema.exceptions import ValidationError, best_match | ||||||
|  |  | ||||||
|  | from freqtrade import constants, OperationalException | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _extend_validator(validator_class): | ||||||
|  |     """ | ||||||
|  |     Extended validator for the Freqtrade configuration JSON Schema. | ||||||
|  |     Currently it only handles defaults for subschemas. | ||||||
|  |     """ | ||||||
|  |     validate_properties = validator_class.VALIDATORS['properties'] | ||||||
|  |  | ||||||
|  |     def set_defaults(validator, properties, instance, schema): | ||||||
|  |         for prop, subschema in properties.items(): | ||||||
|  |             if 'default' in subschema: | ||||||
|  |                 instance.setdefault(prop, subschema['default']) | ||||||
|  |  | ||||||
|  |         for error in validate_properties( | ||||||
|  |             validator, properties, instance, schema, | ||||||
|  |         ): | ||||||
|  |             yield error | ||||||
|  |  | ||||||
|  |     return validators.extend( | ||||||
|  |         validator_class, {'properties': set_defaults} | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FreqtradeValidator = _extend_validator(Draft4Validator) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: | ||||||
|  |     """ | ||||||
|  |     Validate the configuration follow the Config Schema | ||||||
|  |     :param conf: Config in JSON format | ||||||
|  |     :return: Returns the config if valid, otherwise throw an exception | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) | ||||||
|  |         return conf | ||||||
|  |     except ValidationError as e: | ||||||
|  |         logger.critical( | ||||||
|  |             f"Invalid configuration. See config.json.example. Reason: {e}" | ||||||
|  |         ) | ||||||
|  |         raise ValidationError( | ||||||
|  |             best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_config_consistency(conf: Dict[str, Any]) -> None: | ||||||
|  |     """ | ||||||
|  |     Validate the configuration consistency. | ||||||
|  |     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 | ||||||
|  |     """ | ||||||
|  |     # validating trailing stoploss | ||||||
|  |     _validate_trailing_stoploss(conf) | ||||||
|  |     _validate_edge(conf) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: | ||||||
|  |  | ||||||
|  |     # Skip if trailing stoploss is not activated | ||||||
|  |     if not conf.get('trailing_stop', False): | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     tsl_positive = float(conf.get('trailing_stop_positive', 0)) | ||||||
|  |     tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) | ||||||
|  |     tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) | ||||||
|  |  | ||||||
|  |     if tsl_only_offset: | ||||||
|  |         if tsl_positive == 0.0: | ||||||
|  |             raise OperationalException( | ||||||
|  |                 f'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( | ||||||
|  |             f'The config trailing_stop_positive_offset needs ' | ||||||
|  |             'to be greater than trailing_stop_positive_offset in your config.') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_edge(conf: Dict[str, Any]) -> None: | ||||||
|  |     """ | ||||||
|  |     Edge and Dynamic whitelist should not both be enabled, since edge overrides dynamic whitelists. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     if not conf.get('edge', {}).get('enabled'): | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     if conf.get('pairlist', {}).get('method') == 'VolumePairList': | ||||||
|  |         raise OperationalException( | ||||||
|  |             "Edge and VolumePairList are incompatible, " | ||||||
|  |             "Edge will override whatever pairs VolumePairlist selects." | ||||||
|  |         ) | ||||||
| @@ -6,10 +6,11 @@ import warnings | |||||||
| from argparse import Namespace | from argparse import Namespace | ||||||
| from typing import Any, Callable, Dict, List, Optional | from typing import Any, Callable, Dict, List, Optional | ||||||
|  |  | ||||||
| from freqtrade import OperationalException, constants | from freqtrade import constants | ||||||
| from freqtrade.configuration.check_exchange import check_exchange | from freqtrade.configuration.check_exchange import check_exchange | ||||||
| from freqtrade.configuration.create_datadir import create_datadir | from freqtrade.configuration.create_datadir import create_datadir | ||||||
| from freqtrade.configuration.json_schema import validate_config_schema | from freqtrade.configuration.config_validation import (validate_config_schema, | ||||||
|  |                                                        validate_config_consistency) | ||||||
| from freqtrade.configuration.load_config import load_config_file | from freqtrade.configuration.load_config import load_config_file | ||||||
| from freqtrade.loggers import setup_logging | from freqtrade.loggers import setup_logging | ||||||
| from freqtrade.misc import deep_merge_dicts | from freqtrade.misc import deep_merge_dicts | ||||||
| @@ -77,8 +78,6 @@ class Configuration(object): | |||||||
|         # Load all configs |         # Load all configs | ||||||
|         config: Dict[str, Any] = Configuration.from_files(self.args.config) |         config: Dict[str, Any] = Configuration.from_files(self.args.config) | ||||||
|  |  | ||||||
|         self._validate_config_consistency(config) |  | ||||||
|  |  | ||||||
|         self._process_common_options(config) |         self._process_common_options(config) | ||||||
|  |  | ||||||
|         self._process_optimize_options(config) |         self._process_optimize_options(config) | ||||||
| @@ -87,6 +86,8 @@ class Configuration(object): | |||||||
|  |  | ||||||
|         self._process_runmode(config) |         self._process_runmode(config) | ||||||
|  |  | ||||||
|  |         validate_config_consistency(config) | ||||||
|  |  | ||||||
|         return config |         return config | ||||||
|  |  | ||||||
|     def _process_logging_options(self, config: Dict[str, Any]) -> None: |     def _process_logging_options(self, config: Dict[str, Any]) -> None: | ||||||
| @@ -285,35 +286,6 @@ class Configuration(object): | |||||||
|  |  | ||||||
|         config.update({'runmode': self.runmode}) |         config.update({'runmode': self.runmode}) | ||||||
|  |  | ||||||
|     def _validate_config_consistency(self, conf: Dict[str, Any]) -> None: |  | ||||||
|         """ |  | ||||||
|         Validate the configuration consistency |  | ||||||
|         :param conf: Config in JSON format |  | ||||||
|         :return: Returns None if everything is ok, otherwise throw an OperationalException |  | ||||||
|         """ |  | ||||||
|         # validating trailing stoploss |  | ||||||
|         self._validate_trailing_stoploss(conf) |  | ||||||
|  |  | ||||||
|     def _validate_trailing_stoploss(self, conf: Dict[str, Any]) -> None: |  | ||||||
|  |  | ||||||
|         # Skip if trailing stoploss is not activated |  | ||||||
|         if not conf.get('trailing_stop', False): |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         tsl_positive = float(conf.get('trailing_stop_positive', 0)) |  | ||||||
|         tsl_offset = float(conf.get('trailing_stop_positive_offset', 0)) |  | ||||||
|         tsl_only_offset = conf.get('trailing_only_offset_is_reached', False) |  | ||||||
|  |  | ||||||
|         if tsl_only_offset: |  | ||||||
|             if tsl_positive == 0.0: |  | ||||||
|                 raise OperationalException( |  | ||||||
|                     f'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( |  | ||||||
|                 f'The config trailing_stop_positive_offset needs ' |  | ||||||
|                 'to be greater than trailing_stop_positive_offset in your config.') |  | ||||||
|  |  | ||||||
|     def _args_to_config(self, config: Dict[str, Any], argname: str, |     def _args_to_config(self, config: Dict[str, Any], argname: str, | ||||||
|                         logstring: str, logfun: Optional[Callable] = None, |                         logstring: str, logfun: Optional[Callable] = None, | ||||||
|                         deprecated_msg: Optional[str] = None) -> None: |                         deprecated_msg: Optional[str] = None) -> None: | ||||||
|   | |||||||
| @@ -1,53 +0,0 @@ | |||||||
| import logging |  | ||||||
| from typing import Any, Dict |  | ||||||
|  |  | ||||||
| from jsonschema import Draft4Validator, validators |  | ||||||
| from jsonschema.exceptions import ValidationError, best_match |  | ||||||
|  |  | ||||||
| from freqtrade import constants |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def _extend_validator(validator_class): |  | ||||||
|     """ |  | ||||||
|     Extended validator for the Freqtrade configuration JSON Schema. |  | ||||||
|     Currently it only handles defaults for subschemas. |  | ||||||
|     """ |  | ||||||
|     validate_properties = validator_class.VALIDATORS['properties'] |  | ||||||
|  |  | ||||||
|     def set_defaults(validator, properties, instance, schema): |  | ||||||
|         for prop, subschema in properties.items(): |  | ||||||
|             if 'default' in subschema: |  | ||||||
|                 instance.setdefault(prop, subschema['default']) |  | ||||||
|  |  | ||||||
|         for error in validate_properties( |  | ||||||
|             validator, properties, instance, schema, |  | ||||||
|         ): |  | ||||||
|             yield error |  | ||||||
|  |  | ||||||
|     return validators.extend( |  | ||||||
|         validator_class, {'properties': set_defaults} |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| FreqtradeValidator = _extend_validator(Draft4Validator) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: |  | ||||||
|     """ |  | ||||||
|     Validate the configuration follow the Config Schema |  | ||||||
|     :param conf: Config in JSON format |  | ||||||
|     :return: Returns the config if valid, otherwise throw an exception |  | ||||||
|     """ |  | ||||||
|     try: |  | ||||||
|         FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) |  | ||||||
|         return conf |  | ||||||
|     except ValidationError as e: |  | ||||||
|         logger.critical( |  | ||||||
|             f"Invalid configuration. See config.json.example. Reason: {e}" |  | ||||||
|         ) |  | ||||||
|         raise ValidationError( |  | ||||||
|             best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message |  | ||||||
|         ) |  | ||||||
| @@ -16,6 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx | |||||||
| from freqtrade.data.converter import order_book_to_dataframe | from freqtrade.data.converter import order_book_to_dataframe | ||||||
| from freqtrade.data.dataprovider import DataProvider | from freqtrade.data.dataprovider import DataProvider | ||||||
| from freqtrade.edge import Edge | from freqtrade.edge import Edge | ||||||
|  | from freqtrade.configuration import validate_config_consistency | ||||||
| from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date | from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date | ||||||
| from freqtrade.persistence import Trade | from freqtrade.persistence import Trade | ||||||
| from freqtrade.rpc import RPCManager, RPCMessageType | from freqtrade.rpc import RPCManager, RPCMessageType | ||||||
| @@ -51,6 +52,9 @@ class FreqtradeBot(object): | |||||||
|  |  | ||||||
|         self.strategy: IStrategy = StrategyResolver(self.config).strategy |         self.strategy: IStrategy = StrategyResolver(self.config).strategy | ||||||
|  |  | ||||||
|  |         # Check config consistency here since strategies can set certain options | ||||||
|  |         validate_config_consistency(config) | ||||||
|  |  | ||||||
|         self.rpc: RPCManager = RPCManager(self) |         self.rpc: RPCManager = RPCManager(self) | ||||||
|  |  | ||||||
|         self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange |         self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange | ||||||
|   | |||||||
| @@ -2,7 +2,6 @@ | |||||||
| import json | import json | ||||||
| import logging | import logging | ||||||
| import warnings | import warnings | ||||||
| from argparse import Namespace |  | ||||||
| from copy import deepcopy | from copy import deepcopy | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from unittest.mock import MagicMock | from unittest.mock import MagicMock | ||||||
| @@ -11,10 +10,10 @@ import pytest | |||||||
| from jsonschema import Draft4Validator, ValidationError, validate | from jsonschema import Draft4Validator, ValidationError, validate | ||||||
|  |  | ||||||
| from freqtrade import OperationalException, constants | from freqtrade import OperationalException, constants | ||||||
| from freqtrade.configuration import Arguments, Configuration | from freqtrade.configuration import Arguments, Configuration, validate_config_consistency | ||||||
| from freqtrade.configuration.check_exchange import check_exchange | from freqtrade.configuration.check_exchange import check_exchange | ||||||
|  | from freqtrade.configuration.config_validation import validate_config_schema | ||||||
| from freqtrade.configuration.create_datadir import create_datadir | from freqtrade.configuration.create_datadir import create_datadir | ||||||
| from freqtrade.configuration.json_schema import validate_config_schema |  | ||||||
| from freqtrade.configuration.load_config import load_config_file | from freqtrade.configuration.load_config import load_config_file | ||||||
| from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL | from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL | ||||||
| from freqtrade.loggers import _set_loggers | from freqtrade.loggers import _set_loggers | ||||||
| @@ -625,21 +624,34 @@ def test_validate_tsl(default_conf): | |||||||
|     with pytest.raises(OperationalException, |     with pytest.raises(OperationalException, | ||||||
|                        match=r'The config trailing_only_offset_is_reached needs ' |                        match=r'The config trailing_only_offset_is_reached needs ' | ||||||
|                        'trailing_stop_positive_offset to be more than 0 in your config.'): |                        'trailing_stop_positive_offset to be more than 0 in your config.'): | ||||||
|         configuration = Configuration(Namespace()) |         validate_config_consistency(default_conf) | ||||||
|         configuration._validate_config_consistency(default_conf) |  | ||||||
|  |  | ||||||
|     default_conf['trailing_stop_positive_offset'] = 0.01 |     default_conf['trailing_stop_positive_offset'] = 0.01 | ||||||
|     default_conf['trailing_stop_positive'] = 0.015 |     default_conf['trailing_stop_positive'] = 0.015 | ||||||
|     with pytest.raises(OperationalException, |     with pytest.raises(OperationalException, | ||||||
|                        match=r'The config trailing_stop_positive_offset needs ' |                        match=r'The config trailing_stop_positive_offset needs ' | ||||||
|                        'to be greater than trailing_stop_positive_offset in your config.'): |                        'to be greater than trailing_stop_positive_offset in your config.'): | ||||||
|         configuration = Configuration(Namespace()) |         validate_config_consistency(default_conf) | ||||||
|         configuration._validate_config_consistency(default_conf) |  | ||||||
|  |  | ||||||
|     default_conf['trailing_stop_positive'] = 0.01 |     default_conf['trailing_stop_positive'] = 0.01 | ||||||
|     default_conf['trailing_stop_positive_offset'] = 0.015 |     default_conf['trailing_stop_positive_offset'] = 0.015 | ||||||
|     Configuration(Namespace()) |     validate_config_consistency(default_conf) | ||||||
|     configuration._validate_config_consistency(default_conf) |  | ||||||
|  |  | ||||||
|  | def test_validate_edge(edge_conf): | ||||||
|  |     edge_conf.update({"pairlist": { | ||||||
|  |         "method": "VolumePairList", | ||||||
|  |     }}) | ||||||
|  |  | ||||||
|  |     with pytest.raises(OperationalException, | ||||||
|  |                        match="Edge and VolumePairList are incompatible, " | ||||||
|  |                        "Edge will override whatever pairs VolumePairlist selects."): | ||||||
|  |         validate_config_consistency(edge_conf) | ||||||
|  |  | ||||||
|  |     edge_conf.update({"pairlist": { | ||||||
|  |         "method": "StaticPairList", | ||||||
|  |     }}) | ||||||
|  |     validate_config_consistency(edge_conf) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_load_config_test_comments() -> None: | def test_load_config_test_comments() -> None: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user