Repository: freqtrade/freqtrade

Issue:
inheritance strategy can not load  parameter file shared from base class
<!-- 


## Describe your environment

  * Operating system: ubuntu
  * Python Version: 3.8.10`
  * CCXT version: _1.74.63
  * Freqtrade Version: fraqtrade develop
  

## Describe the problem:

if StragegyA derived from StragegyB written in same .py file, 
then both of them share the same json file generated by Hyperopt.

They would could conflict with each other when using Hyperopt to optimize one of them after the other Strategy was optimized.


### Steps to reproduce:

  1. StragegyB derived from  StragegyA and they are in one .py file
  2. optimize StragegyA using Hyperopt ( get file A.json)
  3. optimize StragegyB using Hyperopt 
  4. switching step3 and step2 is the same result
  
### Observed Results:
load file A.json as StragegyB  parameter file, then ERROR

### Relevant code exceptions or logs

2022-03-20 00:59:44,224 - freqtrade.optimize.hyperopt_tools - INFO - Dumping parameters to /home/bourne/freqtrade-develop/user_data/strategies/AStrategy.json

...

2022-03-20 01:01:06,082 - freqtrade.resolvers.iresolver - INFO - Using resolved strategy BStrategy from '/home/bourne/freqtrade-develop/user_data/strategies/AStrategy.py'...
2022-03-20 01:01:06,082 - freqtrade.strategy.hyper - INFO - Loading parameters from file /home/bourne/freqtrade-develop/user_data/strategies/AStrategy.json
2022-03-20 01:01:06,082 - freqtrade - ERROR - Invalid parameter file provided.






Discussion:
all the words gone...

## Describe your environment

  * Operating system: ubuntu
  * Python Version: 3.8.10
  * CCXT version: 1.74.63
  * Freqtrade Version:  freqtrade develop
  


## Describe the problem:

if BStrategy derived from AStrategy written in the same .py file, then they are sharing the same parameter json file.

Confilct happens when loading paremter file.
 

### Steps to reproduce:

  1. BStrategy derived from AStrategy written in the same .py file
  2. optimize AStrategy ( get json file )
  3. optimize BStrategy. it loads json file of AStrategy and goes error.
  
### Observed Results:
it loads json file of AStrategy and goes error.

it should not share parameter file or load carefully


### Relevant code exceptions or logs


  ```
2022-03-20 00:59:44,224 - freqtrade.optimize.hyperopt_tools - INFO - Dumping parameters to /home/bourne/freqtrade-develop/user_data/strategies/StrategyA.json

...
2022-03-20 01:01:06,082 - freqtrade.resolvers.iresolver - INFO - Using resolved strategy StrategyB from '/home/bourne/freqtrade-develop/user_data/strategies/StrategyA.py'...
2022-03-20 01:01:06,082 - freqtrade.strategy.hyper - INFO - Loading parameters from file /home/bourne/freqtrade-develop/user_data/strategies/StrategyA.json
2022-03-20 01:01:06,082 - freqtrade - ERROR - Invalid parameter file provided.
  ```

strategy parameter files are named according to the strategy file - so the file "StrategyXX.py" will have the parameter file "StrategyXX.json".

The load will succeed if the parameter file corresponds to the strategy you're loading (so if the parameter file is for `strategyB` (it's the content, not the name that counts).

in your case it seems like it's not - your parameter file is for strategy `StrategyA` - while you're loading `StrategyB` - therefore the load fails (as the parameters are wrong for your strategy.
Simply put - run optimization for `StrategyB` before loading `StrategyB`.


There's little we can do for this i think - this sounds like intended behaviour - with the "protection" against running a wrong parameter file working.
The "error" you're running into is to avoid loading a non-matching parameter file - which is there intentionally.

Every fix i can think of would remove the "invalid parameter file provided" protection - leading to either simply ignoring an available, but wrong parameter file - or to loading a wrong parameter file. Neither of the 2 are options i'm comfortable with.

In general, i would recommend to have one file per strategy to avoid this behavior.

` one file per strategy`  is not good solution for me，I get 30 sub class files to write.

During i tried the solution, another error occured.
`freqtrade.resolvers.iresolver - WARNING - Could not import /home/bourne/freqtrade-develop/user_data/strategies/StrategyA.py due to 'No module named 'samplestrategy''`

# Steps to reproduce:
1. prepare StrategyA.py
```
import sys
from pathlib import Path
sys.path.append(str(Path(__file__).parent))

from samplestrategy import SampleStrategy

class StrategyA(SampleStrategy): pass
```
2. optimize StrategyA

Resolving patch:
diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py
--- a/freqtrade/resolvers/iresolver.py
+++ b/freqtrade/resolvers/iresolver.py
@@ -6,6 +6,7 @@
 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 @@
 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 @@ def _get_valid_object(cls, module_path: Path, object_name: Optional[str],
 
         # 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])
-
-        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:
+        with PathModifier(module_path.parent):
+            module_name = module_path.stem or ""
+            spec = importlib.util.spec_from_file_location(module_name, 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_name
+                                                 )
+            )
+            # 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


Test patch:
diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py
--- 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 @@ def informative_pairs(self):
         """
         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
diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py
--- a/tests/strategy/strats/strategy_test_v2.py
+++ b/tests/strategy/strats/strategy_test_v2.py
@@ -7,7 +7,7 @@
 
 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):