Merge pull request #2158 from freqtrade/config_consistency
Config consistency checking improvements
This commit is contained in:
commit
c63856dac4
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user