Merge pull request #11618 from mrpabloyeah/hyperopt_loss_max_drawdown_per_pair

Add new loss function based on profit/drawdown ratio per pair
This commit is contained in:
Matthias
2025-04-10 07:14:34 +02:00
committed by GitHub
5 changed files with 103 additions and 3 deletions

View File

@@ -79,6 +79,7 @@ options:
SortinoHyperOptLoss, SortinoHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily,
CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, CalmarHyperOptLoss, MaxDrawDownHyperOptLoss,
MaxDrawDownRelativeHyperOptLoss, MaxDrawDownRelativeHyperOptLoss,
MaxDrawDownPerPairHyperOptLoss,
ProfitDrawDownHyperOptLoss, MultiMetricHyperOptLoss ProfitDrawDownHyperOptLoss, MultiMetricHyperOptLoss
--disable-param-export --disable-param-export
Disable automatic hyperopt parameter export. Disable automatic hyperopt parameter export.

View File

@@ -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. * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation.
* `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown. * `MaxDrawDownHyperOptLoss` - Optimizes Maximum absolute drawdown.
* `MaxDrawDownRelativeHyperOptLoss` - Optimizes both maximum absolute drawdown while also adjusting for maximum relative 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. * `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. * `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. * `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.

View File

@@ -37,6 +37,7 @@ HYPEROPT_LOSS_BUILTIN = [
"CalmarHyperOptLoss", "CalmarHyperOptLoss",
"MaxDrawDownHyperOptLoss", "MaxDrawDownHyperOptLoss",
"MaxDrawDownRelativeHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss",
"MaxDrawDownPerPairHyperOptLoss",
"ProfitDrawDownHyperOptLoss", "ProfitDrawDownHyperOptLoss",
"MultiMetricHyperOptLoss", "MultiMetricHyperOptLoss",
] ]

View File

@@ -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)

View File

@@ -153,6 +153,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) ->
"SharpeHyperOptLossDaily", "SharpeHyperOptLossDaily",
"MaxDrawDownHyperOptLoss", "MaxDrawDownHyperOptLoss",
"MaxDrawDownRelativeHyperOptLoss", "MaxDrawDownRelativeHyperOptLoss",
"MaxDrawDownPerPairHyperOptLoss",
"CalmarHyperOptLoss", "CalmarHyperOptLoss",
"ProfitDrawDownHyperOptLoss", "ProfitDrawDownHyperOptLoss",
"MultiMetricHyperOptLoss", "MultiMetricHyperOptLoss",
@@ -165,6 +166,34 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct
results_under = hyperopt_results.copy() results_under = hyperopt_results.copy()
results_under["profit_abs"] = hyperopt_results["profit_abs"] / 2 - 0.2 results_under["profit_abs"] = hyperopt_results["profit_abs"] / 2 - 0.2
results_under["profit_ratio"] = hyperopt_results["profit_ratio"] / 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}) default_conf.update({"hyperopt_loss": lossfunction})
hl = HyperOptLossResolver.load_hyperoptloss(default_conf) 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), max_date=datetime(2019, 5, 1),
config=default_conf, config=default_conf,
processed=None, 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"], starting_balance=default_conf["dry_run_wallet"],
) )
over = hl.hyperopt_loss_function( 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), max_date=datetime(2019, 5, 1),
config=default_conf, config=default_conf,
processed=None, 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"], starting_balance=default_conf["dry_run_wallet"],
) )
under = hl.hyperopt_loss_function( 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), max_date=datetime(2019, 5, 1),
config=default_conf, config=default_conf,
processed=None, 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"], starting_balance=default_conf["dry_run_wallet"],
) )
assert over < correct assert over < correct