Repository: freqtrade/freqtrade
Pull request: Buy order timeout callback
## Summary
Inspired by the discussions in #2857 - this is a first (very rough) draft of how a strategy interface to customize buy_timeouts could look like.

It's intentionally very simple - it returns False by default (so no impact to users not interrested in this) - however when a strategy implements this (overrides this function) - a user can suddenly very flexibly control how and when to cancel orders.

This may accidentally (on purpose?) also fix #2866 - as the exception message is changed and now clearly points to "strategy code caused this exception".


### Example:

* Cancel order after 3 minutes for pairs with value > 100 USDT
* cancel order after 20 minutes for pairs with value > 10USDT
* cancel order after 24h if value < 10USDT (keep order there).


```python
def check_buy_timeout(self, pair: str, trade: Trade, order: Dict, **kwargs) -> bool:
    if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
        return True
    elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
        return True
    elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
       return True
    return False
```


## Quick changelog

- Introduce generic wrapper for strategy functions (catching all errors users may cause)
- introduce `check_buy_timeout()` - which can be used to customize buy order timeouts
- introduce `check_sell_timeout()` - which can be used to customize sell order timeouts
- add `new-strategy --template advanced` - which populates these optional methods too. Additional methods can then be added easily.

## TODO
* [x] Create same for sell side
* [x] create tests
* [x] Test it actually works as expected
* [x] create documentation
  * [x] add example usage
  * [x] with a few "common" samples
    * [x] validate 2nd example works ... 
  * [x] add to full sample strategy

Problem statement:
Unable to analyze ticker for pair WRX/BTC: negative dimensions are not allowed
I shut down the boot and restarted it. I encountered this error message

```
2020-02-05 11:39:13,738 - VolumePairList - INFO - Searching 20 pairs: ['XRP/BTC', 'ICX/BTC', 'ETH/BTC', 'MATIC/BTC', 'ENJ/BTC', 'WRX/BTC', 'EOS/BTC', 'AION/BTC', 'TRX/BTC', 'LTC/BTC', 'ETC/BTC', 'ADA/BTC', 'XTZ/BTC', 'WAN/BTC', 'BAT/BTC', 'MTL/BTC', 'XLM/BTC', 'ATOM/BTC', 'LINK/BTC', 'RVN/BTC']
2020-02-05 11:39:13,750 - freqtrade.rpc.rpc_manager - INFO - Enabling rpc.telegram ...
2020-02-05 11:39:13,957 - freqtrade.rpc.telegram - INFO - rpc.telegram is listening for following commands: [['status'], ['profit'], ['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], ['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]
2020-02-05 11:39:13,978 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': status, 'status': 'running'}
2020-02-05 11:39:14,076 - freqtrade.worker - INFO - Changing state to: RUNNING
2020-02-05 11:39:14,077 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': warning, 'status': 'Dry run is enabled. All trades are simulated.'}
2020-02-05 11:39:14,117 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': custom, 'status': "*Exchange:* `binance`\n*Stake per trade:* `1 BTC`\n*Minimum ROI:* `{'0': 0.21}`\n*Trailing Stoploss:* `-0.2`\n*Ticker Interval:* `1h`\n*Strategy:* `strateji1`"}
2020-02-05 11:39:14,158 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': status, 'status': "Searching for BTC pairs to buy and sell based on [{'VolumePairList': 'VolumePairList - top 20 volume pairs.'}]"}
2020-02-05 11:39:19,559 - freqtrade.strategy.interface - WARNING - Unable to analyze ticker for pair WRX/BTC: negative dimensions are not allowed
2020-02-05 11:39:20,710 - freqtrade.freqtradebot - INFO - Bot heartbeat. PID=16343
2020-02-05 11:39:21,135 - freqtrade.strategy.interface - WARNING - Unable to analyze ticker for pair WRX/BTC: negative dimensions are not allowed
2020-02-05 11:39:26,142 - freqtrade.strategy.interface - WARNING - Unable to analyze ticker for pair WRX/BTC: negative dimensions are not allowed
^C2020-02-05 11:39:26,244 - freqtrade.utils - INFO - SIGINT received, aborting ...
2020-02-05 11:39:26,245 - freqtrade.utils - INFO - worker found ... calling exit
2020-02-05 11:39:26,245 - freqtrade.rpc.rpc_manager - INFO - Sending rpc message: {'type': status, 'status': 'process died'}
2020-02-05 11:39:26,283 - freqtrade.freqtradebot - INFO - Cleaning up modules ...
2020-02-05 11:39:26,283 - freqtrade.rpc.rpc_manager - INFO - Cleaning up rpc modules ...
```

Hints from issue discussion:
It's basically #1067 - and i think the same solution will apply here, too.

WRX/BTC is a new pair - new pairs usually have a spike in volume the first day - so they're picked up by VolumePairlist.

The error you're receiving is because this pair does not have the full history (it only started trading yesterday) - and depending on how the strategy is build, having only a few records in the history can create this problem.

PrecisionFilter is usually able to filter these pairs out as they start low-priced - so i'd encourage using that filter as a solution - as this can and will happen in the future.


thank you for answering in a short time
@xmatthias should we check this in appropriate place in the code and return an exception with som more user-friendly message instead of generic python error?

If you look at where this message comes from - then this IS the user-friendly version.

https://github.com/freqtrade/freqtrade/blob/b5ee4f17cb610cba273c2663112d6fe4f60154b9/freqtrade/strategy/interface.py#L257-L265

This exception is caused by the too short history - in this case.
However, the error itself is generic - and happens within strategy code - so it could also happen for other reasons (error in the strategy, using an inexisting column, for example).

opening this for now as the reminder to have a look at this, to make the message less generic in this case...

I don't understand how you'd do that / how you'd "less generalize" this message.

there's many things within the strategy could cause a ValueError - and "not enough history" will definitely be the most uncommon one.

example:
using 
``` python
...
    if dataframe:

```

will cause a ValueError.
It's not even relevant if the dataframe has 500, 5000 or 0 rows - it's simply raising a ValueError.
closing it since 1) @xmatthias PR will improve the general message and 2) for my idea I need to find a way to reproduce the "negative dimensions are not allowed" exception, which I'm currently unable, even with "WRX/BTC" in the whitelist and a high timeframe (1d). I'll recall this (I hope) when I met these conditions in the future, closing for now, nothing to change.

Commit message: Merge branch 'develop' into interface_ordertimeoutcallback

Patch to review:
diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
new file mode 100644
index 00000000000..dcb8018f98f
--- /dev/null
+++ b/docs/strategy-advanced.md
@@ -0,0 +1,92 @@
+# Advanced Strategies
+
+This page explains some advanced concepts available for strategies.
+If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first.
+
+## Custom order timeout rules
+
+Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
+
+However, freqtrade also offers a custom callback for both ordertypes, which allows you to decide based on custom criteria if a order did time out or not.
+
+!!! Note
+    Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
+
+### Custom order timeout example
+
+A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
+It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
+
+The function must return either `True` (cancel order) or `False` (keep order alive).
+
+``` python
+from datetime import datetime, timestamp
+from freqtrade.persistence import Trade
+
+class Awesomestrategy(IStrategy):
+
+    # ... populate_* methods
+
+    # Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
+    unfilledtimeout = {
+        'buy': 60 * 25,
+        'sell': 60 * 25
+    }
+
+    def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
+        if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
+            return True
+        elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
+            return True
+        elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
+           return True
+        return False
+
+
+    def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
+        if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
+            return True
+        elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
+            return True
+        elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
+           return True
+        return False
+```
+
+!!! Note
+    For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
+
+
+### Custom order timeout example (using additional data)
+
+``` python
+from datetime import datetime, timestamp
+from freqtrade.persistence import Trade
+
+class Awesomestrategy(IStrategy):
+
+    # ... populate_* methods
+
+    # Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
+    unfilledtimeout = {
+        'buy': 60 * 25,
+        'sell': 60 * 25
+    }
+
+    def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
+        ob = self.dp.orderbook(pair, 1)
+        current_price = ob['bids'][0][0]
+        # Cancel buy order if price is more than 2% above the order.
+        if order['price'] > current_price * 1.02:
+            return True
+        return False
+
+
+    def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
+        ob = self.dp.orderbook(pair, 1)
+        current_price = ob['asks'][0][0]
+        # Cancel sell order if price is more than 2% below the order.
+        if order['price'] < current_price * 0.98:
+            return True
+        return False
+```
diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md
index 7793ea148b1..c4fc5581124 100644
--- a/docs/strategy-customization.md
+++ b/docs/strategy-customization.md
@@ -1,7 +1,6 @@
 # Strategy Customization
 
-This page explains where to customize your strategies, and add new
-indicators.
+This page explains where to customize your strategies, and add new indicators.
 
 ## Install a custom strategy file
 
diff --git a/docs/utils.md b/docs/utils.md
index 269b9affdd0..57210ac7e21 100644
--- a/docs/utils.md
+++ b/docs/utils.md
@@ -77,7 +77,7 @@ Results will be located in `user_data/strategies/<strategyclassname>.py`.
 
 ``` output
 usage: freqtrade new-strategy [-h] [--userdir PATH] [-s NAME]
-                              [--template {full,minimal}]
+                              [--template {full,minimal,advanced}]
 
 optional arguments:
   -h, --help            show this help message and exit
@@ -86,10 +86,10 @@ optional arguments:
   -s NAME, --strategy NAME
                         Specify strategy class name which will be used by the
                         bot.
-  --template {full,minimal}
-                        Use a template which is either `minimal` or `full`
-                        (containing multiple sample indicators). Default:
-                        `full`.
+  --template {full,minimal,advanced}
+                        Use a template which is either `minimal`, `full`
+                        (containing multiple sample indicators) or `advanced`.
+                        Default: `full`.
 
 ```
 
@@ -105,6 +105,12 @@ With custom user directory
 freqtrade new-strategy --userdir ~/.freqtrade/ --strategy AwesomeStrategy
 ```
 
+Using the advanced template (populates all optional functions and methods)
+
+```bash
+freqtrade new-strategy --strategy AwesomeStrategy --template advanced
+```
+
 ## Create new hyperopt
 
 Creates a new hyperopt from a template similar to SampleHyperopt.
@@ -114,7 +120,7 @@ Results will be located in `user_data/hyperopts/<classname>.py`.
 
 ``` output
 usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME]
-                              [--template {full,minimal}]
+                              [--template {full,minimal,advanced}]
 
 optional arguments:
   -h, --help            show this help message and exit
@@ -122,10 +128,10 @@ optional arguments:
                         Path to userdata directory.
   --hyperopt NAME       Specify hyperopt class name which will be used by the
                         bot.
-  --template {full,minimal}
-                        Use a template which is either `minimal` or `full`
-                        (containing multiple sample indicators). Default:
-                        `full`.
+  --template {full,minimal,advanced}
+                        Use a template which is either `minimal`, `full`
+                        (containing multiple sample indicators) or `advanced`.
+                        Default: `full`.
 ```
 
 ### Sample usage of new-hyperopt
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index 5cf1b7fcef7..498ea935989 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -387,9 +387,9 @@ def __init__(self, *args, **kwargs):
     # Templating options
     "template": Arg(
         '--template',
-        help='Use a template which is either `minimal` or '
-        '`full` (containing multiple sample indicators). Default: `%(default)s`.',
-        choices=['full', 'minimal'],
+        help='Use a template which is either `minimal`, '
+        '`full` (containing multiple sample indicators) or `advanced`. Default: `%(default)s`.',
+        choices=['full', 'minimal', 'advanced'],
         default='full',
     ),
     # Plot dataframe
diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py
index f5a68f748a0..a29ba346fd4 100644
--- a/freqtrade/commands/deploy_commands.py
+++ b/freqtrade/commands/deploy_commands.py
@@ -8,7 +8,7 @@
                                                           create_userdata_dir)
 from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
 from freqtrade.exceptions import OperationalException
-from freqtrade.misc import render_template
+from freqtrade.misc import render_template, render_template_with_fallback
 from freqtrade.state import RunMode
 
 logger = logging.getLogger(__name__)
@@ -32,10 +32,27 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
     """
     Deploy new strategy from template to strategy_path
     """
-    indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",)
-    buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",)
-    sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",)
-    plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",)
+    fallback = 'full'
+    indicators = render_template_with_fallback(
+        templatefile=f"subtemplates/indicators_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/indicators_{fallback}.j2",
+        )
+    buy_trend = render_template_with_fallback(
+        templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/buy_trend_{fallback}.j2",
+        )
+    sell_trend = render_template_with_fallback(
+        templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/sell_trend_{fallback}.j2",
+        )
+    plot_config = render_template_with_fallback(
+        templatefile=f"subtemplates/plot_config_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/plot_config_{fallback}.j2",
+    )
+    additional_methods = render_template_with_fallback(
+        templatefile=f"subtemplates/strategy_methods_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/strategy_methods_empty.j2",
+    )
 
     strategy_text = render_template(templatefile='base_strategy.py.j2',
                                     arguments={"strategy": strategy_name,
@@ -43,6 +60,7 @@ def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: st
                                                "buy_trend": buy_trend,
                                                "sell_trend": sell_trend,
                                                "plot_config": plot_config,
+                                               "additional_methods": additional_methods,
                                                })
 
     logger.info(f"Writing strategy to `{strategy_path}`.")
@@ -73,14 +91,23 @@ def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: st
     """
     Deploys a new hyperopt template to hyperopt_path
     """
-    buy_guards = render_template(
-        templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",)
-    sell_guards = render_template(
-        templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",)
-    buy_space = render_template(
-        templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",)
-    sell_space = render_template(
-        templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",)
+    fallback = 'full'
+    buy_guards = render_template_with_fallback(
+        templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2",
+        )
+    sell_guards = render_template_with_fallback(
+        templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2",
+        )
+    buy_space = render_template_with_fallback(
+        templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2",
+        )
+    sell_space = render_template_with_fallback(
+        templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",
+        templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2",
+        )
 
     strategy_text = render_template(templatefile='base_hyperopt.py.j2',
                                     arguments={"hyperopt": hyperopt_name,
diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py
index 2f05ddb57ef..553a691ef0a 100644
--- a/freqtrade/exceptions.py
+++ b/freqtrade/exceptions.py
@@ -35,3 +35,10 @@ class TemporaryError(FreqtradeException):
     This could happen when an exchange is congested, unavailable, or the user
     has networking problems. Usually resolves itself after a time.
     """
+
+
+class StrategyError(FreqtradeException):
+    """
+    Errors with custom user-code deteced.
+    Usually caused by errors in the strategy.
+    """
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 65ba4698749..7ae87e80743 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -27,6 +27,7 @@
 from freqtrade.rpc import RPCManager, RPCMessageType
 from freqtrade.state import State
 from freqtrade.strategy.interface import IStrategy, SellType
+from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
 from freqtrade.wallets import Wallets
 
 logger = logging.getLogger(__name__)
@@ -869,15 +870,24 @@ def check_handle_timedout(self) -> None:
 
             if (order['side'] == 'buy' and (
                     trade_state_update
-                    or self._check_timed_out('buy', order))):
+                    or self._check_timed_out('buy', order)
+                    or strategy_safe_wrapper(self.strategy.check_buy_timeout,
+                                             default_retval=False)(pair=trade.pair,
+                                                                   trade=trade,
+                                                                   order=order))):
+
                 self.handle_timedout_limit_buy(trade, order)
                 self.wallets.update()
                 order_type = self.strategy.order_types['buy']
                 self._notify_buy_cancel(trade, order_type)
 
             elif (order['side'] == 'sell' and (
-                    trade_state_update
-                    or self._check_timed_out('sell', order))):
+                  trade_state_update
+                  or self._check_timed_out('sell', order)
+                  or strategy_safe_wrapper(self.strategy.check_sell_timeout,
+                                           default_retval=False)(pair=trade.pair,
+                                                                 trade=trade,
+                                                                 order=order))):
                 reason = self.handle_timedout_limit_sell(trade, order)
                 self.wallets.update()
                 order_type = self.strategy.order_types['sell']
diff --git a/freqtrade/misc.py b/freqtrade/misc.py
index ea7bab84340..ac6084eb73e 100644
--- a/freqtrade/misc.py
+++ b/freqtrade/misc.py
@@ -163,3 +163,15 @@ def render_template(templatefile: str, arguments: dict = {}) -> str:
     )
     template = env.get_template(templatefile)
     return template.render(**arguments)
+
+
+def render_template_with_fallback(templatefile: str, templatefallbackfile: str,
+                                  arguments: dict = {}) -> str:
+    """
+    Use templatefile if possible, otherwise fall back to templatefallbackfile
+    """
+    from jinja2.exceptions import TemplateNotFound
+    try:
+        return render_template(templatefile, arguments)
+    except TemplateNotFound:
+        return render_template(templatefallbackfile, arguments)
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index c6f711b7463..6268b8a4353 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -3,21 +3,21 @@
 This module defines the interface to apply for strategies
 """
 import logging
+import warnings
 from abc import ABC, abstractmethod
 from datetime import datetime, timezone
 from enum import Enum
 from typing import Dict, List, NamedTuple, Optional, Tuple
-import warnings
 
 import arrow
 from pandas import DataFrame
 
 from freqtrade.data.dataprovider import DataProvider
+from freqtrade.exceptions import StrategyError
 from freqtrade.exchange import timeframe_to_minutes
 from freqtrade.persistence import Trade
+from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
 from freqtrade.wallets import Wallets
-from freqtrade.exceptions import DependencyException
-
 
 logger = logging.getLogger(__name__)
 
@@ -149,6 +149,42 @@ def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
         :return: DataFrame with sell column
         """
 
+    def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
+        """
+        Check buy timeout function callback.
+        This method can be used to override the buy-timeout.
+        It is called whenever a limit buy order has been created,
+        and is not yet fully filled.
+        Configuration options in `unfilledtimeout` will be verified before this,
+        so ensure to set these timeouts high enough.
+
+        When not implemented by a strategy, this simply returns False.
+        :param pair: Pair the trade is for
+        :param trade: trade object.
+        :param order: Order dictionary as returned from CCXT.
+        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
+        :return bool: When True is returned, then the buy-order is cancelled.
+        """
+        return False
+
+    def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
+        """
+        Check sell timeout function callback.
+        This method can be used to override the sell-timeout.
+        It is called whenever a limit sell order has been created,
+        and is not yet fully filled.
+        Configuration options in `unfilledtimeout` will be verified before this,
+        so ensure to set these timeouts high enough.
+
+        When not implemented by a strategy, this simply returns False.
+        :param pair: Pair the trade is for
+        :param trade: trade object.
+        :param order: Order dictionary as returned from CCXT.
+        :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
+        :return bool: When True is returned, then the sell-order is cancelled.
+        """
+        return False
+
     def informative_pairs(self) -> List[Tuple[str, str]]:
         """
         Define additional, informative pair/interval combinations to be cached from the exchange.
@@ -258,8 +294,7 @@ def assert_df(dataframe: DataFrame, df_len: int, df_close: float, df_date: datet
         elif df_date != dataframe["date"].iloc[-1]:
             message = "last date"
         if message:
-            raise DependencyException("Dataframe returned from strategy has mismatching "
-                                      f"{message}.")
+            raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
 
     def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
         """
@@ -276,22 +311,13 @@ def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bo
         latest_date = dataframe['date'].max()
         try:
             df_len, df_close, df_date = self.preserve_df(dataframe)
-            dataframe = self._analyze_ticker_internal(dataframe, {'pair': pair})
+            dataframe = strategy_safe_wrapper(
+                self._analyze_ticker_internal, message=""
+                )(dataframe, {'pair': pair})
             self.assert_df(dataframe, df_len, df_close, df_date)
-        except ValueError as error:
-            logger.warning('Unable to analyze candle (OHLCV) data for pair %s: %s',
-                           pair, str(error))
-            return False, False
-        except DependencyException as error:
-            logger.warning("Unable to analyze candle (OHLCV) data for pair %s: %s",
-                           pair, str(error))
-            return False, False
-        except Exception as error:
-            logger.exception(
-                'Unexpected error when analyzing candle (OHLCV) data for pair %s: %s',
-                pair,
-                str(error)
-            )
+        except StrategyError as error:
+            logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
+
             return False, False
 
         if dataframe.empty:
diff --git a/freqtrade/strategy/strategy_wrapper.py b/freqtrade/strategy/strategy_wrapper.py
new file mode 100644
index 00000000000..7b9da914020
--- /dev/null
+++ b/freqtrade/strategy/strategy_wrapper.py
@@ -0,0 +1,35 @@
+import logging
+
+from freqtrade.exceptions import StrategyError
+
+logger = logging.getLogger(__name__)
+
+
+def strategy_safe_wrapper(f, message: str = "", default_retval=None):
+    """
+    Wrapper around user-provided methods and functions.
+    Caches all exceptions and returns either the default_retval (if it's not None) or raises
+    a StrategyError exception, which then needs to be handled by the calling method.
+    """
+    def wrapper(*args, **kwargs):
+        try:
+            return f(*args, **kwargs)
+        except ValueError as error:
+            logger.warning(
+                f"{message}"
+                f"Strategy caused the following exception: {error}"
+                f"{f}"
+            )
+            if default_retval is None:
+                raise StrategyError(str(error)) from error
+            return default_retval
+        except Exception as error:
+            logger.exception(
+                f"{message}"
+                f"Unexpected error {error} calling {f}"
+            )
+            if default_retval is None:
+                raise StrategyError(str(error)) from error
+            return default_retval
+
+    return wrapper
diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2
index 97a189ff450..c3716456884 100644
--- a/freqtrade/templates/base_strategy.py.j2
+++ b/freqtrade/templates/base_strategy.py.j2
@@ -137,3 +137,4 @@ class {{ strategy }}(IStrategy):
             ),
             'sell'] = 1
         return dataframe
+    {{ additional_methods | indent(4) }}
diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
new file mode 100644
index 00000000000..0ca35e117c4
--- /dev/null
+++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
@@ -0,0 +1,40 @@
+
+def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
+    """
+    Check buy timeout function callback.
+    This method can be used to override the buy-timeout.
+    It is called whenever a limit buy order has been created,
+    and is not yet fully filled.
+    Configuration options in `unfilledtimeout` will be verified before this,
+    so ensure to set these timeouts high enough.
+
+    For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
+
+    When not implemented by a strategy, this simply returns False.
+    :param pair: Pair the trade is for
+    :param trade: trade object.
+    :param order: Order dictionary as returned from CCXT.
+    :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
+    :return bool: When True is returned, then the buy-order is cancelled.
+    """
+    return False
+
+def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
+    """
+    Check sell timeout function callback.
+    This method can be used to override the sell-timeout.
+    It is called whenever a limit sell order has been created,
+    and is not yet fully filled.
+    Configuration options in `unfilledtimeout` will be verified before this,
+    so ensure to set these timeouts high enough.
+
+    For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
+
+    When not implemented by a strategy, this simply returns False.
+    :param pair: Pair the trade is for
+    :param trade: trade object.
+    :param order: Order dictionary as returned from CCXT.
+    :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
+    :return bool: When True is returned, then the sell-order is cancelled.
+    """
+    return False
diff --git a/freqtrade/templates/subtemplates/strategy_methods_empty.j2 b/freqtrade/templates/subtemplates/strategy_methods_empty.j2
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/mkdocs.yml b/mkdocs.yml
index 4e7e6ff7514..ae24e150c89 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -24,6 +24,7 @@ nav:
         - Plotting: plotting.md
     - SQL Cheatsheet: sql_cheatsheet.md
     - Advanced Post-installation Tasks: advanced-setup.md
+    - Advanced Strategy: strategy-advanced.md
     - Advanced Hyperopt: advanced-hyperopt.md
     - Sandbox Testing: sandbox-testing.md
     - Deprecated Features: deprecated.md
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
index 1c31aeb6a77..dd6b11a06ec 100644
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -9,10 +9,11 @@
 
 from freqtrade.configuration import TimeRange
 from freqtrade.data.history import load_data
-from freqtrade.exceptions import DependencyException
+from freqtrade.exceptions import StrategyError
 from freqtrade.persistence import Trade
 from freqtrade.resolvers import StrategyResolver
-from tests.conftest import get_patched_exchange, log_has
+from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
+from tests.conftest import get_patched_exchange, log_has, log_has_re
 
 from .strats.default_strategy import DefaultStrategy
 
@@ -71,7 +72,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his
     )
     assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'],
                                                   ohlcv_history)
-    assert log_has('Unable to analyze candle (OHLCV) data for pair foo: xyz', caplog)
+    assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
 
 
 def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
@@ -121,7 +122,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
     caplog.set_level(logging.INFO)
     mocker.patch.object(
         _STRATEGY, 'assert_df',
-        side_effect=DependencyException('Dataframe returned...')
+        side_effect=StrategyError('Dataframe returned...')
     )
     assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'],
                                                   ohlcv_history)
@@ -134,15 +135,15 @@ def test_assert_df(default_conf, mocker, ohlcv_history):
     _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
                         ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
 
-    with pytest.raises(DependencyException, match=r"Dataframe returned from strategy.*length\."):
+    with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."):
         _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
                             ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date'])
 
-    with pytest.raises(DependencyException,
+    with pytest.raises(StrategyError,
                        match=r"Dataframe returned from strategy.*last close price\."):
         _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
                             ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date'])
-    with pytest.raises(DependencyException,
+    with pytest.raises(StrategyError,
                        match=r"Dataframe returned from strategy.*last date\."):
         _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
                             ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date'])
@@ -389,3 +390,38 @@ def test_is_pair_locked(default_conf):
     pair = 'ETH/BTC'
     strategy.unlock_pair(pair)
     assert not strategy.is_pair_locked(pair)
+
+
+@pytest.mark.parametrize('error', [
+    ValueError, KeyError, Exception,
+])
+def test_strategy_safe_wrapper_error(caplog, error):
+    def failing_method():
+        raise error('This is an error.')
+
+    def working_method(argumentpassedin):
+        return argumentpassedin
+
+    with pytest.raises(StrategyError, match=r'This is an error.'):
+        strategy_safe_wrapper(failing_method, message='DeadBeef')()
+
+    assert log_has_re(r'DeadBeef.*', caplog)
+    ret = strategy_safe_wrapper(failing_method, message='DeadBeef', default_retval=True)()
+
+    assert isinstance(ret, bool)
+    assert ret
+
+
+@pytest.mark.parametrize('value', [
+    1, 22, 55, True, False, {'a': 1, 'b': '112'},
+    [1, 2, 3, 4], (4, 2, 3, 6)
+])
+def test_strategy_safe_wrapper(value):
+
+    def working_method(argumentpassedin):
+        return argumentpassedin
+
+    ret = strategy_safe_wrapper(working_method, message='DeadBeef')(value)
+
+    assert type(ret) == type(value)
+    assert ret == value
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index c3e78c9bedf..6f2ce9f3c8c 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1939,6 +1939,53 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
         freqtrade.handle_trade(trade)
 
 
+def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade,
+                                              fee, mocker) -> None:
+    default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30}
+
+    rpc_mock = patch_RPCManager(mocker)
+    cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
+    patch_exchange(mocker)
+    mocker.patch.multiple(
+        'freqtrade.exchange.Exchange',
+        fetch_ticker=ticker,
+        get_order=MagicMock(return_value=limit_buy_order_old),
+        cancel_order=cancel_order_mock,
+        get_fee=fee
+    )
+    freqtrade = FreqtradeBot(default_conf)
+
+    Trade.session.add(open_trade)
+
+    # Return false - trade remains open
+    freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
+    freqtrade.check_handle_timedout()
+    assert cancel_order_mock.call_count == 0
+    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    nb_trades = len(trades)
+    assert nb_trades == 1
+    assert freqtrade.strategy.check_buy_timeout.call_count == 1
+
+    # Raise Keyerror ... (no impact on trade)
+    freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
+    freqtrade.check_handle_timedout()
+    assert cancel_order_mock.call_count == 0
+    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    nb_trades = len(trades)
+    assert nb_trades == 1
+    assert freqtrade.strategy.check_buy_timeout.call_count == 1
+
+    freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
+    # Trade should be closed since the function returns true
+    freqtrade.check_handle_timedout()
+    assert cancel_order_mock.call_count == 1
+    assert rpc_mock.call_count == 1
+    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
+    nb_trades = len(trades)
+    assert nb_trades == 0
+    assert freqtrade.strategy.check_buy_timeout.call_count == 1
+
+
 def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
                                    fee, mocker) -> None:
     rpc_mock = patch_RPCManager(mocker)
@@ -1955,6 +2002,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
 
     Trade.session.add(open_trade)
 
+    freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
     # check it does cancel buy orders over the time limit
     freqtrade.check_handle_timedout()
     assert cancel_order_mock.call_count == 1
@@ -1962,6 +2010,8 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
     trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
     nb_trades = len(trades)
     assert nb_trades == 0
+    # Custom user buy-timeout is never called
+    assert freqtrade.strategy.check_buy_timeout.call_count == 0
 
 
 def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, open_trade,
@@ -2018,6 +2068,51 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord
     assert nb_trades == 1
 
 
+def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker,
+                                               open_trade) -> None:
+    default_conf["unfilledtimeout"] = {"buy": 1440, "sell": 1440}
+    rpc_mock = patch_RPCManager(mocker)
+    cancel_order_mock = MagicMock()
+    patch_exchange(mocker)
+    mocker.patch.multiple(
+        'freqtrade.exchange.Exchange',
+        fetch_ticker=ticker,
+        get_order=MagicMock(return_value=limit_sell_order_old),
+        cancel_order=cancel_order_mock
+    )
+    freqtrade = FreqtradeBot(default_conf)
+
+    open_trade.open_date = arrow.utcnow().shift(hours=-5).datetime
+    open_trade.close_date = arrow.utcnow().shift(minutes=-601).datetime
+    open_trade.is_open = False
+
+    Trade.session.add(open_trade)
+
+    freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
+    # Return false - No impact
+    freqtrade.check_handle_timedout()
+    assert cancel_order_mock.call_count == 0
+    assert rpc_mock.call_count == 0
+    assert open_trade.is_open is False
+    assert freqtrade.strategy.check_sell_timeout.call_count == 1
+
+    freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError)
+    # Return Error - No impact
+    freqtrade.check_handle_timedout()
+    assert cancel_order_mock.call_count == 0
+    assert rpc_mock.call_count == 0
+    assert open_trade.is_open is False
+    assert freqtrade.strategy.check_sell_timeout.call_count == 1
+
+    # Return True - sells!
+    freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True)
+    freqtrade.check_handle_timedout()
+    assert cancel_order_mock.call_count == 1
+    assert rpc_mock.call_count == 1
+    assert open_trade.is_open is True
+    assert freqtrade.strategy.check_sell_timeout.call_count == 1
+
+
 def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker,
                                     open_trade) -> None:
     rpc_mock = patch_RPCManager(mocker)
@@ -2037,11 +2132,14 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
 
     Trade.session.add(open_trade)
 
+    freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
     # check it does cancel sell orders over the time limit
     freqtrade.check_handle_timedout()
     assert cancel_order_mock.call_count == 1
     assert rpc_mock.call_count == 1
     assert open_trade.is_open is True
+    # Custom user sell-timeout is never called
+    assert freqtrade.strategy.check_sell_timeout.call_count == 0
 
 
 def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade,
diff --git a/tests/test_misc.py b/tests/test_misc.py
index 832bcb3a92c..9fd6164d54b 100644
--- a/tests/test_misc.py
+++ b/tests/test_misc.py
@@ -9,7 +9,9 @@
 from freqtrade.data.converter import ohlcv_to_dataframe
 from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json,
                             file_load_json, format_ms_time, pair_to_filename,
-                            plural, safe_value_fallback, shorten_date)
+                            plural, render_template,
+                            render_template_with_fallback, safe_value_fallback,
+                            shorten_date)
 
 
 def test_shorten_date() -> None:
@@ -144,3 +146,17 @@ def test_plural() -> None:
     assert plural(1.5, "ox", "oxen") == "oxen"
     assert plural(-0.5, "ox", "oxen") == "oxen"
     assert plural(-1.5, "ox", "oxen") == "oxen"
+
+
+def test_render_template_fallback(mocker):
+    from jinja2.exceptions import TemplateNotFound
+    with pytest.raises(TemplateNotFound):
+        val = render_template(
+            templatefile='subtemplates/indicators_does-not-exist.j2',)
+
+    val = render_template_with_fallback(
+        templatefile='subtemplates/indicators_does-not-exist.j2',
+        templatefallbackfile='subtemplates/indicators_minimal.j2',
+    )
+    assert isinstance(val, str)
+    assert 'if self.dp' in val


Code review comments:
diff_hunk: @@ -0,0 +1,92 @@
+# Advanced Strategies
+
+This page explains some advanced concepts available for strategies.
+If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first.
+
+## Custom order timeout rules
+
+Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section.
+
+However, freqtrade also offers a custom callback for both ordertypes, which allows you to decide based on custom criteria if a order did time out or not.
+
+!!! Note
+    Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
+
+### Custom order timeout example
+
+A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
+It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
+
+The function must return either `True` (cancel order) or `False` (keep order alive).
+
+``` python
+from datetime import datetime, timestamp
+from freqtrade.persistence import Trade
+
+class Awesomestrategy(IStrategy):
+
+    # ... populate_* methods
+
+    # Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
+    unfilledtimeout = {
+        'buy': 60 * 25,
+        'sell': 60 * 25
+    }
+
+    def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
+        if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
+            return True
+        elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
+            return True
+        elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
+           return True
+        return False
+
+
+    def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
+        if trade.open_rate > 100 and trade.open_date < datetime.utcnow() - timedelta(minutes=5):
+            return True
+        elif trade.open_rate > 10 and trade.open_date < datetime.utcnow() - timedelta(minutes=3):
+            return True
+        elif trade.open_rate < 1 and trade.open_date < datetime.utcnow() - timedelta(hours=24):
+           return True
+        return False
+```
+
+!!! Note
+    For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first.
+
+
+### Custom order timeout example (using additional data)
+
+``` python
+from datetime import datetime, timestamp
+from freqtrade.persistence import Trade
+
+class Awesomestrategy(IStrategy):
+
+    # ... populate_* methods
+
+    # Set unfilledtimeout to 25 hours, since our maximum timeout from below is 24 hours.
+    unfilledtimeout = {
+        'buy': 60 * 25,
+        'sell': 60 * 25
+    }
+
+    def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
+        ob = self.dp.orderbook(pair, 1)
+        current_price = ob['bids'][0][0]
+        # Cancel buy order if price is more than 2% above the order.
+        if order['price'] > current_price * 1.02:
path: docs/strategy-advanced.md
text: @user1:
should be reversed? (to be "Cancel buy order if price is more than 2% above the order" as stated in the comment)
```
if current_price > order['price'] * 1.02:
```
???

(same for sell part)

@author:
no ... it's correct this way (i got this wrong initially, too).

Assume i put my buy-order in at 100 USDT (using bids-side).
* price immediately jumps to 101 USDT (my trade is now in 15th spot in the orderbook ).
* Price jumps to 100 USDT - so my chances of beeing filled at 100 USDT are pretty slim -> i decide to cancel the order
therefore the calculation / comparsion is correct since it's: `102 > 100 * 1.02`.

If it where different - i place the buy-order at 100 - and price drops to 98 - then i'd have been filled already fully ... so it would never reach that point ... 

(the same applies to sell side - but obviously in the other direction.



@user1:
but 102 here is `current_price`, and `order['price']` is 100, so it should be
```
if current_price > order['price'] * 1.02:
```
to be 
```
102 > 100 * 1.02
```