2018-02-04 06:42:03 +00:00
|
|
|
"""
|
|
|
|
This module contains the configuration class
|
|
|
|
"""
|
|
|
|
|
|
|
|
import json
|
2018-03-25 19:37:14 +00:00
|
|
|
import logging
|
2018-06-03 19:30:25 +00:00
|
|
|
from os import path, makedirs
|
2018-03-17 21:43:59 +00:00
|
|
|
from argparse import Namespace
|
2018-05-30 20:38:09 +00:00
|
|
|
from typing import Optional, Dict, Any
|
2018-02-04 06:42:03 +00:00
|
|
|
from jsonschema import Draft4Validator, validate
|
|
|
|
from jsonschema.exceptions import ValidationError, best_match
|
2018-03-30 20:14:35 +00:00
|
|
|
import ccxt
|
2018-02-04 06:42:03 +00:00
|
|
|
|
2018-05-02 20:49:55 +00:00
|
|
|
from freqtrade import OperationalException, constants
|
2018-03-25 19:37:14 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
2018-02-04 06:42:03 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Configuration(object):
|
|
|
|
"""
|
|
|
|
Class to read and init the bot configuration
|
|
|
|
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
|
|
|
|
"""
|
2018-03-17 21:43:59 +00:00
|
|
|
def __init__(self, args: Namespace) -> None:
|
2018-02-04 06:42:03 +00:00
|
|
|
self.args = args
|
2018-05-30 20:38:09 +00:00
|
|
|
self.config: Optional[Dict[str, Any]] = None
|
2018-02-04 06:42:03 +00:00
|
|
|
|
2018-03-03 21:39:39 +00:00
|
|
|
def load_config(self) -> Dict[str, Any]:
|
2018-02-04 06:42:03 +00:00
|
|
|
"""
|
|
|
|
Extract information for sys.argv and load the bot configuration
|
|
|
|
:return: Configuration dictionary
|
|
|
|
"""
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Using config: %s ...', self.args.config)
|
2018-02-04 06:42:03 +00:00
|
|
|
config = self._load_config_file(self.args.config)
|
|
|
|
|
2018-03-27 16:20:15 +00:00
|
|
|
# Set strategy if not specified in config and or if it's non default
|
2018-04-02 14:42:53 +00:00
|
|
|
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
2018-03-27 16:15:49 +00:00
|
|
|
config.update({'strategy': self.args.strategy})
|
2018-02-04 06:42:03 +00:00
|
|
|
|
2018-03-25 14:28:04 +00:00
|
|
|
if self.args.strategy_path:
|
|
|
|
config.update({'strategy_path': self.args.strategy_path})
|
2018-02-04 06:42:03 +00:00
|
|
|
|
2018-03-03 21:39:39 +00:00
|
|
|
# Load Common configuration
|
|
|
|
config = self._load_common_config(config)
|
2018-03-02 13:46:32 +00:00
|
|
|
|
|
|
|
# Load Backtesting
|
2018-02-09 07:35:38 +00:00
|
|
|
config = self._load_backtesting_config(config)
|
|
|
|
|
2018-03-02 13:46:32 +00:00
|
|
|
# Load Hyperopt
|
|
|
|
config = self._load_hyperopt_config(config)
|
|
|
|
|
2018-02-04 06:42:03 +00:00
|
|
|
return config
|
|
|
|
|
|
|
|
def _load_config_file(self, path: str) -> Dict[str, Any]:
|
|
|
|
"""
|
|
|
|
Loads a config file from the given path
|
|
|
|
:param path: path as str
|
|
|
|
:return: configuration as dictionary
|
|
|
|
"""
|
2018-03-05 04:22:40 +00:00
|
|
|
try:
|
|
|
|
with open(path) as file:
|
|
|
|
conf = json.load(file)
|
|
|
|
except FileNotFoundError:
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.critical(
|
2018-03-05 04:22:40 +00:00
|
|
|
'Config file "%s" not found. Please create your config file',
|
|
|
|
path
|
|
|
|
)
|
|
|
|
exit(0)
|
2018-02-04 06:42:03 +00:00
|
|
|
|
|
|
|
if 'internals' not in conf:
|
|
|
|
conf['internals'] = {}
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Validating configuration ...')
|
2018-02-04 06:42:03 +00:00
|
|
|
|
|
|
|
return self._validate_config(conf)
|
|
|
|
|
2018-03-03 21:39:39 +00:00
|
|
|
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
"""
|
|
|
|
Extract information for sys.argv and load common configuration
|
|
|
|
:return: configuration as dictionary
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Log level
|
|
|
|
if 'loglevel' in self.args and self.args.loglevel:
|
|
|
|
config.update({'loglevel': self.args.loglevel})
|
2018-03-25 19:37:14 +00:00
|
|
|
logging.basicConfig(
|
|
|
|
level=config['loglevel'],
|
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
|
)
|
|
|
|
logger.info('Log level set to %s', logging.getLevelName(config['loglevel']))
|
2018-03-03 21:39:39 +00:00
|
|
|
|
|
|
|
# Add dynamic_whitelist if found
|
|
|
|
if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist:
|
|
|
|
config.update({'dynamic_whitelist': self.args.dynamic_whitelist})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info(
|
2018-03-03 21:39:39 +00:00
|
|
|
'Parameter --dynamic-whitelist detected. '
|
|
|
|
'Using dynamically generated whitelist. '
|
|
|
|
'(not applicable with Backtesting and Hyperopt)'
|
|
|
|
)
|
|
|
|
|
|
|
|
# Add dry_run_db if found and the bot in dry run
|
|
|
|
if self.args.dry_run_db and config.get('dry_run', False):
|
|
|
|
config.update({'dry_run_db': True})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter --dry-run-db detected ...')
|
2018-03-03 21:39:39 +00:00
|
|
|
|
|
|
|
if config.get('dry_run_db', False):
|
|
|
|
if config.get('dry_run', False):
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
|
2018-03-03 21:39:39 +00:00
|
|
|
else:
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Dry run is disabled. (--dry_run_db ignored)')
|
2018-03-03 21:39:39 +00:00
|
|
|
|
2018-03-30 20:14:35 +00:00
|
|
|
# Check if the exchange set by the user is supported
|
2018-04-04 20:05:17 +00:00
|
|
|
self.check_exchange(config)
|
2018-03-30 20:14:35 +00:00
|
|
|
|
2018-03-03 21:39:39 +00:00
|
|
|
return config
|
|
|
|
|
2018-02-09 07:35:38 +00:00
|
|
|
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
"""
|
2018-03-02 13:46:32 +00:00
|
|
|
Extract information for sys.argv and load Backtesting configuration
|
2018-02-09 07:35:38 +00:00
|
|
|
:return: configuration as dictionary
|
|
|
|
"""
|
2018-03-03 21:39:39 +00:00
|
|
|
|
2018-02-09 07:35:38 +00:00
|
|
|
# If -i/--ticker-interval is used we override the configuration parameter
|
|
|
|
# (that will override the strategy configuration)
|
|
|
|
if 'ticker_interval' in self.args and self.args.ticker_interval:
|
|
|
|
config.update({'ticker_interval': self.args.ticker_interval})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter -i/--ticker-interval detected ...')
|
|
|
|
logger.info('Using ticker_interval: %s ...', config.get('ticker_interval'))
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
# If -l/--live is used we add it to the configuration
|
|
|
|
if 'live' in self.args and self.args.live:
|
|
|
|
config.update({'live': True})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter -l/--live detected ...')
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
# If --realistic-simulation is used we add it to the configuration
|
|
|
|
if 'realistic_simulation' in self.args and self.args.realistic_simulation:
|
|
|
|
config.update({'realistic_simulation': True})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter --realistic-simulation detected ...')
|
|
|
|
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
# If --timerange is used we add it to the configuration
|
|
|
|
if 'timerange' in self.args and self.args.timerange:
|
|
|
|
config.update({'timerange': self.args.timerange})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter --timerange detected: %s ...', self.args.timerange)
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
# If --datadir is used we add it to the configuration
|
2018-06-03 17:54:05 +00:00
|
|
|
# If not passed as an arg, we build testdata path including 'exchange' as a sub dir
|
2018-02-09 07:35:38 +00:00
|
|
|
if 'datadir' in self.args and self.args.datadir:
|
|
|
|
config.update({'datadir': self.args.datadir})
|
2018-06-03 17:54:05 +00:00
|
|
|
else:
|
|
|
|
exchange = config.get('exchange', {}).get('name').lower()
|
|
|
|
if exchange:
|
2018-06-03 19:30:25 +00:00
|
|
|
default = path.join('freqtrade', 'tests', 'testdata', exchange)
|
2018-06-03 17:54:05 +00:00
|
|
|
config.update({'datadir': default})
|
|
|
|
# What if user has no exchange as arg or in file - set catchall
|
|
|
|
else:
|
|
|
|
logger.info("No exchange set")
|
2018-06-03 19:30:25 +00:00
|
|
|
default = path.join('freqtrade', 'tests', 'testdata', 'catchall')
|
2018-06-03 17:54:05 +00:00
|
|
|
|
2018-06-03 19:30:25 +00:00
|
|
|
if not path.exists(config['datadir']):
|
|
|
|
makedirs(config['datadir'])
|
2018-06-03 17:54:05 +00:00
|
|
|
logger.info("Made directory: %s", config['datadir'])
|
|
|
|
|
|
|
|
logger.info('Using data folder: %s ...', config['datadir'])
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
# If -r/--refresh-pairs-cached is used we add it to the configuration
|
|
|
|
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
|
|
|
|
config.update({'refresh_pairs': True})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter -r/--refresh-pairs-cached detected ...')
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
# If --export is used we add it to the configuration
|
|
|
|
if 'export' in self.args and self.args.export:
|
|
|
|
config.update({'export': self.args.export})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter --export detected: %s ...', self.args.export)
|
2018-02-09 07:35:38 +00:00
|
|
|
|
|
|
|
return config
|
|
|
|
|
2018-03-02 13:46:32 +00:00
|
|
|
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
"""
|
|
|
|
Extract information for sys.argv and load Hyperopt configuration
|
|
|
|
:return: configuration as dictionary
|
|
|
|
"""
|
|
|
|
# If --realistic-simulation is used we add it to the configuration
|
|
|
|
if 'epochs' in self.args and self.args.epochs:
|
|
|
|
config.update({'epochs': self.args.epochs})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter --epochs detected ...')
|
|
|
|
logger.info('Will run Hyperopt with for %s epochs ...', config.get('epochs'))
|
2018-03-02 13:46:32 +00:00
|
|
|
|
|
|
|
# If --mongodb is used we add it to the configuration
|
|
|
|
if 'mongodb' in self.args and self.args.mongodb:
|
|
|
|
config.update({'mongodb': self.args.mongodb})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter --use-mongodb detected ...')
|
2018-03-02 13:46:32 +00:00
|
|
|
|
2018-03-04 08:51:22 +00:00
|
|
|
# If --spaces is used we add it to the configuration
|
|
|
|
if 'spaces' in self.args and self.args.spaces:
|
|
|
|
config.update({'spaces': self.args.spaces})
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.info('Parameter -s/--spaces detected: %s', config.get('spaces'))
|
2018-03-04 08:51:22 +00:00
|
|
|
|
2018-03-02 13:46:32 +00:00
|
|
|
return config
|
|
|
|
|
2018-02-04 06:42:03 +00:00
|
|
|
def _validate_config(self, 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:
|
2018-04-02 14:42:53 +00:00
|
|
|
validate(conf, constants.CONF_SCHEMA)
|
2018-02-04 06:42:03 +00:00
|
|
|
return conf
|
|
|
|
except ValidationError as exception:
|
2018-05-30 20:38:09 +00:00
|
|
|
logger.critical(
|
2018-02-04 06:42:03 +00:00
|
|
|
'Invalid configuration. See config.json.example. Reason: %s',
|
|
|
|
exception
|
|
|
|
)
|
|
|
|
raise ValidationError(
|
2018-04-02 14:42:53 +00:00
|
|
|
best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message
|
2018-02-04 06:42:03 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
def get_config(self) -> Dict[str, Any]:
|
|
|
|
"""
|
|
|
|
Return the config. Use this method to get the bot config
|
|
|
|
:return: Dict: Bot config
|
|
|
|
"""
|
2018-03-03 21:39:39 +00:00
|
|
|
if self.config is None:
|
|
|
|
self.config = self.load_config()
|
|
|
|
|
2018-02-04 06:42:03 +00:00
|
|
|
return self.config
|
2018-03-30 20:14:35 +00:00
|
|
|
|
2018-04-04 20:05:17 +00:00
|
|
|
def check_exchange(self, config: Dict[str, Any]) -> bool:
|
2018-03-30 20:14:35 +00:00
|
|
|
"""
|
|
|
|
Check if the exchange name in the config file is supported by Freqtrade
|
|
|
|
:return: True or raised an exception if the exchange if not supported
|
|
|
|
"""
|
2018-04-04 20:05:17 +00:00
|
|
|
exchange = config.get('exchange', {}).get('name').lower()
|
2018-03-30 20:14:35 +00:00
|
|
|
if exchange not in ccxt.exchanges:
|
|
|
|
|
|
|
|
exception_msg = 'Exchange "{}" not supported.\n' \
|
|
|
|
'The following exchanges are supported: {}'\
|
|
|
|
.format(exchange, ', '.join(ccxt.exchanges))
|
|
|
|
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.critical(exception_msg)
|
2018-03-30 20:14:35 +00:00
|
|
|
raise OperationalException(
|
|
|
|
exception_msg
|
|
|
|
)
|
|
|
|
|
2018-03-25 19:37:14 +00:00
|
|
|
logger.debug('Exchange "%s" supported', exchange)
|
2018-03-30 20:14:35 +00:00
|
|
|
return True
|