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.
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 pddef cagr(equity, periods=252):years = (len(equity) - 1) / periodsreturn (equity.iloc[-1] / equity.iloc[0]) ** (1 / years) - 1def max_drawdown(equity):dd = equity / equity.cummax() - 1return dd.min() # nombre négatif, p. ex. -0.25def sharpe(returns, rf=0.0, periods=252):ex = returns - rf / periodsreturn 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 à 0return 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, pytestfrom metrics import cagr, max_drawdown, profit_factordef 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.
- 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.)
- 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 %.
- 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.
- 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.
- 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
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
ddofet 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.
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.