Repository: freqtrade/freqtrade

Issue:
Unique optimization values per pair
Operating system: Debian
Python Version: 3.9.12
CCXT version: 1.83.10
Freqtrade Version: develop-d5486f17d

As every market has unique arithmetic and behavior it's illogical to assume that the same indicator settings will apply perfectly for a group of pairs.

Some crypto markets have a strong correlation, yet the actual performance of the indicators needs to be perfectly fitted (as possible) per market.

Therefore I would like to suggest the following:

1. As unique parameters were introduced with buy/sell hyperopt settings, introducing a per-pair flag (rather in boot or in config) to define rather FT optimizes a per-pair scenario
2. A per-pair scenario should be guided using {self.quote} or similar in strategy for proper identification of each dataframe
3. the saved hyperopt JSON file should include per-pair parameters
4. Backtesting should be transparent 

Having this feature I believe will greatly improve FT performance in live/dry trading 


Discussion:
Optimizing indicators per pair will give you a perfect fit (assuming enough epochs).

While that sounds good in theory (all trades are now perfect) - that strategy would not perform well on unseen data - therefore i think this whole approach is not a good idea / approach - and will most likely fail the moment you attempt to run it on unseen data.


For a "per pair" optimized parameter - you'd also always have to use a static pairlist (otherwise you'd run with parameters from a random pair).
As then all pairs are known, you can pretty easily use "per pair" parameters directly in the strategy (just prefix/postfix the parameter name by pair). 
I don't see why this should not be accomplishable from within the strategy without changes to freqtrade - if you think this is something that'll give you more profits (i think it won't - and will fail on future data).


At the moment, I therefore don't see freqtrade complicating it's code in this area (reducing maintainability as a consequence) - as i 
a) don't see the benefit of this approach
b) don't see why it can't be accomplished in the strategy

if you think this is a must and there's things preventing the implementation in a strategy - you're obviously free to submit a Pull request adding this, assuming the code is well made, tested - and will not break existing functionality.


@xmatthias I've been running a hacked strategy with unique optimization settings for each pair against aggressive leveraged markets (20-30 pairs per bot instance), with quite simple strategy.

Here's some results:
**FTX:**
**Performance:**
1.  VETBULL/USD 7.485 USD (16.33%) (13)
2.  TRXBULL/USD 4.319 USD (9.77%) (2)
3.  ALTBULL/USD 3.25 USD (6.59%) (14)
4.  ADABULL/USD 3.115 USD (6.71%) (14)
5.  DRGNBULL/USD 2.825 USD (5.25%) (5)
6.  PRIVBULL/USD 2.603 USD (5.28%) (2)
7.  KNCBULL/USD 2.211 USD (4.56%) (2)
8.  ETCBULL/USD 1.707 USD (3.48%) (2)
9.  ATOMBULL/USD 0.952 USD (2.30%) (6)

**KuCoin:**
**Performance:**
1.  VET3L/USDT 29.925 USDT (25.80%) (11)
2.  UNI3L/USDT 26.068 USDT (34.16%) (8)
3.  XRP3L/USDT 24.011 USDT (13.68%) (5)
4.  AXS3L/USDT 15.949 USDT (9.74%) (5)
5.  AVAX3L/USDT 14.148 USDT (18.32%) (6)

Of course, not all pairs will perform as expected but that's more signals related, the idea is to find the most profitable pairs and NOT from backtesting results. We can only achieve this with unique parameters and inputs from actual real-time signals, it's logical.

The bottom line is obviously much better results with unique parameters per pair.

I wish I would have the coding skills to perform this, I can just suggest at this point the benefits as I see it performs better.

Do you think it can be accomplished through strategy without any code modifications? 

Maybe introducing hyperopt settings/parameters in lower levels within the strategy class can 'ease' on users to correlate hyperopt-settings and a quote, but that's just a thought ;)


Using leveraged pairs is usually misleading - as they almost always have very low volume - and performance will be market depending (all your FTX results are on BULL - but once market changes just a little bit, you'll have high losses on this). 
You'll have to have a strategy running live/dry for months to properly confirm such a thing - especially in these markets, as the shown profits above can be wiped in half a day - once market behaviour (speak trend) changes slightly.

Optimizing per pair will base your strategy on one (false) assumption - which is that markets continue to move in line with how they did move in the past.

> Do you think it can be accomplished through strategy without any code modifications?

Nothing prevents you from having a parameter `buy_VETBULL = ` ... - and then use the that when VETBULL/USD is the pair (you can get that via metadata).
it may be a bit of work (not really - you can quite easily define this for a given list of pairs) - and then only requires some magic to get the proper value


```python

class whatever(IStrategy):

    buy_rsi = IntParameter(10, 40, default=30, space="buy")

    def populate_entry_trend(self, dataframe, metadata):
       
       asset=metadata['pair'].split('/')[0] # not 100% reliable
      # buy_rsi becomes the fallback. your real parameters will be defined below
       val = getattr(self, f"buy_rsi_{asset}", self.buy_rsi)
       # signal assignment

# Careful, this must be AFTER the strategy - at the same level as "class" 
pairs = ['VETBULL', 'TRXBULL', ] # ...
for p in pairs:
    setattr(DynamicParams, f'buy_rsi_{p}', IntParameter(10, 40, default=30, space="buy"))

```

With the above example, i get 3 buy parameters (`buy_rsi`, `buy_rsi_VETBULL` and `buy_rsi_TRXBULL`).
We'll obviously want a "generic fallback" so it's not failing (you could handle this differently, like skipping the signal assignment if no dedicated parameter is found.

You'll obviously have to keep the pairlist here - but i don't see this as a problem, really, as you explicitly said you'd want "per pair" parameters, so using a dynamic pairlist (one you didn't hyperopt on) is out of the question anyway.
Very interesting, it is good to discuss practice, even without code modification.

So potentially a static pairlist can be achieved on the class level overriding a dynamic pairlist with unique parameters.

That's good enough ;)

your approach implicitly prohibits dynamic pairlists (you'd be missing parameters for the "new" pairs) :) 
> your approach implicitly prohibits dynamic pairlists (you'd be missing parameters for the "new" pairs) :)

But potentially simply passing pairlist array in the class level can help keep dynamic pairlists and unique parameters/dataframes

With my suggested approach, every pair will have unique parameters saved per hyperopt session, your suggesting to unique the actual variables, that almost the same thing ;)

Accessing pairlist array on class level is something to consider? 
> Accessing pairlist array on class level is something to consider?

No - this impossible.

Resolving patch:
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -187,9 +187,7 @@ def _set_strategy(self, strategy: IStrategy):
         # since a "perfect" stoploss-exit is assumed anyway
         # And the regular "stoploss" function would not apply to that case
         self.strategy.order_types['stoploss_on_exchange'] = False
-        if self.dataprovider.runmode == RunMode.BACKTEST:
-            # in hyperopt mode - don't re-init params
-            self.strategy.ft_load_hyper_params(False)
+
         self.strategy.ft_bot_start()
 
     def _load_protections(self, strategy: IStrategy):
diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py
--- a/freqtrade/strategy/hyper.py
+++ b/freqtrade/strategy/hyper.py
@@ -4,9 +4,8 @@
 """
 import logging
 from pathlib import Path
-from typing import Any, Dict, Iterator, List, Tuple
+from typing import Any, Dict, Iterator, List, Tuple, Type, Union
 
-from freqtrade.enums import RunMode
 from freqtrade.exceptions import OperationalException
 from freqtrade.misc import deep_merge_dicts, json_load
 from freqtrade.optimize.hyperopt_tools import HyperoptTools
@@ -34,9 +33,7 @@ def __init__(self, config: Dict[str, Any], *args, **kwargs):
         params = self.load_params_from_file()
         params = params.get('params', {})
         self._ft_params_from_file = params
-
-        if config.get('runmode') != RunMode.BACKTEST:
-            self.ft_load_hyper_params(config.get('runmode') == RunMode.HYPEROPT)
+        # Init/loading of parameters is done as part of ft_bot_start().
 
     def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, BaseParameter]]:
         """
@@ -56,28 +53,13 @@ def enumerate_parameters(self, category: str = None) -> Iterator[Tuple[str, Base
         for par in params:
             yield par.name, par
 
-    @classmethod
-    def detect_parameters(cls, category: str) -> Iterator[Tuple[str, BaseParameter]]:
-        """ Detect all parameters for 'category' """
-        for attr_name in dir(cls):
-            if not attr_name.startswith('__'):  # Ignore internals, not strictly necessary.
-                attr = getattr(cls, attr_name)
-                if issubclass(attr.__class__, BaseParameter):
-                    if (attr_name.startswith(category + '_')
-                            and attr.category is not None and attr.category != category):
-                        raise OperationalException(
-                            f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
-                    if (category == attr.category or
-                            (attr_name.startswith(category + '_') and attr.category is None)):
-                        yield attr_name, attr
-
     @classmethod
     def detect_all_parameters(cls) -> Dict:
         """ Detect all parameters and return them as a list"""
         params: Dict[str, Any] = {
-            'buy': list(cls.detect_parameters('buy')),
-            'sell': list(cls.detect_parameters('sell')),
-            'protection': list(cls.detect_parameters('protection')),
+            'buy': list(detect_parameters(cls, 'buy')),
+            'sell': list(detect_parameters(cls, 'sell')),
+            'protection': list(detect_parameters(cls, 'protection')),
         }
         params.update({
             'count': len(params['buy'] + params['sell'] + params['protection'])
@@ -159,7 +141,7 @@ def _ft_load_params(self, params: Dict, space: str, hyperopt: bool = False) -> N
             logger.info(f"No params for {space} found, using default values.")
         param_container: List[BaseParameter] = getattr(self, f"ft_{space}_params")
 
-        for attr_name, attr in self.detect_parameters(space):
+        for attr_name, attr in detect_parameters(self, space):
             attr.name = attr_name
             attr.in_space = hyperopt and HyperoptTools.has_space(self.config, space)
             if not attr.category:
@@ -190,3 +172,25 @@ def get_no_optimize_params(self):
             if not p.optimize or not p.in_space:
                 params[p.category][name] = p.value
         return params
+
+
+def detect_parameters(
+        obj: Union[HyperStrategyMixin, Type[HyperStrategyMixin]],
+        category: str
+        ) -> Iterator[Tuple[str, BaseParameter]]:
+    """
+    Detect all parameters for 'category' for "obj"
+    :param obj: Strategy object or class
+    :param category: category - usually `'buy', 'sell', 'protection',...
+    """
+    for attr_name in dir(obj):
+        if not attr_name.startswith('__'):  # Ignore internals, not strictly necessary.
+            attr = getattr(obj, attr_name)
+            if issubclass(attr.__class__, BaseParameter):
+                if (attr_name.startswith(category + '_')
+                        and attr.category is not None and attr.category != category):
+                    raise OperationalException(
+                        f'Inconclusive parameter name {attr_name}, category: {attr.category}.')
+                if (category == attr.category or
+                        (attr_name.startswith(category + '_') and attr.category is None)):
+                    yield attr_name, attr
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -14,6 +14,7 @@
 from freqtrade.data.dataprovider import DataProvider
 from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType,
                              SignalType, TradingMode)
+from freqtrade.enums.runmode import RunMode
 from freqtrade.exceptions import OperationalException, StrategyError
 from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date, timeframe_to_seconds
 from freqtrade.persistence import Order, PairLocks, Trade
@@ -151,6 +152,8 @@ def ft_bot_start(self, **kwargs) -> None:
         """
         strategy_safe_wrapper(self.bot_start)()
 
+        self.ft_load_hyper_params(self.config.get('runmode') == RunMode.HYPEROPT)
+
     @abstractmethod
     def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
         """


Test patch:
diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -509,7 +509,6 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
     hyperopt.min_date = Arrow(2017, 12, 10)
     hyperopt.max_date = Arrow(2017, 12, 13)
     hyperopt.init_spaces()
-    hyperopt.dimensions = hyperopt.dimensions
     generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
     assert generate_optimizer_value == response_expected
 
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
@@ -27,7 +27,6 @@ class HyperoptableStrategy(StrategyTestV2):
         'sell_minusdi': 0.4
     }
 
-    buy_rsi = IntParameter([0, 50], default=30, space='buy')
     buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
     sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
     sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
@@ -45,6 +44,12 @@ def protections(self):
             })
         return prot
 
+    def bot_start(self, **kwargs) -> None:
+        """
+        Parameters can also be defined here ...
+        """
+        self.buy_rsi = IntParameter([0, 50], default=30, space='buy')
+
     def informative_pairs(self):
         """
         Define additional, informative pair/interval combinations to be cached from the exchange.
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -16,6 +16,7 @@
 from freqtrade.optimize.space import SKDecimal
 from freqtrade.persistence import PairLocks, Trade
 from freqtrade.resolvers import StrategyResolver
+from freqtrade.strategy.hyper import detect_parameters
 from freqtrade.strategy.parameters import (BaseParameter, BooleanParameter, CategoricalParameter,
                                            DecimalParameter, IntParameter, RealParameter)
 from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
@@ -893,7 +894,7 @@ def test_auto_hyperopt_interface(default_conf):
     default_conf.update({'strategy': 'HyperoptableStrategy'})
     PairLocks.timeframe = default_conf['timeframe']
     strategy = StrategyResolver.load_strategy(default_conf)
-
+    strategy.ft_bot_start()
     with pytest.raises(OperationalException):
         next(strategy.enumerate_parameters('deadBeef'))
 
@@ -908,15 +909,18 @@ def test_auto_hyperopt_interface(default_conf):
     assert strategy.sell_minusdi.value == 0.5
     all_params = strategy.detect_all_parameters()
     assert isinstance(all_params, dict)
-    assert len(all_params['buy']) == 2
+    # Only one buy param at class level
+    assert len(all_params['buy']) == 1
+    # Running detect params at instance level reveals both parameters.
+    assert len(list(detect_parameters(strategy, 'buy'))) == 2
     assert len(all_params['sell']) == 2
     # Number of Hyperoptable parameters
-    assert all_params['count'] == 6
+    assert all_params['count'] == 5
 
     strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy')
 
     with pytest.raises(OperationalException, match=r"Inconclusive parameter.*"):
-        [x for x in strategy.detect_parameters('sell')]
+        [x for x in detect_parameters(strategy, 'sell')]
 
 
 def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog):