Repository: freqtrade/freqtrade

Issue:
Showing on CAGR and MDD in backtest results
This is a very simple enchantment, but I think it is so helpful for intuitively comparing strat's performances.

<img width="656" alt="Example" src="https://user-images.githubusercontent.com/85345804/121263953-5bef1200-c8f1-11eb-9e71-89b45f29d48c.PNG">
Such as this image, showing CAGR and MDD is extremely intuitive for comparing strategies, higher CAGR(profitability) and lower MDD(risk) is better!

It's about showing CAGR (geometric mean of yearly profits, `(current total stake)/(starting wallet)^(1/years passed)`, (simply,) years_passed = days_passed/365)  and MDD (`((strat's)max - min / max) * 100%`, currently freqtrade has the _drawdown_, but it is unintuitive because it gives ratio based on starting wallet stakes).

This will make freqtrade's backtests look more intuitive and easier to compare strats.
How do you guys think about this very little enchantments?


Discussion:
~~_**Consideration:**_ (this is not essential)
Maybe adding the sharp ratio for backtest can be also good. And we have codes in the hyperopt so all we need to do is just using that code. But can there be a little performance changes for backtesting? If so, just adding option params(such as --sharp true) to show sharp ratio might be good.~~
we do have an issue for sharp already (#1907 ) - but so far, it was not important enough for anyone to actually spend time to implement it.

We have a section for strategy metrics - i'll surely not mind having others added there as well if they make sense.
I'm however not sure what MDD will be based on - you say "max - min / max" ... but max / min of what? absolute wallet? relative profits ... you can do min/max calculations on many things - so it's kinda unclear to me for now how this calculation should look like.
That means absolute wallet including current open positions(maximum total stake - minimum total stake / maximum total stake)

I think it has already there named max drawdown high and low. That will do exact thing. 
The problem of current drawdown ratio is it is calculated through (high-low / starting stake) but it is not intuitive because profit compounds, so if drawdown happened later on, it gives bigger ratio, so it highly depends on when max drawdowns happened. So adding one more metrics that does not influenced by compounding. (And I found this exact thing extremely useful in other systemic trading tools, they are using it too.)

![1](https://user-images.githubusercontent.com/85345804/121329020-c9319000-c94f-11eb-94c5-42f5bf9f4354.png)
This means (-)21% MDD 

(And for sharp ratio - I agree it's not a necessary thing. So can bother with it later.)
Hello, Guitheg and me have implemented the "MDD". We computed MDD on each trade since drawdown are always linked to a position, then we took the one with maximum amplitude. In fact, we computed de maximum MDD in the set of trades.

With our function, people can see the worst drawdown among all their trades.

systemquant : We didn't really understand "That means absolute wallet including current open positions(maximum total stake - minimum total stake / maximum total stake)".
Do you mean considering the value of all position (in USDT for exemple), sum them all to make a function that fluctuate like crypto market curve then compute a "MDD" on it. By the way it won't be a true MDD but it could be interesting

We doing the pull request right now
EDIT : The issue is not closed since CAGR is not implemented yet


Resolving patch:
diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py
--- a/freqtrade/data/btanalysis.py
+++ b/freqtrade/data/btanalysis.py
@@ -553,3 +553,14 @@ def calculate_csum(trades: pd.DataFrame, starting_balance: float = 0) -> Tuple[f
     csum_max = csum_df['sum'].max() + starting_balance
 
     return csum_min, csum_max
+
+
+def calculate_cagr(days_passed: int, starting_balance: float, final_balance: float) -> float:
+    """
+    Calculate CAGR
+    :param days_passed: Days passed between start and ending balance
+    :param starting_balance: Starting balance
+    :param final_balance: Final balance to calculate CAGR against
+    :return: CAGR
+    """
+    return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1
diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py
--- a/freqtrade/optimize/optimize_reports.py
+++ b/freqtrade/optimize/optimize_reports.py
@@ -9,7 +9,7 @@
 from tabulate import tabulate
 
 from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN, UNLIMITED_STAKE_AMOUNT
-from freqtrade.data.btanalysis import (calculate_csum, calculate_market_change,
+from freqtrade.data.btanalysis import (calculate_cagr, calculate_csum, calculate_market_change,
                                        calculate_max_drawdown)
 from freqtrade.misc import (decimals_per_coin, file_dump_joblib, file_dump_json,
                             get_backtest_metadata_filename, round_coin_value)
@@ -446,6 +446,7 @@ def generate_strategy_stats(pairlist: List[str],
         'profit_total_abs': results['profit_abs'].sum(),
         'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
         'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
+        'cagr': calculate_cagr(backtest_days, start_balance, content['final_balance']),
         'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
         'backtest_start_ts': int(min_date.timestamp() * 1000),
         'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
@@ -746,6 +747,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
             ('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
                                                   strat_results['stake_currency'])),
             ('Total profit %', f"{strat_results['profit_total']:.2%}"),
+            ('CAGR %', f"{strat_results['cagr']:.2%}"),
             ('Trades per day', strat_results['trades_per_day']),
             ('Avg. daily profit %',
              f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),


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
@@ -8,13 +8,13 @@
 
 from freqtrade.configuration import TimeRange
 from freqtrade.constants import LAST_BT_RESULT_FN
-from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_csum,
-                                       calculate_market_change, calculate_max_drawdown,
-                                       calculate_underwater, combine_dataframes_with_mean,
-                                       create_cum_profit, extract_trades_of_period,
-                                       get_latest_backtest_filename, get_latest_hyperopt_file,
-                                       load_backtest_data, load_backtest_metadata, load_trades,
-                                       load_trades_from_db)
+from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_cagr,
+                                       calculate_csum, calculate_market_change,
+                                       calculate_max_drawdown, calculate_underwater,
+                                       combine_dataframes_with_mean, create_cum_profit,
+                                       extract_trades_of_period, get_latest_backtest_filename,
+                                       get_latest_hyperopt_file, load_backtest_data,
+                                       load_backtest_metadata, load_trades, load_trades_from_db)
 from freqtrade.data.history import load_data, load_pair_history
 from freqtrade.exceptions import OperationalException
 from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades
@@ -336,6 +336,19 @@ def test_calculate_csum(testdatadir):
         csum_min, csum_max = calculate_csum(DataFrame())
 
 
+@pytest.mark.parametrize('start,end,days, expected', [
+    (64900, 176000, 3 * 365, 0.3945),
+    (64900, 176000, 365, 1.7119),
+    (1000, 1000, 365, 0.0),
+    (1000, 1500, 365, 0.5),
+    (1000, 1500, 100, 3.3927),  # sub year
+    (0.01000000, 0.01762792, 120, 4.6087),  # sub year BTC values
+])
+def test_calculate_cagr(start, end, days, expected):
+
+    assert round(calculate_cagr(days, start, end), 4) == expected
+
+
 def test_calculate_max_drawdown2():
     values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
               -0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,