
Dans ce guide, je vais partager ma façon de travailler avec l'hyperopt de Freqtrade, mon expérience avec chaque hyperopt-loss, et comment je choisis la bonne fonction selon mes besoins.
Objectif de ce post : Comprendre l'hyperopt de Freqtrade, découvrir chaque hyperopt-loss disponible, et apprendre à choisir selon ton style de trading. Du débutant qui découvre l'optimisation à l'expert qui veut créer sa propre hyperopt-loss custom, je partage mes méthodes et retours d'expérience !
Comme d'habitude, si ce post t'aide, n'hĂ©site pas Ă le partager et Ă me soutenir avec un cafĂ© â - cela m'aide Ă maintenir ces guides techniques Ă jour !
1ïžâŁ L'Hyperopt Freqtrade : Les Fondamentaux
Qu'est-ce que l'hyperopt en réalité ?
L'hyperopt (hyperparameter optimization), c'est ton assistant personnel qui va tester des milliers de combinaisons de paramÚtres pour faire des backtests automatiques. Il analyse ensuite les résultats de tous ces backtests et retient la meilleure configuration selon les rÚgles définies par l'hyperopt-loss. Plus besoin de modifier manuellement tes seuils RSI ou tes niveaux de take profit - Freqtrade le fait pour toi !
â ïž Point important : L'hyperopt reste dĂ©pendant de la qualitĂ© des backtests. Si le backtest est faillible (et il l'est toujours un peu), l'hyperopt le sera aussi. C'est mieux que rien, mais il faut rester vigilant !
Comment ça fonctionne techniquement :
đČ Phase 1 : Une succession de combinaisons alĂ©atoires pour explorer l'espace, en gros les paramĂštres possibles sont testĂ©s de façon totalement alĂ©atoire.
đ§ Phase 2 : Puis dans un second temps, optimisation bayĂ©sienne avec l'algorithme NSGAIIISampler d'Optuna, en gros il s'agit d'un affinement
L'epochs (l'itération de l'hyperopt) à laquelle commence cet affinement varie selon la quantité d'epoch indiquée en paramÚtre
--epochdans la commande de l'hyperopt. Et du peu de recherche que j'ai fait : l'epoch Ă laquelle commence cette phase 2 n'est pas ajustable autrement qu'en allant modifier le code en profondeur.
đ„ RĂ©sultat : Si la commande est coupĂ©e avec CTRL+C ou que l'hyperopt a terminĂ© : La meilleure combinaison selon le critĂšre choisi (hyperopt-loss) est enregistrĂ© dans un fichier .json qui contiendra la valeur Ă mettre pour chaque paramĂštre.
â ïž QualitĂ© du code : Un prĂ©requis essentiel
Le backtest sera d'autant plus faillible si le code est de mauvaise qualité. Attention particuliÚrement à :
Lookahead bias : Utilisation d'informations futures dans le passé
Récursivité : Calculs qui s'auto-alimentent de maniÚre incorrecte
Signaux mal définis : Conditions d'entrée/sortie ambiguës
đĄ Mon conseil : Toujours valider la qualitĂ© de ton code avant l'optimisation !
Les espaces d'optimisation disponibles
Mode opératoire : Chaque espace peut disposer d'un à plusieurs paramÚtres d'optimisation. Il faudra alors :
Déclarer ce/ces paramÚtres
Les borner (valeur min et max)
Leur attribuer une valeur par défaut
Types de paramĂštres disponibles
Freqtrade propose 4 types de paramĂštres pour l'optimisation :
1. IntParameter - Pour les nombres entiers
# Exemple : Période du RSI entre 10 et 40 buy_rsi_period = IntParameter(10, 40, default=14, space='buy') # Dans la stratégie dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.buy_rsi_period.value)
2. DecimalParameter - Pour les nombres décimaux
# Exemple : Seuil RSI entre 20.0 et 40.0 avec précision de 1 décimale buy_rsi_threshold = DecimalParameter(20.0, 40.0, default=30.0, decimals=1, space='buy') # Dans la stratégie conditions.append(dataframe['rsi'] < self.buy_rsi_threshold.value)
3. CategoricalParameter - Pour des choix parmi une liste
# Exemple : Choisir le type de moyenne mobile buy_ma_type = CategoricalParameter(['sma', 'ema', 'wma'], default='ema', space='buy') # Dans la stratégie if self.buy_ma_type.value == 'sma': dataframe['ma'] = ta.SMA(dataframe, timeperiod=20) elif self.buy_ma_type.value == 'ema': dataframe['ma'] = ta.EMA(dataframe, timeperiod=20) elif self.buy_ma_type.value == 'wma': dataframe['ma'] = ta.WMA(dataframe, timeperiod=20)
4. BooleanParameter - Pour activer/désactiver une condition
# Exemple : Activer ou non une condition de volume buy_volume_check = BooleanParameter(default=True, space='buy') # Dans la stratégie if self.buy_volume_check.value: conditions.append(dataframe['volume'] > dataframe['volume'].shift(1))
Exemple complet d'une stratégie avec paramÚtres optimisables
class OptimizableStrategy(IStrategy):
# ParamĂštres d'achat
buy_rsi_period = IntParameter(5, 50, default=14, space='buy')
buy_rsi_threshold = DecimalParameter(20, 40, default=30, decimals=1, space='buy')
buy_trigger = CategoricalParameter(['rsi', 'bb', 'macd'], default='rsi', space='buy')
buy_volume_check = BooleanParameter(default=True, space='buy')
# ParamĂštres de vente
sell_rsi_threshold = DecimalParameter(60, 80, default=70, decimals=1, space='sell')
sell_take_profit = DecimalParameter(0.01, 0.10, default=0.03, decimals=3, space='sell')
# ParamĂštres de protection
stoploss = DecimalParameter(-0.35, -0.02, default=-0.10, decimals=2, space='stoploss')
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# RSI avec période optimisable
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=self.buy_rsi_period.value)
# Autres indicateurs...
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# Condition selon le trigger choisi
if self.buy_trigger.value == 'rsi':
conditions.append(dataframe['rsi'] < self.buy_rsi_threshold.value)
elif self.buy_trigger.value == 'bb':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
# etc...
# Condition de volume optionnelle
if self.buy_volume_check.value:
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[reduce(lambda x, y: x & y, conditions), 'buy'] = 1
return dataframe
Les espaces que tu peux optimiser
Tu peux optimiser différents aspects de ta stratégie selon tes besoins :
buy : ParamÚtres des signaux d'entrée (RSI, MACD, etc.)
sell : ParamĂštres des signaux de sortie
roi : Table de ROI minimal (quand prendre tes bénéfices)
stoploss : Niveau de stop-loss (protection contre les pertes)
trailing : Configuration du trailing stop (suivre les prix)
protection : ParamĂštres des protections (cooldown, etc.)
all : Optimise tout sauf trailing, trades et protection
đĄ Mon conseil : Je commence toujours par optimiser un seul espace Ă la fois pour comprendre l'impact de chaque paramĂštre.
Pour celles et ceux qui me soutiennent, j'ai rĂ©alisĂ© un guide complet pour expliquer comment prĂ©parer une stratĂ©gie Ă l'hyperopt : đŻđ„ Comment optimiser une stratĂ©gie ? Guide complet + VidĂ©o
đŻ Max Open Trades : Le paramĂštre critique
Note d'un trader algorithmique : Un trader nommé "Bigbroseur" m'a conseillé de mentionner son approche concernant le paramÚtre max_open_trades (MOT). Pour lui, aprÚs plusieurs essais, un MOT à 3 est idéal pour toutes les stratégies (backtest, hyperopt, dry-run et live).
Ce n'est pas forcément ma façon de faire, mais il trouve pertinent que j'en fasse mention : avec un MOT de 3, ses résultats restent cohérents. DÚs qu'il s'éloigne de cette valeur, il observe des effets de bord qui vont souvent dans le mauvais sens. Plus il s'éloigne de 3, plus c'est problématique selon son expérience.
Je vais tester cette approche de me limiter à un MOT de 3 durant les prochains mois pour mes nouvelles stratégies et je ferai un post à ce sujet si cela rejoint l'approche de Bigbroseur.
Comment l'algorithme "apprend" de chaque epoch
L'optimisation bayésienne, concrÚtement : Au lieu de tester des combinaisons au hasard pendant 1000 epochs, l'algorithme NSGAIIISampler d'Optuna devient plus intelligent à chaque itération. Voici comment ça fonctionne dans Freqtrade :
Les X premiĂšres epochs sont purement alĂ©atoires - l'algorithme explore l'espace des paramĂštres sans a priori. C'est la phase d'exploration oĂč on teste un peu partout pour comprendre le "paysage" des possibilitĂ©s. Tu verras souvent des rĂ©sultats trĂšs variĂ©s (et souvent mauvais) dans cette phase.
AprĂšs ces X premiĂšres epochs, la magie opĂšre : l'algorithme analyse tous les rĂ©sultats prĂ©cĂ©dents et construit un modĂšle probabiliste. Il se dit : "OK, quand RSI Ă©tait entre 25-30 et le stoploss Ă -5%, j'ai eu de bons rĂ©sultats. Testons des valeurs proches." C'est la phase d'exploitation oĂč l'algorithme affine progressivement autour des zones prometteuses.
ConcrĂštement pendant l'optimisation :
Les X premiÚres epochs : Résultats erratiques, l'algo explore (ex: profit de -50% à +30%)
Ensuite, les epochs suivantes : Convergence progressive, l'algo trouve les bonnes zones puis fait un affinement fin, gains marginaux (ex: passe de +25% Ă +27%)
Le problÚme c'est d'aller trop loin dans cette convergence progressive : Risque de sur-optimisation, l'algo cherche des micro-améliorations
Mon observation personnelle sur le nombre d'epochs :
200 epochs : Pour aller vite et avoir une idée si une stratégie est gagnante ou perdante
500 epochs : C'est du sûr, bon équilibre entre temps et résultats
1000 epochs : Le maximum raisonnable
Au-delà de 1000 : On dérive sur de la sur-optimisation - l'algo optimise du bruit plutÎt que du signal
Le piÚge à éviter : L'algorithme va TOUJOURS trouver une meilleure combinaison si tu le laisses tourner assez longtemps, mais "meilleur sur l'historique" ne veut pas dire "meilleur pour le futur".
Pour faire une métaphore bancale, imaginez des élÚves à l'école. Pendant un examen :
Un élÚve A : mémorise les réponses d'un examen par coeur sans forcément comprendre ce qu'il écrit (overfitting).
Un élÚve B : comprend la matiÚre et son cours (généralisation) mais ne connait pas les réponses par coeur à l'examen.
Lors du premier examen, l'élÚve A aura certainement une meilleure note que l'élÚve B. Néanmoins il est fortement probable que l'élÚve B ait de meilleurs notes à tous les prochains examens, tandis que l'élÚve A aura des notes catastrophiques.
Imaginez que le premier examen est un backtest, les examens suivants correspondent Ă une execution en dry-run ou en live-run. Vous voulez ĂȘtre l'Ă©lĂšve A ou l'Ă©lĂšve B ?
2ïžâŁ Les 10 Hyperopt-Loss de Freqtrade :
Chaque hyperopt-loss va privilégier différents aspects de ta stratégie. Pour mieux comprendre, voici 3 résultats de backtest fictifs que j'utiliserai pour tous les exemples :
đ Les 3 Backtests de RĂ©fĂ©rence
Backtest A - "Le Rentable Risqué" :
Profit total : +45%
Max drawdown : -18%
Win rate : 48%
Nombre de trades : 156
Durée moyenne : 8.5 heures
Profit factor : 1.4
Sharpe ratio : 1.2
Backtest B - "L'ĂquilibrĂ©" :
Profit total : +28%
Max drawdown : -6%
Win rate : 62%
Nombre de trades : 89
Durée moyenne : 4.2 heures
Profit factor : 1.85
Sharpe ratio : 2.4
Backtest C - "Le Conservateur Rapide" :
Profit total : +15%
Max drawdown : -3%
Win rate : 71%
Nombre de trades : 234
Durée moyenne : 1.5 heures
Profit factor : 2.1
Sharpe ratio : 1.8
Maintenant, voyons quelle stratégie chaque hyperopt-loss privilégierait et pourquoi.
đââïž ShortTradeDurHyperOptLoss
Ce qu'elle fait : Optimise pour des trades courts et évite les pertes
RĂ©sultat choisi : Backtest C â
Durée moyenne la plus courte (1.5h)
Win rate élevé (71%)
Drawdown minimal
Pourquoi ? ShortTradeDur privilĂ©gie la durĂ©e courte des trades avant tout. MĂȘme si le profit est moindre, elle prĂ©fĂšre les stratĂ©gies qui ne gardent pas les positions longtemps.
Mon avis : Intéressant pour du scalping ou si vous tradez sur des timeframes trÚs courtes. Pour en savoir plus sur les différents types de stratégies (scalping, swing, etc.), consultez mon guide complet sur les stratégies Freqtrade.
Commande type :
freqtrade hyperopt --config config.json \ --hyperopt-loss ShortTradeDurHyperOptLoss \ --strategy ScalpingStrategy \ -e 500 \ --spaces buy sell
đ° OnlyProfitHyperOptLoss
Ce qu'elle fait : Se concentre uniquement sur le profit absolu total
RĂ©sultat choisi : Backtest A â
Profit maximum (+45%)
Ignore totalement le drawdown de -18%
Win rate faible non pénalisé
Pourquoi ? OnlyProfit ne regarde QUE le profit final. Le risque n'entre pas dans l'équation.
Mon expĂ©rience : Je l'utilise rarement seule car elle peut crĂ©er des stratĂ©gies trĂšs volatiles. Utile en premiĂšre approche pour voir le potentiel maximum, mais Ă affiner ensuite. N'espĂ©rez jamais avoir les mĂȘmes rĂ©sultats
đ SharpeHyperOptLoss / SharpeHyperOptLossDaily
Ce qu'elle fait : Optimise le ratio de Sharpe (rendement/volatilité)
RĂ©sultat choisi : Backtest B â
Meilleur Sharpe ratio (2.4)
Ăquilibre profit/risque optimal
Volatilité maßtrisée
Pourquoi ? Le ratio de Sharpe cherche le meilleur rendement ajusté au risque. Backtest B offre le meilleur compromis.
Mon utilisation : J'utilise souvent SharpeHyperOptLossDaily pour une premiÚre optimisation car elle donne des résultats équilibrés.
đŻ SortinoHyperOptLoss / SortinoHyperOptLossDaily
Ce qu'elle fait : Version améliorée du Sharpe, se concentre uniquement sur le risque de baisse
Résultat choisi : Backtest B ou C selon les paramÚtres
B si on privilégie le rendement
C si on privilégie la protection contre les pertes
Pourquoi ? Sortino ne pénalise que la volatilité négative. Les gains importants ne sont pas considérés comme du "risque".
Mon point de vue : Plus pertinent que Sharpe selon moi, car la volatilité positive (les gros gains) n'est pas un problÚme !
đĄïž CalmarHyperOptLoss
Ce qu'elle fait : Optimise le ratio de Calmar (rendement annualisé / max drawdown)
RĂ©sultat choisi : Backtest C â
Ratio Calmar : 15%/3% = 5.0 (excellent)
B aurait : 28%/6% = 4.67
A aurait : 45%/18% = 2.5
Pourquoi ? Calmar dĂ©teste les drawdowns. MĂȘme avec moins de profit, C gagne grĂące Ă son DD minimal.
Mon usage : Je l'utilise souvent comme premiÚre approche sur une longue période. C'est ma fonction préférée pour des stratégies destinées à tourner sans surveillance.
đ MaxDrawDownHyperOptLoss
Ce qu'elle fait : Minimise le drawdown absolu maximum
RĂ©sultat choisi : Backtest C â
Drawdown minimal (-3%)
Profit secondaire dans le calcul
Pourquoi ? Cette fonction cherche avant tout à protéger le capital. Le profit reste important mais passe aprÚs la sécurité.
Mon conseil : Parfait pour les dĂ©butants ou pour des comptes avec peu de marge de manĆuvre. Pour comprendre l'importance du drawdown dans vos backtests, consultez mon guide complet sur les backtests.
âïž MaxDrawDownRelativeHyperOptLoss
Ce qu'elle fait : Optimise drawdown absolu ET relatif simultanément
RĂ©sultat choisi : Backtest B â
Meilleur équilibre DD absolu/relatif
Profit correct avec risque maßtrisé
Mon avis : Plus intelligent que MaxDrawDown simple car il prend en compte l'évolution du capital.
đ MaxDrawDownPerPairHyperOptLoss
(Addition récente et intéressante dans l'une des derniÚre version de freqtrade)
Ce qu'elle fait : Calcule profit/drawdown par paire, retourne le PIRE résultat
Cette hyperopt-loss rĂ©cente force votre stratĂ©gie Ă ĂȘtre Ă©quilibrĂ©e sur l'ensemble du portfolio. D'aprĂšs mon expĂ©rience, c'est trĂšs efficace pour Ă©viter les stratĂ©gies qui ne fonctionnent que sur quelques paires.
Exemple avec nos backtests : Si Backtest B a ces résultats par paire :
BTC/USDT: +8% profit, -2% DD â
ETH/USDT: +7% profit, -3% DD â
ADA/USDT: +5% profit, -4% DD â
SOL/USDT: +8% profit, -2% DD â â Score basĂ© sur ADA (le pire)
Mon utilisation : Je trouve cette approche trÚs stricte mais utile pour des stratégies multi-paires. Elle garantit qu'aucune paire n'est laissée pour compte et évite sur sur-optimisation sur seulement quelques paires.
đ§ ProfitDrawDownHyperOptLoss
Ce qu'elle fait : Optimise profit et drawdown avec pondération ajustable
Résultat choisi : Dépend du DRAWDOWN_MULT
DRAWDOWN_MULT = 0.05 â Backtest A (focus profit)
DRAWDOWN_MULT = 0.10 â Backtest B (Ă©quilibrĂ©)
DRAWDOWN_MULT = 0.15 â Backtest C (focus sĂ©curitĂ©)
Mon approche : J'ajuste DRAWDOWN_MULT selon le type de compte. Pour modifier ce paramÚtre, éditez directement le fichier de la classe dans le code source de Freqtrade.
đ MultiMetricHyperOptLoss (Mon choix pour les dĂ©butants)
Ce qu'elle fait : Optimise simultanément profit, drawdown, profit factor, expectancy et win rate
RĂ©sultat choisi : Backtest B â
Meilleur score composite sur toutes les métriques
Profit factor équilibré (1.85)
Win rate correct (62%)
Drawdown acceptable (-6%)
Pourquoi je la recommande aux débutants : D'aprÚs mon expérience, MultiMetricHyperOptLoss offre le meilleur équilibre sans configuration complexe. Elle évite naturellement les piÚges classiques :
Pas de sur-optimisation sur une seule métrique
Résultats généralement stables et reproductibles
Protection automatique contre les stratĂ©gies extrĂȘmes
Configuration pour débutants :
# Commande simple que je recommande pour commencer
freqtrade hyperopt --hyperopt-loss MultiMetricHyperOptLoss \
--strategy TaPremiereStrategie \
--timerange 20240101-20241201 \
-e 500 \
--spaces buy sell roi \
--min-trades 50
Mon conseil : Si tu débutes, commence avec MultiMetricHyperOptLoss. C'est selon moi le choix le plus sûr pour avoir une stratégie équilibrée.
3ïžâŁ MoutonHyperOptLoss : Ma premiĂšre crĂ©ation custom
J'ai développé MoutonHyperOptLoss avant que MultiMetricHyperOptLoss n'existe. à l'époque, je cherchais une solution pour privilégier les performances récentes et éviter l'over-fitting sur des données anciennes.
Principe de MoutonHyperOptLoss
Mon idée était d'introduire :
Pondération trimestrielle : Les trimestres récents comptent plus dans le score
R-squared : Pour mesurer la stabilité de la courbe d'équité
Gestion du drawdown : Pénalité exponentielle selon mes préférences
ParamĂštres ajustables (dans le fichier)
MIN_TRADES = 1 # Trades minimum par trimestre
DECAY_RATE = 0.80 # -20% d'importance par trimestre passé
DRAWDOWN_WEIGHT = 12 # Sévérité de la pénalité DD
MAX_DRAWDOWN_ALLOWED = 0.28 # DD max toléré
GLOBAL_WEIGHT = 0.6 # Poids du score global vs trimestriel
Mon usage actuel : Je l'utilise encore parfois pour des optimisations sur de trĂšs longues pĂ©riodes (2+ ans) oĂč je veux vraiment privilĂ©gier les performances rĂ©centes. Pour comprendre l'importance de la pĂ©riode de backtest, voir mon post sur les piĂšges des backtests.
Code complet MoutonHyperOptLoss.py Ă mettre dans user_data/hyperopts/:
# -*- coding: utf-8 -*-
from __future__ import annotations
from datetime import datetime
from typing import Any
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
# Explication :
# Cet hyperopt calcule :
# - un score pour chaque trimestre (selon le drawdown, le profit et R-squared)
# - un score pour l'ensemble de la période de backtest (selon le drawdown, le profit et R-squared)
# Ces 2 scores sont ensuite additionnés et donne un score global.
# ---------------------------------------------------------------------------
# ParamĂštres
# ---------------------------------------------------------------------------
# Trade minimum pour qu'un trimestre soit comptabilisé dans le score par trimestre.
MIN_TRADES: int = 1
# DECAY_RATE
# influence temporelle (Chaque trimestre qui s'éloigne d'aujourd'hui vaut 15% de moins (1.0 - 0.85) dans le score par trimestre)
# autrement dit plus cette valeur est basse, plus les trimestres récents ont de l'importance par rapport aux vieux trimestres
DECAY_RATE: float = 0.80
# DRAWDOWN_WEIGHT
# pénalité exponentielle
# (plus ce chiffre est élévé, plus un résultat avec un gros drawdown pénalisera les scores des trimestres et le score global)
DRAWDOWN_WEIGHT: int = 12
# MAX_DRAWDOWN_ALLOWED
# Drawdown maximum toléré
# rejet dur (si un résultat d'hyperopt a -39% de drawdown sur l'ensemble du backtest : on ne le prend pas en compte du tout)
MAX_DRAWDOWN_ALLOWED = 0.28
# GLOBAL_WEIGHT
# Lorsque les scores sont additionnés (score par trimestre et score global), cette variable représente le poids que l'on souhaite donné au score global
# Si 1.0 (100%) alors le score global vaut autant que le score par trimestre.
# Si 0.0 (0%) alors on ne considĂšre pas le score global et seul le score par trimestre compte
# Si 0.5 (50%) alors le score global vaut 2 fois moins que le score par trimestre.
GLOBAL_WEIGHT: float = 0.6 # poids du bloc global (par rapport au poids par trimestre)
# ---------------------------------------------------------------------------
# Utilitaires
# ---------------------------------------------------------------------------
def _r_squared(arr: np.ndarray) -> float:
if len(arr) < 2 or np.allclose(arr.var(), 0.0):
return 0.0
x = np.arange(len(arr), dtype=float)
slope, intercept = np.polyfit(x, arr, 1)
y_pred = slope * x + intercept
ss_res = np.sum((arr - y_pred) ** 2)
ss_tot = np.sum((arr - arr.mean()) ** 2)
return max(0.0, min(1.0, 1.0 - ss_res / ss_tot))
def _drawdown(df: DataFrame, value_col: str = "equity") -> float:
try:
res = calculate_max_drawdown(df, value_col=value_col) # type: ignore[arg-type]
for attr in ("relative_account_drawdown", "relative_drawdown"):
if hasattr(res, attr):
return float(getattr(res, attr))
if isinstance(res, (list, tuple)) and len(res) >= 6: # compat. †2024
return float(res[5])
except (ValueError, ZeroDivisionError):
pass
return 0.0
def _to_period_q(dt: datetime) -> pd.Period:
"""
Convertit un datetime (naĂŻf OU tz-aware) en pd.Period('Q') UTC,
sans jamais passer tz="UTC" à un objet déjà zoné.
"""
ts = pd.Timestamp(dt) # crée toujours un Timestamp
if ts.tz is None: # â naĂŻf
ts = ts.tz_localize("UTC")
else: # â dĂ©jĂ tz-aware
ts = ts.tz_convert("UTC")
# Supprimer l'info de timezone avant conversion en Period pour éviter le warning
ts = ts.tz_localize(None)
return ts.to_period("Q")
# ---------------------------------------------------------------------------
# Loss trimestriel
# ---------------------------------------------------------------------------
def _quarter_loss(df_q: DataFrame,
start_cap: float) -> float:
if len(df_q) < MIN_TRADES:
return 0.0
profit = float(df_q["profit_abs"].sum())
if profit <= 0.0:
return 0.0
df_q = df_q.sort_values("close_date").copy()
df_q["equity"] = start_cap + df_q["profit_abs"].cumsum()
r2 = _r_squared(df_q["equity"].to_numpy(float))
dd = _drawdown(df_q)
reward = np.log1p(profit / max(start_cap, 1.0)) * r2
penalty = np.exp(DRAWDOWN_WEIGHT * dd) # exponentiel
return -reward / penalty # (plus bas = mieux)
# ---------------------------------------------------------------------------
# Classe principale
# ---------------------------------------------------------------------------
class MoutonHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(
*,
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
processed: dict[str, DataFrame] | None = None,
backtest_stats: dict[str, Any] | None = None,
starting_balance: float | None = None,
**kwargs: Any,
) -> float:
# ----------------------------------------------------------------------
# WORK-AROUND ROBUSTE : empĂȘcher Joblib de planter
# ----------------------------------------------------------------------
now = pd.Timestamp.utcnow()
# ------------------------------------------------------------------
# 1ïžâŁ CrĂ©er un DataFrame minimal si vide
# ------------------------------------------------------------------
if results.empty:
results = pd.DataFrame({
"profit_abs": [1e-8, -1e-8],
"open_date": [now - pd.Timedelta(minutes=4), now - pd.Timedelta(minutes=2)],
"close_date": [now - pd.Timedelta(minutes=3), now - pd.Timedelta(minutes=1)],
"duration": [1.0, 1.0],
})
# ------------------------------------------------------------------
# 2ïžâŁ Assurer l'existence des colonnes critiques
# ------------------------------------------------------------------
# Assurer que open_date et close_date existent
if "open_date" not in results.columns:
results["open_date"] = now - pd.Timedelta(minutes=2)
if "close_date" not in results.columns:
results["close_date"] = now - pd.Timedelta(minutes=1)
# Convertir en datetime si nécessaire
results["open_date"] = pd.to_datetime(results["open_date"])
results["close_date"] = pd.to_datetime(results["close_date"])
# ------------------------------------------------------------------
# 3ïžâŁ Recalculer TOUTES les durĂ©es de maniĂšre robuste
# ------------------------------------------------------------------
# Recalcul systématique des durées
duration_calc = (results["close_date"] - results["open_date"]).dt.total_seconds().div(60)
# Remplacer toute durée invalide (NaN, <=0) par 1 minute
duration_calc = duration_calc.fillna(1.0)
duration_calc = duration_calc.where(duration_calc > 0, 1.0)
# Assigner la colonne duration corrigée
results["duration"] = duration_calc
# ------------------------------------------------------------------
# 4ïžâŁ Ajouter des trades factices si nĂ©cessaire
# ------------------------------------------------------------------
# Vérifier qu'il y a au moins un trade gagnant
if (results["profit_abs"] > 0).sum() == 0:
dummy_win = pd.DataFrame({
"profit_abs": [1e-8],
"open_date": [now - pd.Timedelta(minutes=4)],
"close_date": [now - pd.Timedelta(minutes=3)],
"duration": [1.0],
})
results = pd.concat([results, dummy_win], ignore_index=True)
# Vérifier qu'il y a au moins un trade perdant
if (results["profit_abs"] < 0).sum() == 0:
dummy_loss = pd.DataFrame({
"profit_abs": [-1e-8],
"open_date": [now - pd.Timedelta(minutes=2)],
"close_date": [now - pd.Timedelta(minutes=1)],
"duration": [1.0],
})
results = pd.concat([results, dummy_loss], ignore_index=True)
# ------------------------------------------------------------------
# 5ïžâŁ VĂ©rification finale et correction forcĂ©e
# ------------------------------------------------------------------
# Double vérification : s'assurer qu'aucune durée n'est NaN ou <= 0
mask_invalid = (results["duration"].isna()) | (results["duration"] <= 0)
if mask_invalid.any():
results.loc[mask_invalid, "duration"] = 1.0
# Triple sécurité : forcer au moins une durée valide pour chaque type de trade
winning_mask = results["profit_abs"] > 0
losing_mask = results["profit_abs"] < 0
if winning_mask.any():
win_durations = results.loc[winning_mask, "duration"]
if win_durations.isna().all() or (win_durations <= 0).all():
first_win_idx = results[winning_mask].index[0]
results.at[first_win_idx, "duration"] = 1.0
if losing_mask.any():
loss_durations = results.loc[losing_mask, "duration"]
if loss_durations.isna().all() or (loss_durations <= 0).all():
first_loss_idx = results[losing_mask].index[0]
results.at[first_loss_idx, "duration"] = 1.0
# ------------------------------------------------------------------
# Sanity check sur le nombre de trades
# ------------------------------------------------------------------
if trade_count < MIN_TRADES:
return 1e6
# ------------------------------------------------------------------
# Préparation DataFrame
# ------------------------------------------------------------------
df = results.copy()
df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
df.sort_values("close_date", inplace=True)
start_cap = starting_balance or (backtest_stats or {}).get("starting_balance", 0.0) or 0.0
# ------------------------------------------------------------------
# Découpage trimestriel
# ------------------------------------------------------------------
df["quarter"] = df["close_date"].apply(_to_period_q)
now_q = _to_period_q(max_date if max_date else datetime.utcnow())
quarter_losses: list[float] = []
for q, df_q in df.groupby("quarter", sort=False):
quarters_ago = (now_q.year - q.year) * 4 + (now_q.quarter - q.quarter)
weight = DECAY_RATE ** max(0, quarters_ago)
quarter_losses.append(weight * _quarter_loss(df_q, start_cap))
weighted_quarter_loss = float(np.sum(quarter_losses))
# ------------------------------------------------------------------
# Calculs globaux (profit, RÂČ, DD depuis backtest_stats)
# ------------------------------------------------------------------
stats = backtest_stats or {}
global_dd = float(
stats.get("relative_account_drawdown")
or stats.get("max_relative_drawdown")
or stats.get("relative_drawdown", 0.0)
)
# Si absent, fallback calcul maison
if global_dd == 0.0 and stats.get("relative_account_drawdown") is None:
df["equity"] = start_cap + df["profit_abs"].cumsum()
global_dd = _drawdown(df)
if global_dd > MAX_DRAWDOWN_ALLOWED:
return 1e6 + global_dd * 100
total_profit = float(df["profit_abs"].sum())
global_r2 = _r_squared((start_cap + df["profit_abs"].cumsum()).to_numpy(float))
reward_glob = np.log1p(total_profit / max(start_cap, 1.0)) * global_r2
penalty_glob = np.exp(DRAWDOWN_WEIGHT * global_dd)
global_loss = (-reward_glob / penalty_glob) * GLOBAL_WEIGHT
# ------------------------------------------------------------------
# Score final
# ------------------------------------------------------------------
final_loss = weighted_quarter_loss + global_loss
return float(final_loss) if np.isfinite(final_loss) else 1e6
4ïžâŁ Mouton2HyperOptLoss : Mon Ă©volution personnelle
Pourquoi j'ai créé Mouton2
AprÚs la sortie de MultiMetricHyperOptLoss et MaxDrawDownPerPairHyperOptLoss dans une des derniÚres versions de freqtrade, j'ai réalisé que je pouvais combiner les meilleures idées en un hyperoptloss :
MoutonHyperOptLoss â Mon systĂšme de pondĂ©ration temporelle
MultiMetricHyperOptLoss â Les mĂ©triques multiples
MaxDrawDownPerPairHyperOptLoss â L'Ă©quilibrage par paire
Comment je l'utilise
C'est devenu mon outil par défaut quand j'ai peu de temps. Par exemple, quand je veux rapidement adapter une stratégie Binance pour Hyperliquid :
# Ma commande habituelle
freqtrade hyperopt \
--hyperopt-loss Mouton2HyperOptLoss \
--strategy MyStrategy \
--config bybit_config.json \
--timerange 20230101-20241201 \
-e 1000 \
--spaces all \
-j 8
Ce que Mouton2 apporte (selon mon expérience)
D'aprĂšs mes tests, Mouton2HyperOptLoss aide Ă :
Réduire la sur-optimisation en privilégiant les trades récents
Ăquilibrer les performances sur toutes les paires
Combiner plusieurs métriques importantes
Obtenir des stratégies plus robustes sur différents exchanges
Attention : Ce n'est pas une solution magique ! C'est simplement ma façon de combiner plusieurs approches qui me semblent complémentaires, il n'y a pas d'hyperoptloss magique qui vont permettront d'améliorer n'importe quelle stratégie, mais si cela devait exister, voici une hyperoptloss qui s'en rapprocherait le plus, en tout cas c'est sa vocation.
ParamĂštres configurables
# Dans Mouton2HyperOptLoss.py
MIN_TRADES_QUARTER = 3 # Par trimestre
DECAY_RATE = 0.85 # Poids temporel
PROFIT_FACTOR_WEIGHT = 0.25 # Importance du profit factor
R_SQUARED_WEIGHT = 0.40 # Importance de la stabilité
GLOBAL_SCORE_WEIGHT = 0.75 # 75% global, 25% par paire
Code complet Mouton2HyperOptLoss.py Ă mettre dans user_data/hyperopts/:
# -*- coding: utf-8 -*-
from __future__ import annotations
from datetime import datetime
from typing import Any
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.constants import Config
from freqtrade.data.metrics import calculate_max_drawdown
from freqtrade.optimize.hyperopt import IHyperOptLoss
# =============================================================================
# Mouton2HyperOptLoss : Version avancée combinant MultiMetric + Trimestres + Par paire
# =============================================================================
# Cette hyperopt-loss combine :
# - Score trimestriel avec pondération temporelle (trimestres récents plus importants)
# - MĂ©triques MultiMetric (profit factor, expectancy, win rate, RÂČ)
# - Analyse par paire pour éviter la sur-optimisation sur une seule paire
# - Ăquilibre 75% score global / 25% score par paire
# =============================================================================
# ---------------------------------------------------------------------------
# ParamĂštres configurables
# ---------------------------------------------------------------------------
# ParamÚtres trimestriels (hérités de MoutonHyperOptLoss)
MIN_TRADES_QUARTER = 3 # Trades minimum par trimestre pour validation
DECAY_RATE = 0.85 # Poids des trimestres anciens (0.85 = -15% par trimestre)
DRAWDOWN_WEIGHT = 10 # Pénalité exponentielle sur le drawdown
MAX_DRAWDOWN_ALLOWED = 0.45 # Drawdown maximum toléré (45%)
# ParamĂštres MultiMetric
PROFIT_FACTOR_WEIGHT = 0.25 # Poids du profit factor
EXPECTANCY_WEIGHT = 0.20 # Poids de l'expectancy ratio
WIN_RATE_WEIGHT = 0.15 # Poids du win rate
R_SQUARED_WEIGHT = 0.40 # Poids du RÂČ (stabilitĂ© de la courbe d'Ă©quitĂ©)
# ParamÚtres d'équilibrage
GLOBAL_SCORE_WEIGHT = 0.75 # 75% score global
PER_PAIR_SCORE_WEIGHT = 0.25 # 25% score par paire
MIN_TRADES_TOTAL = 30 # Nombre minimum de trades total
MIN_TRADES_PER_PAIR = 3 # Nombre minimum de trades par paire
# Pénalités supplémentaires
TRADE_COUNT_PENALTY_MULT = 10 # Multiplicateur de pénalité pour trades insuffisants
MIN_PAIRS_PROFITABLE = 0.6 # Minimum 60% des paires doivent ĂȘtre profitables
# ---------------------------------------------------------------------------
# Fonctions utilitaires
# ---------------------------------------------------------------------------
def _r_squared(arr: np.ndarray) -> float:
"""Calcule le coefficient RÂČ d'une sĂ©rie (rĂ©gression linĂ©aire)"""
if len(arr) < 2 or np.allclose(arr.var(), 0.0):
return 0.0
x = np.arange(len(arr), dtype=float)
slope, intercept = np.polyfit(x, arr, 1)
y_pred = slope * x + intercept
ss_res = np.sum((arr - y_pred) ** 2)
ss_tot = np.sum((arr - arr.mean()) ** 2)
return max(0.0, min(1.0, 1.0 - ss_res / ss_tot))
def _drawdown(df: DataFrame, value_col: str = "equity") -> float:
"""Calcule le drawdown relatif maximum"""
try:
res = calculate_max_drawdown(df, value_col=value_col)
for attr in ("relative_account_drawdown", "relative_drawdown"):
if hasattr(res, attr):
return float(getattr(res, attr))
if isinstance(res, (list, tuple)) and len(res) >= 6:
return float(res[5])
except (ValueError, ZeroDivisionError):
pass
return 0.0
def _to_period_q(dt: datetime) -> pd.Period:
"""Convertit un datetime en période trimestrielle"""
ts = pd.Timestamp(dt)
if ts.tz is None:
ts = ts.tz_localize("UTC")
else:
ts = ts.tz_convert("UTC")
ts = ts.tz_localize(None)
return ts.to_period("Q")
def _calculate_profit_factor(results: DataFrame) -> float:
"""Calcule le profit factor (gains totaux / pertes totales)"""
if results.empty:
return 0.0
wins = results[results['profit_abs'] > 0]['profit_abs'].sum()
losses = abs(results[results['profit_abs'] < 0]['profit_abs'].sum())
if losses == 0:
return wins if wins > 0 else 0.0
return wins / losses
def _calculate_expectancy(results: DataFrame) -> float:
"""Calcule l'expectancy ratio (espérance de gain par trade)"""
if results.empty or len(results) == 0:
return 0.0
avg_win = results[results['profit_abs'] > 0]['profit_abs'].mean()
avg_loss = abs(results[results['profit_abs'] < 0]['profit_abs'].mean())
win_rate = len(results[results['profit_abs'] > 0]) / len(results)
if pd.isna(avg_win):
avg_win = 0
if pd.isna(avg_loss):
avg_loss = 0
return (win_rate * avg_win) - ((1 - win_rate) * avg_loss)
def _calculate_win_rate(results: DataFrame) -> float:
"""Calcule le taux de réussite des trades"""
if results.empty or len(results) == 0:
return 0.0
return len(results[results['profit_abs'] > 0]) / len(results)
# ---------------------------------------------------------------------------
# Calcul du score MultiMetric
# ---------------------------------------------------------------------------
def _multi_metric_score(df: DataFrame, start_cap: float) -> float:
"""
Calcule un score composite basé sur plusieurs métriques
Inspiré de MultiMetricHyperOptLoss mais adapté pour nos besoins
"""
if len(df) < MIN_TRADES_TOTAL:
return 0.0
# Calcul des métriques individuelles
profit_factor = _calculate_profit_factor(df)
expectancy = _calculate_expectancy(df)
win_rate = _calculate_win_rate(df)
# Calcul du RÂČ sur la courbe d'Ă©quitĂ©
df_sorted = df.sort_values("close_date").copy()
df_sorted["equity"] = start_cap + df_sorted["profit_abs"].cumsum()
r_squared = _r_squared(df_sorted["equity"].to_numpy(float))
# Normalisation et pondération des métriques
# Profit factor : bon si > 1.5, excellent si > 2
pf_score = min((profit_factor - 1.0) / 2.0, 1.0) if profit_factor > 1 else 0
# Expectancy : normalisé par capital initial
exp_score = min(expectancy / (start_cap * 0.01), 1.0) if expectancy > 0 else 0
# Win rate : bon si > 50%, excellent si > 70%
wr_score = min((win_rate - 0.3) / 0.4, 1.0) if win_rate > 0.3 else 0
# RÂČ : directement utilisable (0-1)
r2_score = r_squared
# Score composite pondéré
composite_score = (
pf_score * PROFIT_FACTOR_WEIGHT +
exp_score * EXPECTANCY_WEIGHT +
wr_score * WIN_RATE_WEIGHT +
r2_score * R_SQUARED_WEIGHT
)
return composite_score
# ---------------------------------------------------------------------------
# Calcul du score par trimestre
# ---------------------------------------------------------------------------
def _quarter_loss_with_metrics(df_q: DataFrame, start_cap: float) -> float:
"""
Calcule le score d'un trimestre en intégrant les métriques MultiMetric
"""
if len(df_q) < MIN_TRADES_QUARTER:
return 0.0
# Profit total du trimestre
profit = float(df_q["profit_abs"].sum())
if profit <= 0.0:
return 0.0
# Calcul du drawdown trimestriel
df_q = df_q.sort_values("close_date").copy()
df_q["equity"] = start_cap + df_q["profit_abs"].cumsum()
dd = _drawdown(df_q)
# Score MultiMetric pour ce trimestre
metric_score = _multi_metric_score(df_q, start_cap)
# RÂČ spĂ©cifique au trimestre
r2_quarter = _r_squared(df_q["equity"].to_numpy(float))
# Combinaison des scores
profit_score = np.log1p(profit / max(start_cap, 1.0))
combined_score = profit_score * (0.4 + metric_score * 0.6) * r2_quarter
# Pénalité exponentielle pour le drawdown
penalty = np.exp(DRAWDOWN_WEIGHT * dd)
return -combined_score / penalty # Négatif car on minimise
# ---------------------------------------------------------------------------
# Calcul du score par paire
# ---------------------------------------------------------------------------
def _calculate_per_pair_score(results: DataFrame, start_cap: float) -> float:
"""
Calcule le score en analysant chaque paire individuellement
Retourne le pire score parmi toutes les paires (pour éviter la sur-optimisation)
"""
if 'pair' not in results.columns:
return 0.0
pairs = results['pair'].unique()
if len(pairs) == 0:
return 0.0
pair_scores = []
profitable_pairs = 0
for pair in pairs:
pair_data = results[results['pair'] == pair].copy()
# Vérifier le nombre minimum de trades pour cette paire
if len(pair_data) < MIN_TRADES_PER_PAIR:
# Pénalité pour paire avec trop peu de trades
pair_scores.append(-10.0)
continue
# Calcul du profit et drawdown pour cette paire
pair_profit = pair_data['profit_abs'].sum()
if pair_profit > 0:
profitable_pairs += 1
# Calcul de l'équité et du drawdown
pair_data = pair_data.sort_values("close_date")
pair_data["equity"] = start_cap + pair_data["profit_abs"].cumsum()
pair_dd = _drawdown(pair_data)
# Score MultiMetric pour cette paire
pair_metric_score = _multi_metric_score(pair_data, start_cap)
# RÂČ pour cette paire
pair_r2 = _r_squared(pair_data["equity"].to_numpy(float))
# Score final de la paire
if pair_dd > 0 and pair_profit > 0:
pair_score = (pair_profit / start_cap) * pair_metric_score * pair_r2 / (1 + pair_dd * 10)
else:
pair_score = -5.0
pair_scores.append(pair_score)
# Vérifier que suffisamment de paires sont profitables
profitable_ratio = profitable_pairs / len(pairs) if len(pairs) > 0 else 0
if profitable_ratio < MIN_PAIRS_PROFITABLE:
return -100.0 # Forte pénalité si trop de paires perdantes
# Retourner une combinaison du pire score et de la moyenne
# Cela pénalise les stratégies avec des paires trÚs mauvaises
if pair_scores:
worst_score = min(pair_scores)
avg_score = np.mean(pair_scores)
return worst_score * 0.7 + avg_score * 0.3
return -100.0
# ---------------------------------------------------------------------------
# Classe principale Mouton2HyperOptLoss
# ---------------------------------------------------------------------------
class Mouton2HyperOptLoss(IHyperOptLoss):
"""
Hyperopt Loss avancée combinant :
- Analyse trimestrielle avec pondération temporelle
- MĂ©triques multiples (profit factor, expectancy, win rate, RÂČ)
- Analyse par paire pour éviter la sur-optimisation
- Ăquilibrage 75% global / 25% par paire
"""
@staticmethod
def hyperopt_loss_function(
*,
results: DataFrame,
trade_count: int,
min_date: datetime,
max_date: datetime,
config: Config,
processed: dict[str, DataFrame] | None = None,
backtest_stats: dict[str, Any] | None = None,
starting_balance: float | None = None,
**kwargs: Any,
) -> float:
# ----------------------------------------------------------------------
# Protection contre les erreurs et initialisation
# ----------------------------------------------------------------------
now = pd.Timestamp.utcnow()
# DataFrame minimal si vide
if results.empty:
results = pd.DataFrame({
"profit_abs": [1e-8, -1e-8],
"profit_ratio": [1e-8, -1e-8],
"open_date": [now - pd.Timedelta(minutes=4), now - pd.Timedelta(minutes=2)],
"close_date": [now - pd.Timedelta(minutes=3), now - pd.Timedelta(minutes=1)],
"duration": [1.0, 1.0],
"pair": ["BTC/USDT", "BTC/USDT"]
})
# Assurer les colonnes critiques
if "open_date" not in results.columns:
results["open_date"] = now - pd.Timedelta(minutes=2)
if "close_date" not in results.columns:
results["close_date"] = now - pd.Timedelta(minutes=1)
if "pair" not in results.columns:
results["pair"] = "UNKNOWN/USDT"
if "profit_ratio" not in results.columns and "profit_abs" in results.columns:
results["profit_ratio"] = results["profit_abs"] / max(starting_balance or 1000, 1)
# Conversion des dates
results["open_date"] = pd.to_datetime(results["open_date"])
results["close_date"] = pd.to_datetime(results["close_date"])
# Calcul des durées
duration_calc = (results["close_date"] - results["open_date"]).dt.total_seconds().div(60)
duration_calc = duration_calc.fillna(1.0)
duration_calc = duration_calc.where(duration_calc > 0, 1.0)
results["duration"] = duration_calc
# Vérification du nombre minimum de trades
if trade_count < MIN_TRADES_TOTAL:
return 1e6
# ----------------------------------------------------------------------
# Préparation des données
# ----------------------------------------------------------------------
df = results.copy()
df["close_date"] = pd.to_datetime(df["close_date"], utc=True)
df.sort_values("close_date", inplace=True)
start_cap = starting_balance or (backtest_stats or {}).get("starting_balance", 1000) or 1000
# ----------------------------------------------------------------------
# 1. Score trimestriel avec pondération temporelle
# ----------------------------------------------------------------------
df["quarter"] = df["close_date"].apply(_to_period_q)
now_q = _to_period_q(max_date if max_date else datetime.utcnow())
quarter_losses = []
for q, df_q in df.groupby("quarter", sort=False):
# Calcul de l'ancienneté du trimestre
quarters_ago = (now_q.year - q.year) * 4 + (now_q.quarter - q.quarter)
weight = DECAY_RATE ** max(0, quarters_ago)
# Score du trimestre avec métriques avancées
quarter_score = _quarter_loss_with_metrics(df_q, start_cap)
quarter_losses.append(weight * quarter_score)
weighted_quarter_loss = float(np.sum(quarter_losses))
# ----------------------------------------------------------------------
# 2. Score global avec métriques MultiMetric
# ----------------------------------------------------------------------
stats = backtest_stats or {}
# Drawdown global
global_dd = float(
stats.get("relative_account_drawdown") or
stats.get("max_relative_drawdown") or
stats.get("relative_drawdown", 0.0)
)
if global_dd == 0.0:
df["equity"] = start_cap + df["profit_abs"].cumsum()
global_dd = _drawdown(df)
# Vérification du drawdown maximum autorisé
if global_dd > MAX_DRAWDOWN_ALLOWED:
return 1e6 + global_dd * 100
# Score MultiMetric global
global_metric_score = _multi_metric_score(df, start_cap)
# Profit total et RÂČ global
total_profit = float(df["profit_abs"].sum())
global_r2 = _r_squared((start_cap + df["profit_abs"].cumsum()).to_numpy(float))
# Combinaison des métriques globales
if total_profit > 0:
profit_ratio = total_profit / start_cap
global_score = profit_ratio * global_metric_score * global_r2
dd_penalty = np.exp(DRAWDOWN_WEIGHT * global_dd)
global_loss = -global_score / dd_penalty
else:
global_loss = 1000.0
# ----------------------------------------------------------------------
# 3. Score par paire (pour éviter la sur-optimisation)
# ----------------------------------------------------------------------
per_pair_score = _calculate_per_pair_score(df, start_cap)
# ----------------------------------------------------------------------
# 4. Pénalités supplémentaires
# ----------------------------------------------------------------------
# Pénalité pour nombre de trades insuffisant
trade_penalty = 0
if trade_count < MIN_TRADES_TOTAL * 2:
trade_penalty = ((MIN_TRADES_TOTAL * 2 - trade_count) / MIN_TRADES_TOTAL) * TRADE_COUNT_PENALTY_MULT
# ----------------------------------------------------------------------
# 5. Score final combiné
# ----------------------------------------------------------------------
# Combinaison pondérée : 75% global, 25% par paire
combined_metric_loss = (
(weighted_quarter_loss + global_loss) * GLOBAL_SCORE_WEIGHT +
(-per_pair_score) * PER_PAIR_SCORE_WEIGHT
)
# Ajout des pénalités
final_loss = combined_metric_loss + trade_penalty
# Protection contre les valeurs infinies
if not np.isfinite(final_loss):
return 1e6
return float(final_loss)5ïžâŁ Guide Pratique : Installation et Configuration
Pour utiliser les hyperopt-loss customs
1. Créer le dossier (si pas déjà fait) :
mkdir -p user_data/hyperopts
2. Placer les fichiers :
MoutonHyperOptLoss.py dans user_data/hyperopts/
Mouton2HyperOptLoss.py dans user_data/hyperopts/
3. Vérifier l'installation :
freqtrade list-hyperopts
â ïž Point important : VĂ©rification des conflits de noms Toujours vĂ©rifier qu'il n'y a pas de conflit de nom de stratĂ©gie. Je fais souvent du versioning sur le nom de fichier mais il faut aussi le faire sur le nom de la stratĂ©gie dans le code, sinon tu crois utiliser la derniĂšre version mais en fait tu utilises toujours l'ancienne !
# Commande essentielle à exécuter avant chaque hyperopt
freqtrade list-strategies
Vérifie qu'il n'y a pas de doublons ni d'erreur !
Configuration des paramĂštres
Pour les hyperopt-loss natives : Aucune configuration nécessaire (à l'exception de ProfitDrawDownHyperOptLoss), spécifiez juste l'hyperopt-loss dans la commande.
Pour les customs fournis dans ce post : Modifiez les paramĂštres directement dans les fichiers Python selon vos besoins.
6ïžâŁ Mon Arbre de DĂ©cision Personnel
Comment je choisis mon hyperopt-loss
PremiĂšre optimisation d'une stratĂ©gie : â MultiMetricHyperOptLoss (vue d'ensemble Ă©quilibrĂ©e)
StratĂ©gie pour compte important : â CalmarHyperOptLoss (focus sur le ratio rendement/DD)
Adaptation multi-exchange rapide : â Mouton2HyperOptLoss (mon custom tout-en-un)
Scalping ou trades courts : â ShortTradeDurHyperOptLoss (optimise la durĂ©e)
Portfolio multi-paires : â MaxDrawDownPerPairHyperOptLoss ou Mouton2HyperOptLoss
Méthodologie avec les régimes de marché
Note : Cette approche m'a été suggérée par un trader algorithmique nommé "Bigbroseur". Ce n'est pas forcément ma méthode principale, mais il trouve que c'est la meilleure approche aprÚs plusieurs essais et m'a conseillé de la mentionner.
Phase 1 : Backtest rapide Le backtest est pour contrÎler que ta stratégie fonctionne et que tous tes signaux se déclenchent correctement. Pas besoin de faire une longue période.
Phase 2 : HyperOpt selon les régimes de marché Deux approches possibles (pour en savoir plus sur les régimes de marché, consultez mon post dédié) :
Option A : Une longue période HyperOpt sur une période incluant au moins 3-4 régimes de marché différents (bull, bear, range, volatil)
Option B : Multiple HyperOpt par régime (Approche préférée de Bigbroseur)
3-4 HyperOpt avec chacun son régime de marché
On obtient des optimisations propres à chaque régime
Il faudra faire un
/reload_configavec les paramÚtres correspondants au régime courant
Phase 3 : Validation Backtest sur période différente et sur exchange différent. Si mauvais résultat : retour à la phase 2
Phase 4 : Dry-run 2-3 semaines minimum (avec MOT = 3 selon l'approche de Bigbroseur)
Mes erreurs passées (à éviter)
1. Trop d'epochs : J'ai déjà fait tourner 5000 epochs... Résultat : sur-optimisation garantie. Maintenant je reste sous 1000 max.
2. OnlyProfit sur compte réel : J'ai testé plusieurs fois... mauvais résultat sur le long terme, des drawdowns violents vous attendent. Maintenant j'utilise toujours une loss qui prend en compte le risque.
3. Optimiser sur période trop courte : 2 semaines de données = stratégie qui marche 2 semaines. Maintenant : minimum 3-6 mois de données, idéalement au moins 2 ans.
4. NĂ©gliger le MOT : Selon le retour de Bigbroseur, des valeurs de max_open_trades trop Ă©loignĂ©es de 3 crĂ©ent des effets de bord imprĂ©visibles (mĂȘme si personnellement j'utilise aussi d'autres valeurs).
5. Oublier de vérifier les conflits de noms : Combien de fois j'ai optimisé la mauvaise version d'une stratégie !
Pour le passage en live, vous pouvez consulter mon guide sur les bots en live.
đ Conclusion : Mon Approche de l'Hyperopt
Ce que j'ai appris avec le temps
Le choix de l'hyperopt-loss est crucial. C'est selon moi plus important que le nombre d'epochs ou mĂȘme les paramĂštres optimisĂ©s. Une mauvaise loss = une mauvaise stratĂ©gie, mĂȘme avec 10000 epochs.
N'oublie jamais : l'hyperopt reste dĂ©pendant de la qualitĂ© du backtest, et le backtest lui-mĂȘme a ses limites. C'est mieux que rien, mais ce n'est pas une garantie !
Mes recommandations personnelles
Si tu dĂ©butes : â Commence avec MultiMetricHyperOptLoss. C'est Ă©quilibrĂ© et difficile de se tromper. â Certains traders comme Bigbroseur recommandent MOT = 3 (teste et vois ce qui marche pour toi) â VĂ©rifie toujours les conflits de noms avec
freqtrade list-strategies
Si tu veux ma mĂ©thode : â Essaye Mouton2HyperOptLoss aprĂšs avoir compris les bases. Je ne garantie rien mais pour le moment c'est ce que j'utilise. â Pense aux rĂ©gimes de marchĂ© dans tes optimisations (voir mon post sur les phases de marchĂ©)
Si tu veux expĂ©rimenter : â Teste diffĂ©rentes loss sur la mĂȘme stratĂ©gie et compare en mettant en dry-run les rĂ©sultats de chaque loss !
Les points essentiels (selon mon expérience)
1ïžâŁ Aucune hyperopt-loss n'est parfaite - Chacune a ses forces et faiblesses
2ïžâŁ La validation est obligatoire - Toujours tester sur une pĂ©riode diffĂ©rente et des exchanges diffĂ©rents
3ïžâŁ Adapter au contexte - Scalping â Swing â Hold. Pour comprendre les diffĂ©rences, voir mon post sur les types de trading
4ïžâŁ MOT = 3 selon Bigbroseur - Un trader expĂ©rimentĂ© m'a conseillĂ© cette valeur aprĂšs plusieurs essais, mĂȘme si ce n'est pas forcĂ©ment ma pratique exclusive
5ïžâŁ VĂ©rifier avant d'optimiser - Une commande freqtrade list-strategies peut vous Ă©viter des heures perdues
N'oublie pas : Ces approches sont basées sur mon expérience personnelle et les retours de traders expérimentés. Ce qui marche pour moi ou pour eux ne marchera pas forcément pour toi. L'important est de tester, comprendre et adapter !
Si ce guide et mes hyperopt-loss customs t'ont aidĂ©, n'hĂ©site pas Ă me soutenir avec un cafĂ© â !
