From 47a6ef4f00707e57fac348035d6092c9bd5d7b71 Mon Sep 17 00:00:00 2001 From: Nicolas Papp Date: Sun, 10 Apr 2022 12:53:47 -0300 Subject: [PATCH] Max relative drawdown --- docs/hyperopt.md | 7 ++- freqtrade/constants.py | 3 +- freqtrade/data/btanalysis.py | 23 ++++++---- .../hyperopt_loss_max_drawdown_relative.py | 45 +++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 9 +++- 5 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 3f613a208..bab062fad 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -116,7 +116,9 @@ optional arguments: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily, - CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss + CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, + MaxDrawDownRelativeHyperOptLoss, + ProfitDrawDownHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. --ignore-missing-spaces, --ignore-unparameterized-spaces @@ -563,7 +565,8 @@ Currently, the following loss functions are builtin: * `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation. * `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation. * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. -* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown. +* `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown. +* `MaxDrawDownRelativeHyperOptLoss` - Similar as the above, but also optimizes Maximum relative drawdown. * `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown. * `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a06e2771f..e2d4d9a13 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -28,7 +28,8 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', 'CalmarHyperOptLoss', - 'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss'] + 'MaxDrawDownHyperOptLoss', 'MaxDrawDownRelativeHyperOptLoss', + 'ProfitDrawDownHyperOptLoss'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index c8654cfda..66d7b4ad5 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -441,18 +441,22 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, return df -def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str - ) -> pd.DataFrame: +def _calc_drawdown_series(profit_results: pd.DataFrame, *, date_col: str, value_col: str, + starting_balance : Optional[float] = 0.0) -> pd.DataFrame: max_drawdown_df = pd.DataFrame() max_drawdown_df['cumulative'] = profit_results[value_col].cumsum() max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax() max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value'] max_drawdown_df['date'] = profit_results.loc[:, date_col] + if starting_balance: + cumulative_balance = starting_balance + max_drawdown_df['cumulative'] + max_balance = starting_balance + max_drawdown_df['high_value'] + max_drawdown_df['drawdown_relative'] = ((max_balance - cumulative_balance) / max_balance) return max_drawdown_df def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_ratio' + value_col: str = 'profit_ratio', starting_balance : Optional[float] = 0.0 ): """ Calculate max drawdown and the corresponding close dates @@ -466,13 +470,14 @@ def calculate_underwater(trades: pd.DataFrame, *, date_col: str = 'close_date', if len(trades) == 0: raise ValueError("Trade dataframe empty.") profit_results = trades.sort_values(date_col).reset_index(drop=True) - max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col) + max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance) return max_drawdown_df def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', - value_col: str = 'profit_abs', starting_balance: float = 0 + value_col: str = 'profit_abs', starting_balance: float = 0, + relative: bool = False ) -> Tuple[float, pd.Timestamp, pd.Timestamp, float, float, float]: """ Calculate max drawdown and the corresponding close dates @@ -488,9 +493,9 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' if len(trades) == 0: raise ValueError("Trade dataframe empty.") profit_results = trades.sort_values(date_col).reset_index(drop=True) - max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col) + max_drawdown_df = _calc_drawdown_series(profit_results, date_col=date_col, value_col=value_col, starting_balance=starting_balance) - idxmin = max_drawdown_df['drawdown'].idxmin() + idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative else max_drawdown_df['drawdown'].idxmin() if idxmin == 0: raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] @@ -499,8 +504,8 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' ['high_value'].idxmax(), 'cumulative'] low_val = max_drawdown_df.loc[idxmin, 'cumulative'] max_drawdown_rel = 0.0 - if high_val + starting_balance != 0: - max_drawdown_rel = (high_val - low_val) / (high_val + starting_balance) + if starting_balance != 0: + max_drawdown_rel = max_drawdown_df.loc[idxmin, 'drawdown_relative'] return ( abs(min(max_drawdown_df['drawdown'])), diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py b/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py new file mode 100644 index 000000000..c4dd843b8 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown_relative.py @@ -0,0 +1,45 @@ +""" +MaxDrawDownRelativeHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from datetime import datetime +from typing import Dict + +from pandas import DataFrame + +from freqtrade.data.btanalysis import calculate_underwater, calculate_max_drawdown +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +class MaxDrawDownRelativeHyperOptLoss(IHyperOptLoss): + + """ + Defines the loss function for hyperopt. + + This implementation optimizes for max draw down and profit + Less max drawdown more profit -> Lower return value + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, config: Dict, + *args, **kwargs) -> float: + + """ + Objective function. + + Uses profit ratio weighted max_drawdown when drawdown is available. + Otherwise directly optimizes profit ratio. + """ + total_profit = results['profit_abs'].sum() + try: + drawdown_df = calculate_underwater(results, value_col='profit_abs', starting_balance=config['available_capital']) + max_drawdown = abs(min(drawdown_df['drawdown'])) + relative_drawdown = max(drawdown_df['drawdown_relative']) + if max_drawdown == 0: + return -total_profit + return -total_profit / max_drawdown / relative_drawdown + except (Exception, ValueError): + return -total_profit + diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1966c7ad1..2bf09d71b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -474,9 +474,12 @@ def generate_strategy_stats(pairlist: List[str], (drawdown_abs, drawdown_start, drawdown_end, high_val, low_val, max_drawdown) = calculate_max_drawdown( results, value_col='profit_abs', starting_balance=start_balance) + (_, _, _, _, _, max_relative_drawdown) = calculate_max_drawdown( + results, value_col='profit_abs', starting_balance=start_balance, relative=True) strat_stats.update({ 'max_drawdown': max_drawdown_legacy, # Deprecated - do not use 'max_drawdown_account': max_drawdown, + 'max_relative_drawdown': max_relative_drawdown, 'max_drawdown_abs': drawdown_abs, 'drawdown_start': drawdown_start.strftime(DATETIME_PRINT_FORMAT), 'drawdown_start_ts': drawdown_start.timestamp() * 1000, @@ -497,6 +500,7 @@ def generate_strategy_stats(pairlist: List[str], strat_stats.update({ 'max_drawdown': 0.0, 'max_drawdown_account': 0.0, + 'max_relative_drawdown': 0.0, 'max_drawdown_abs': 0.0, 'max_drawdown_low': 0.0, 'max_drawdown_high': 0.0, @@ -760,10 +764,11 @@ def text_table_add_metrics(strat_results: Dict) -> str: strat_results['stake_currency'])), # Compatibility to show old hyperopt results - ('Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}") + ('Max % of account underwater', f"{strat_results['max_relative_drawdown']:.2%}"), + ('Absolute Drawdown (Account)', f"{strat_results['max_drawdown_account']:.2%}") if 'max_drawdown_account' in strat_results else ( 'Drawdown', f"{strat_results['max_drawdown']:.2%}"), - ('Drawdown', round_coin_value(strat_results['max_drawdown_abs'], + ('Absolute Drawdown', round_coin_value(strat_results['max_drawdown_abs'], strat_results['stake_currency'])), ('Drawdown high', round_coin_value(strat_results['max_drawdown_high'], strat_results['stake_currency'])),