From 692bb36c80d6cf2a2679f1ed7e5b3a74e1095238 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Apr 2025 17:06:43 +0200 Subject: [PATCH 1/8] Add new loss function based on profit/drawodown ratio per pair --- .../hyperopt_loss_max_drawdown_per_pair.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py 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..7d9a43bac --- /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, Dict +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 objetive, 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) + From 9373779fba49d229efe23a2d90b069a746184631 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Apr 2025 17:29:12 +0200 Subject: [PATCH 2/8] Fix for passing CI --- .../hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 7d9a43bac..d806d227d 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py @@ -6,6 +6,7 @@ Hyperoptimization. """ from typing import Any, Dict + from freqtrade.optimize.hyperopt import IHyperOptLoss class MaxDrawDownPerPairHyperOptLoss(IHyperOptLoss): @@ -22,8 +23,7 @@ class MaxDrawDownPerPairHyperOptLoss(IHyperOptLoss): """ @staticmethod - def hyperopt_loss_function(backtest_stats: Dict[str, Any], - *args, **kwargs) -> float: + def hyperopt_loss_function(backtest_stats: dict[str, Any], *args, **kwargs) -> float: """ Objective function, returns smaller number for better results. """ From 904580b91422f8c1490b02652effc9e3251b17fa Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Apr 2025 17:41:13 +0200 Subject: [PATCH 3/8] Fix 2 for passing CI --- .../hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index d806d227d..19ce29455 100644 --- a/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py +++ b/freqtrade/optimize/hyperopt_loss/hyperopt_loss_max_drawdown_per_pair.py @@ -5,16 +5,17 @@ This module defines the alternative HyperOptLoss class which can be used for Hyperoptimization. """ -from typing import Any, Dict +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 objetive, forcing hyperopt to optimize + 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 @@ -56,4 +57,3 @@ class MaxDrawDownPerPairHyperOptLoss(IHyperOptLoss): score_per_pair.append(score) return -min(score_per_pair) - From df50e03239d2b429382897f3b52246b562a1d161 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Apr 2025 22:58:42 +0200 Subject: [PATCH 4/8] Complete the integration on freqtrade --- freqtrade/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1b99dd6ec..964aab4c5 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -37,6 +37,7 @@ HYPEROPT_LOSS_BUILTIN = [ "CalmarHyperOptLoss", "MaxDrawDownHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss", + "MaxDrawDownPerPairHyperOptLoss", "ProfitDrawDownHyperOptLoss", "MultiMetricHyperOptLoss", ] From 9a3ada65f55c53dc068786a383a1a1000c205e10 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Apr 2025 23:12:51 +0200 Subject: [PATCH 5/8] Complete the integration on freqtrade --- docs/commands/hyperopt.md | 1 + docs/hyperopt.md | 1 + tests/optimize/test_hyperoptloss.py | 52 +++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 3 deletions(-) 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..89708486d 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` - Optimizes the profit/drawdown ratio calculated individually for each pair in the pairlist. * `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/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 8f1b1c786..7a356ace9 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,42 @@ 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 = [ + { + "key": "ETH/USDT", + "max_drawdown_abs": 25.0, + "profit_total_abs": 200.0, + }, + { + "key": "BTC/USDT", + "max_drawdown_abs": 25.0, + "profit_total_abs": 200.0, + } + ] + pair_results_under = [ + { + "key": "ETH/USDT", + "max_drawdown_abs": 100.0, + "profit_total_abs": 50.0, + }, + { + "key": "BTC/USDT", + "max_drawdown_abs": 100.0, + "profit_total_abs": 50.0, + } + ] default_conf.update({"hyperopt_loss": lossfunction}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) @@ -175,7 +212,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 +225,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 +238,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 From c2296d83c379138e64f79c5e6fdc3c0e98f88ec7 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Tue, 8 Apr 2025 23:17:34 +0200 Subject: [PATCH 6/8] Fix for passing CI --- tests/optimize/test_hyperoptloss.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 7a356ace9..87380ee0e 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -176,7 +176,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct "key": "BTC/USDT", "max_drawdown_abs": 50.0, "profit_total_abs": 100.0, - } + }, ] pair_results_over = [ { @@ -188,7 +188,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct "key": "BTC/USDT", "max_drawdown_abs": 25.0, "profit_total_abs": 200.0, - } + }, ] pair_results_under = [ { @@ -200,7 +200,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct "key": "BTC/USDT", "max_drawdown_abs": 100.0, "profit_total_abs": 50.0, - } + }, ] default_conf.update({"hyperopt_loss": lossfunction}) From 568c579cabed06f36ecf9249bfe43a24e3943631 Mon Sep 17 00:00:00 2001 From: mrpabloyeah Date: Thu, 10 Apr 2025 00:46:07 +0200 Subject: [PATCH 7/8] Improve documentation --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 89708486d..ecd32552e 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -471,7 +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` - Optimizes the profit/drawdown ratio calculated individually for each pair in the pairlist. +* `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. From 16379bbcc5648a7964cf1b2e9aa9112b1c3e3207 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Apr 2025 06:43:12 +0200 Subject: [PATCH 8/8] test: small adjustment to test (simplifying future usage) --- tests/optimize/test_hyperoptloss.py | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 87380ee0e..6d3110509 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -180,27 +180,19 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct ] pair_results_over = [ { - "key": "ETH/USDT", - "max_drawdown_abs": 25.0, - "profit_total_abs": 200.0, - }, - { - "key": "BTC/USDT", - "max_drawdown_abs": 25.0, - "profit_total_abs": 200.0, - }, + **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 = [ { - "key": "ETH/USDT", - "max_drawdown_abs": 100.0, - "profit_total_abs": 50.0, - }, - { - "key": "BTC/USDT", - "max_drawdown_abs": 100.0, - "profit_total_abs": 50.0, - }, + **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})