Merge branch 'develop' into align_userdata

This commit is contained in:
Matthias
2019-08-18 15:00:12 +02:00
63 changed files with 1517 additions and 941 deletions

View File

@@ -1,2 +1,3 @@
from freqtrade.configuration.arguments import Arguments, TimeRange # noqa: F401
from freqtrade.configuration.arguments import Arguments # noqa: F401
from freqtrade.configuration.timerange import TimeRange # noqa: F401
from freqtrade.configuration.configuration import Configuration # noqa: F401

View File

@@ -2,10 +2,8 @@
This module contains the argument manager class
"""
import argparse
import re
from typing import List, NamedTuple, Optional
from typing import List, Optional
import arrow
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade import constants
@@ -23,7 +21,8 @@ ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_pos
ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
"position_stacking", "epochs", "spaces",
"use_max_market_positions", "print_all", "hyperopt_jobs",
"use_max_market_positions", "print_all",
"print_colorized", "print_json", "hyperopt_jobs",
"hyperopt_random_state", "hyperopt_min_trades",
"hyperopt_continue", "hyperopt_loss"]
@@ -44,18 +43,6 @@ ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY +
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"])
class TimeRange(NamedTuple):
"""
NamedTuple defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is None, don't use corresponding startvalue.
"""
starttype: Optional[str] = None
stoptype: Optional[str] = None
startts: int = 0
stopts: int = 0
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
@@ -138,45 +125,3 @@ class Arguments(object):
)
list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
@staticmethod
def parse_timerange(text: Optional[str]) -> TimeRange:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start: int = 0
stop: int = 0
if stype[0]:
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)

View File

@@ -2,9 +2,9 @@ import logging
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.exchange import (is_exchange_bad, is_exchange_available,
is_exchange_officially_supported, available_exchanges)
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
is_exchange_available, is_exchange_bad,
is_exchange_officially_supported)
logger = logging.getLogger(__name__)
@@ -31,9 +31,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
)
if check_for_bad and is_exchange_bad(exchange):
logger.warning(f'Exchange "{exchange}" is known to not work with the bot yet. '
f'Use it only for development and testing purposes.')
return False
raise OperationalException(f'Exchange "{exchange}" is known to not work with the bot yet. '
f'Reason: {get_exchange_bad_reason(exchange)}')
if is_exchange_officially_supported(exchange):
logger.info(f'Exchange "{exchange}" is officially supported '

View File

@@ -196,6 +196,19 @@ AVAILABLE_CLI_OPTIONS = {
action='store_true',
default=False,
),
"print_colorized": Arg(
'--no-color',
help='Disable colorization of hyperopt results. May be useful if you are '
'redirecting output to a file.',
action='store_false',
default=True,
),
"print_json": Arg(
'--print-json',
help='Print best result detailization in JSON format.',
action='store_true',
default=False,
),
"hyperopt_jobs": Arg(
'-j', '--job-workers',
help='The number of concurrently running jobs for hyperoptimization '
@@ -231,7 +244,9 @@ AVAILABLE_CLI_OPTIONS = {
'--hyperopt-loss',
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
'Different functions can generate completely different results, '
'since the target for optimization is different. (default: `%(default)s`).',
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
'(default: `%(default)s`).',
metavar='NAME',
default=constants.DEFAULT_HYPEROPT_LOSS,
),

View File

@@ -5,11 +5,12 @@ import logging
import warnings
from argparse import Namespace
from pathlib import Path
from typing import Any, Callable, Dict, Optional
from typing import Any, Callable, Dict, List, Optional
from freqtrade import OperationalException, constants
from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
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
@@ -40,43 +41,43 @@ class Configuration(object):
return self.config
def _load_config_files(self) -> Dict[str, Any]:
@staticmethod
def from_files(files: List[str]) -> Dict[str, Any]:
"""
Iterate through the config files passed in the args,
loading all of them and merging their contents.
Iterate through the config files passed in, loading all of them
and merging their contents.
Files are loaded in sequence, parameters in later configuration files
override the same parameter from an earlier file (last definition wins).
:param files: List of file paths
:return: configuration dictionary
"""
# Keep this method as staticmethod, so it can be used from interactive environments
config: Dict[str, Any] = {}
# We expect here a list of config filenames
for path in self.args.config:
logger.info('Using config: %s ...', path)
for path in files:
logger.info(f'Using config: {path} ...')
# Merge config options, overwriting old values
config = deep_merge_dicts(load_config_file(path), config)
return config
def _normalize_config(self, config: Dict[str, Any]) -> None:
"""
Make config more canonical -- i.e. for example add missing parts that we expect
to be normally in it...
"""
# Normalize config
if 'internals' not in config:
config['internals'] = {}
# validate configuration before returning
logger.info('Validating configuration ...')
validate_config_schema(config)
return config
def load_config(self) -> Dict[str, Any]:
"""
Extract information for sys.argv and load the bot configuration
:return: Configuration dictionary
"""
# Load all configs
config: Dict[str, Any] = self._load_config_files()
# Make resulting config more canonical
self._normalize_config(config)
logger.info('Validating configuration ...')
validate_config_schema(config)
config: Dict[str, Any] = Configuration.from_files(self.args.config)
self._validate_config_consistency(config)
@@ -146,7 +147,7 @@ class Configuration(object):
config['internals'].update({'sd_notify': True})
# Check if the exchange set by the user is supported
check_exchange(config)
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
def _process_datadir_options(self, config: Dict[str, Any]) -> None:
"""
@@ -244,6 +245,15 @@ class Configuration(object):
self._args_to_config(config, argname='print_all',
logstring='Parameter --print-all detected ...')
if 'print_colorized' in self.args and not self.args.print_colorized:
logger.info('Parameter --no-color detected ...')
config.update({'print_colorized': False})
else:
config.update({'print_colorized': True})
self._args_to_config(config, argname='print_json',
logstring='Parameter --print-json detected ...')
self._args_to_config(config, argname='hyperopt_jobs',
logstring='Parameter -j/--job-workers detected: {}')
@@ -280,7 +290,7 @@ class Configuration(object):
if not self.runmode:
# Handle real mode, infer dry/live from config
self.runmode = RunMode.DRY_RUN if config.get('dry_run', True) else RunMode.LIVE
logger.info("Runmode set to {self.runmode}.")
logger.info(f"Runmode set to {self.runmode}.")
config.update({'runmode': self.runmode})

View File

@@ -1,7 +1,7 @@
"""
This module contain functions to load the configuration file
"""
import json
import rapidjson
import logging
import sys
from typing import Any, Dict
@@ -12,6 +12,9 @@ from freqtrade import OperationalException
logger = logging.getLogger(__name__)
CONFIG_PARSE_MODE = rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS
def load_config_file(path: str) -> Dict[str, Any]:
"""
Loads a config file from the given path
@@ -21,7 +24,7 @@ def load_config_file(path: str) -> Dict[str, Any]:
try:
# Read config from stdin if requested in the options
with open(path) if path != '-' else sys.stdin as file:
config = json.load(file)
config = rapidjson.load(file, parse_mode=CONFIG_PARSE_MODE)
except FileNotFoundError:
raise OperationalException(
f'Config file "{path}" not found!'

View File

@@ -0,0 +1,70 @@
"""
This module contains the argument manager class
"""
import re
from typing import Optional
import arrow
class TimeRange():
"""
object defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is None, don't use corresponding startvalue.
"""
def __init__(self, starttype: Optional[str] = None, stoptype: Optional[str] = None,
startts: int = 0, stopts: int = 0):
self.starttype: Optional[str] = starttype
self.stoptype: Optional[str] = stoptype
self.startts: int = startts
self.stopts: int = stopts
def __eq__(self, other):
"""Override the default Equals behavior"""
return (self.starttype == other.starttype and self.stoptype == other.stoptype
and self.startts == other.startts and self.stopts == other.stopts)
@staticmethod
def parse_timerange(text: Optional[str]):
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
for rex, stype in syntax:
# Apply the regular expression to text
match = re.match(rex, text)
if match: # Regex has matched
rvals = match.groups()
index = 0
start: int = 0
stop: int = 0
if stype[0]:
starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(starts)
index += 1
if stype[1]:
stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)