Merge pull request #2712 from freqtrade/strategylist

add list-strategies subcommand
This commit is contained in:
hroff-1902 2019-12-28 12:32:06 +03:00 committed by GitHub
commit 004993583b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 245 additions and 138 deletions

View File

@ -108,6 +108,47 @@ With custom user directory
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
``` ```
## List Strategies
Use the `list-strategies` subcommand to see all strategies in one particular directory.
```
freqtrade list-strategies --help
usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--strategy-path PATH] [-1]
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.
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.
-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.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
!!! Warning
Using this command will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
Example: search default strategy directory within userdir
``` bash
freqtrade list-strategies --userdir ~/.freqtrade/
```
Example: search dedicated strategy path
``` bash
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
```
## List Exchanges ## List Exchanges
Use the `list-exchanges` subcommand to see the exchanges available for the bot. Use the `list-exchanges` subcommand to see the exchanges available for the bot.

View File

@ -30,6 +30,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"]
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
@ -62,7 +64,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
"print_json", "hyperopt_show_no_header"] "print_json", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
"hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit"] "list-strategies", "hyperopt-list", "hyperopt-show", "plot-dataframe",
"plot-profit"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
@ -131,8 +134,9 @@ class Arguments:
from freqtrade.utils import (start_create_userdir, start_download_data, from freqtrade.utils import (start_create_userdir, start_download_data,
start_hyperopt_list, start_hyperopt_show, start_hyperopt_list, start_hyperopt_show,
start_list_exchanges, start_list_markets, start_list_exchanges, start_list_markets,
start_new_hyperopt, start_new_strategy, start_list_strategies, start_new_hyperopt,
start_list_timeframes, start_test_pairlist, start_trading) start_new_strategy, start_list_timeframes,
start_test_pairlist, start_trading)
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
subparsers = self.parser.add_subparsers(dest='command', subparsers = self.parser.add_subparsers(dest='command',
@ -185,6 +189,15 @@ class Arguments:
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd) self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
# Add list-strategies subcommand
list_strategies_cmd = subparsers.add_parser(
'list-strategies',
help='Print available strategies.',
parents=[_common_parser],
)
list_strategies_cmd.set_defaults(func=start_list_strategies)
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
# Add list-exchanges subcommand # Add list-exchanges subcommand
list_exchanges_cmd = subparsers.add_parser( list_exchanges_cmd = subparsers.add_parser(
'list-exchanges', 'list-exchanges',

View File

@ -14,6 +14,7 @@ class ExchangeResolver(IResolver):
""" """
This class contains all the logic to load a custom exchange class This class contains all the logic to load a custom exchange class
""" """
object_type = Exchange
@staticmethod @staticmethod
def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange: def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange:

View File

@ -5,7 +5,7 @@ This module load custom hyperopt
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, Dict from typing import Dict
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS
@ -20,6 +20,10 @@ class HyperOptResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt class This class contains all the logic to load custom hyperopt class
""" """
object_type = IHyperOpt
object_type_str = "Hyperopt"
user_subdir = USERPATH_HYPEROPTS
initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
@staticmethod @staticmethod
def load_hyperopt(config: Dict) -> IHyperOpt: def load_hyperopt(config: Dict) -> IHyperOpt:
@ -33,7 +37,8 @@ class HyperOptResolver(IResolver):
hyperopt_name = config['hyperopt'] hyperopt_name = config['hyperopt']
hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config, hyperopt = HyperOptResolver.load_object(hyperopt_name, config,
kwargs={'config': config},
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
if not hasattr(hyperopt, 'populate_indicators'): if not hasattr(hyperopt, 'populate_indicators'):
@ -47,36 +52,15 @@ class HyperOptResolver(IResolver):
"Using populate_sell_trend from the strategy.") "Using populate_sell_trend from the strategy.")
return hyperopt return hyperopt
@staticmethod
def _load_hyperopt(
hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
"""
Search and loads the specified hyperopt.
:param hyperopt_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given hyperopt
:return: HyperOpt instance or None
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS,
extra_dir=extra_dir)
hyperopt = IResolver._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name, kwargs={'config': config})
if hyperopt:
return hyperopt
raise OperationalException(
f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist "
"or contains Python code errors."
)
class HyperOptLossResolver(IResolver): class HyperOptLossResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt loss class This class contains all the logic to load custom hyperopt loss class
""" """
object_type = IHyperOptLoss
object_type_str = "HyperoptLoss"
user_subdir = USERPATH_HYPEROPTS
initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
@staticmethod @staticmethod
def load_hyperoptloss(config: Dict) -> IHyperOptLoss: def load_hyperoptloss(config: Dict) -> IHyperOptLoss:
@ -89,8 +73,9 @@ class HyperOptLossResolver(IResolver):
# default hyperopt loss # default hyperopt loss
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
hyperoptloss = HyperOptLossResolver._load_hyperoptloss( hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name,
hyperoptloss_name, config, extra_dir=config.get('hyperopt_path')) config, kwargs={},
extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt # Assign ticker_interval to be used in hyperopt
hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
@ -100,29 +85,3 @@ class HyperOptLossResolver(IResolver):
f"Found HyperoptLoss class {hyperoptloss_name} does not " f"Found HyperoptLoss class {hyperoptloss_name} does not "
"implement `hyperopt_loss_function`.") "implement `hyperopt_loss_function`.")
return hyperoptloss return hyperoptloss
@staticmethod
def _load_hyperoptloss(hyper_loss_name: str, config: Dict,
extra_dir: Optional[str] = None) -> IHyperOptLoss:
"""
Search and loads the specified hyperopt loss class.
:param hyper_loss_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given hyperopt
:return: HyperOptLoss instance or None
"""
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=USERPATH_HYPEROPTS,
extra_dir=extra_dir)
hyperoptloss = IResolver._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name)
if hyperoptloss:
return hyperoptloss
raise OperationalException(
f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist "
"or contains Python code errors."
)

View File

@ -7,7 +7,9 @@ import importlib.util
import inspect import inspect
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional, Tuple, Union, Generator from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union
from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -16,12 +18,17 @@ class IResolver:
""" """
This class contains all the logic to load custom classes This class contains all the logic to load custom classes
""" """
# Childclasses need to override this
object_type: Type[Any]
object_type_str: str
user_subdir: Optional[str] = None
initial_search_path: Path
@staticmethod @classmethod
def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None, def build_search_paths(cls, config, user_subdir: Optional[str] = None,
extra_dir: Optional[str] = None) -> List[Path]: extra_dir: Optional[str] = None) -> List[Path]:
abs_paths: List[Path] = [current_path] abs_paths: List[Path] = [cls.initial_search_path]
if user_subdir: if user_subdir:
abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir)) abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
@ -32,12 +39,11 @@ class IResolver:
return abs_paths return abs_paths
@staticmethod @classmethod
def _get_valid_object(object_type, module_path: Path, def _get_valid_object(cls, module_path: Path,
object_name: str) -> Generator[Any, None, None]: object_name: Optional[str]) -> Generator[Any, None, None]:
""" """
Generator returning objects with matching object_type and object_name in the path given. Generator returning objects with matching object_type and object_name in the path given.
:param object_type: object_type (class)
:param module_path: absolute path to the module :param module_path: absolute path to the module
:param object_name: Class name of the object :param object_name: Class name of the object
:return: generator containing matching objects :return: generator containing matching objects
@ -45,7 +51,7 @@ class IResolver:
# Generate spec based on absolute path # Generate spec based on absolute path
# Pass object_name as first argument to have logging print a reasonable name. # Pass object_name as first argument to have logging print a reasonable name.
spec = importlib.util.spec_from_file_location(object_name, str(module_path)) spec = importlib.util.spec_from_file_location(object_name or "", str(module_path))
module = importlib.util.module_from_spec(spec) module = importlib.util.module_from_spec(spec)
try: try:
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
@ -55,19 +61,20 @@ class IResolver:
valid_objects_gen = ( valid_objects_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass) obj for name, obj in inspect.getmembers(module, inspect.isclass)
if object_name == name and object_type in obj.__bases__ if (object_name is None or object_name == name) and cls.object_type in obj.__bases__
) )
return valid_objects_gen return valid_objects_gen
@staticmethod @classmethod
def _search_object(directory: Path, object_type, object_name: str, def _search_object(cls, directory: Path, object_name: str
kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: ) -> Union[Tuple[Any, Path], Tuple[None, None]]:
""" """
Search for the objectname in the given directory Search for the objectname in the given directory
:param directory: relative or absolute directory path :param directory: relative or absolute directory path
:return: object instance :param object_name: ClassName of the object to load
:return: object class
""" """
logger.debug("Searching for %s %s in '%s'", object_type.__name__, object_name, directory) logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'")
for entry in directory.iterdir(): for entry in directory.iterdir():
# Only consider python files # Only consider python files
if not str(entry).endswith('.py'): if not str(entry).endswith('.py'):
@ -75,14 +82,14 @@ class IResolver:
continue continue
module_path = entry.resolve() module_path = entry.resolve()
obj = next(IResolver._get_valid_object(object_type, module_path, object_name), None) obj = next(cls._get_valid_object(module_path, object_name), None)
if obj: if obj:
return (obj(**kwargs), module_path) return (obj, module_path)
return (None, None) return (None, None)
@staticmethod @classmethod
def _load_object(paths: List[Path], object_type, object_name: str, def _load_object(cls, paths: List[Path], object_name: str,
kwargs: dict = {}) -> Optional[Any]: kwargs: dict = {}) -> Optional[Any]:
""" """
Try to load object from path list. Try to load object from path list.
@ -90,16 +97,63 @@ class IResolver:
for _path in paths: for _path in paths:
try: try:
(module, module_path) = IResolver._search_object(directory=_path, (module, module_path) = cls._search_object(directory=_path,
object_type=object_type, object_name=object_name)
object_name=object_name,
kwargs=kwargs)
if module: if module:
logger.info( logger.info(
f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} "
f"from '{module_path}'...") f"from '{module_path}'...")
return module return module(**kwargs)
except FileNotFoundError: except FileNotFoundError:
logger.warning('Path "%s" does not exist.', _path.resolve()) logger.warning('Path "%s" does not exist.', _path.resolve())
return None return None
@classmethod
def load_object(cls, object_name: str, config: dict, kwargs: dict,
extra_dir: Optional[str] = None) -> Any:
"""
Search and loads the specified object as configured in hte child class.
:param objectname: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given pairlist
:raises: OperationalException if the class is invalid or does not exist.
:return: Object instance or None
"""
abs_paths = cls.build_search_paths(config,
user_subdir=cls.user_subdir,
extra_dir=extra_dir)
pairlist = cls._load_object(paths=abs_paths, object_name=object_name,
kwargs=kwargs)
if pairlist:
return pairlist
raise OperationalException(
f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist "
"or contains Python code errors."
)
@classmethod
def search_all_objects(cls, directory: Path) -> List[Dict[str, Any]]:
"""
Searches a directory for valid objects
:param directory: Path to search
:return: List of dicts containing 'name', 'class' and 'location' entires
"""
logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'")
objects = []
for entry in directory.iterdir():
# Only consider python files
if not str(entry).endswith('.py'):
logger.debug('Ignoring %s', entry)
continue
module_path = entry.resolve()
logger.debug(f"Path {module_path}")
for obj in cls._get_valid_object(module_path, object_name=None):
objects.append(
{'name': obj.__name__,
'class': obj,
'location': entry,
})
return objects

View File

@ -6,7 +6,6 @@ This module load custom pairlists
import logging import logging
from pathlib import Path from pathlib import Path
from freqtrade import OperationalException
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@ -17,6 +16,10 @@ class PairListResolver(IResolver):
""" """
This class contains all the logic to load custom PairList class This class contains all the logic to load custom PairList class
""" """
object_type = IPairList
object_type_str = "Pairlist"
user_subdir = None
initial_search_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
@staticmethod @staticmethod
def load_pairlist(pairlist_name: str, exchange, pairlistmanager, def load_pairlist(pairlist_name: str, exchange, pairlistmanager,
@ -31,33 +34,10 @@ class PairListResolver(IResolver):
:param pairlist_pos: Position of the pairlist in the list of pairlists :param pairlist_pos: Position of the pairlist in the list of pairlists
:return: initialized Pairlist class :return: initialized Pairlist class
""" """
return PairListResolver.load_object(pairlist_name, config,
return PairListResolver._load_pairlist(pairlist_name, config,
kwargs={'exchange': exchange, kwargs={'exchange': exchange,
'pairlistmanager': pairlistmanager, 'pairlistmanager': pairlistmanager,
'config': config, 'config': config,
'pairlistconfig': pairlistconfig, 'pairlistconfig': pairlistconfig,
'pairlist_pos': pairlist_pos}) 'pairlist_pos': pairlist_pos},
@staticmethod
def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList:
"""
Search and loads the specified pairlist.
:param pairlist_name: name of the module to import
:param config: configuration dictionary
:param extra_dir: additional directory to search for the given pairlist
:return: PairList instance or None
"""
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = IResolver.build_search_paths(config, current_path=current_path,
user_subdir=None, extra_dir=None)
pairlist = IResolver._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs)
if pairlist:
return pairlist
raise OperationalException(
f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist "
"or contains Python code errors."
) )

View File

@ -11,7 +11,9 @@ from inspect import getfullargspec
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
from freqtrade import constants, OperationalException from freqtrade import OperationalException
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES,
USERPATH_STRATEGY)
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
@ -22,6 +24,10 @@ class StrategyResolver(IResolver):
""" """
This class contains the logic to load custom strategy class This class contains the logic to load custom strategy class
""" """
object_type = IStrategy
object_type_str = "Strategy"
user_subdir = USERPATH_STRATEGY
initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
@staticmethod @staticmethod
def load_strategy(config: Optional[Dict] = None) -> IStrategy: def load_strategy(config: Optional[Dict] = None) -> IStrategy:
@ -114,11 +120,11 @@ class StrategyResolver(IResolver):
@staticmethod @staticmethod
def _strategy_sanity_validations(strategy): def _strategy_sanity_validations(strategy):
if not all(k in strategy.order_types for k in constants.REQUIRED_ORDERTYPES): if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-types mapping is incomplete.") f"Order-types mapping is incomplete.")
if not all(k in strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-time-in-force mapping is incomplete.") f"Order-time-in-force mapping is incomplete.")
@ -132,10 +138,9 @@ class StrategyResolver(IResolver):
:param extra_dir: additional directory to search for the given strategy :param extra_dir: additional directory to search for the given strategy
:return: Strategy instance or None :return: Strategy instance or None
""" """
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = IResolver.build_search_paths(config, current_path=current_path, abs_paths = StrategyResolver.build_search_paths(config,
user_subdir=constants.USERPATH_STRATEGY, user_subdir=USERPATH_STRATEGY,
extra_dir=extra_dir) extra_dir=extra_dir)
if ":" in strategy_name: if ":" in strategy_name:
@ -154,8 +159,9 @@ class StrategyResolver(IResolver):
# register temp path with the bot # register temp path with the bot
abs_paths.insert(0, temp.resolve()) abs_paths.insert(0, temp.resolve())
strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy, strategy = StrategyResolver._load_object(paths=abs_paths,
object_name=strategy_name, kwargs={'config': config}) object_name=strategy_name,
kwargs={'config': config})
if strategy: if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)

View File

@ -23,7 +23,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv,
from freqtrade.exchange import (available_exchanges, ccxt_exchanges, from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
market_is_active, symbol_is_pair) market_is_active, symbol_is_pair)
from freqtrade.misc import plural, render_template from freqtrade.misc import plural, render_template
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -223,6 +223,24 @@ def start_download_data(args: Dict[str, Any]) -> None:
f"on exchange {exchange.name}.") f"on exchange {exchange.name}.")
def start_list_strategies(args: Dict[str, Any]) -> None:
"""
Print Strategies available in a directory
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY))
strategies = StrategyResolver.search_all_objects(directory)
# Sort alphabetically
strategies = sorted(strategies, key=lambda x: x['name'])
strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies]
if args['print_one_column']:
print('\n'.join([s['name'] for s in strategies]))
else:
print(tabulate(strats_to_print, headers='keys', tablefmt='pipe'))
def start_list_timeframes(args: Dict[str, Any]) -> None: def start_list_timeframes(args: Dict[str, Any]) -> None:
""" """
Print ticker intervals (timeframes) available on Exchange Print ticker intervals (timeframes) available on Exchange

View File

@ -159,7 +159,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
delattr(hyperopt, 'populate_buy_trend') delattr(hyperopt, 'populate_buy_trend')
delattr(hyperopt, 'populate_sell_trend') delattr(hyperopt, 'populate_sell_trend')
mocker.patch( mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt', 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object',
MagicMock(return_value=hyperopt(default_conf)) MagicMock(return_value=hyperopt(default_conf))
) )
default_conf.update({'hyperopt': 'DefaultHyperOpt'}) default_conf.update({'hyperopt': 'DefaultHyperOpt'})
@ -195,7 +195,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
hl = DefaultHyperOptLoss hl = DefaultHyperOptLoss
mocker.patch( mocker.patch(
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object',
MagicMock(return_value=hl) MagicMock(return_value=hl)
) )
x = HyperOptLossResolver.load_hyperoptloss(default_conf) x = HyperOptLossResolver.load_hyperoptloss(default_conf)

View File

@ -15,26 +15,29 @@ from tests.conftest import log_has, log_has_re
def test_search_strategy(): def test_search_strategy():
default_config = {}
default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() default_location = Path(__file__).parent.parent.joinpath('strategy').resolve()
s, _ = StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_type=IStrategy,
kwargs={'config': default_config},
object_name='DefaultStrategy' object_name='DefaultStrategy'
) )
assert isinstance(s, IStrategy) assert issubclass(s, IStrategy)
s, _ = StrategyResolver._search_object( s, _ = StrategyResolver._search_object(
directory=default_location, directory=default_location,
object_type=IStrategy,
kwargs={'config': default_config},
object_name='NotFoundStrategy' object_name='NotFoundStrategy'
) )
assert s is None assert s is None
def test_search_all_strategies():
directory = Path(__file__).parent
strategies = StrategyResolver.search_all_objects(directory)
assert isinstance(strategies, list)
assert len(strategies) == 3
assert isinstance(strategies[0], dict)
def test_load_strategy(default_conf, result): def test_load_strategy(default_conf, result):
default_conf.update({'strategy': 'SampleStrategy', default_conf.update({'strategy': 'SampleStrategy',
'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates')

View File

@ -7,11 +7,12 @@ import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.utils import (setup_utils_configuration, start_create_userdir, from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
start_download_data, start_list_exchanges, start_download_data, start_hyperopt_list,
start_list_markets, start_list_timeframes, start_hyperopt_show, start_list_exchanges,
start_new_hyperopt, start_new_strategy, start_list_markets, start_list_strategies,
start_test_pairlist, start_trading, start_list_timeframes, start_new_hyperopt,
start_hyperopt_list, start_hyperopt_show) start_new_strategy, start_test_pairlist,
start_trading)
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
patched_configuration_load_config_file) patched_configuration_load_config_file)
@ -630,6 +631,37 @@ def test_download_data_trades(mocker, caplog):
assert convert_mock.call_count == 1 assert convert_mock.call_count == 1
def test_start_list_strategies(mocker, caplog, capsys):
args = [
"list-strategies",
"--strategy-path",
str(Path(__file__).parent / "strategy"),
"-1"
]
pargs = get_args(args)
# pargs['config'] = None
start_list_strategies(pargs)
captured = capsys.readouterr()
assert "TestStrategyLegacy" in captured.out
assert "legacy_strategy.py" not in captured.out
assert "DefaultStrategy" in captured.out
# Test regular output
args = [
"list-strategies",
"--strategy-path",
str(Path(__file__).parent / "strategy"),
]
pargs = get_args(args)
# pargs['config'] = None
start_list_strategies(pargs)
captured = capsys.readouterr()
assert "TestStrategyLegacy" in captured.out
assert "legacy_strategy.py" in captured.out
assert "DefaultStrategy" in captured.out
def test_start_test_pairlist(mocker, caplog, markets, tickers, default_conf, capsys): def test_start_test_pairlist(mocker, caplog, markets, tickers, default_conf, capsys):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),