Merge pull request #2712 from freqtrade/strategylist
add list-strategies subcommand
This commit is contained in:
commit
004993583b
@ -108,6 +108,47 @@ With custom user directory
|
||||
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
|
||||
|
||||
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
||||
|
@ -30,6 +30,8 @@ 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_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||
|
||||
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"]
|
||||
|
||||
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"]
|
||||
|
||||
@ -131,8 +134,9 @@ class Arguments:
|
||||
from freqtrade.utils import (start_create_userdir, start_download_data,
|
||||
start_hyperopt_list, start_hyperopt_show,
|
||||
start_list_exchanges, start_list_markets,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_list_timeframes, start_test_pairlist, start_trading)
|
||||
start_list_strategies, start_new_hyperopt,
|
||||
start_new_strategy, start_list_timeframes,
|
||||
start_test_pairlist, start_trading)
|
||||
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
|
||||
|
||||
subparsers = self.parser.add_subparsers(dest='command',
|
||||
@ -185,6 +189,15 @@ class Arguments:
|
||||
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
|
||||
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
|
||||
list_exchanges_cmd = subparsers.add_parser(
|
||||
'list-exchanges',
|
||||
|
@ -14,6 +14,7 @@ class ExchangeResolver(IResolver):
|
||||
"""
|
||||
This class contains all the logic to load a custom exchange class
|
||||
"""
|
||||
object_type = Exchange
|
||||
|
||||
@staticmethod
|
||||
def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange:
|
||||
|
@ -5,7 +5,7 @@ This module load custom hyperopt
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade import OperationalException
|
||||
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
|
||||
"""
|
||||
object_type = IHyperOpt
|
||||
object_type_str = "Hyperopt"
|
||||
user_subdir = USERPATH_HYPEROPTS
|
||||
initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
|
||||
|
||||
@staticmethod
|
||||
def load_hyperopt(config: Dict) -> IHyperOpt:
|
||||
@ -33,8 +37,9 @@ class HyperOptResolver(IResolver):
|
||||
|
||||
hyperopt_name = config['hyperopt']
|
||||
|
||||
hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config,
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
hyperopt = HyperOptResolver.load_object(hyperopt_name, config,
|
||||
kwargs={'config': config},
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
if not hasattr(hyperopt, 'populate_indicators'):
|
||||
logger.warning("Hyperopt class does not provide populate_indicators() method. "
|
||||
@ -47,36 +52,15 @@ class HyperOptResolver(IResolver):
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
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):
|
||||
"""
|
||||
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
|
||||
def load_hyperoptloss(config: Dict) -> IHyperOptLoss:
|
||||
@ -89,8 +73,9 @@ class HyperOptLossResolver(IResolver):
|
||||
# default hyperopt loss
|
||||
hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS
|
||||
|
||||
hyperoptloss = HyperOptLossResolver._load_hyperoptloss(
|
||||
hyperoptloss_name, config, extra_dir=config.get('hyperopt_path'))
|
||||
hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name,
|
||||
config, kwargs={},
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
# Assign ticker_interval to be used in hyperopt
|
||||
hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
|
||||
@ -100,29 +85,3 @@ class HyperOptLossResolver(IResolver):
|
||||
f"Found HyperoptLoss class {hyperoptloss_name} does not "
|
||||
"implement `hyperopt_loss_function`.")
|
||||
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."
|
||||
)
|
||||
|
@ -7,7 +7,9 @@ import importlib.util
|
||||
import inspect
|
||||
import logging
|
||||
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__)
|
||||
|
||||
@ -16,12 +18,17 @@ class IResolver:
|
||||
"""
|
||||
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
|
||||
def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None,
|
||||
@classmethod
|
||||
def build_search_paths(cls, config, user_subdir: Optional[str] = None,
|
||||
extra_dir: Optional[str] = None) -> List[Path]:
|
||||
|
||||
abs_paths: List[Path] = [current_path]
|
||||
abs_paths: List[Path] = [cls.initial_search_path]
|
||||
|
||||
if user_subdir:
|
||||
abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir))
|
||||
@ -32,12 +39,11 @@ class IResolver:
|
||||
|
||||
return abs_paths
|
||||
|
||||
@staticmethod
|
||||
def _get_valid_object(object_type, module_path: Path,
|
||||
object_name: str) -> Generator[Any, None, None]:
|
||||
@classmethod
|
||||
def _get_valid_object(cls, module_path: Path,
|
||||
object_name: Optional[str]) -> Generator[Any, None, None]:
|
||||
"""
|
||||
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 object_name: Class name of the object
|
||||
:return: generator containing matching objects
|
||||
@ -45,7 +51,7 @@ class IResolver:
|
||||
|
||||
# Generate spec based on absolute path
|
||||
# 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)
|
||||
try:
|
||||
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
|
||||
@ -55,19 +61,20 @@ class IResolver:
|
||||
|
||||
valid_objects_gen = (
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def _search_object(directory: Path, object_type, object_name: str,
|
||||
kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]:
|
||||
@classmethod
|
||||
def _search_object(cls, directory: Path, object_name: str
|
||||
) -> Union[Tuple[Any, Path], Tuple[None, None]]:
|
||||
"""
|
||||
Search for the objectname in the given directory
|
||||
: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():
|
||||
# Only consider python files
|
||||
if not str(entry).endswith('.py'):
|
||||
@ -75,14 +82,14 @@ class IResolver:
|
||||
continue
|
||||
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:
|
||||
return (obj(**kwargs), module_path)
|
||||
return (obj, module_path)
|
||||
return (None, None)
|
||||
|
||||
@staticmethod
|
||||
def _load_object(paths: List[Path], object_type, object_name: str,
|
||||
@classmethod
|
||||
def _load_object(cls, paths: List[Path], object_name: str,
|
||||
kwargs: dict = {}) -> Optional[Any]:
|
||||
"""
|
||||
Try to load object from path list.
|
||||
@ -90,16 +97,63 @@ class IResolver:
|
||||
|
||||
for _path in paths:
|
||||
try:
|
||||
(module, module_path) = IResolver._search_object(directory=_path,
|
||||
object_type=object_type,
|
||||
object_name=object_name,
|
||||
kwargs=kwargs)
|
||||
(module, module_path) = cls._search_object(directory=_path,
|
||||
object_name=object_name)
|
||||
if module:
|
||||
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}'...")
|
||||
return module
|
||||
return module(**kwargs)
|
||||
except FileNotFoundError:
|
||||
logger.warning('Path "%s" does not exist.', _path.resolve())
|
||||
|
||||
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
|
||||
|
@ -6,7 +6,6 @@ This module load custom pairlists
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
from freqtrade.resolvers import IResolver
|
||||
|
||||
@ -17,6 +16,10 @@ class PairListResolver(IResolver):
|
||||
"""
|
||||
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
|
||||
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
|
||||
:return: initialized Pairlist class
|
||||
"""
|
||||
|
||||
return PairListResolver._load_pairlist(pairlist_name, config,
|
||||
kwargs={'exchange': exchange,
|
||||
'pairlistmanager': pairlistmanager,
|
||||
'config': config,
|
||||
'pairlistconfig': pairlistconfig,
|
||||
'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."
|
||||
)
|
||||
return PairListResolver.load_object(pairlist_name, config,
|
||||
kwargs={'exchange': exchange,
|
||||
'pairlistmanager': pairlistmanager,
|
||||
'config': config,
|
||||
'pairlistconfig': pairlistconfig,
|
||||
'pairlist_pos': pairlist_pos},
|
||||
)
|
||||
|
@ -11,7 +11,9 @@ from inspect import getfullargspec
|
||||
from pathlib import Path
|
||||
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.strategy.interface import IStrategy
|
||||
|
||||
@ -22,6 +24,10 @@ class StrategyResolver(IResolver):
|
||||
"""
|
||||
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
|
||||
def load_strategy(config: Optional[Dict] = None) -> IStrategy:
|
||||
@ -114,11 +120,11 @@ class StrategyResolver(IResolver):
|
||||
|
||||
@staticmethod
|
||||
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__}'. "
|
||||
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__}'. "
|
||||
f"Order-time-in-force mapping is incomplete.")
|
||||
|
||||
@ -132,11 +138,10 @@ class StrategyResolver(IResolver):
|
||||
:param extra_dir: additional directory to search for the given strategy
|
||||
: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,
|
||||
user_subdir=constants.USERPATH_STRATEGY,
|
||||
extra_dir=extra_dir)
|
||||
abs_paths = StrategyResolver.build_search_paths(config,
|
||||
user_subdir=USERPATH_STRATEGY,
|
||||
extra_dir=extra_dir)
|
||||
|
||||
if ":" in strategy_name:
|
||||
logger.info("loading base64 encoded strategy")
|
||||
@ -154,8 +159,9 @@ class StrategyResolver(IResolver):
|
||||
# register temp path with the bot
|
||||
abs_paths.insert(0, temp.resolve())
|
||||
|
||||
strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy,
|
||||
object_name=strategy_name, kwargs={'config': config})
|
||||
strategy = StrategyResolver._load_object(paths=abs_paths,
|
||||
object_name=strategy_name,
|
||||
kwargs={'config': config})
|
||||
if strategy:
|
||||
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
|
||||
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
|
||||
|
@ -23,7 +23,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv,
|
||||
from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
|
||||
market_is_active, symbol_is_pair)
|
||||
from freqtrade.misc import plural, render_template
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.state import RunMode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -223,6 +223,24 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
||||
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:
|
||||
"""
|
||||
Print ticker intervals (timeframes) available on Exchange
|
||||
|
@ -159,7 +159,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
||||
delattr(hyperopt, 'populate_buy_trend')
|
||||
delattr(hyperopt, 'populate_sell_trend')
|
||||
mocker.patch(
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt',
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object',
|
||||
MagicMock(return_value=hyperopt(default_conf))
|
||||
)
|
||||
default_conf.update({'hyperopt': 'DefaultHyperOpt'})
|
||||
@ -195,7 +195,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
|
||||
|
||||
hl = DefaultHyperOptLoss
|
||||
mocker.patch(
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss',
|
||||
'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object',
|
||||
MagicMock(return_value=hl)
|
||||
)
|
||||
x = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||
|
@ -15,26 +15,29 @@ from tests.conftest import log_has, log_has_re
|
||||
|
||||
|
||||
def test_search_strategy():
|
||||
default_config = {}
|
||||
default_location = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||
|
||||
s, _ = StrategyResolver._search_object(
|
||||
directory=default_location,
|
||||
object_type=IStrategy,
|
||||
kwargs={'config': default_config},
|
||||
object_name='DefaultStrategy'
|
||||
)
|
||||
assert isinstance(s, IStrategy)
|
||||
assert issubclass(s, IStrategy)
|
||||
|
||||
s, _ = StrategyResolver._search_object(
|
||||
directory=default_location,
|
||||
object_type=IStrategy,
|
||||
kwargs={'config': default_config},
|
||||
object_name='NotFoundStrategy'
|
||||
)
|
||||
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):
|
||||
default_conf.update({'strategy': 'SampleStrategy',
|
||||
'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates')
|
||||
|
@ -7,11 +7,12 @@ import pytest
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
|
||||
start_download_data, start_list_exchanges,
|
||||
start_list_markets, start_list_timeframes,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_test_pairlist, start_trading,
|
||||
start_hyperopt_list, start_hyperopt_show)
|
||||
start_download_data, start_hyperopt_list,
|
||||
start_hyperopt_show, start_list_exchanges,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_hyperopt,
|
||||
start_new_strategy, start_test_pairlist,
|
||||
start_trading)
|
||||
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
|
||||
patched_configuration_load_config_file)
|
||||
|
||||
@ -630,6 +631,37 @@ def test_download_data_trades(mocker, caplog):
|
||||
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):
|
||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||
markets=PropertyMock(return_value=markets),
|
||||
|
Loading…
Reference in New Issue
Block a user