Repository: freqtrade/freqtrade
Pull request: Add IStrategy.custom_sell method which allows per-trade sell signal evaluation
Implementing `custom_sell()` in a strategy is conceptually equal to setting `sell` signal in dataframe.


closes #4276

Problem statement:
availability of trade profit while generating sell signals
Hi first. I am sorry for my english i know it's not good. I am 32 years old and my hobby is trading. Its a hobby because i won a lot more with my company on accountability. 

For the last 2 years  I was away from trading because I was studying an MBA. But now i finished and for the last 3 moth i was studying  freqtrade. My last bot was wiht Gekko.

Finished that little intro. I want to say Freqtrade has 2 problems for me:

1) I have 3 signal for buy and I want to use different strategy  for sell For example if i buy with the signal A i will use the logic C for sell

2) This is the worst problem: I need to control the sell signal with the profit.
For example if my profit of the current trade is >3% I will use the logic A, but if my profit is between 0 and 3% I will use a different strategy.
And if my profit an hour ago was negative i will use another strategy

1/2 its necessary to use on backtest (if you can't test the strategy you can't go to productive)

This is my see point . Freqtrade  is a great work !! But need to have more flexibility

Hints from issue discussion:
First of all, please pick a better title next time.
"freqtrade's worst problem" is not a valid title, and i'll simply close the next issue if you can't come up with something clearly identifying it's content.
it may be the worst problem for you, but it's really not a problem, but an enhancement request (and one we will probably not be able to fullfill).

---

what you're asking for is probably not possible without sacrificing the backtesting speed (which is however not an option, as it'll force us to remove hyperopt).

Hyperopt is ONLY made possible by having a quick (vectorized) backtesting.
This means indicators (and timeframes) are calculated across the whole timerange at once. obviously, you'll need to be careful with "lookahead" bias, but it's worth the tradeoff to backtest a year of 1m data in under 1 minute.

If you need to know the trade's profit while generating the sell signal, we'd be enforced to drop this method, and instead use a loop to generate the signals.
This would make backtesting VERY slow (basically unusable, at least for hyperopting) - and would therefore force us to remove hyperopt (which relies on running backtesting quickly).
As hyperopt is however a core feature, i don't think that's a route we'll go anytime soon, especially not to facilitate a request that's most likely not going to be used by 90% of the people, as it's to facilitate one random strategy (out of many).


for live / dry run, you can do this already by combining some of the methods [documented here](https://www.freqtrade.io/en/latest/strategy-customization/#additional-data-dataprovider) - but as you correctly say, that's not backtestable so you will need to rely on dry-runs to confirm your logic.


You might try to go with [custom stoploss](https://www.freqtrade.io/en/latest/strategy-advanced/#custom-stoploss) - which while not acting as sell signal, will still allow you to pull stoploss rather close, so it'll sell the next time profit drops below this.
Hi xmatthias, thanks for your response! First sorry for my title after my MBA i am bit more "salesman" :) 
Why you say only the 10% will use this feature? I don't understand who can put a signal sell without knowing your current profit. 

But I understand your point and I know Freqtrade  is a great product!

Is it possible to know on custom stop loss the trend or change on price? Why -> because for example I will sell my position if after 3h my profit is between 1 and 3% but ONLY if I am losing profit. If a 30 min ago I was -1% and now (3h) is between 1 / 3% I won't sell because the trend is up, and I can have the position a bit more.
I was wondering something similair.

An option in the backtest functionality to have it iterate over the data and feed it to the strategy like it would in dry-run or live mode would be very useful.

This is a basic strategy I came up with to do part of what aleeep was asking for: Buy, sell when there is profit, wait until price goes down to sell again, etc.
Because there are many variables and calls to the database this would be very slow to test, but having that option would be great. 

My suggestion is having a parameter in the backtest function to enable simple iterations. I would not mind having to wait an hour to test a month worth of data, it would still be very useful to be able to test spaghetti code like this, for people like me who don't know python or want to do crazy things without messing with the rest of the source code.


```

from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from functools import reduce
from pandas import DataFrame
from freqtrade.persistence import Trade
from datetime import datetime, date, timedelta


import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy  # noqa
import logging

logger = logging.getLogger(__name__)

class jeroen_test(IStrategy):

    # Minimal ROI designed for the strategy.
    minimal_roi = {
        "0": 0.02
    }

    order_types = {
        'buy': 'market',
        'sell': 'market',
        'stoploss': 'market',
        'stoploss_on_exchange': False
    }

    # Optimal stoploss designed for the strategy
    stoploss = -10

    # Optimal timeframe for the strategy
    timeframe = '1m'

    def calc_profit(self, price: float, current: float) -> float:
        fee = 1.0007
        profit = ((current*fee) -
            (price*fee))

        return float(f"{profit:.8f}")

    def calc_percentage_lower(self, price: float, current: float) -> float:
        fee = 1.0007
        price = price*fee
        current = current*fee
        lowerpercent = ((price-current)/(price*fee))*100

        return float(f"{lowerpercent:.8f}")

    def bot_loop_start(self, **kwargs) -> None:
        print(" ")

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """ Adds several different TA indicators to the given DataFrame
        """
        profit = False
        profit_percent = False
        percent_lower = False
        current_price = dataframe['close'].iloc[-1]

        dataframe['should_sell'] = False
        dataframe['should_buy'] = False

        # Get the previous trade
        trade = Trade.get_trades_proxy(is_open=False, pair=metadata['pair'])
        if trade:
            trade = trade[-1]
            lsp = trade.close_rate
            if lsp:
                percent_lower = self.calc_percentage_lower(price=lsp, current=current_price)
            # Found a bug? When force selling it doesnt close it
            else:
                lsp = trade.open_rate
                if lsp:
                    percent_lower = self.calc_percentage_lower(price=lsp, current=current_price)
        else:
            lsp = 0.00

        # Get the current Trade
        trade = Trade.get_trades_proxy(is_open=True, pair=metadata['pair'])
        if trade:
            trade = trade[-1]
            lbp = trade.open_rate
            open_trade = True
            profit = self.calc_profit(price=lbp, current=current_price)
            profit_percent = (profit/lbp)*100
        else:
            lbp = 0.00
            open_trade = False
            profit = False
            profit_percent = False


        print("------------")

        print("Last Sold For:", lsp)

        if open_trade:
            print("Bought for: ", lbp)
        print("Current Price: ", current_price)
        if profit:
            print("Current Profit: ", profit, " ", float(f"{profit_percent:.8f}"), "%")
        if percent_lower and not open_trade:
            print("Percent Lower: ", float(f"{percent_lower:.8f}"), "%")


        # Should we Sell?
        if profit_percent:
            if profit_percent > 1:
                dataframe['should_sell'] = True

        # Should we buy?
        if not open_trade:
            if (lsp == 0.00 ) & (lbp == 0.00):
                dataframe['should_buy'] = True
            # Is the percentage of what we sold for and the current price 2% lower
            if percent_lower > 2:
                dataframe['should_buy'] = True


        dataframe['last_sell_price'] = lsp
        dataframe['last_buy_price'] = lbp


        print("Current Dataframe:")
        print(dataframe.tail(1))


        return dataframe

    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        dataframe.loc[(
            # We have not bought or sold anything yet, lets buy!
            ((dataframe['last_sell_price'] == 0.00) & (dataframe['last_buy_price'] == 0.00) ) |
            (
                # Make sure the last selling price is higher than the current price
                ((dataframe['last_sell_price']) > dataframe['close']) &

                # Calculated earlier
                (dataframe['should_buy'] == True)
            )
            ), 'buy'
        ] = 1

        return dataframe

    def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        dataframe.loc[(
                # Make at least profit
                (dataframe['last_buy_price'] < (dataframe['close'])) &

                # Calculated earlier
                (dataframe['should_sell'] == True) &

                # If we have nothing we bought, there is nothing to sell
                (dataframe['last_buy_price'] > 0.00)
            ), 'sell'
        ] = 1

        return dataframe
```

Commit message: Fix mypy complaints.

Patch to review:
diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index 96c927965b0..f869a3c3a5b 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -42,32 +42,34 @@ class AwesomeStrategy(IStrategy):
 
 ***
 
-### Storing custom information using DatetimeIndex from `dataframe`
+## Custom sell signal
 
-Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
+It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade.
 
-```python
-import talib.abstract as ta
-class AwesomeStrategy(IStrategy):
-    # Create custom dictionary
-    custom_info = {}
+An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day:
 
-    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
-        # using "ATR" here as example
-        dataframe['atr'] = ta.ATR(dataframe)
-        if self.dp.runmode.value in ('backtest', 'hyperopt'):
-          # add indicator mapped to correct DatetimeIndex to custom_info
-          self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
-        return dataframe
+``` python
+class AwesomeStrategy(IStrategy):
+    def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
+                    current_profit: float, dataframe: Dataframe, **kwargs) -> bool:
+        trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze()
+
+        # Sell when price falls below value in stoploss column of taken buy signal.
+        if 'stop_loss' in trade_row:
+            if current_rate <= trade_row['stop_loss'] < trade.open_rate:
+                return 'stop_loss'
+
+        # Sell when price reaches value in take_profit column of taken buy signal.
+        if 'take_profit' in trade_row:
+            if trade.open_rate < trade_row['take_profit'] <= current_rate:
+                return 'take_profit'
+
+        # Sell any positions at a loss if they are helpd for more than two days.
+        if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1:
+            return 'unclog'
 ```
 
-!!! Warning
-    The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
-
-!!! Note
-    If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
-
-See `custom_stoploss` examples below on how to access the saved dataframe columns
+See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. 
 
 ## Custom stoploss
 
@@ -93,7 +95,8 @@ class AwesomeStrategy(IStrategy):
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
-                        current_rate: float, current_profit: float, **kwargs) -> float:
+                        current_rate: float, current_profit: float, dataframe: Dataframe,
+                        **kwargs) -> float:
         """
         Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
         e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -143,7 +146,8 @@ class AwesomeStrategy(IStrategy):
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
-                        current_rate: float, current_profit: float, **kwargs) -> float:
+                        current_rate: float, current_profit: float, dataframe: DataFrame,
+                        **kwargs) -> float:
 
         # Make sure you have the longest interval first - these conditions are evaluated from top to bottom.
         if current_time - timedelta(minutes=120) > trade.open_date_utc:
@@ -169,7 +173,8 @@ class AwesomeStrategy(IStrategy):
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
-                        current_rate: float, current_profit: float, **kwargs) -> float:
+                        current_rate: float, current_profit: float, dataframe: DataFrame,
+                        **kwargs) -> float:
 
         if pair in ('ETH/BTC', 'XRP/BTC'):
             return -0.10
@@ -195,7 +200,8 @@ class AwesomeStrategy(IStrategy):
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
-                        current_rate: float, current_profit: float, **kwargs) -> float:
+                        current_rate: float, current_profit: float, dataframe: DataFrame,
+                        **kwargs) -> float:
 
         if current_profit < 0.04:
             return -1 # return a value bigger than the inital stoploss to keep using the inital stoploss
@@ -235,7 +241,8 @@ class AwesomeStrategy(IStrategy):
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
-                        current_rate: float, current_profit: float, **kwargs) -> float:
+                        current_rate: float, current_profit: float, dataframe: DataFrame,
+                        **kwargs) -> float:
 
         # evaluate highest to lowest, so that highest possible stop is used
         if current_profit > 0.40:
@@ -252,14 +259,20 @@ class AwesomeStrategy(IStrategy):
 
 Imagine you want to use `custom_stoploss()` to use a trailing indicator like e.g. "ATR"
 
-See: "Storing custom information using DatetimeIndex from `dataframe`" example above) on how to store the indicator into `custom_info`
-
 !!! Warning
-    only use .iat[-1] in live mode, not in backtesting/hyperopt
-    otherwise you will look into the future
+    Only use `dataframe` values up until and including `current_time` value. Reading past
+    `current_time` you will look into the future, which will produce incorrect backtesting results
+    and throw an exception in dry/live runs.
     see [Common mistakes when developing strategies](strategy-customization.md#common-mistakes-when-developing-strategies) for more info.
 
+!!! Note
+    DataFrame is indexed by candle date. During dry/live runs `current_time` and
+    `trade.open_date_utc` will not match candle dates precisely and using them as indices will throw
+    an error. Use `date = timeframe_to_prev_date(self.timeframe, date)` to round a date to previous
+    candle before using it as a `dataframe` index.
+
 ``` python
+from freqtrade.exchange import timeframe_to_prev_date
 from freqtrade.persistence import Trade
 from freqtrade.state import RunMode
 
@@ -270,28 +283,19 @@ class AwesomeStrategy(IStrategy):
     use_custom_stoploss = True
 
     def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
-                        current_rate: float, current_profit: float, **kwargs) -> float:
+                        current_rate: float, current_profit: float, dataframe: DataFrame,
+                        **kwargs) -> float:
 
         result = 1
         if self.custom_info and pair in self.custom_info and trade:
-            # using current_time directly (like below) will only work in backtesting.
-            # so check "runmode" to make sure that it's only used in backtesting/hyperopt
-            if self.dp and self.dp.runmode.value in ('backtest', 'hyperopt'):
-              relative_sl = self.custom_info[pair].loc[current_time]['atr']
-            # in live / dry-run, it'll be really the current time
-            else:
-              # but we can just use the last entry from an already analyzed dataframe instead
-              dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair,
-                                                                       timeframe=self.timeframe)
-              # WARNING
-              # only use .iat[-1] in live mode, not in backtesting/hyperopt
-              # otherwise you will look into the future
-              # see: https://www.freqtrade.io/en/latest/strategy-customization/#common-mistakes-when-developing-strategies
-              relative_sl = dataframe['atr'].iat[-1]
-
-            if (relative_sl is not None):
+            # Using current_time directly would only work in backtesting. Live/dry runs need time to
+            # be rounded to previous candle to be used as dataframe index. Rounding must also be 
+            # applied to `trade.open_date(_utc)` if it is used for `dataframe` indexing.
+            current_time = timeframe_to_prev_date(self.timeframe, current_time)
+            current_row = dataframe.loc[dataframe['date'] == current_time].squeeze()
+            if 'atr' in current_row:
                 # new stoploss relative to current_rate
-                new_stoploss = (current_rate-relative_sl)/current_rate
+                new_stoploss = (current_rate - current_row['atr']) / current_rate
                 # turn into relative negative offset required by `custom_stoploss` return implementation
                 result = new_stoploss - 1
 
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index ceb822472ad..6dcd920c74d 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -11,6 +11,7 @@
 
 import arrow
 from cachetools import TTLCache
+from pandas import DataFrame
 
 from freqtrade import __version__, constants
 from freqtrade.configuration import validate_config_consistency
@@ -28,7 +29,7 @@
 from freqtrade.resolvers import ExchangeResolver, StrategyResolver
 from freqtrade.rpc import RPCManager, RPCMessageType
 from freqtrade.state import State
-from freqtrade.strategy.interface import IStrategy, SellType
+from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
 from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
 from freqtrade.wallets import Wallets
 
@@ -783,10 +784,10 @@ def handle_trade(self, trade: Trade) -> bool:
 
         config_ask_strategy = self.config.get('ask_strategy', {})
 
+        analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
+                                                                  self.strategy.timeframe)
         if (config_ask_strategy.get('use_sell_signal', True) or
                 config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
-            analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
-                                                                      self.strategy.timeframe)
 
             (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
 
@@ -813,13 +814,13 @@ def handle_trade(self, trade: Trade) -> bool:
                 # resulting in outdated RPC messages
                 self._sell_rate_cache[trade.pair] = sell_rate
 
-                if self._check_and_execute_sell(trade, sell_rate, buy, sell):
+                if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell):
                     return True
 
         else:
             logger.debug('checking sell')
             sell_rate = self.get_sell_rate(trade.pair, True)
-            if self._check_and_execute_sell(trade, sell_rate, buy, sell):
+            if self._check_and_execute_sell(analyzed_df, trade, sell_rate, buy, sell):
                 return True
 
         logger.debug('Found no sell signal for %s.', trade)
@@ -850,7 +851,8 @@ def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
             trade.stoploss_order_id = None
             logger.error(f'Unable to place a stoploss order on exchange. {e}')
             logger.warning('Selling the trade forcefully')
-            self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
+            self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
+                sell_type=SellType.EMERGENCY_SELL))
 
         except ExchangeError:
             trade.stoploss_order_id = None
@@ -949,19 +951,19 @@ def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> Non
                     logger.warning(f"Could not create trailing stoploss order "
                                    f"for pair {trade.pair}.")
 
-    def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
+    def _check_and_execute_sell(self, dataframe: DataFrame, trade: Trade, sell_rate: float,
                                 buy: bool, sell: bool) -> bool:
         """
         Check and execute sell
         """
         should_sell = self.strategy.should_sell(
-            trade, sell_rate, datetime.now(timezone.utc), buy, sell,
+            dataframe, trade, sell_rate, datetime.now(timezone.utc), buy, sell,
             force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
         )
 
         if should_sell.sell_flag:
             logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
-            self.execute_sell(trade, sell_rate, should_sell.sell_type)
+            self.execute_sell(trade, sell_rate, should_sell)
             return True
         return False
 
@@ -1150,16 +1152,18 @@ def _safe_sell_amount(self, pair: str, amount: float) -> float:
             raise DependencyException(
                 f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
 
-    def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool:
+    def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
         """
         Executes a limit sell for the given trade and limit
         :param trade: Trade instance
         :param limit: limit rate for the sell order
-        :param sellreason: Reason the sell was triggered
+        :param sell_reason: Reason the sell was triggered
+        :param custom_reason: A custom sell reason. Provided only if
+        sell_reason == SellType.CUSTOM_SELL,
         :return: True if it succeeds (supported) False (not supported)
         """
         sell_type = 'sell'
-        if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
+        if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
             sell_type = 'stoploss'
 
         # if stoploss is on exchange and we are on dry_run mode,
@@ -1176,10 +1180,10 @@ def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> boo
                 logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
 
         order_type = self.strategy.order_types[sell_type]
-        if sell_reason == SellType.EMERGENCY_SELL:
+        if sell_reason.sell_type == SellType.EMERGENCY_SELL:
             # Emergency sells (default to market!)
             order_type = self.strategy.order_types.get("emergencysell", "market")
-        if sell_reason == SellType.FORCE_SELL:
+        if sell_reason.sell_type == SellType.FORCE_SELL:
             # Force sells (default to the sell_type defined in the strategy,
             # but we allow this value to be changed)
             order_type = self.strategy.order_types.get("forcesell", order_type)
@@ -1190,7 +1194,7 @@ def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> boo
         if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
                 pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
                 time_in_force=time_in_force,
-                sell_reason=sell_reason.value):
+                sell_reason=sell_reason.sell_reason):
             logger.info(f"User requested abortion of selling {trade.pair}")
             return False
 
@@ -1213,7 +1217,7 @@ def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> boo
         trade.open_order_id = order['id']
         trade.sell_order_status = ''
         trade.close_rate_requested = limit
-        trade.sell_reason = sell_reason.value
+        trade.sell_reason = sell_reason.sell_reason
         # In case of market sell orders the order can be closed immediately
         if order.get('status', 'unknown') == 'closed':
             self.update_trade_state(trade, trade.open_order_id, order)
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index fb9826a238c..88957bafb0c 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -247,15 +247,16 @@ def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTup
         else:
             return sell_row[OPEN_IDX]
 
-    def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
+    def _get_sell_trade_entry(self, dataframe: DataFrame, trade: LocalTrade,
+                              sell_row: Tuple) -> Optional[LocalTrade]:
 
-        sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX],  # type: ignore
+        sell = self.strategy.should_sell(dataframe, trade, sell_row[OPEN_IDX],  # type: ignore
                                          sell_row[DATE_IDX], sell_row[BUY_IDX], sell_row[SELL_IDX],
                                          low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
 
         if sell.sell_flag:
             trade.close_date = sell_row[DATE_IDX]
-            trade.sell_reason = sell.sell_type.value
+            trade.sell_reason = sell.sell_reason
             trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
             closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
 
@@ -265,7 +266,7 @@ def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[
                     pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
                     rate=closerate,
                     time_in_force=time_in_force,
-                    sell_reason=sell.sell_type.value):
+                    sell_reason=sell.sell_reason):
                 return None
 
             trade.close(closerate, show_msg=False)
@@ -396,7 +397,7 @@ def backtest(self, processed: Dict,
 
                 for trade in open_trades[pair]:
                     # also check the buying candle for sell conditions.
-                    trade_entry = self._get_sell_trade_entry(trade, row)
+                    trade_entry = self._get_sell_trade_entry(processed[pair], trade, row)
                     # Sell occured
                     if trade_entry:
                         # logger.debug(f"{pair} - Backtesting sell {trade}")
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 4a890758285..fd97ad7d4a5 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -24,7 +24,7 @@
 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
 from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
 from freqtrade.state import State
-from freqtrade.strategy.interface import SellType
+from freqtrade.strategy.interface import SellCheckTuple, SellType
 
 
 logger = logging.getLogger(__name__)
@@ -554,7 +554,8 @@ def _exec_forcesell(trade: Trade) -> None:
             if not fully_canceled:
                 # Get current rate and execute sell
                 current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
-                self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
+                sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
+                self._freqtrade.execute_sell(trade, current_rate, sell_reason)
         # ---- EOF def _exec_forcesell ----
 
         if self._freqtrade.state != State.RUNNING:
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 54c7f2353ad..503a587ea0f 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -7,7 +7,7 @@
 from abc import ABC, abstractmethod
 from datetime import datetime, timedelta, timezone
 from enum import Enum
-from typing import Dict, List, NamedTuple, Optional, Tuple
+from typing import Dict, List, Optional, Tuple, Union
 
 import arrow
 from pandas import DataFrame
@@ -24,6 +24,7 @@
 
 
 logger = logging.getLogger(__name__)
+CUSTOM_SELL_MAX_LENGTH = 64
 
 
 class SignalType(Enum):
@@ -45,6 +46,7 @@ class SellType(Enum):
     SELL_SIGNAL = "sell_signal"
     FORCE_SELL = "force_sell"
     EMERGENCY_SELL = "emergency_sell"
+    CUSTOM_SELL = "custom_sell"
     NONE = ""
 
     def __str__(self):
@@ -52,12 +54,20 @@ def __str__(self):
         return self.value
 
 
-class SellCheckTuple(NamedTuple):
+class SellCheckTuple(object):
     """
     NamedTuple for Sell type + reason
     """
-    sell_flag: bool
     sell_type: SellType
+    sell_reason: str = ''
+
+    def __init__(self, sell_type: SellType, sell_reason: str = ''):
+        self.sell_type = sell_type
+        self.sell_reason = sell_reason or sell_type.value
+
+    @property
+    def sell_flag(self):
+        return self.sell_type != SellType.NONE
 
 
 class IStrategy(ABC, HyperStrategyMixin):
@@ -264,7 +274,7 @@ def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: f
         return True
 
     def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
-                        current_profit: float, **kwargs) -> float:
+                        current_profit: float, dataframe: DataFrame, **kwargs) -> float:
         """
         Custom stoploss logic, returning the new distance relative to current_rate (as ratio).
         e.g. returning -0.05 would create a stoploss 5% below current_rate.
@@ -285,6 +295,31 @@ def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, curre
         """
         return self.stoploss
 
+    def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
+                    current_profit: float, dataframe: DataFrame,
+                    **kwargs) -> Optional[Union[str, bool]]:
+        """
+        Custom sell signal logic indicating that specified position should be sold. Returning a
+        string or True from this method is equal to setting sell signal on a candle at specified
+        time. This method is not called when sell signal is set.
+
+        This method should be overridden to create sell signals that depend on trade parameters. For
+        example you could implement a stoploss relative to candle when trade was opened, or a custom
+        1:2 risk-reward ROI.
+
+        Custom sell reason max length is 64. Exceeding this limit will raise OperationalException.
+
+        :param pair: Pair that's currently analyzed
+        :param trade: trade object.
+        :param current_time: datetime object, containing the current datetime
+        :param current_rate: Rate, calculated based on pricing settings in ask_strategy.
+        :param current_profit: Current profit (as ratio), calculated based on current_rate.
+        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
+        :return: To execute sell, return a string with custom sell reason or True. Otherwise return
+        None or False.
+        """
+        return None
+
     def informative_pairs(self) -> ListPairsWithTimeframes:
         """
         Define additional, informative pair/interval combinations to be cached from the exchange.
@@ -500,8 +535,8 @@ def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
         else:
             return False
 
-    def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
-                    sell: bool, low: float = None, high: float = None,
+    def should_sell(self, dataframe: DataFrame, trade: Trade, rate: float, date: datetime,
+                    buy: bool, sell: bool, low: float = None, high: float = None,
                     force_stoploss: float = 0) -> SellCheckTuple:
         """
         This function evaluates if one of the conditions required to trigger a sell
@@ -517,8 +552,9 @@ def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
 
         trade.adjust_min_max_rates(high or current_rate)
 
-        stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
-                                              current_time=date, current_profit=current_profit,
+        stoplossflag = self.stop_loss_reached(dataframe=dataframe, current_rate=current_rate,
+                                              trade=trade, current_time=date,
+                                              current_profit=current_profit,
                                               force_stoploss=force_stoploss, high=high)
 
         # Set current rate to high for backtesting sell
@@ -531,12 +567,28 @@ def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
                        and self.min_roi_reached(trade=trade, current_profit=current_profit,
                                                 current_time=date))
 
+        sell_signal = SellType.NONE
+        custom_reason = ''
         if (ask_strategy.get('sell_profit_only', False)
                 and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
             # sell_profit_only and profit doesn't reach the offset - ignore sell signal
-            sell_signal = False
-        else:
-            sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)
+            pass
+        elif ask_strategy.get('use_sell_signal', True) and not buy:
+            if sell:
+                sell_signal = SellType.SELL_SIGNAL
+            else:
+                custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
+                    trade.pair, trade, date, current_rate, current_profit, dataframe)
+                if custom_reason:
+                    sell_signal = SellType.CUSTOM_SELL
+                    if isinstance(custom_reason, str):
+                        if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
+                            logger.warning(f'Custom sell reason returned from custom_sell is too '
+                                           f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} '
+                                           f'characters.')
+                            custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
+                    else:
+                        custom_reason = None
             # TODO: return here if sell-signal should be favored over ROI
 
         # Start evaluations
@@ -545,26 +597,27 @@ def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
         # Sell-signal
         # Stoploss
         if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
-            logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
+            logger.debug(f"{trade.pair} - Required profit reached. "
                          f"sell_type=SellType.ROI")
-            return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
+            return SellCheckTuple(sell_type=SellType.ROI)
 
-        if sell_signal:
-            logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
-                         f"sell_type=SellType.SELL_SIGNAL")
-            return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
+        if sell_signal != SellType.NONE:
+            logger.debug(f"{trade.pair} - Sell signal received. "
+                         f"sell_type=SellType.{sell_signal.name}" +
+                         (f", custom_reason={custom_reason}" if custom_reason else ""))
+            return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
 
         if stoplossflag.sell_flag:
 
-            logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
+            logger.debug(f"{trade.pair} - Stoploss hit. "
                          f"sell_type={stoplossflag.sell_type}")
             return stoplossflag
 
         # This one is noisy, commented out...
-        # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
-        return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
+        # logger.debug(f"{trade.pair} - No sell signal.")
+        return SellCheckTuple(sell_type=SellType.NONE)
 
-    def stop_loss_reached(self, current_rate: float, trade: Trade,
+    def stop_loss_reached(self, dataframe: DataFrame, current_rate: float, trade: Trade,
                           current_time: datetime, current_profit: float,
                           force_stoploss: float, high: float = None) -> SellCheckTuple:
         """
@@ -582,7 +635,8 @@ def stop_loss_reached(self, current_rate: float, trade: Trade,
                                                     )(pair=trade.pair, trade=trade,
                                                       current_time=current_time,
                                                       current_rate=current_rate,
-                                                      current_profit=current_profit)
+                                                      current_profit=current_profit,
+                                                      dataframe=dataframe)
             # Sanity check - error cases will return None
             if stop_loss_value:
                 # logger.info(f"{trade.pair} {stop_loss_value=} {current_profit=}")
@@ -626,9 +680,9 @@ def stop_loss_reached(self, current_rate: float, trade: Trade,
                 logger.debug(f"{trade.pair} - Trailing stop saved "
                              f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
 
-            return SellCheckTuple(sell_flag=True, sell_type=sell_type)
+            return SellCheckTuple(sell_type=sell_type)
 
-        return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
+        return SellCheckTuple(sell_type=SellType.NONE)
 
     def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
         """
diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py
index ec7b3c33d32..a8862e9c904 100644
--- a/tests/strategy/test_default_strategy.py
+++ b/tests/strategy/test_default_strategy.py
@@ -41,4 +41,5 @@ def test_default_strategy(result, fee):
                                        rate=20000, time_in_force='gtc', sell_reason='roi') is True
 
     assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(),
-                                    current_rate=20_000, current_profit=0.05) == strategy.stoploss
+                                    current_rate=20_000, current_profit=0.05, dataframe=None
+                                    ) == strategy.stoploss
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
index 347d35b19af..d2a09e46653 100644
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -360,7 +360,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
     now = arrow.utcnow().datetime
     sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade,
                                          current_time=now, current_profit=profit,
-                                         force_stoploss=0, high=None)
+                                         force_stoploss=0, high=None, dataframe=None)
     assert isinstance(sl_flag, SellCheckTuple)
     assert sl_flag.sell_type == expected
     if expected == SellType.NONE:
@@ -371,7 +371,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
 
     sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade,
                                          current_time=now, current_profit=profit2,
-                                         force_stoploss=0, high=None)
+                                         force_stoploss=0, high=None, dataframe=None)
     assert sl_flag.sell_type == expected2
     if expected2 == SellType.NONE:
         assert sl_flag.sell_flag is False
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index ad000515ec6..7a2f6a1edd9 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1973,7 +1973,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
     # if ROI is reached we must sell
     patch_get_signal(freqtrade, value=(False, True))
     assert freqtrade.handle_trade(trade)
-    assert log_has("ETH/BTC - Required profit reached. sell_flag=True, sell_type=SellType.ROI",
+    assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI",
                    caplog)
 
 
@@ -2002,7 +2002,7 @@ def test_handle_trade_use_sell_signal(
 
     patch_get_signal(freqtrade, value=(False, True))
     assert freqtrade.handle_trade(trade)
-    assert log_has("ETH/BTC - Sell signal received. sell_flag=True, sell_type=SellType.SELL_SIGNAL",
+    assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL",
                    caplog)
 
 
@@ -2606,14 +2606,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
         fetch_ticker=ticker_sell_up
     )
     # Prevented sell ...
-    freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
+    freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
+                           sell_reason=SellCheckTuple(sell_type=SellType.ROI))
     assert rpc_mock.call_count == 0
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
 
     # Repatch with true
     freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
 
-    freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
+    freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
+                           sell_reason=SellCheckTuple(sell_type=SellType.ROI))
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
 
     assert rpc_mock.call_count == 1
@@ -2665,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
     )
 
     freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
-                           sell_reason=SellType.STOP_LOSS)
+                           sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     assert rpc_mock.call_count == 2
     last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -2722,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
 
     trade.stop_loss = 0.00001099 * 0.99
     freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
-                           sell_reason=SellType.STOP_LOSS)
+                           sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     assert rpc_mock.call_count == 2
     last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -2774,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
     trade.stoploss_order_id = "abcd"
 
     freqtrade.execute_sell(trade=trade, limit=1234,
-                           sell_reason=SellType.STOP_LOSS)
+                           sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
     assert sellmock.call_count == 1
     assert log_has('Could not cancel stoploss order abcd', caplog)
 
@@ -2824,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
     )
 
     freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
-                           sell_reason=SellType.SELL_SIGNAL)
+                           sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     trade = Trade.query.first()
     assert trade
@@ -2929,7 +2931,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
     )
     freqtrade.config['order_types']['sell'] = 'market'
 
-    freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
+    freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
+                           sell_reason=SellCheckTuple(sell_type=SellType.ROI))
 
     assert not trade.is_open
     assert trade.close_profit == 0.0620716
@@ -2983,8 +2986,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee,
         fetch_ticker=ticker_sell_up
     )
 
+    sell_reason = SellCheckTuple(sell_type=SellType.ROI)
     assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'],
-                                      sell_reason=SellType.ROI)
+                                      sell_reason=sell_reason)
     assert mock_insuf.call_count == 1
 
 
@@ -3077,7 +3081,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o
     freqtrade = FreqtradeBot(default_conf)
     patch_get_signal(freqtrade)
     freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
-        sell_flag=False, sell_type=SellType.NONE))
+        sell_type=SellType.NONE))
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
@@ -3226,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
     )
 
     freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'],
-                           sell_reason=SellType.STOP_LOSS)
+                           sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
     trade.close(ticker_sell_down()['bid'])
     assert freqtrade.strategy.is_pair_locked(trade.pair)
 
diff --git a/tests/test_integration.py b/tests/test_integration.py
index be0dd1137ce..2179109613a 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -51,8 +51,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
         side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
     # Sell 3rd trade (not called for the first trade)
     should_sell_mock = MagicMock(side_effect=[
-        SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
-        SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
+        SellCheckTuple(sell_type=SellType.NONE),
+        SellCheckTuple(sell_type=SellType.SELL_SIGNAL)]
     )
     cancel_order_mock = MagicMock()
     mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
@@ -156,11 +156,11 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc
         _notify_sell=MagicMock(),
     )
     should_sell_mock = MagicMock(side_effect=[
-        SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
-        SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL),
-        SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
-        SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
-        SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)]
+        SellCheckTuple(sell_type=SellType.NONE),
+        SellCheckTuple(sell_type=SellType.SELL_SIGNAL),
+        SellCheckTuple(sell_type=SellType.NONE),
+        SellCheckTuple(sell_type=SellType.NONE),
+        SellCheckTuple(sell_type=SellType.NONE)]
     )
     mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
 


Code review comments:
diff_hunk: @@ -42,32 +42,34 @@ class AwesomeStrategy(IStrategy):
 
 ***
 
-### Storing custom information using DatetimeIndex from `dataframe`
+## Custom sell signal
 
-Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
+It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade.
 
-```python
-import talib.abstract as ta
-class AwesomeStrategy(IStrategy):
-    # Create custom dictionary
-    custom_info = {}
+An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day:
 
-    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
-        # using "ATR" here as example
-        dataframe['atr'] = ta.ATR(dataframe)
-        if self.dp.runmode.value in ('backtest', 'hyperopt'):
-          # add indicator mapped to correct DatetimeIndex to custom_info
-          self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
-        return dataframe
+``` python
+class AwesomeStrategy(IStrategy):
+    def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
+                    current_profit: float, dataframe: Dataframe, **kwargs) -> bool:
+        trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze()
+
+        # Sell when price falls below value in stoploss column of taken buy signal.
+        if 'stop_loss' in trade_row:
+            if current_rate <= trade_row['stop_loss'] < trade.open_rate:
+                return 'stop_loss'
path: docs/strategy-advanced.md
text: @user1:
I find variable / column naming here quite confusing. 

using "stop_loss" somehow indicates that it has something to do with stoploss - but without explaining what, really.
maybe using atr (as in the stoploss example further down) would make more sense?

Also, by using stop_loss, it'll collide in the sell-reason list with the "real" stoploss


@author:
Yeah its just what i lifted from my experiments strategy. Might not be the best thing to include in docs. However this kind of usage made me think about it from a bit different angle.

I find it _extremely_ convenient to have absolute stoploss and take-profit (or roi) values in dataframe, having them calculated for each buy signal. This would be extremely handy for less-technical users. This does not work well with backtesting however. Might be a feature worthy of another PR.

@user1:
i'm not sure why / what would not work well for backtesting? 

@author:
Backtester has no notion of stoploss or roi from these functions. What we do is basically emit a sell signal, which follows sell signal rules.

> Sell-signal sells happen at open-price of the consecutive candle

So suppose we have a current in-progress candle that reaches take-profit target, but then falls down and closes below. Since a wick touched our take-profit target a dry run would try to sell that price (correct me if im wrong here). But in backtest sell signal would not even happen because candle closed below take-profit target. Same is true for stoploss. Actually `custom_sell` for stoploss seems like a very bad idea. I think we should purge it from docs. It is both inaccurate and too slow (no stoploss order placed on exchange).

@user1:
well yes, sell-signal and stop / trailing stoploss do serve a different purpose for sure, while the custom_stop function allows you to trail the current price (avoid large pullbacks / sells on large pullbacks) - also with stoploss on exchange in place - custom-sell will definitely not allow this (it's also not the purpose of this function).

you can however still have multiple different sell-reasons - like - you "notice" that the pattern you entered on does not play out - so you bail out even at a loss (avoiding a greater loss via Stoploss).

> So suppose we have a current in-progress candle that reaches take-profit target, but then falls down and closes below.

This is an assumption about movements backtesting does not know about - maybe it reached the take-profit target first, then fell - but maybe it fell first, and then reached the take-profit target (you'd be out of the trade in dry-run in that case too).
Obviously it's an assumption backtesting makes (and is documented), and it's a totally different argument if we assume "price went to high first" or "price went to low first" (green / red candles will not determine this ...)


@author:
I am not having any assumptions where price went first though. Consider the situation where wick touches take-profit target, but candle does not hit stoploss. It does not matter which way price went first because if it went down - nothing would happen anyway, but if it went up - we can take profit. There is no issue with backtesting assumptions at all, its just that such columns could act more like roi and stoploss instead of sell signal.

@user1:
i'm ... not quite certain what you're referring to with this (probably we should discuss this in chat, as i don't THINK it's related to this PR directly).

If ROI happens (minimal ROI) - then the ROI value is assumed as sell price - not high or low or open.
it's similar with stoploss ... so this column (virtually) exists already ...

@user1:
I do agree with you though - i think this example should be ... more generic - at least it should not use stoploss / take_profit directly, as that might be missleading.

@user1:
I've updated the sample now to not use take-profit / stoploss, but to simply use different indicators.

I'm sure others will come up with additional / better samples ... but i think this will do for now.

diff_hunk: @@ -42,32 +42,34 @@ class AwesomeStrategy(IStrategy):
 
 ***
 
-### Storing custom information using DatetimeIndex from `dataframe`
+## Custom sell signal
 
-Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
+It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade.
 
-```python
-import talib.abstract as ta
-class AwesomeStrategy(IStrategy):
-    # Create custom dictionary
-    custom_info = {}
+An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day:
 
-    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
-        # using "ATR" here as example
-        dataframe['atr'] = ta.ATR(dataframe)
-        if self.dp.runmode.value in ('backtest', 'hyperopt'):
-          # add indicator mapped to correct DatetimeIndex to custom_info
-          self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
-        return dataframe
+``` python
+class AwesomeStrategy(IStrategy):
+    def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
+                    current_profit: float, dataframe: Dataframe, **kwargs) -> bool:
+        trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze()
+
+        # Sell when price falls below value in stoploss column of taken buy signal.
+        if 'stop_loss' in trade_row:
+            if current_rate <= trade_row['stop_loss'] < trade.open_rate:
+                return 'stop_loss'
+
+        # Sell when price reaches value in take_profit column of taken buy signal.
+        if 'take_profit' in trade_row:
+            if trade.open_rate < trade_row['take_profit'] <= current_rate:
+                return 'take_profit'
+
+        # Sell any positions at a loss if they are helpd for more than two days.
+        if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1:
+            return 'unclog'
 ```
 
-!!! Warning
-    The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash.
-
-!!! Note
-    If the data is pair-specific, make sure to use pair as one of the keys in the dictionary.
-
-See `custom_stoploss` examples below on how to access the saved dataframe columns
+See [Custom stoploss using an indicator from dataframe example](strategy-customization.md#custom-stoploss-using-an-indicator-from-dataframe-example) for explanation on how to use `dataframe` parameter. 
path: docs/strategy-advanced.md
text: @user1:
Where's that link supposed to point to? it doesn't work / does not point to the proper anchor in the strategy page ...



@author:
Oopsie. I was trying to link to custom stoploss with indicators example since it has more details on `dataframe` usage.

diff_hunk: @@ -42,32 +42,34 @@ class AwesomeStrategy(IStrategy):
 
 ***
 
-### Storing custom information using DatetimeIndex from `dataframe`
+## Custom sell signal
 
-Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
+It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade.
 
-```python
-import talib.abstract as ta
-class AwesomeStrategy(IStrategy):
-    # Create custom dictionary
-    custom_info = {}
+An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day:
 
-    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
-        # using "ATR" here as example
-        dataframe['atr'] = ta.ATR(dataframe)
-        if self.dp.runmode.value in ('backtest', 'hyperopt'):
-          # add indicator mapped to correct DatetimeIndex to custom_info
-          self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
-        return dataframe
+``` python
+class AwesomeStrategy(IStrategy):
+    def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
+                    current_profit: float, dataframe: Dataframe, **kwargs) -> bool:
+        trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze()
+
+        # Sell when price falls below value in stoploss column of taken buy signal.
+        if 'stop_loss' in trade_row:
+            if current_rate <= trade_row['stop_loss'] < trade.open_rate:
+                return 'stop_loss'
+
+        # Sell when price reaches value in take_profit column of taken buy signal.
+        if 'take_profit' in trade_row:
+            if trade.open_rate < trade_row['take_profit'] <= current_rate:
+                return 'take_profit'
+
+        # Sell any positions at a loss if they are helpd for more than two days.
+        if current_profit < 0 and (current_time.replace(tzinfo=trade.open_date_utc.tzinfo) - trade.open_date_utc).days >= 1:
path: docs/strategy-advanced.md
text: @user1:
under what circumstance did you get current_time without utc timezone? (backtest / dry / unit tests)

Having it here indicates that you ran into it - which would point to a bug as this date should ALWAYS have utc timezone assigned.

@author:
Hyperopt or backtesting, im not 100% sure. However these dates were UTC, but tzinfo objects had different types (even though both said 'UTC'), so it threw an exception.

diff_hunk: @@ -42,32 +42,34 @@ class AwesomeStrategy(IStrategy):
 
 ***
 
-### Storing custom information using DatetimeIndex from `dataframe`
+## Custom sell signal
 
-Imagine you need to store an indicator like `ATR` or `RSI` into `custom_info`. To use this in a meaningful way, you will not only need the raw data of the indicator, but probably also need to keep the right timestamps.
+It is possible to define custom sell signals. This is very useful when we need to customize sell conditions for each individual trade.
 
-```python
-import talib.abstract as ta
-class AwesomeStrategy(IStrategy):
-    # Create custom dictionary
-    custom_info = {}
+An example of how we can set stop-loss and take-profit targets in the dataframe and also sell trades that were open longer than 1 day:
 
-    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
-        # using "ATR" here as example
-        dataframe['atr'] = ta.ATR(dataframe)
-        if self.dp.runmode.value in ('backtest', 'hyperopt'):
-          # add indicator mapped to correct DatetimeIndex to custom_info
-          self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
-        return dataframe
+``` python
+class AwesomeStrategy(IStrategy):
+    def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
+                    current_profit: float, dataframe: Dataframe, **kwargs) -> bool:
+        trade_row = dataframe.loc[dataframe['date'] == timeframe_to_prev_date(trade.open_date_utc)].squeeze()
path: docs/strategy-advanced.md
text: @user1:
we should check that trade_row is not empty.

in the (most probably unlikely) case that the trade was opened more than 500 (in reality ohlcv_limit, so most likely 1000 for binance) candles ago, you might get an empty row here.

i understand that can't happen due to the last condition - but this is docs - so people will probably copy/paste parts of it only.


@author:
What do you mean it cant happen due to last condition? You just said it can happen 👀 I did not think about old-enough trades.. Which makes me wonder if this is a robust solution at all. Old way (copying columns) also suffered from this but.. Be nice if we could 100% reliably access candle data around trades.

@user1:
> What do you mean it cant happen due to last condition

at the end of this function, you're selling if the pair is > 1 day - so it's unlikely that you'll not have enough data for this example if a user copies the whole function (Which i would not rely on ...).

> Which makes me wonder if this is a robust solution at all

guess why i am not a huge fan of this ... as there's a lot of "ifs" that you need to consider.

Using "more" data can be problematic - assume i use a slow trading strategy which keeps trades open for weeks (ignore timeframe for now ...)
The current "live" lookback is 1 call to the exchange's ohlcv / klines endpoint. going back 1 week on 5m candles would mean 2-3 calls - which will slow the data gathering down (by quite some time, if you need to do this for 20 open trades and 5 weeks old trades - and potentially result in DDos problems (considering that binance seems to tighten these limits lately ...) - which obviously can be resolved by using a "slower" ratelimit... 



@author:
> at the end of this function, you're selling if the pair is > 1 day - so it's unlikely that you'll not have enough data for this example if a user copies the whole function (Which i would not rely on ...).

It still can happen though. For example if bot was offline for a longer period of time and then was brought back online this would throw an exception for any unsold trades.

> Using "more" data can be problematic

I can hardly imagine where we would need more data than some candles around buy signal or trade open. It would probably cover 99% of usecases if data around those times is available, even if there is a hole between that time and latest candles.