diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index c69135117..5396ae682 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -82,7 +82,7 @@ class Arguments(object): '-s', '--strategy', help='specify strategy file (default: %(default)s)', dest='strategy', - default='default_strategy', + default='DefaultStrategy', type=str, metavar='PATH', ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a3f91d774..61adf307a 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -14,7 +14,7 @@ class Constants(object): TICKER_INTERVAL = 5 # min HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec - DEFAULT_STRATEGY = 'default_strategy' + DEFAULT_STRATEGY = 'DefaultStrategy' # Required json-schema for user specified config CONF_SCHEMA = { diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index ea37735b7..4b645dbd0 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -7,8 +7,6 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.indicator_helpers import fishers_inverse from freqtrade.strategy.interface import IStrategy -class_name = 'DefaultStrategy' - class DefaultStrategy(IStrategy): """ diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 583f24a3f..fd48d1f1c 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -3,11 +3,11 @@ """ This module load custom strategies """ -import importlib +import importlib.util +import inspect import os -import sys from collections import OrderedDict -from typing import Optional, Dict +from typing import Optional, Dict, Type from pandas import DataFrame @@ -15,8 +15,6 @@ from freqtrade.constants import Constants from freqtrade.logger import Logger from freqtrade.strategy.interface import IStrategy -sys.path.insert(0, r'../../user_data/strategies') - class StrategyResolver(object): """ @@ -38,7 +36,7 @@ class StrategyResolver(object): else: strategy = Constants.DEFAULT_STRATEGY - # Load the strategy + # Try to load the strategy self._load_strategy(strategy) # Set attributes @@ -72,26 +70,27 @@ class StrategyResolver(object): def _load_strategy(self, strategy_name: str) -> None: """ - Search and load the custom strategy. If no strategy found, fallback on the default strategy - Set the object into self.custom_strategy + Search and loads the specified strategy. :param strategy_name: name of the module to import :return: None """ - try: - # Start by sanitizing the file name (remove any extensions) - strategy_name = self._sanitize_module_name(filename=strategy_name) - - # Search where can be the strategy file - path = self._search_strategy(filename=strategy_name) - - # Load the strategy - self.custom_strategy = self._load_class(path + strategy_name) + current_path = os.path.dirname(os.path.realpath(__file__)) + abs_paths = [ + os.path.join(current_path, '..', '..', 'user_data', 'strategies'), + current_path, + ] + for path in abs_paths: + self.custom_strategy = self._search_strategy(path, strategy_name) + if self.custom_strategy: + self.logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) + return None + raise ImportError('not found') # Fallback to the default strategy except (ImportError, TypeError) as error: self.logger.error( - "Impossible to load Strategy 'user_data/strategies/%s.py'. This file does not exist" + "Impossible to load Strategy '%s'. This class does not exist" " or contains Python code errors", strategy_name ) @@ -100,50 +99,44 @@ class StrategyResolver(object): error ) - def _load_class(self, filename: str) -> IStrategy: - """ - Import a strategy as a module - :param filename: path to the strategy (path from freqtrade/strategy/) - :return: return the strategy class - """ - module = importlib.import_module(filename, __package__) - custom_strategy = getattr(module, module.class_name) - - self.logger.info("Load strategy class: %s (%s.py)", module.class_name, filename) - return custom_strategy() - @staticmethod - def _sanitize_module_name(filename: str) -> str: + def _get_valid_strategies(module_path: str, strategy_name: str) -> Optional[Type[IStrategy]]: """ - Remove any extension from filename - :param filename: filename to sanatize - :return: return the filename without extensions + Returns a list of all possible strategies for the given module_path + :param module_path: absolute path to the module + :param strategy_name: Class name of the strategy + :return: Tuple with (name, class) or None """ - filename = os.path.basename(filename) - filename = os.path.splitext(filename)[0] - return filename - @staticmethod - def _search_strategy(filename: str) -> str: - """ - Search for the Strategy file in different folder - 1. search into the user_data/strategies folder - 2. search into the freqtrade/strategy folder - 3. if nothing found, return None - :param strategy_name: module name to search - :return: module path where is the strategy - """ - pwd = os.path.dirname(os.path.realpath(__file__)) + '/' - user_data = os.path.join(pwd, '..', '..', 'user_data', 'strategies', filename + '.py') - strategy_folder = os.path.join(pwd, filename + '.py') + # Generate spec based on absolute path + spec = importlib.util.spec_from_file_location('user_data.strategies', module_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) - path = None - if os.path.isfile(user_data): - path = 'user_data.strategies.' - elif os.path.isfile(strategy_folder): - path = '.' + valid_strategies_gen = ( + obj for name, obj in inspect.getmembers(module, inspect.isclass) + if strategy_name == name and IStrategy in obj.__bases__ + ) + return next(valid_strategies_gen, None) - return path + def _search_strategy(self, directory: str, strategy_name: str) -> Optional[IStrategy]: + """ + Search for the strategy_name in the given directory + :param directory: relative or absolute directory path + :return: name of the strategy class + """ + self.logger.debug('Searching for strategy %s in \'%s\'', strategy_name, directory) + for entry in os.listdir(directory): + # Only consider python files + if not entry.endswith('.py'): + self.logger.debug('Ignoring %s', entry) + continue + strategy = StrategyResolver._get_valid_strategies( + os.path.abspath(os.path.join(directory, entry)), strategy_name + ) + if strategy: + return strategy() + return None def populate_indicators(self, dataframe: DataFrame) -> DataFrame: """ diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 021474d5c..146da8faa 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -174,7 +174,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting' ] @@ -215,7 +215,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', 'backtesting', '--ticker-interval', '1', @@ -277,7 +277,7 @@ def test_start(mocker, default_conf, caplog) -> None: )) args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting' ] args = get_args(args) @@ -498,7 +498,7 @@ def test_backtest_ticks(default_conf): def test_backtest_clash_buy_sell(default_conf): - # Override the default buy trend function in our default_strategy + # Override the default buy trend function in our DefaultStrategy def fun(dataframe=None): buy_value = 1 sell_value = 1 @@ -510,7 +510,7 @@ def test_backtest_clash_buy_sell(default_conf): def test_backtest_only_sell(default_conf): - # Override the default buy trend function in our default_strategy + # Override the default buy trend function in our DefaultStrategy def fun(dataframe=None): buy_value = 0 sell_value = 1 @@ -578,12 +578,12 @@ def test_backtest_start_live(default_conf, mocker, caplog): args.live = True args.datadir = None args.export = None - args.strategy = 'default_strategy' + args.strategy = 'DefaultStrategy' args.timerange = '-100' # needed due to MagicMock malleability args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting', '--ticker-interval', '1', '--live', diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 688a7aa8d..affb0a6d3 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -57,12 +57,12 @@ def test_start(mocker, default_conf, caplog) -> None: )) args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'hyperopt', '--epochs', '5' ] args = get_args(args) - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) start(args) import pprint @@ -80,7 +80,7 @@ def test_loss_calculation_prefer_correct_trade_count() -> None: Test Hyperopt.calculate_loss() """ hyperopt = _HYPEROPT - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(1, hyperopt.target_trades + 100, 20) @@ -171,7 +171,7 @@ def test_fmin_best_results(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.logger.Logger.set_format', MagicMock()) - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) hyperopt.trials = create_trials(mocker) hyperopt.tickerdata_to_dataframe = MagicMock() @@ -215,7 +215,7 @@ def test_fmin_throw_value_error(mocker, default_conf, caplog) -> None: conf.update({'spaces': 'all'}) mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.logger.Logger.set_format', MagicMock()) - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) hyperopt.trials = create_trials(mocker) hyperopt.tickerdata_to_dataframe = MagicMock() @@ -258,7 +258,7 @@ def test_resuming_previous_hyperopt_results_succeeds(mocker, default_conf) -> No mocker.patch('freqtrade.optimize.hyperopt.hyperopt_optimize_conf', return_value=conf) mocker.patch('freqtrade.logger.Logger.set_format', MagicMock()) - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) hyperopt = Hyperopt(conf) hyperopt.trials = trials hyperopt.tickerdata_to_dataframe = MagicMock() diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py index 2b91fbec5..5c9be0b32 100644 --- a/freqtrade/tests/strategy/test_default_strategy.py +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -4,7 +4,7 @@ import pytest from pandas import DataFrame from freqtrade.analyze import Analyze -from freqtrade.strategy.default_strategy import DefaultStrategy, class_name +from freqtrade.strategy.default_strategy import DefaultStrategy @pytest.fixture @@ -13,10 +13,6 @@ def result(): return Analyze.parse_ticker_dataframe(json.load(data_file)) -def test_default_strategy_class_name(): - assert class_name == DefaultStrategy.__name__ - - def test_default_strategy_structure(): assert hasattr(DefaultStrategy, 'minimal_roi') assert hasattr(DefaultStrategy, 'stoploss') diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 7dc602f58..a34aba4b5 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -5,20 +5,10 @@ import logging from freqtrade.strategy.resolver import StrategyResolver -def test_sanitize_module_name(): - assert StrategyResolver._sanitize_module_name('default_strategy') == 'default_strategy' - assert StrategyResolver._sanitize_module_name('default_strategy.py') == 'default_strategy' - assert StrategyResolver._sanitize_module_name('../default_strategy.py') == 'default_strategy' - assert StrategyResolver._sanitize_module_name('../default_strategy') == 'default_strategy' - assert StrategyResolver._sanitize_module_name('.default_strategy') == '.default_strategy' - assert StrategyResolver._sanitize_module_name('foo-bar') == 'foo-bar' - assert StrategyResolver._sanitize_module_name('foo/bar') == 'bar' - - def test_search_strategy(): - assert StrategyResolver._search_strategy('default_strategy') == '.' - assert StrategyResolver._search_strategy('test_strategy') == 'user_data.strategies.' - assert StrategyResolver._search_strategy('super_duper') is None + assert StrategyResolver._search_strategy('DefaultStrategy') == '.' + assert StrategyResolver._search_strategy('TestStrategy') == 'user_data.strategies.' + assert StrategyResolver._search_strategy('NotFoundStrategy') is None def test_strategy_structure(): @@ -32,7 +22,7 @@ def test_load_strategy(result): strategy.logger = logging.getLogger(__name__) assert not hasattr(StrategyResolver, 'custom_strategy') - strategy._load_strategy('test_strategy') + strategy._load_strategy('TestStrategy') assert not hasattr(StrategyResolver, 'custom_strategy') @@ -47,13 +37,13 @@ def test_load_not_found_strategy(caplog): assert not hasattr(StrategyResolver, 'custom_strategy') strategy._load_strategy('NotFoundStrategy') - error_msg = "Impossible to load Strategy 'user_data/strategies/{}.py'. This file does not " \ + error_msg = "Impossible to load Strategy '{}'. This class does not " \ "exist or contains Python code errors".format('NotFoundStrategy') assert ('test_strategy', logging.ERROR, error_msg) in caplog.record_tuples def test_strategy(result): - strategy = StrategyResolver({'strategy': 'default_strategy'}) + strategy = StrategyResolver({'strategy': 'DefaultStrategy'}) assert hasattr(strategy.custom_strategy, 'minimal_roi') assert strategy.minimal_roi[0] == 0.04 @@ -76,7 +66,7 @@ def test_strategy(result): def test_strategy_override_minimal_roi(caplog): caplog.set_level(logging.INFO) config = { - 'strategy': 'default_strategy', + 'strategy': 'DefaultStrategy', 'minimal_roi': { "0": 0.5 } @@ -94,7 +84,7 @@ def test_strategy_override_minimal_roi(caplog): def test_strategy_override_stoploss(caplog): caplog.set_level(logging.INFO) config = { - 'strategy': 'default_strategy', + 'strategy': 'DefaultStrategy', 'stoploss': -0.5 } strategy = StrategyResolver(config) @@ -111,7 +101,7 @@ def test_strategy_override_ticker_interval(caplog): caplog.set_level(logging.INFO) config = { - 'strategy': 'default_strategy', + 'strategy': 'DefaultStrategy', 'ticker_interval': 60 } strategy = StrategyResolver(config) @@ -134,7 +124,7 @@ def test_strategy_fallback_default_strategy(): def test_strategy_singleton(): - strategy1 = StrategyResolver({'strategy': 'default_strategy'}) + strategy1 = StrategyResolver({'strategy': 'DefaultStrategy'}) assert hasattr(strategy1.custom_strategy, 'minimal_roi') assert strategy1.minimal_roi[0] == 0.04 diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index 558ea7ee5..a4f1ba549 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -16,7 +16,7 @@ from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.tests.conftest import log_has # Avoid to reinit the same object again and again -_ANALYZE = Analyze({'strategy': 'default_strategy'}) +_ANALYZE = Analyze({'strategy': 'DefaultStrategy'}) def test_signaltype_object() -> None: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 002eac722..1085b0060 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -99,7 +99,7 @@ def test_load_config(default_conf, mocker) -> None: validated_conf = configuration.load_config() assert 'strategy' in validated_conf - assert validated_conf['strategy'] == 'default_strategy' + assert validated_conf['strategy'] == 'DefaultStrategy' assert 'dynamic_whitelist' not in validated_conf assert 'dry_run_db' not in validated_conf @@ -114,7 +114,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: args = [ '--dynamic-whitelist', '10', - '--strategy', 'test_strategy', + '--strategy', 'TestStrategy', '--dry-run-db' ] args = Arguments(args, '').get_parsed_arg() @@ -125,7 +125,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: assert 'dynamic_whitelist' in validated_conf assert validated_conf['dynamic_whitelist'] == 10 assert 'strategy' in validated_conf - assert validated_conf['strategy'] == 'test_strategy' + assert validated_conf['strategy'] == 'TestStrategy' assert 'dry_run_db' in validated_conf assert validated_conf['dry_run_db'] is True @@ -140,7 +140,7 @@ def test_show_info(default_conf, mocker, caplog) -> None: args = [ '--dynamic-whitelist', '10', - '--strategy', 'test_strategy', + '--strategy', 'TestStrategy', '--dry-run-db' ] args = Arguments(args, '').get_parsed_arg() @@ -184,7 +184,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', 'backtesting' ] @@ -228,7 +228,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non args = [ '--config', 'config.json', - '--strategy', 'default_strategy', + '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', 'backtesting', '--ticker-interval', '1', diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py index fc79cb74c..b739ae370 100644 --- a/freqtrade/tests/test_dataframe.py +++ b/freqtrade/tests/test_dataframe.py @@ -15,19 +15,19 @@ def load_dataframe_pair(pairs): assert isinstance(pairs[0], str) dataframe = ld[pairs[0]] - analyze = Analyze({'strategy': 'default_strategy'}) + analyze = Analyze({'strategy': 'DefaultStrategy'}) dataframe = analyze.analyze_ticker(dataframe) return dataframe def test_dataframe_load(): - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) dataframe = load_dataframe_pair(_pairs) assert isinstance(dataframe, pandas.core.frame.DataFrame) def test_dataframe_columns_exists(): - StrategyResolver({'strategy': 'default_strategy'}) + StrategyResolver({'strategy': 'DefaultStrategy'}) dataframe = load_dataframe_pair(_pairs) assert 'high' in dataframe.columns assert 'low' in dataframe.columns diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index a164812c4..4ba1dbe17 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -10,10 +10,6 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib import numpy # noqa -# Update this variable if you change the class name -class_name = 'TestStrategy' - - # This class is a sample. Feel free to customize it. class TestStrategy(IStrategy): """