Repository: freqtrade/freqtrade

Issue:
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.


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.



Resolving patch:
diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py
--- 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:


Test patch:
diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py
--- 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
--- 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):