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
|
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.
|
||||||
|
@ -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',
|
||||||
|
@ -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:
|
||||||
|
@ -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."
|
|
||||||
)
|
|
||||||
|
@ -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
|
||||||
|
@ -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."
|
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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),
|
||||||
|
Loading…
Reference in New Issue
Block a user