Repository: freqtrade/freqtrade
Pull request: Add exit signals to export in backtesting
## Summary

Adding exit signals as data frame to be analysed during backtesting analysis.

Solve the issue: #8902

## Quick changelog

- Store exit_signals in a separate `.pkl` file

## What's new?

- Provides a way to deepdive into backtesting analysis by making indicators available from exit signals as well.


Problem statement:
backtesting-analysis  - show signal candle indicators for exit signals
## Describe your environment

  * Operating system: _Docker___
  * Python Version: __Python 3.11.4
  * CCXT version: __ccxt==3.1.44
  * Freqtrade Version: __docker-2023.6.dev-e16c433c
  
In reference to backtesting analysis I can only see signal candle indicators for buy signals. Is it possible to extract also signal candle indicators for sell signals (indicator values on signal candles)?

Hints from issue discussion:
it's .. not immediately clear what you're looking for if i'm honest.

`backtesting-analysis` is looking at enter-reasons - and you can do the same for exit-reasons if that's what you're asking
If not - then please try to phrase your question differently, or more in detial what you're getting on the entry side, but not on the exit side.
backtesting-analysis won't currently store exit signal candle data, only entry signals. This might be implemented in future.
I was referring to this doc: https://www.freqtrade.io/en/stable/advanced-backtesting/
running freqtrade backtesting-analysis with --exit-reason-list appears to provide only parameters in detail with entry for each indicator not exits. (each value for every indicator where the trade was taken) indeed a useful feature. 
Yes, that's correct.
Seems like something worth pursuing.
Marking it as enhancement for now - shouldn't be too difficult to implement, either.
Appreciate your input on this, It would be a very useful feature in the toolset if possible to integrate. 
Hey @xmatthias / @froggleston : i was thinking of picking up this enhancement. Although would like to get some suggestions. 

Currently while backtesting with --signals parameter two files are generated:
- rejected.pkl
- signals.pkl
Here signals.pkl just consists the indicator values against the entry signals. I am planning to create another .pkl file exited.pkl or something which holds indicator values against the exit signals. 
The same will be used in backtesting analysis. 

To start with i am planning to run the same set of analysis on this exited.pkl file that runs for signals.pkl. 

Let me know if this sounds good!  Anyways will be raising the PR soon as i am trying to analyse the same with one of my strategies. Although not a data or statistics guy so struggles a bit. 

That's been on my list for a long time and I've never got round to it. Sure - I'm happy to take a look at any code you produce and work together to get it developed! 👍 
> Here signals.pkl just consists the indicator values against the entry signals. I am planning to create another .pkl file exited.pkl or something which holds indicator values against the exit signals.

Either this, or enhance signals.pkl to also contain the exit data (i'm not sure we'll need a separate file - but i didn't look at the precise format (or format required), either.
Have created a draft PR: https://github.com/freqtrade/freqtrade/pull/10485

We can discuss some more technical details there. @xmatthias i understand your point and have provided the reasons on the PR.

Commit message: chore: type safety and refactoring

Patch to review:
diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md
index 563e5df08b8..c01d763f8c6 100644
--- a/docs/advanced-backtesting.md
+++ b/docs/advanced-backtesting.md
@@ -18,15 +18,15 @@ freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_nam
 ```
 
 This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding
-DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy
-makes, this file may get quite large, so periodically check your `user_data/backtest_results`
-folder to delete old exports.
+DataFrame of the candles that resulted in buy and sell signals.
+Depending on how many buys your strategy makes, this file may get quite large, 
+so periodically check your `user_data/backtest_results` folder to delete old exports.
 
 Before running your next backtest, make sure you either delete your old backtest results or run
 backtesting with the `--cache none` option to make sure no cached results are used.
 
-If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` file in the
-`user_data/backtest_results` folder.
+If all goes well, you should now see a `backtest-result-{timestamp}_signals.pkl` and
+`backtest-result-{timestamp}_exited.pkl` files in the `user_data/backtest_results` folder.
 
 To analyze the entry/exit tags, we now need to use the `freqtrade backtesting-analysis` command
 with `--analysis-groups` option provided with space-separated arguments:
@@ -141,6 +141,14 @@ Use the `--rejected-signals` option to print out rejected signals.
 freqtrade backtesting-analysis -c <config.json> --rejected-signals
 ```
 
+### Printing analysis on exit signals
+
+Use the `--exit-signals` option to print out analysis on exited signals.
+
+```bash
+freqtrade backtesting-analysis -c <config.json> --exit-signals
+```
+
 ### Writing tables to CSV
 
 Some of the tabular outputs can become large, so printing them out to the terminal is not preferable.
diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index 0c93af78aaa..a7ecfb83c7b 100755
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -222,6 +222,7 @@
     "indicator_list",
     "timerange",
     "analysis_rejected",
+    "analysis_exited",
     "analysis_to_csv",
     "analysis_csv_path",
 ]
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index b9236a0ab42..72df9b0c72f 100755
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -720,6 +720,11 @@ def __init__(self, *args, **kwargs):
         help="Analyse rejected signals",
         action="store_true",
     ),
+    "analysis_exited": Arg(
+        "--exit-signals",
+        help="Analyse indicators at exit signals",
+        action="store_true",
+    ),
     "analysis_to_csv": Arg(
         "--analysis-to-csv",
         help="Save selected analysis tables to individual CSVs",
diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py
index 184f9decf03..2e3eff74064 100644
--- a/freqtrade/configuration/configuration.py
+++ b/freqtrade/configuration/configuration.py
@@ -401,6 +401,7 @@ def _process_analyze_options(self, config: Config) -> None:
             ("indicator_list", "Analysis indicator list: {}"),
             ("timerange", "Filter trades by timerange: {}"),
             ("analysis_rejected", "Analyse rejected signals: {}"),
+            ("analysis_exited", "Analyse exited signals: {}"),
             ("analysis_to_csv", "Store analysis tables to CSV: {}"),
             ("analysis_csv_path", "Path to store analysis CSVs: {}"),
             # Lookahead analysis results
diff --git a/freqtrade/data/entryexitanalysis.py b/freqtrade/data/entryexitanalysis.py
index e76f2dff752..99b0e3bcb40 100644
--- a/freqtrade/data/entryexitanalysis.py
+++ b/freqtrade/data/entryexitanalysis.py
@@ -1,6 +1,6 @@
 import logging
 from pathlib import Path
-from typing import List
+from typing import Dict, List
 
 import joblib
 import pandas as pd
@@ -47,9 +47,14 @@ def _load_signal_candles(backtest_dir: Path):
     return _load_backtest_analysis_data(backtest_dir, "signals")
 
 
-def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_candles):
-    analysed_trades_dict = {}
-    analysed_trades_dict[strategy_name] = {}
+def _load_exit_signal_candles(backtest_dir: Path) -> Dict[str, Dict[str, pd.DataFrame]]:
+    return _load_backtest_analysis_data(backtest_dir, "exited")
+
+
+def _process_candles_and_indicators(
+    pairlist, strategy_name, trades, signal_candles, analyse_on="open_date"
+):
+    analysed_trades_dict = {strategy_name: {}}
 
     try:
         logger.info(f"Processing {strategy_name} : {len(pairlist)} pairs")
@@ -57,7 +62,7 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
         for pair in pairlist:
             if pair in signal_candles[strategy_name]:
                 analysed_trades_dict[strategy_name][pair] = _analyze_candles_and_indicators(
-                    pair, trades, signal_candles[strategy_name][pair]
+                    pair, trades, signal_candles[strategy_name][pair], analyse_on
                 )
     except Exception as e:
         print(f"Cannot process entry/exit reasons for {strategy_name}: ", e)
@@ -65,7 +70,9 @@ def _process_candles_and_indicators(pairlist, strategy_name, trades, signal_cand
     return analysed_trades_dict
 
 
-def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles: pd.DataFrame):
+def _analyze_candles_and_indicators(
+    pair: str, trades: pd.DataFrame, signal_candles: pd.DataFrame, analyse_on="open_date"
+) -> pd.DataFrame:
     buyf = signal_candles
 
     if len(buyf) > 0:
@@ -75,8 +82,8 @@ def _analyze_candles_and_indicators(pair, trades: pd.DataFrame, signal_candles:
         trades_inds = pd.DataFrame()
 
         if trades_red.shape[0] > 0 and buyf.shape[0] > 0:
-            for t, v in trades_red.open_date.items():
-                allinds = buyf.loc[(buyf["date"] < v)]
+            for t, v in trades_red.iterrows():
+                allinds = buyf.loc[(buyf["date"] < v[analyse_on])]
                 if allinds.shape[0] > 0:
                     tmp_inds = allinds.iloc[[-1]]
 
@@ -235,7 +242,7 @@ def _select_rows_by_tags(df, enter_reason_list, exit_reason_list):
 
 def prepare_results(
     analysed_trades, stratname, enter_reason_list, exit_reason_list, timerange=None
-):
+) -> pd.DataFrame:
     res_df = pd.DataFrame()
     for pair, trades in analysed_trades[stratname].items():
         if trades.shape[0] > 0:
@@ -257,8 +264,11 @@ def print_results(
     csv_path: Path,
     rejected_signals=None,
     to_csv=False,
+    exited_signals=False,
 ):
     if res_df.shape[0] > 0:
+        if exited_signals is True:
+            print("Analysing on exit signals.")
         if analysis_groups:
             _do_group_table_output(res_df, analysis_groups, to_csv=to_csv, csv_path=csv_path)
 
@@ -316,6 +326,7 @@ def process_entry_exit_reasons(config: Config):
         enter_reason_list = config.get("enter_reason_list", ["all"])
         exit_reason_list = config.get("exit_reason_list", ["all"])
         indicator_list = config.get("indicator_list", [])
+        do_exited = config.get("analysis_exited", False)
         do_rejected = config.get("analysis_rejected", False)
         to_csv = config.get("analysis_to_csv", False)
         csv_path = Path(config.get("analysis_csv_path", config["exportfilename"]))
@@ -332,7 +343,12 @@ def process_entry_exit_reasons(config: Config):
             trades = load_backtest_data(config["exportfilename"], strategy_name)
 
             if trades is not None and not trades.empty:
-                signal_candles = _load_signal_candles(config["exportfilename"])
+                analyse_on = "open_date"
+                if do_exited is True:
+                    signal_candles = _load_exit_signal_candles(config["exportfilename"])
+                    analyse_on = "close_date"
+                else:
+                    signal_candles = _load_signal_candles(config["exportfilename"])
 
                 rej_df = None
                 if do_rejected:
@@ -346,7 +362,11 @@ def process_entry_exit_reasons(config: Config):
                     )
 
                 analysed_trades_dict = _process_candles_and_indicators(
-                    config["exchange"]["pair_whitelist"], strategy_name, trades, signal_candles
+                    config["exchange"]["pair_whitelist"],
+                    strategy_name,
+                    trades,
+                    signal_candles,
+                    analyse_on,
                 )
 
                 res_df = prepare_results(
@@ -364,6 +384,7 @@ def process_entry_exit_reasons(config: Config):
                     rejected_signals=rej_df,
                     to_csv=to_csv,
                     csv_path=csv_path,
+                    exited_signals=do_exited,
                 )
 
     except ValueError as e:
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index c28c080f5cb..bb088d064a6 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -122,6 +122,7 @@ def __init__(self, config: Config, exchange: Optional[Exchange] = None) -> None:
         self.processed_dfs: Dict[str, Dict] = {}
         self.rejected_dict: Dict[str, List] = {}
         self.rejected_df: Dict[str, Dict] = {}
+        self.exited_dfs: Dict[str, Dict] = {}
 
         self._exchange_name = self.config["exchange"]["name"]
         if not exchange:
@@ -1559,11 +1560,14 @@ def backtest_one_strategy(
             and self.dataprovider.runmode == RunMode.BACKTEST
         ):
             self.processed_dfs[strategy_name] = generate_trade_signal_candles(
-                preprocessed_tmp, results
+                preprocessed_tmp, results, "open_date"
             )
             self.rejected_df[strategy_name] = generate_rejected_signals(
                 preprocessed_tmp, self.rejected_dict
             )
+            self.exited_dfs[strategy_name] = generate_trade_signal_candles(
+                preprocessed_tmp, results, "close_date"
+            )
 
         return min_date, max_date
 
@@ -1639,7 +1643,11 @@ def start(self) -> None:
                 and self.dataprovider.runmode == RunMode.BACKTEST
             ):
                 store_backtest_analysis_results(
-                    self.config["exportfilename"], self.processed_dfs, self.rejected_df, dt_appendix
+                    self.config["exportfilename"],
+                    self.processed_dfs,
+                    self.rejected_df,
+                    self.exited_dfs,
+                    dt_appendix,
                 )
 
         # Results may be mixed up now. Sort them so they follow --strategy-list order.
diff --git a/freqtrade/optimize/optimize_reports/bt_storage.py b/freqtrade/optimize/optimize_reports/bt_storage.py
index ea899133731..ed0667cf4a7 100644
--- a/freqtrade/optimize/optimize_reports/bt_storage.py
+++ b/freqtrade/optimize/optimize_reports/bt_storage.py
@@ -90,7 +90,12 @@ def _store_backtest_analysis_data(
 
 
 def store_backtest_analysis_results(
-    recordfilename: Path, candles: Dict[str, Dict], trades: Dict[str, Dict], dtappendix: str
+    recordfilename: Path,
+    candles: Dict[str, Dict],
+    trades: Dict[str, Dict],
+    exited: Dict[str, Dict],
+    dtappendix: str,
 ) -> None:
     _store_backtest_analysis_data(recordfilename, candles, dtappendix, "signals")
     _store_backtest_analysis_data(recordfilename, trades, dtappendix, "rejected")
+    _store_backtest_analysis_data(recordfilename, exited, dtappendix, "exited")
diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py
index ef5fce0e195..2b0fd54bf7c 100644
--- a/freqtrade/optimize/optimize_reports/optimize_reports.py
+++ b/freqtrade/optimize/optimize_reports/optimize_reports.py
@@ -25,8 +25,8 @@
 
 
 def generate_trade_signal_candles(
-    preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any]
-) -> DataFrame:
+    preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any], analysis_on="open_date"
+) -> Dict[str, DataFrame]:
     signal_candles_only = {}
     for pair in preprocessed_df.keys():
         signal_candles_only_df = DataFrame()
@@ -36,8 +36,8 @@ def generate_trade_signal_candles(
         pairresults = resdf.loc[(resdf["pair"] == pair)]
 
         if pairdf.shape[0] > 0:
-            for t, v in pairresults.open_date.items():
-                allinds = pairdf.loc[(pairdf["date"] < v)]
+            for t, v in pairresults.iterrows():
+                allinds = pairdf.loc[(pairdf["date"] < v[analysis_on])]
                 signal_inds = allinds.iloc[[-1]]
                 signal_candles_only_df = concat(
                     [signal_candles_only_df.infer_objects(), signal_inds.infer_objects()]
diff --git a/tests/data/test_entryexitanalysis.py b/tests/data/test_entryexitanalysis.py
index e7909c33967..66e0179f8d0 100644
--- a/tests/data/test_entryexitanalysis.py
+++ b/tests/data/test_entryexitanalysis.py
@@ -140,6 +140,7 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
     )
     start_analysis_entries_exits(args)
     captured = capsys.readouterr()
+    assert "Analysing on exit signals." not in captured.out
     assert "LTC/BTC" in captured.out
     assert "ETH/BTC" in captured.out
     assert "enter_tag_long_a" in captured.out
@@ -245,3 +246,237 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
     start_analysis_entries_exits(args)
     captured = capsys.readouterr()
     assert "no rejected signals" in captured.out
+
+
+def test_backtest_analysis_on_exit_signals_nomock(
+    default_conf, mocker, caplog, testdatadir, user_dir, capsys
+):
+    caplog.set_level(logging.INFO)
+    (user_dir / "backtest_results").mkdir(parents=True, exist_ok=True)
+
+    default_conf.update(
+        {
+            "use_exit_signal": True,
+            "exit_profit_only": False,
+            "exit_profit_offset": 0.0,
+            "ignore_roi_if_entry_signal": False,
+        }
+    )
+    patch_exchange(mocker)
+    result1 = pd.DataFrame(
+        {
+            "pair": ["ETH/BTC", "LTC/BTC", "ETH/BTC", "LTC/BTC"],
+            "profit_ratio": [0.025, 0.05, -0.1, -0.05],
+            "profit_abs": [0.5, 2.0, -4.0, -2.0],
+            "open_date": pd.to_datetime(
+                [
+                    "2018-01-29 18:40:00",
+                    "2018-01-30 03:30:00",
+                    "2018-01-30 08:10:00",
+                    "2018-01-31 13:30:00",
+                ],
+                utc=True,
+            ),
+            "close_date": pd.to_datetime(
+                [
+                    "2018-01-29 20:45:00",
+                    "2018-01-30 05:35:00",
+                    "2018-01-30 09:10:00",
+                    "2018-01-31 15:00:00",
+                ],
+                utc=True,
+            ),
+            "trade_duration": [235, 40, 60, 90],
+            "is_open": [False, False, False, False],
+            "stake_amount": [0.01, 0.01, 0.01, 0.01],
+            "open_rate": [0.104445, 0.10302485, 0.10302485, 0.10302485],
+            "close_rate": [0.104969, 0.103541, 0.102041, 0.102541],
+            "is_short": [False, False, False, False],
+            "enter_tag": [
+                "enter_tag_long_a",
+                "enter_tag_long_b",
+                "enter_tag_long_a",
+                "enter_tag_long_b",
+            ],
+            "exit_reason": [
+                ExitType.ROI.value,
+                ExitType.EXIT_SIGNAL.value,
+                ExitType.STOP_LOSS.value,
+                ExitType.TRAILING_STOP_LOSS.value,
+            ],
+        }
+    )
+
+    backtestmock = MagicMock(
+        side_effect=[
+            {
+                "results": result1,
+                "config": default_conf,
+                "locks": [],
+                "rejected_signals": 20,
+                "timedout_entry_orders": 0,
+                "timedout_exit_orders": 0,
+                "canceled_trade_entries": 0,
+                "canceled_entry_orders": 0,
+                "replaced_entry_orders": 0,
+                "final_balance": 1000,
+            }
+        ]
+    )
+    mocker.patch(
+        "freqtrade.plugins.pairlistmanager.PairListManager.whitelist",
+        PropertyMock(return_value=["ETH/BTC", "LTC/BTC", "DASH/BTC"]),
+    )
+    mocker.patch("freqtrade.optimize.backtesting.Backtesting.backtest", backtestmock)
+
+    patched_configuration_load_config_file(mocker, default_conf)
+
+    args = [
+        "backtesting",
+        "--config",
+        "config.json",
+        "--datadir",
+        str(testdatadir),
+        "--user-data-dir",
+        str(user_dir),
+        "--timeframe",
+        "5m",
+        "--timerange",
+        "1515560100-1517287800",
+        "--export",
+        "signals",
+        "--cache",
+        "none",
+    ]
+    args = get_args(args)
+    start_backtesting(args)
+
+    captured = capsys.readouterr()
+    assert "BACKTESTING REPORT" in captured.out
+    assert "EXIT REASON STATS" in captured.out
+    assert "LEFT OPEN TRADES REPORT" in captured.out
+
+    base_args = [
+        "backtesting-analysis",
+        "--config",
+        "config.json",
+        "--datadir",
+        str(testdatadir),
+        "--user-data-dir",
+        str(user_dir),
+    ]
+
+    # test group 0 and indicator list
+    args = get_args(
+        base_args
+        + [
+            "--analysis-groups",
+            "0",
+            "--exit-signals",
+            "--indicator-list",
+            "close",
+            "rsi",
+            "profit_abs",
+        ]
+    )
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "Analysing on exit signals." in captured.out
+    assert "LTC/BTC" in captured.out
+    assert "ETH/BTC" in captured.out
+    assert "enter_tag_long_a" in captured.out
+    assert "enter_tag_long_b" in captured.out
+    assert "exit_signal" in captured.out
+    assert "roi" in captured.out
+    assert "stop_loss" in captured.out
+    assert "trailing_stop_loss" in captured.out
+    assert "0.5" in captured.out
+    assert "-4" in captured.out
+    assert "-2" in captured.out
+    assert "57.654" in captured.out
+    assert "44.428" in captured.out
+    assert "0" in captured.out
+    assert "0.104" in captured.out
+    assert "0.016" in captured.out
+    assert "52.829" in captured.out
+
+    # test group 1
+    args = get_args(base_args + ["--analysis-groups", "1"])
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "enter_tag_long_a" in captured.out
+    assert "enter_tag_long_b" in captured.out
+    assert "total_profit_pct" in captured.out
+    assert "-3.5" in captured.out
+    assert "-1.75" in captured.out
+    assert "-7.5" in captured.out
+    assert "-3.75" in captured.out
+    assert "0" in captured.out
+
+    # test group 2
+    args = get_args(base_args + ["--analysis-groups", "2"])
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "enter_tag_long_a" in captured.out
+    assert "enter_tag_long_b" in captured.out
+    assert "exit_signal" in captured.out
+    assert "roi" in captured.out
+    assert "stop_loss" in captured.out
+    assert "trailing_stop_loss" in captured.out
+    assert "total_profit_pct" in captured.out
+    assert "-10" in captured.out
+    assert "-5" in captured.out
+    assert "2.5" in captured.out
+
+    # test group 3
+    args = get_args(base_args + ["--analysis-groups", "3"])
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "LTC/BTC" in captured.out
+    assert "ETH/BTC" in captured.out
+    assert "enter_tag_long_a" in captured.out
+    assert "enter_tag_long_b" in captured.out
+    assert "total_profit_pct" in captured.out
+    assert "-7.5" in captured.out
+    assert "-3.75" in captured.out
+    assert "-1.75" in captured.out
+    assert "0" in captured.out
+    assert "2" in captured.out
+
+    # test group 4
+    args = get_args(base_args + ["--analysis-groups", "4"])
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "LTC/BTC" in captured.out
+    assert "ETH/BTC" in captured.out
+    assert "enter_tag_long_a" in captured.out
+    assert "enter_tag_long_b" in captured.out
+    assert "exit_signal" in captured.out
+    assert "roi" in captured.out
+    assert "stop_loss" in captured.out
+    assert "trailing_stop_loss" in captured.out
+    assert "total_profit_pct" in captured.out
+    assert "-10" in captured.out
+    assert "-5" in captured.out
+    assert "-4" in captured.out
+    assert "0.5" in captured.out
+    assert "1" in captured.out
+    assert "2.5" in captured.out
+
+    # test group 5
+    args = get_args(base_args + ["--analysis-groups", "5"])
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "exit_signal" in captured.out
+    assert "roi" in captured.out
+    assert "stop_loss" in captured.out
+    assert "trailing_stop_loss" in captured.out
+
+    # test date filtering
+    args = get_args(
+        base_args + ["--analysis-groups", "0", "1", "2", "--timerange", "20180129-20180130"]
+    )
+    start_analysis_entries_exits(args)
+    captured = capsys.readouterr()
+    assert "enter_tag_long_a" in captured.out
+    assert "enter_tag_long_b" not in captured.out
diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py
index 4c7ce06e8da..abbed1e54e2 100644
--- a/tests/optimize/test_optimize_reports.py
+++ b/tests/optimize/test_optimize_reports.py
@@ -293,17 +293,17 @@ def test_store_backtest_candles(testdatadir, mocker):
     candle_dict = {"DefStrat": {"UNITTEST/BTC": pd.DataFrame()}}
 
     # mock directory exporting
-    store_backtest_analysis_results(testdatadir, candle_dict, {}, "2022_01_01_15_05_13")
+    store_backtest_analysis_results(testdatadir, candle_dict, {}, {}, "2022_01_01_15_05_13")
 
-    assert dump_mock.call_count == 2
+    assert dump_mock.call_count == 3
     assert isinstance(dump_mock.call_args_list[0][0][0], Path)
     assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
 
     dump_mock.reset_mock()
     # mock file exporting
     filename = Path(testdatadir / "testresult")
-    store_backtest_analysis_results(filename, candle_dict, {}, "2022_01_01_15_05_13")
-    assert dump_mock.call_count == 2
+    store_backtest_analysis_results(filename, candle_dict, {}, {}, "2022_01_01_15_05_13")
+    assert dump_mock.call_count == 3
     assert isinstance(dump_mock.call_args_list[0][0][0], Path)
     # result will be testdatadir / testresult-<timestamp>_signals.pkl
     assert str(dump_mock.call_args_list[0][0][0]).endswith("_signals.pkl")
@@ -315,7 +315,7 @@ def test_write_read_backtest_candles(tmp_path):
 
     # test directory exporting
     sample_date = "2022_01_01_15_05_13"
-    store_backtest_analysis_results(tmp_path, candle_dict, {}, sample_date)
+    store_backtest_analysis_results(tmp_path, candle_dict, {}, {}, sample_date)
     stored_file = tmp_path / f"backtest-result-{sample_date}_signals.pkl"
     with stored_file.open("rb") as scp:
         pickled_signal_candles = joblib.load(scp)
@@ -330,7 +330,7 @@ def test_write_read_backtest_candles(tmp_path):
 
     # test file exporting
     filename = tmp_path / "testresult"
-    store_backtest_analysis_results(filename, candle_dict, {}, sample_date)
+    store_backtest_analysis_results(filename, candle_dict, {}, {}, sample_date)
     stored_file = tmp_path / f"testresult-{sample_date}_signals.pkl"
     with stored_file.open("rb") as scp:
         pickled_signal_candles = joblib.load(scp)


Code review comments:
diff_hunk: @@ -25,8 +25,8 @@
 
 
 def generate_trade_signal_candles(
-    preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any]
-) -> DataFrame:
+    preprocessed_df: Dict[str, DataFrame], bt_results: Dict[str, Any], analysis_on="open_date"
path: freqtrade/optimize/optimize_reports/optimize_reports.py
text: @user1:
i'd omit the "open_date" default.
all usages i can see ad-hock seem to provide it - and i'd not consider this function as external interface (which people import in their custom code).

@author:
makes sense.. also do you feel `analysis_on` is the correct variable name here? have been struggling with terminologies in this PR. :)

@user1:
i think in other contexts, we've used "column" or "date_col" (after quickly looking through the metrics.py file ...

@author:
cool.. i think i ll go with `date_col`. Thanks.

diff_hunk: @@ -140,6 +140,7 @@ def test_backtest_analysis_nomock(default_conf, mocker, caplog, testdatadir, use
     )
     start_analysis_entries_exits(args)
     captured = capsys.readouterr()
+    assert "Analysing on exit signals." not in captured.out
path: tests/data/test_entryexitanalysis.py
text: @user1:
checking for "non-existance" of a log or output message seems kinda difficult.
While i understand what you're trying to assert here - i'm not sure it's reliable.

i've found numerous instances in the past where the log-message was slightly changed - and the `not` test passed, simply because of a typo fix, not because it's actually testing what it should (if i change the print statement, this test will not bother).

If we assume this to be useful, you may want to extract this (and the corresponding line below) into a constant.
Alternatively, remove this assert - as i'd not see it as THAT useful (or necessary).

diff_hunk: @@ -18,15 +18,15 @@ freqtrade backtesting -c <config.json> --timeframe <tf> --strategy <strategy_nam
 ```
 
 This will tell freqtrade to output a pickled dictionary of strategy, pairs and corresponding
-DataFrame of the candles that resulted in buy signals. Depending on how many buys your strategy
-makes, this file may get quite large, so periodically check your `user_data/backtest_results`
-folder to delete old exports.
+DataFrame of the candles that resulted in buy and sell signals.
path: docs/advanced-backtesting.md
text: @user1:
maybe we can upate terminology to "entry and exit" signals ? 

that feels more fitting - as in a short context, current terminology seems wrong.