Repository: freqtrade/freqtrade
Pull request: Bt analysis maxopen at any time
## Summary
Expose the algorithm to get max_open_trades at each candle to users.

Closes #2449

## Quick changelog

- Add `analyze_trade_parallelism()`
- Document `analyze_trade_parallelism()` as part of the juptyer notebook (Export of the notebook is in `strategy_analysis_example.md `).

Problem statement:
Strategy analysis -> how many max_open_trades i will use for this strategy ?
As an algo trade
I want to perform statistics like maximum drawdown, profit and find an optimum between stake_amount and max_open_trade so i run and export the backtest with --disable-max-market-positions flag and make simulations in jupyter.
So that i can find a comprise between risk and profit.

To archive that i need to find trades that happens at the same time and flag them (open_trade_number) and then filter then.

I don't know if it can be useful to have this straight in a column of the dataframe of load_backtest_data() ?

Maybe it is a too specific need.

Have a nice day.

Hints from issue discussion:
I don't think it should be part of `load_backtest_data()` - since this returns a dataframe with data per pair. Adding the open_trade_number to each line would taint it with non-pair information ... which i don't think is good.

Also (even worse), `load_backtest_data()` includes an open and close time per line. What would `"open_trade_number" = 20` mean for this record? 
20 open trades - at the open? close? maximum? .... it could vary greatly if you assume one pair is open for a few days.

It'll definitely need to be a seperate function, which returns a real timeseries (`load_backtest_data()` does not return a timeseries).

There is a `evaluate_result_multi()` in btanalysis, which (almost) does what you ask for, but compares it to the max_open_trades value passed in.

This can easily be refactored to 2 functions, one returning the timeseries contianing open trades per "interval" - and one doing the comparison.

Commit message: Add documentation sample for parallel_trade_analysis

Patch to review:
diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md
index 49800bbb31a..55f1bd908df 100644
--- a/docs/strategy_analysis_example.md
+++ b/docs/strategy_analysis_example.md
@@ -107,6 +107,22 @@ trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
 trades.groupby("pair")["sell_reason"].value_counts()
 ```
 
+## Analyze the loaded trades for trade parallelism
+This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.
+
+`parallel_trade_analysis()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle.
+
+
+```python
+from freqtrade.data.btanalysis import parallel_trade_analysis
+
+# Analyze the above
+parallel_trades = parallel_trade_analysis(trades, '5m')
+
+
+parallel_trades.plot()
+```
+
 ## Plot results
 
 Freqtrade offers interactive plotting capabilities based on plotly.
diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py
index 0f5d395ffb2..b9625e74512 100644
--- a/freqtrade/data/btanalysis.py
+++ b/freqtrade/data/btanalysis.py
@@ -52,16 +52,18 @@ def load_backtest_data(filename) -> pd.DataFrame:
     return df
 
 
-def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int) -> pd.DataFrame:
+def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataFrame:
     """
     Find overlapping trades by expanding each trade once per period it was open
-    and then counting overlaps
+    and then counting overlaps.
     :param results: Results Dataframe - can be loaded
-    :param freq: Frequency used for the backtest
-    :param max_open_trades: parameter max_open_trades used during backtest run
-    :return: dataframe with open-counts per time-period in freq
+    :param timeframe: Timeframe used for backtest
+    :return: dataframe with open-counts per time-period in timeframe
     """
-    dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq))
+    from freqtrade.exchange import timeframe_to_minutes
+    timeframe_min = timeframe_to_minutes(timeframe)
+    dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time,
+                                     freq=f"{timeframe_min}min"))
              for row in results[['open_time', 'close_time']].iterrows()]
     deltas = [len(x) for x in dates]
     dates = pd.Series(pd.concat(dates).values, name='date')
@@ -69,8 +71,23 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
 
     df2 = pd.concat([dates, df2], axis=1)
     df2 = df2.set_index('date')
-    df_final = df2.resample(freq)[['pair']].count()
-    return df_final[df_final['pair'] > max_open_trades]
+    df_final = df2.resample(f"{timeframe_min}min")[['pair']].count()
+    df_final = df_final.rename({'pair': 'open_trades'}, axis=1)
+    return df_final
+
+
+def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
+                          max_open_trades: int) -> pd.DataFrame:
+    """
+    Find overlapping trades by expanding each trade once per period it was open
+    and then counting overlaps
+    :param results: Results Dataframe - can be loaded
+    :param timeframe: Frequency used for the backtest
+    :param max_open_trades: parameter max_open_trades used during backtest run
+    :return: dataframe with open-counts per time-period in freq
+    """
+    df_final = analyze_trade_parallelism(results, timeframe)
+    return df_final[df_final['open_trades'] > max_open_trades]
 
 
 def load_trades_from_db(db_url: str) -> pd.DataFrame:
diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py
index 78781cffd02..b49344bbd26 100644
--- a/tests/data/test_btanalysis.py
+++ b/tests/data/test_btanalysis.py
@@ -10,7 +10,7 @@
                                        create_cum_profit,
                                        extract_trades_of_period,
                                        load_backtest_data, load_trades,
-                                       load_trades_from_db)
+                                       load_trades_from_db, analyze_trade_parallelism)
 from freqtrade.data.history import load_data, load_pair_history
 from tests.test_persistence import create_mock_trades
 
@@ -32,7 +32,7 @@ def test_load_backtest_data(testdatadir):
 
 
 @pytest.mark.usefixtures("init_persistence")
-def test_load_trades_db(default_conf, fee, mocker):
+def test_load_trades_from_db(default_conf, fee, mocker):
 
     create_mock_trades(fee)
     # remove init so it does not init again
@@ -84,6 +84,17 @@ def test_extract_trades_of_period(testdatadir):
     assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime
 
 
+def test_analyze_trade_parallelism(default_conf, mocker, testdatadir):
+    filename = testdatadir / "backtest-result_test.json"
+    bt_data = load_backtest_data(filename)
+
+    res = analyze_trade_parallelism(bt_data, "5m")
+    assert isinstance(res, DataFrame)
+    assert 'open_trades' in res.columns
+    assert res['open_trades'].max() == 3
+    assert res['open_trades'].min() == 0
+
+
 def test_load_trades(default_conf, mocker):
     db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock())
     bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock())
diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py
index ba87848ec81..5912c54891c 100644
--- a/tests/optimize/test_backtesting.py
+++ b/tests/optimize/test_backtesting.py
@@ -714,9 +714,9 @@ def _trend_alternate_hold(dataframe=None, metadata=None):
     results = backtesting.backtest(backtest_conf)
 
     # Make sure we have parallel trades
-    assert len(evaluate_result_multi(results, '5min', 2)) > 0
+    assert len(evaluate_result_multi(results, '5m', 2)) > 0
     # make sure we don't have trades with more than configured max_open_trades
-    assert len(evaluate_result_multi(results, '5min', 3)) == 0
+    assert len(evaluate_result_multi(results, '5m', 3)) == 0
 
     backtest_conf = {
         'stake_amount': default_conf['stake_amount'],
@@ -727,7 +727,7 @@ def _trend_alternate_hold(dataframe=None, metadata=None):
         'end_date': max_date,
     }
     results = backtesting.backtest(backtest_conf)
-    assert len(evaluate_result_multi(results, '5min', 1)) == 0
+    assert len(evaluate_result_multi(results, '5m', 1)) == 0
 
 
 def test_backtest_record(default_conf, fee, mocker):
diff --git a/user_data/notebooks/strategy_analysis_example.ipynb b/user_data/notebooks/strategy_analysis_example.ipynb
index b9576e0bb32..edb05a7ca2f 100644
--- a/user_data/notebooks/strategy_analysis_example.ipynb
+++ b/user_data/notebooks/strategy_analysis_example.ipynb
@@ -68,9 +68,7 @@
   {
    "cell_type": "code",
    "execution_count": null,
-   "metadata": {
-    "scrolled": true
-   },
+   "metadata": {},
    "outputs": [],
    "source": [
     "# Load strategy using values set above\n",
@@ -169,6 +167,31 @@
     "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
    ]
   },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Analyze the loaded trades for trade parallelism\n",
+    "This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n",
+    "\n",
+    "`parallel_trade_analysis()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from freqtrade.data.btanalysis import parallel_trade_analysis\n",
+    "\n",
+    "# Analyze the above\n",
+    "parallel_trades = parallel_trade_analysis(trades, '5m')\n",
+    "\n",
+    "\n",
+    "parallel_trades.plot()"
+   ]
+  },
   {
    "cell_type": "markdown",
    "metadata": {},


Code review comments:
diff_hunk: @@ -107,6 +107,22 @@ trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
 trades.groupby("pair")["sell_reason"].value_counts()
 ```
 
+## Analyze the loaded trades for trade parallelism
+This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.
+
+`parallel_trade_analysis()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle.
path: docs/strategy_analysis_example.md
text: @user1:
seems like there is no analyze_trade_parallelism() you call it analyze_trade_parallelism() ?

@author:
nice catch - i renamed the function last-minute since i found `analyze_trade_parallelism()` to be easier to grasp ... but apparently forgot to swap it everywhere ...