diff --git a/docs/commands/hyperopt.md b/docs/commands/hyperopt.md index 2fc522d0f..28ad45dd2 100644 --- a/docs/commands/hyperopt.md +++ b/docs/commands/hyperopt.md @@ -79,6 +79,7 @@ options: SortinoHyperOptLoss, SortinoHyperOptLossDaily, CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, MaxDrawDownRelativeHyperOptLoss, + MaxDrawDownPerPairHyperOptLoss, ProfitDrawDownHyperOptLoss, MultiMetricHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 51a6a5187..ecd32552e 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -471,6 +471,7 @@ Currently, the following loss functions are builtin: * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. * `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown. * `MaxDrawDownRelativeHyperOptLoss` - Optimizes both maximum absolute drawdown while also adjusting for maximum relative drawdown. +* `MaxDrawDownPerPairHyperOptLoss` - Calculates the profit/drawdown ratio per pair and returns the worst result as objective, forcing hyperopt to optimize the parameters for all pairs in the pairlist. This way, we prevent one or more pairs with good results from inflating the metrics, while the pairs with poor results are not represented and therefore not optimized. * `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. * `MultiMetricHyperOptLoss` - Optimizes by several key metrics to achieve balanced performance. The primary focus is on maximizing Profit and minimizing Drawdown, while also considering additional metrics such as Profit Factor, Expectancy Ratio and Winrate. Moreover, it applies a penalty for epochs with a low number of trades, encouraging strategies with adequate trade frequency. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fb1c3a4c0..a5c07ae1f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -37,6 +37,7 @@ HYPEROPT_LOSS_BUILTIN = [ "CalmarHyperOptLoss", "MaxDrawDownHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss", + "MaxDrawDownPerPairHyperOptLoss", "ProfitDrawDownHyperOptLoss", "MultiMetricHyperOptLoss", ] diff --git a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py new file mode 100644 index 000000000..19ce29455 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py @@ -0,0 +1,59 @@ +""" +MaxDrawDownPerPairHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" + +from typing import Any + +from freqtrade.optimize.hyperopt import IHyperOptLoss + + +class MaxDrawDownPerPairHyperOptLoss(IHyperOptLoss): + """ + Defines the loss function for hyperopt. + + This implementation calculates the profit/drawdown ratio per pair and + returns the worst result as objective, forcing hyperopt to optimize + the parameters for all pairs in the pairlist. + + This way, we prevent one or more pairs with good results from inflating + the metrics, while the rest of the pairs with poor results are not + represented and therefore not optimized. + """ + + @staticmethod + def hyperopt_loss_function(backtest_stats: dict[str, Any], *args, **kwargs) -> float: + """ + Objective function, returns smaller number for better results. + """ + + ############################################## + # Configurable parameters + ############################################## + # Minimum acceptable profit/drawdown per pair + min_acceptable_profit_dd = 1.0 + # Penalty when acceptable minimum are not met + penalty = 20 + ############################################## + + score_per_pair = [] + for p in backtest_stats["results_per_pair"]: + if p["key"] != "TOTAL": + profit = p.get("profit_total_abs", 0) + drawdown = p.get("max_drawdown_abs", 0) + + if drawdown != 0 and profit != 0: + profit_dd = profit / drawdown + else: + profit_dd = profit + + if profit_dd < min_acceptable_profit_dd: + score = profit_dd - penalty + else: + score = profit_dd + + score_per_pair.append(score) + + return -min(score_per_pair) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 8f1b1c786..6d3110509 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -153,6 +153,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> "SharpeHyperOptLossDaily", "MaxDrawDownHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss", + "MaxDrawDownPerPairHyperOptLoss", "CalmarHyperOptLoss", "ProfitDrawDownHyperOptLoss", "MultiMetricHyperOptLoss", @@ -165,6 +166,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct results_under = hyperopt_results.copy() results_under["profit_abs"] = hyperopt_results["profit_abs"] / 2 - 0.2 results_under["profit_ratio"] = hyperopt_results["profit_ratio"] / 2 + pair_results = [ + { + "key": "ETH/USDT", + "max_drawdown_abs": 50.0, + "profit_total_abs": 100.0, + }, + { + "key": "BTC/USDT", + "max_drawdown_abs": 50.0, + "profit_total_abs": 100.0, + }, + ] + pair_results_over = [ + { + **p, + "max_drawdown_abs": p["max_drawdown_abs"] * 0.5, + "profit_total_abs": p["profit_total_abs"] * 2, + } + for p in pair_results + ] + pair_results_under = [ + { + **p, + "max_drawdown_abs": p["max_drawdown_abs"] * 2, + "profit_total_abs": p["profit_total_abs"] * 0.5, + } + for p in pair_results + ] default_conf.update({"hyperopt_loss": lossfunction}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) @@ -175,7 +204,10 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct max_date=datetime(2019, 5, 1), config=default_conf, processed=None, - backtest_stats={"profit_total": hyperopt_results["profit_abs"].sum()}, + backtest_stats={ + "profit_total": hyperopt_results["profit_abs"].sum(), + "results_per_pair": pair_results, + }, starting_balance=default_conf["dry_run_wallet"], ) over = hl.hyperopt_loss_function( @@ -185,7 +217,10 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct max_date=datetime(2019, 5, 1), config=default_conf, processed=None, - backtest_stats={"profit_total": results_over["profit_abs"].sum()}, + backtest_stats={ + "profit_total": results_over["profit_abs"].sum(), + "results_per_pair": pair_results_over, + }, starting_balance=default_conf["dry_run_wallet"], ) under = hl.hyperopt_loss_function( @@ -195,7 +230,10 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct max_date=datetime(2019, 5, 1), config=default_conf, processed=None, - backtest_stats={"profit_total": results_under["profit_abs"].sum()}, + backtest_stats={ + "profit_total": results_under["profit_abs"].sum(), + "results_per_pair": pair_results_under, + }, starting_balance=default_conf["dry_run_wallet"], ) assert over < correct