From 49e087df5b79d90381ac9ee64d0eaac88e044f74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 13:07:06 +0100 Subject: [PATCH 1/3] Allow Strategy subclassing in different files by enabling local imports --- docs/strategy-advanced.md | 9 ++-- freqtrade/resolvers/iresolver.py | 58 ++++++++++++++++------- tests/strategy/strats/strategy_test_v2.py | 2 +- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 3793abacf..fa1c09560 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -164,16 +164,15 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. !!! Note "Parent-strategy in different files" - If you have the parent-strategy in a different file, you'll need to add the following to the top of your "child"-file to ensure proper loading, otherwise freqtrade may not be able to load the parent strategy correctly. + If you have the parent-strategy in a different file, you can still import the strategy. + Assuming `myawesomestrategy.py` is the filename, and `MyAwesomeStrategy` the strategy you need to import: ``` python - import sys - from pathlib import Path - sys.path.append(str(Path(__file__).parent)) - from myawesomestrategy import MyAwesomeStrategy ``` + This is the recommended way to derive strategies to avoid problems with hyperopt parameter files. + ## Embedding Strategies Freqtrade provides you with an easy way to embed the strategy into your configuration file. diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index c6f97c976..8d132da70 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -6,6 +6,7 @@ This module load custom objects import importlib.util import inspect import logging +import sys from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union @@ -15,6 +16,22 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) +class PathModifier: + def __init__(self, path: Path): + self.path = path + + def __enter__(self): + """Inject path to allow importing with relative imports.""" + sys.path.insert(0, str(self.path)) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Undo insertion of local path.""" + str_path = str(self.path) + if str_path in sys.path: + sys.path.remove(str_path) + + class IResolver: """ This class contains all the logic to load custom classes @@ -57,27 +74,32 @@ 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 or "", str(module_path)) - if not spec: - return iter([None]) + with PathModifier(module_path.parent): - module = importlib.util.module_from_spec(spec) - try: - spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: - # Catch errors in case a specific module is not installed - logger.warning(f"Could not import {module_path} due to '{err}'") - if enum_failed: + spec = importlib.util.spec_from_file_location(module_path.stem or "", str(module_path)) + if not spec: return iter([None]) - valid_objects_gen = ( - (obj, inspect.getsource(module)) for - name, obj in inspect.getmembers( - module, inspect.isclass) if ((object_name is None or object_name == name) - and issubclass(obj, cls.object_type) - and obj is not cls.object_type) - ) - return valid_objects_gen + module = importlib.util.module_from_spec(spec) + try: + spec.loader.exec_module(module) # type: ignore # importlib does not use typehints + except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: + # Catch errors in case a specific module is not installed + logger.warning(f"Could not import {module_path} due to '{err}'") + if enum_failed: + return iter([None]) + + valid_objects_gen = ( + (obj, inspect.getsource(module)) for + name, obj in inspect.getmembers( + module, inspect.isclass) if ((object_name is None or object_name == name) + and issubclass(obj, cls.object_type) + and obj is not cls.object_type + and obj.__module__ == module_path.stem or "" + ) + ) + # The __module__ check ensures we only use strategies that are defined in this folder. + return valid_objects_gen @classmethod def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index c57becdad..59f1f569e 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -7,7 +7,7 @@ from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib from freqtrade.persistence import Trade -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class StrategyTestV2(IStrategy): From 6ec7b84b92ec24264d5f573f8289cd466322196e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 13:12:26 +0100 Subject: [PATCH 2/3] Modify hyperoptable strategy to use relative importing --- .../strategy/strats/hyperoptable_strategy.py | 88 +------------------ 1 file changed, 3 insertions(+), 85 deletions(-) diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 88bdd078e..dc6b03a3e 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -1,14 +1,13 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -import talib.abstract as ta from pandas import DataFrame +from strategy_test_v2 import StrategyTestV2 import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, - RealParameter) +from freqtrade.strategy import BooleanParameter, DecimalParameter, IntParameter, RealParameter -class HyperoptableStrategy(IStrategy): +class HyperoptableStrategy(StrategyTestV2): """ Default Strategy provided by freqtrade bot. Please do not modify this strategy, it's intended for internal use only. @@ -16,38 +15,6 @@ class HyperoptableStrategy(IStrategy): or strategy repository https://github.com/freqtrade/freqtrade-strategies for samples and inspiration. """ - INTERFACE_VERSION = 2 - - # Minimal ROI designed for the strategy - minimal_roi = { - "40": 0.0, - "30": 0.01, - "20": 0.02, - "0": 0.04 - } - - # Optimal stoploss designed for the strategy - stoploss = -0.10 - - # Optimal ticker interval for the strategy - timeframe = '5m' - - # Optional order type mapping - order_types = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False - } - - # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 - - # Optional time in force for orders - order_time_in_force = { - 'buy': 'gtc', - 'sell': 'gtc', - } buy_params = { 'buy_rsi': 35, @@ -91,55 +58,6 @@ class HyperoptableStrategy(IStrategy): """ return [] - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - :param dataframe: Dataframe with data from the exchange - :param metadata: Additional information, like the currently traded pair - :return: a Dataframe with all mandatory indicators for the strategies - """ - - # Momentum Indicator - # ------------------------------------ - - # ADX - dataframe['adx'] = ta.ADX(dataframe) - - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['macdhist'] = macd['macdhist'] - - # Minus Directional Indicator / Movement - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - - # Plus Directional Indicator / Movement - dataframe['plus_di'] = ta.PLUS_DI(dataframe) - - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - - # Stoch fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['fastk'] = stoch_fast['fastk'] - - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_middleband'] = bollinger['mid'] - dataframe['bb_upperband'] = bollinger['upper'] - - # EMA - Exponential Moving Average - dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) - - return dataframe - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe From e9c4e6a69d9c173b30c6b4d2263ad658b1950b42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Mar 2022 13:14:52 +0100 Subject: [PATCH 3/3] Update derived strategy documentation --- docs/strategy-advanced.md | 11 +++++++---- freqtrade/resolvers/iresolver.py | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index fa1c09560..533402528 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -146,7 +146,7 @@ def version(self) -> str: The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: -``` python +``` python title="user_data/strategies/myawesomestrategy.py" class MyAwesomeStrategy(IStrategy): ... stoploss = 0.13 @@ -155,6 +155,10 @@ class MyAwesomeStrategy(IStrategy): # should be in any custom strategy... ... +``` + +``` python title="user_data/strategies/MyAwesomeStrategy2.py" +from myawesomestrategy import MyAwesomeStrategy class MyAwesomeStrategy2(MyAwesomeStrategy): # Override something stoploss = 0.08 @@ -163,16 +167,15 @@ class MyAwesomeStrategy2(MyAwesomeStrategy): Both attributes and methods may be overridden, altering behavior of the original strategy in a way you need. +While keeping the subclass in the same file is technically possible, it can lead to some problems with hyperopt parameter files. + !!! Note "Parent-strategy in different files" If you have the parent-strategy in a different file, you can still import the strategy. Assuming `myawesomestrategy.py` is the filename, and `MyAwesomeStrategy` the strategy you need to import: ``` python - from myawesomestrategy import MyAwesomeStrategy ``` - This is the recommended way to derive strategies to avoid problems with hyperopt parameter files. - ## Embedding Strategies Freqtrade provides you with an easy way to embed the strategy into your configuration file. diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 8d132da70..3ab461041 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -75,8 +75,8 @@ class IResolver: # Generate spec based on absolute path # Pass object_name as first argument to have logging print a reasonable name. with PathModifier(module_path.parent): - - spec = importlib.util.spec_from_file_location(module_path.stem or "", str(module_path)) + module_name = module_path.stem or "" + spec = importlib.util.spec_from_file_location(module_name, str(module_path)) if not spec: return iter([None]) @@ -95,7 +95,7 @@ class IResolver: module, inspect.isclass) if ((object_name is None or object_name == name) and issubclass(obj, cls.object_type) and obj is not cls.object_type - and obj.__module__ == module_path.stem or "" + and obj.__module__ == module_name ) ) # The __module__ check ensures we only use strategies that are defined in this folder.