Merge branch 'develop' into align_userdata
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from freqtrade.configuration.arguments import Arguments # noqa: F401
|
||||
from freqtrade.configuration.timerange import TimeRange # noqa: F401
|
||||
from freqtrade.configuration.configuration import Configuration # noqa: F401
|
||||
from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401
|
||||
|
@@ -32,7 +32,7 @@ ARGS_LIST_EXCHANGES = ["print_one_column"]
|
||||
|
||||
ARGS_CREATE_USERDIR = ["user_data_dir"]
|
||||
|
||||
ARGS_DOWNLOADER = ARGS_COMMON + ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
|
||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"]
|
||||
|
||||
ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY +
|
||||
["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
|
||||
@@ -42,6 +42,8 @@ ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY +
|
||||
ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY +
|
||||
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"])
|
||||
|
||||
NO_CONF_REQURIED = ["start_download_data"]
|
||||
|
||||
|
||||
class Arguments(object):
|
||||
"""
|
||||
@@ -77,7 +79,10 @@ class Arguments(object):
|
||||
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
if not self._no_default_config and parsed_arg.config is None:
|
||||
# Allow no-config for certain commands (like downloading / plotting)
|
||||
if (not self._no_default_config and parsed_arg.config is None
|
||||
and not (hasattr(parsed_arg, 'func')
|
||||
and parsed_arg.func.__name__ in NO_CONF_REQURIED)):
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
@@ -95,7 +100,8 @@ class Arguments(object):
|
||||
:return: None
|
||||
"""
|
||||
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
|
||||
from freqtrade.utils import start_create_userdir, start_list_exchanges
|
||||
from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||
|
||||
# Add backtesting subcommand
|
||||
@@ -125,3 +131,11 @@ class Arguments(object):
|
||||
)
|
||||
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
|
||||
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
|
||||
|
||||
# Add download-data subcommand
|
||||
download_data_cmd = subparsers.add_parser(
|
||||
'download-data',
|
||||
help='Download backtesting data.'
|
||||
)
|
||||
download_data_cmd.set_defaults(func=start_download_data)
|
||||
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
|
||||
|
@@ -259,7 +259,8 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
# Script options
|
||||
"pairs": Arg(
|
||||
'-p', '--pairs',
|
||||
help='Show profits for only these pairs. Pairs are comma-separated.',
|
||||
help='Show profits for only these pairs. Pairs are space-separated.',
|
||||
nargs='+',
|
||||
),
|
||||
# Download data
|
||||
"pairs_file": Arg(
|
||||
@@ -281,9 +282,10 @@ AVAILABLE_CLI_OPTIONS = {
|
||||
"timeframes": Arg(
|
||||
'-t', '--timeframes',
|
||||
help=f'Specify which tickers to download. Space-separated list. '
|
||||
f'Default: `{constants.DEFAULT_DOWNLOAD_TICKER_INTERVALS}`.',
|
||||
f'Default: `1m 5m`.',
|
||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||
default=['1m', '5m'],
|
||||
nargs='+',
|
||||
),
|
||||
"erase": Arg(
|
||||
|
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."
|
||||
)
|
@@ -9,12 +9,13 @@ from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.configuration.check_exchange import check_exchange
|
||||
from freqtrade.configuration.config_validation import (
|
||||
validate_config_consistency, validate_config_schema)
|
||||
from freqtrade.configuration.directory_operations import (create_datadir,
|
||||
create_userdata_dir)
|
||||
from freqtrade.configuration.json_schema import validate_config_schema
|
||||
from freqtrade.configuration.load_config import load_config_file
|
||||
from freqtrade.loggers import setup_logging
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
from freqtrade.misc import deep_merge_dicts, json_load
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -54,6 +55,9 @@ class Configuration(object):
|
||||
# Keep this method as staticmethod, so it can be used from interactive environments
|
||||
config: Dict[str, Any] = {}
|
||||
|
||||
if not files:
|
||||
return constants.MINIMAL_CONFIG.copy()
|
||||
|
||||
# We expect here a list of config filenames
|
||||
for path in files:
|
||||
logger.info(f'Using config: {path} ...')
|
||||
@@ -79,8 +83,6 @@ class Configuration(object):
|
||||
# Load all configs
|
||||
config: Dict[str, Any] = Configuration.from_files(self.args.config)
|
||||
|
||||
self._validate_config_consistency(config)
|
||||
|
||||
self._process_common_options(config)
|
||||
|
||||
self._process_optimize_options(config)
|
||||
@@ -89,6 +91,13 @@ class Configuration(object):
|
||||
|
||||
self._process_runmode(config)
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||
|
||||
self._resolve_pairs_list(config)
|
||||
|
||||
validate_config_consistency(config)
|
||||
|
||||
return config
|
||||
|
||||
def _process_logging_options(self, config: Dict[str, Any]) -> None:
|
||||
@@ -146,9 +155,6 @@ class Configuration(object):
|
||||
if 'sd_notify' in self.args and self.args.sd_notify:
|
||||
config['internals'].update({'sd_notify': True})
|
||||
|
||||
# Check if the exchange set by the user is supported
|
||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||
|
||||
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Extract information for sys.argv and load directory configurations
|
||||
@@ -285,6 +291,19 @@ class Configuration(object):
|
||||
self._args_to_config(config, argname='trade_source',
|
||||
logstring='Using trades from: {}')
|
||||
|
||||
self._args_to_config(config, argname='erase',
|
||||
logstring='Erase detected. Deleting existing data.')
|
||||
|
||||
self._args_to_config(config, argname='timeframes',
|
||||
logstring='timeframes --timeframes: {}')
|
||||
|
||||
self._args_to_config(config, argname='days',
|
||||
logstring='Detected --days: {}')
|
||||
|
||||
if "exchange" in self.args and self.args.exchange:
|
||||
config['exchange']['name'] = self.args.exchange
|
||||
logger.info(f"Using exchange {config['exchange']['name']}")
|
||||
|
||||
def _process_runmode(self, config: Dict[str, Any]) -> None:
|
||||
|
||||
if not self.runmode:
|
||||
@@ -294,35 +313,6 @@ class Configuration(object):
|
||||
|
||||
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,
|
||||
logstring: str, logfun: Optional[Callable] = None,
|
||||
deprecated_msg: Optional[str] = None) -> None:
|
||||
@@ -344,3 +334,39 @@ class Configuration(object):
|
||||
logger.info(logstring.format(config[argname]))
|
||||
if deprecated_msg:
|
||||
warnings.warn(f"DEPRECATED: {deprecated_msg}", DeprecationWarning)
|
||||
|
||||
def _resolve_pairs_list(self, config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
Helper for download script.
|
||||
Takes first found:
|
||||
* -p (pairs argument)
|
||||
* --pairs-file
|
||||
* whitelist from config
|
||||
"""
|
||||
|
||||
if "pairs" in config:
|
||||
return
|
||||
|
||||
if "pairs_file" in self.args and self.args.pairs_file:
|
||||
pairs_file = Path(self.args.pairs_file)
|
||||
logger.info(f'Reading pairs file "{pairs_file}".')
|
||||
# Download pairs from the pairs file if no config is specified
|
||||
# or if pairs file is specified explicitely
|
||||
if not pairs_file.exists():
|
||||
raise OperationalException(f'No pairs file found with path "{pairs_file}".')
|
||||
with pairs_file.open('r') as f:
|
||||
config['pairs'] = json_load(f)
|
||||
config['pairs'].sort()
|
||||
return
|
||||
|
||||
if "config" in self.args and self.args.config:
|
||||
logger.info("Using pairlist from configuration.")
|
||||
config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
|
||||
else:
|
||||
# Fall back to /dl_path/pairs.json
|
||||
pairs_file = Path(config['datadir']) / config['exchange']['name'].lower() / "pairs.json"
|
||||
if pairs_file.exists():
|
||||
with pairs_file.open('r') as f:
|
||||
config['pairs'] = json_load(f)
|
||||
if 'pairs' in config:
|
||||
config['pairs'].sort()
|
||||
|
@@ -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
|
||||
)
|
Reference in New Issue
Block a user