Repository: freqtrade/freqtrade

Issue:
Trailing stop hyperopt invalid results
##  Describe your environment

  * Operating system: linux
  * Python Version: 3.7.5
  * Branch:  Develop
  * Last Commit ID: 8dd9b5c6fba973b98defc0f9a9e2e3c01af735dd
  
## Describe the problem:

Depending on the strategy, hyperopt may produce invalid results.

My trailing hyperopt just returned the following result.
However, configuring trailing_stop_positive > trailing_stop_positive_offset is not possible (we prevent this as part of config validation).

```
Stoploss:
{'stoploss': -0.34474}
Trailing stop:
{   'trailing_only_offset_is_reached': True,
    'trailing_stop': True,
    'trailing_stop_positive': 0.33186,
    'trailing_stop_positive_offset': 0.0542
}

```

Leaving out the irrationally high numbers my stoplosses reach... this configuration results in `The config trailing_stop_positive_offset needs to be greater than trailing_stop_positive in your config.` (also shown on slack).

### Further thoughts

Now i'm not 100% certain if the error is with hyperopt thinking about it.

The current validation we have in place ensures that "positive stoploss" cannot lead back into negative values. (5% offset and trailing_stop_positive at 10% could be a -5% sell).
However, I think it might be a valid case to say that a stoploss is -10% initially, and once the trade reaches 2% profit, it should be pulled tighter - to 5%. 
This could lead to -3% loss - but on the other hand it is not 10% loss anymore.







Discussion:
I can be worked around/fixed

What is the complete list of constraints on trailing stop parameters?

* `trailing_stop_positive_offset > trailing_stop_positive`, got it,
* `trailing_stop_positive > 0`, as I can see from configuration_validation. This is satisfied due to the min. boundary for the dimension for this parameter in the hyperopt space...
* any dependencies on the value of stoploss setting?

no - the 2 conditions within `config_validation` are the only ones i'm aware of.

as said - i'm not certain if the conditions make sense though ... also thinking about it - `trailing_stop_positive > 0` seems wrong - 
it's a stoploss so setting it to 0.05 will mean it trails by 5% - not that it's set to 5% profit - 
so -0.05 would make more sense.

stoploss is used as `-1 * abs(stoploss)` anyway since we need it to be negative - and we accept only negative values for `stoploss` - so this is a major inconsistency.

As for stoploss, stoploss should also be positive value in the config/strategy, and the needed sign is applied to it (minus for long trades, plus for shorts (in the future).
Because you say "stoploss is N points/pips", "stoploss is N percents", "stoploss is N USD", with no "minus" in that sentences...


Resolving patch:
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -177,8 +177,7 @@ def _get_params_details(self, params: Dict) -> Dict:
             result['stoploss'] = {p.name: params.get(p.name)
                                   for p in self.hyperopt_space('stoploss')}
         if self.has_space('trailing'):
-            result['trailing'] = {p.name: params.get(p.name)
-                                  for p in self.hyperopt_space('trailing')}
+            result['trailing'] = self.custom_hyperopt.generate_trailing_params(params)
 
         return result
 
@@ -353,13 +352,13 @@ def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict:
             self.backtesting.strategy.stoploss = params_dict['stoploss']
 
         if self.has_space('trailing'):
-            self.backtesting.strategy.trailing_stop = params_dict['trailing_stop']
-            self.backtesting.strategy.trailing_stop_positive = \
-                params_dict['trailing_stop_positive']
+            d = self.custom_hyperopt.generate_trailing_params(params_dict)
+            self.backtesting.strategy.trailing_stop = d['trailing_stop']
+            self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive']
             self.backtesting.strategy.trailing_stop_positive_offset = \
-                params_dict['trailing_stop_positive_offset']
+                d['trailing_stop_positive_offset']
             self.backtesting.strategy.trailing_only_offset_is_reached = \
-                params_dict['trailing_only_offset_is_reached']
+                d['trailing_only_offset_is_reached']
 
         processed = load(self.tickerdata_pickle)
 
diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py
--- a/freqtrade/optimize/hyperopt_interface.py
+++ b/freqtrade/optimize/hyperopt_interface.py
@@ -174,6 +174,19 @@ def stoploss_space() -> List[Dimension]:
             Real(-0.35, -0.02, name='stoploss'),
         ]
 
+    @staticmethod
+    def generate_trailing_params(params: Dict) -> Dict:
+        """
+        Create dict with trailing stop parameters.
+        """
+        return {
+            'trailing_stop': params['trailing_stop'],
+            'trailing_stop_positive': params['trailing_stop_positive'],
+            'trailing_stop_positive_offset': (params['trailing_stop_positive'] +
+                                              params['trailing_stop_positive_offset_p1']),
+            'trailing_only_offset_is_reached': params['trailing_only_offset_is_reached'],
+        }
+
     @staticmethod
     def trailing_space() -> List[Dimension]:
         """
@@ -190,8 +203,15 @@ def trailing_space() -> List[Dimension]:
             # other 'trailing' hyperspace parameters.
             Categorical([True], name='trailing_stop'),
 
-            Real(0.02, 0.35, name='trailing_stop_positive'),
-            Real(0.01, 0.1, name='trailing_stop_positive_offset'),
+            Real(0.01, 0.35, name='trailing_stop_positive'),
+
+            # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
+            # so this intermediate parameter is used as the value of the difference between
+            # them. The value of the 'trailing_stop_positive_offset' is constructed in the
+            # generate_trailing_params() method.
+            # # This is similar to the hyperspace dimensions used for constructing the ROI tables.
+            Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
+
             Categorical([True, False], name='trailing_only_offset_is_reached'),
         ]
 


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
@@ -636,7 +636,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
         'stoploss': -0.4,
         'trailing_stop': True,
         'trailing_stop_positive': 0.02,
-        'trailing_stop_positive_offset': 0.1,
+        'trailing_stop_positive_offset_p1': 0.05,
         'trailing_only_offset_is_reached': False,
     }
     response_expected = {
@@ -670,7 +670,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
                            'trailing': {'trailing_only_offset_is_reached': False,
                                         'trailing_stop': True,
                                         'trailing_stop_positive': 0.02,
-                                        'trailing_stop_positive_offset': 0.1}},
+                                        'trailing_stop_positive_offset': 0.07}},
         'params_dict': optimizer_param,
         'results_metrics': {'avg_profit': 2.3117,
                             'duration': 100.0,