diff --git a/docs/utils.md b/docs/utils.md index abb7fd0db..cdf0c31af 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -144,38 +144,47 @@ freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt Use the `list-strategies` subcommand to see all strategies in one particular directory and the `list-hyperopts` subcommand to list custom Hyperopts. +These subcommands are useful for finding problems in your environment with loading strategies or hyperopt classes: modules with strategies or hyperopt classes that contain errors and failed to load are printed in red (LOAD FAILED), while strategies or hyperopt classes with duplicate names are printed in yellow (DUPLICATE NAME). + ``` -freqtrade list-strategies --help -usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--strategy-path PATH] [-1] +usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [--strategy-path PATH] [-1] [--no-color] optional arguments: -h, --help show this help message and exit --strategy-path PATH Specify additional strategy lookup path. -1, --one-column Print output in one column. + --no-color Disable colorization of hyperopt results. May be + useful if you are redirecting output to a file. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details. + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to `-` - to read config from stdin. + Specify configuration file (default: `config.json`). + Multiple --config options may be used. Can be set to + `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. ``` ``` -freqtrade list-hyperopts --help usage: freqtrade list-hyperopts [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] - [--hyperopt-path PATH] [-1] + [--hyperopt-path PATH] [-1] [--no-color] optional arguments: -h, --help show this help message and exit --hyperopt-path PATH Specify additional lookup path for Hyperopt and Hyperopt Loss functions. -1, --one-column Print output in one column. + --no-color Disable colorization of hyperopt results. May be + useful if you are redirecting output to a file. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 580c9e298..f3d4b0c84 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -30,9 +30,9 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] -ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"] +ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column", "print_colorized"] -ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"] +ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column", "print_colorized"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index f2b6bf995..49674b81a 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -3,8 +3,10 @@ import logging import sys from collections import OrderedDict from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, List +from colorama import init as colorama_init +from colorama import Fore, Style import rapidjson from tabulate import tabulate @@ -36,6 +38,29 @@ def start_list_exchanges(args: Dict[str, Any]) -> None: print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}") +def _print_objs_tabular(objs: List, print_colorized: bool) -> None: + if print_colorized: + colorama_init(autoreset=True) + red = Fore.RED + yellow = Fore.YELLOW + reset = Style.RESET_ALL + else: + red = '' + yellow = '' + reset = '' + + names = [s['name'] for s in objs] + objss_to_print = [{ + 'name': s['name'] if s['name'] else "--", + 'location': s['location'].name, + 'status': (red + "LOAD FAILED" + reset if s['class'] is None + else "OK" if names.count(s['name']) == 1 + else yellow + "DUPLICATE NAME" + reset) + } for s in objs] + + print(tabulate(objss_to_print, headers='keys', tablefmt='pipe')) + + def start_list_strategies(args: Dict[str, Any]) -> None: """ Print files with Strategy custom classes available in the directory @@ -43,15 +68,14 @@ def start_list_strategies(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES)) - strategies = StrategyResolver.search_all_objects(directory) + strategy_objs = StrategyResolver.search_all_objects(directory, not args['print_one_column']) # Sort alphabetically - strategies = sorted(strategies, key=lambda x: x['name']) - strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies] + strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) if args['print_one_column']: - print('\n'.join([s['name'] for s in strategies])) + print('\n'.join([s['name'] for s in strategy_objs])) else: - print(tabulate(strats_to_print, headers='keys', tablefmt='pipe')) + _print_objs_tabular(strategy_objs, config.get('print_colorized', False)) def start_list_hyperopts(args: Dict[str, Any]) -> None: @@ -63,15 +87,14 @@ def start_list_hyperopts(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS)) - hyperopts = HyperOptResolver.search_all_objects(directory) + hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column']) # Sort alphabetically - hyperopts = sorted(hyperopts, key=lambda x: x['name']) - hyperopts_to_print = [{'name': s['name'], 'location': s['location'].name} for s in hyperopts] + hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name']) if args['print_one_column']: - print('\n'.join([s['name'] for s in hyperopts])) + print('\n'.join([s['name'] for s in hyperopt_objs])) else: - print(tabulate(hyperopts_to_print, headers='keys', tablefmt='pipe')) + _print_objs_tabular(hyperopt_objs, config.get('print_colorized', False)) def start_list_timeframes(args: Dict[str, Any]) -> None: diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index a75c45933..922a2700a 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union +from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from freqtrade.exceptions import OperationalException @@ -40,12 +40,14 @@ class IResolver: return abs_paths @classmethod - def _get_valid_object(cls, module_path: Path, - object_name: Optional[str]) -> Generator[Any, None, None]: + def _get_valid_object(cls, module_path: Path, object_name: Optional[str], + enum_failed: bool = False) -> Iterator[Any]: """ Generator returning objects with matching object_type and object_name in the path given. :param module_path: absolute path to the module :param object_name: Class name of the object + :param enum_failed: If True, will return None for modules which fail. + Otherwise, failing modules are skipped. :return: generator containing matching objects """ @@ -58,6 +60,8 @@ class IResolver: except (ModuleNotFoundError, SyntaxError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") + if enum_failed: + return iter([None]) valid_objects_gen = ( obj for name, obj in inspect.getmembers(module, inspect.isclass) @@ -136,10 +140,13 @@ class IResolver: ) @classmethod - def search_all_objects(cls, directory: Path) -> List[Dict[str, Any]]: + def search_all_objects(cls, directory: Path, + enum_failed: bool) -> List[Dict[str, Any]]: """ Searches a directory for valid objects :param directory: Path to search + :param enum_failed: If True, will return None for modules which fail. + Otherwise, failing modules are skipped. :return: List of dicts containing 'name', 'class' and 'location' entires """ logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'") @@ -151,9 +158,10 @@ class IResolver: continue module_path = entry.resolve() logger.debug(f"Path {module_path}") - for obj in cls._get_valid_object(module_path, object_name=None): + for obj in cls._get_valid_object(module_path, object_name=None, + enum_failed=enum_failed): objects.append( - {'name': obj.__name__, + {'name': obj.__name__ if obj is not None else '', 'class': obj, 'location': entry, }) diff --git a/tests/strategy/failing_strategy.py b/tests/strategy/failing_strategy.py new file mode 100644 index 000000000..f8eaac3c3 --- /dev/null +++ b/tests/strategy/failing_strategy.py @@ -0,0 +1,9 @@ +# The strategy which fails to load due to non-existent dependency + +import nonexiting_module # noqa + +from freqtrade.strategy.interface import IStrategy + + +class TestStrategyLegacy(IStrategy): + pass diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index d3977ae44..379260599 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -30,14 +30,25 @@ def test_search_strategy(): assert s is None -def test_search_all_strategies(): +def test_search_all_strategies_no_failed(): directory = Path(__file__).parent - strategies = StrategyResolver.search_all_objects(directory) + strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) assert len(strategies) == 3 assert isinstance(strategies[0], dict) +def test_search_all_strategies_with_failed(): + directory = Path(__file__).parent + strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) + assert isinstance(strategies, list) + assert len(strategies) == 4 + # with enum_failed=True search_all_objects() shall find 3 good strategies + # and 1 which fails to load + assert len([x for x in strategies if x['class'] is not None]) == 3 + assert len([x for x in strategies if x['class'] is None]) == 1 + + def test_load_strategy(default_conf, result): default_conf.update({'strategy': 'SampleStrategy', 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates')