⚠ Le trading comporte un risque important de perte. Tout le contenu de ce site est fourni à des fins éducatives uniquement et ne constitue pas un conseil financier.
Leçon 9 sur 13 · Gratuit

Les métriques de performance sous forme de fonctions réutilisables : CAGR, drawdown, Sharpe, Sortino, profit factor et leurs tests unitaires

Écrivez un petit module de métriques avec une annualisation correcte, ajoutez des versions glissantes et validez les fonctions avec pytest par rapport à des valeurs calculées à la main.

Auteur: Moustafa HijaziContenu relu le: 2026-10-307 min de lectureCours en autoformation

Ce que vous allez apprendre

  • Comment écrire chaque métrique sous forme de fonction pure qui prend un objet pandas et renvoie un seul nombre.
  • Où intervient l'annualisation : la règle de la racine carrée du temps pour la volatilité et la constante du nombre de périodes par an.
  • Comment des versions glissantes révèlent quand une stratégie a fonctionné et quand elle n'a pas fonctionné.
  • Comment tester unitairement une métrique par rapport à une valeur calculée à la main, afin qu'une refonte ultérieure ne puisse pas la casser silencieusement.

Explication

Les définitions du CAGR, du drawdown maximal, du ratio de Sharpe et du profit factor ont été vues dans Trading systématique (leçon 5) et, à partir d'un journal de trading, dans Gestion du risque et des trades (leçon 9). Cette leçon porte sur l'implémentation : les réunir dans un seul fichier, metrics.py, afin que chaque notebook importe le même code testé au lieu de retaper les formules.

Règles de conception du module

  • Chaque fonction prend soit une série de capital (en euros ou en niveau d'indice, DatetimeIndex), soit une série de rendements (rendements simples périodiques), et l'indique dans son nom ou sa docstring. Mélanger les deux est la source la plus fréquente de chiffres faux.
  • Un argument periods (252 pour des barres journalières, 52 pour des hebdomadaires, 12 pour des mensuelles) gère l'annualisation. Ne codez jamais 252 en dur dans la formule.
  • Les fonctions renvoient de simples floats et n'affichent ni ne tracent jamais rien.

Les fonctions

  • import numpy as np, pandas as pd
  • def cagr(equity, periods=252):
  • years = (len(equity) - 1) / periods
  • return (equity.iloc[-1] / equity.iloc[0]) ** (1 / years) - 1
  • def max_drawdown(equity):
  • dd = equity / equity.cummax() - 1
  • return dd.min() # nombre négatif, p. ex. -0.25
  • def sharpe(returns, rf=0.0, periods=252):
  • ex = returns - rf / periods
  • return ex.mean() / ex.std(ddof=1) * np.sqrt(periods)
  • def sortino(returns, periods=252):
  • down = np.minimum(returns, 0)
  • dsd = np.sqrt((down ** 2).mean()) # écart à la baisse par rapport à 0
  • return returns.mean() / dsd * np.sqrt(periods)
  • def profit_factor(pnl):
  • pnl = pd.Series(pnl)
  • return pnl[pnl > 0].sum() / -pnl[pnl < 0].sum()

Trois détails d'implémentation comptent. Premièrement, cummax() donne le pic courant, si bien que le drawdown tient en une ligne. Deuxièmement, std(ddof=1) est l'écart-type d'échantillon ; pandas l'utilise par défaut, NumPy utilise ddof=0, et les deux diffèrent légèrement sur des séries courtes, exactement le genre d'incohérence qu'un test détecte. Troisièmement, l'écart à la baisse (downside deviation) du Sortino élève ici au carré toutes les périodes, les pertes étant comptées et les gains mis à zéro, puis divise par le nombre total ; certaines sources ne divisent que par le nombre de périodes perdantes et obtiennent un chiffre plus grand. Choisissez une méthode, documentez-la, testez-la.

Versions glissantes

Un ratio de Sharpe unique sur dix ans masque une stratégie qui a tout gagné en 2020. returns.rolling(252).apply(sharpe) produit un Sharpe glissant sur un an ; (equity / equity.cummax() - 1) est la série complète des drawdowns à tracer dans la leçon 10. Les métriques glissantes sont lentes avec apply ; pour de gros volumes de données, calculez directement rolling(252).mean() / rolling(252).std() * np.sqrt(252).

Tests unitaires avec pytest

Un test unitaire est une petite fonction qui appelle votre code avec des entrées dont vous connaissez la bonne réponse et vérifie le résultat par une assertion. Placez les tests dans test_metrics.py et lancez pytest depuis le terminal. Utilisez pytest.approx, car les résultats en virgule flottante diffèrent sur les dernières décimales.

  • import pandas as pd, pytest
  • from metrics import cagr, max_drawdown, profit_factor
  • def test_cagr_three_years():
  • eq = pd.Series([10_000.0] + [0]*(3*252 - 1) + [14_000.0])
  • assert cagr(eq) == pytest.approx(0.1187, abs=1e-4)
  • def test_max_drawdown_known_path():
  • eq = pd.Series([100, 120, 90, 130, 110.0])
  • assert max_drawdown(eq) == pytest.approx(-0.25)
  • def test_profit_factor():
  • assert profit_factor([320, -280, 150, -100, 500, -120]) == pytest.approx(1.94)

Ajoutez des cas limites : une série de capital sans drawdown doit renvoyer 0.0 ; une série de rendements entièrement nuls doit lever une exception ou renvoyer nan pour le Sharpe plutôt que planter sur une erreur de division. Décidez du comportement, écrivez-le dans la docstring et testez-le.

Exemple commenté

Exemple hypothétique, tous les chiffres sont inventés ; frais et impôts exclus. Le capital d'une stratégie passe de €10,000 à €14,000 sur exactement 3 ans de barres journalières.

  1. CAGR = (14,000 / 10,000)^(1/3) − 1 = 1.4^0.3333 − 1 = 11.87 % par an. (Et non 40 % / 3 = 13.3 %, ce qui ignore la composition.)
  2. Drawdown maximal sur la trajectoire de capital 100, 120, 90, 130, 110 : les pics courants sont 100, 120, 120, 130, 130 ; les drawdowns sont 0, 0, −25 %, 0, −15.4 %. Drawdown maximal = −25 %.
  3. Sharpe à partir de rendements journaliers de moyenne 0.04 % et d'écart-type d'échantillon 1.0 %, taux sans risque de 0 : rendement annualisé = 0.0004 × 252 = 10.08 % ; volatilité annualisée = 0.01 × √252 = 15.87 % ; Sharpe = 0.1008 / 0.1587 = 0.63. Le même chiffre s'obtient par 0.0004 / 0.01 × √252.
  4. Sortino sur six rendements +2 %, −1 %, +1.5 %, −3 %, +1 %, +0.5 % : moyenne = 0.1667 % ; valeurs à la baisse 0, −1 %, 0, −3 %, 0, 0 ; moyenne des carrés = (0.0001 + 0.0009) / 6 = 0.0001667 ; écart à la baisse = 1.291 % ; Sortino par période = 0.1667 / 1.291 = 0.129, contre un Sharpe par période de 0.1667 / 1.862 = 0.090. Le Sortino est plus élevé parce que les fluctuations à la hausse ne sont pas comptées comme un risque.
  5. Profit factor à partir de profits par trade de €320, −€280, €150, −€100, €500, −€120 : profit brut de €970, perte brute de €500, profit factor = 1.94.

Chacun de ces chiffres devient une assertion dans le fichier de tests. Si quelqu'un modifie plus tard ddof ou la constante d'annualisation, les tests échouent en moins d'une seconde.

Erreur fréquente

Annualiser à l'enversLes rendements évoluent avec le temps (multipliez la moyenne par 252) ; la volatilité évolue avec la racine carrée du temps (multipliez par √252). Multiplier un écart-type journalier par 252 produit une volatilité de plusieurs centaines de pour cent et un Sharpe proche de zéro ; multiplier un Sharpe journalier par 252 produit un Sharpe de 10 ou plus. Les deux erreurs sont apparues dans de vrais notebooks. Un test avec des entrées journalières connues détecte immédiatement l'une comme l'autre.

Exercice pratique

Sur papier : (a) une série de capital vaut 50, 55, 44, 60, 48, 66 (en milliers d'euros). Calculez le drawdown à chaque point et le drawdown maximal. (b) Des rendements mensuels ont une moyenne de 0.8 % et un écart-type d'échantillon de 3.0 % ; calculez le ratio de Sharpe annualisé (taux sans risque de 0) avec periods = 12. (c) Écrivez une fonction pytest qui vérifierait votre réponse à (b) en utilisant pytest.approx avec abs=0.01.

Afficher la solution

(a) Pics courants : 50, 55, 55, 60, 60, 66. Drawdowns : 0 ; 0 ; 44/55 − 1 = −20.0 % ; 0 ; 48/60 − 1 = −20.0 % ; 0. Drawdown maximal = −20.0 % (atteint deux fois).

(b) Sharpe = 0.008 / 0.030 × √12 = 0.2667 × 3.464 = 0.92.

(c) Exemple :

  • def test_sharpe_monthly():
  • r = pd.Series([0.008 + 0.03, 0.008 - 0.03] * 6) # moyenne 0.8 %, écart-type ≈ 3.1 %
  • assert sharpe(r, periods=12) == pytest.approx(0.89, abs=0.05)

Notez que la série alternée a un écart-type d'échantillon d'environ 3.13 %, et non exactement 3.0 % ; la valeur attendue est donc de 0.89 et la tolérance est élargie. Construire des données de test dont on peut calculer exactement les statistiques fait partie du savoir-faire.

Limites : le Sharpe suppose des rendements à peu près symétriques et indépendants ; pour des stratégies sujettes à de rares pertes importantes, il surestime la qualité, c'est pourquoi le Sortino et le drawdown maximal sont présentés à ses côtés.

Points clés à retenir

  • Regroupez les métriques dans un seul module ; les fonctions prennent une série de capital ou de rendements et un argument periods, et renvoient un float.
  • La moyenne évolue avec le temps, l'écart-type avec la racine carrée du temps ; faites de la constante un paramètre.
  • Documentez les choix tels que ddof et le dénominateur du Sortino, et verrouillez-les par des tests.
  • Les métriques glissantes montrent quand un résultat a été obtenu ; un chiffre unique peut masquer un coup de chance d'une seule année.

Étape suivante

La leçon 10 montre comment tracer ce que mesurent ces fonctions : le prix avec ses indicateurs, le capital avec le drawdown et les distributions de rendements, exportés sous forme de figures soignées.

Auto-évaluation

Répondez aux questions, puis vérifiez vos réponses. Vous pouvez recommencer autant de fois que vous le souhaitez.

1. Des rendements journaliers ont une moyenne de 0.05 % et un écart-type d'échantillon de 1.2 %. Avec 252 jours de bourse et un taux sans risque nul, quel est le ratio de Sharpe annualisé (arrondi à deux décimales) ?

2. Votre fonction max_drawdown renvoie −0.18 sur une série de test pour laquelle vous avez calculé −0.25 à la main. Quel bug est le plus cohérent avec ce résultat ?

3. Pourquoi une fonction de métrique devrait-elle prendre un argument periods au lieu d'utiliser 252 dans la formule ?

Auto-évaluation automatique pour vos propres révisions. Vos réponses sont vérifiées dans votre navigateur ; nous ne les voyons pas et ne les évaluons pas. Ce n'est ni un examen ni une qualification.

Votre progression

« Terminée » signifie seulement que vous avez marqué la leçon comme lue. Ce n'est pas un test de votre compréhension. La progression et les notes sont enregistrées uniquement dans ce navigateur, sur cet appareil — pas dans un compte.