Analyse du risque et backtest d’un pairs trade, partie II
La partie I a construit la simulation. Ici, je l’utilise pour estimer le risque implicite du modèle, puis je confronte ce résultat à un backtest historique simple. Le contraste est gênant, mais utile. Une stratégie peut sembler solide dans son modèle et perdre beaucoup sur les prix réalisés.
Analyse du risque par Monte Carlo
Construction du portefeuille
Allocation du capital : $100,000 (9 octobre 2023)
Dimensionnement des positions :
- Long PEP : 663 actions × $150.67 = $99,892
- Short KO : 2,008 actions × $49.80 = $99,990
- Exposition brute : $199,882
Écart initial : $S_0 = \$49.80 - 0.333 \times $150.67 = -$0.38$
Cible de retour à la moyenne. J’utilise des statistiques glissantes sur 60 jours afin que la cible de l’écart reflète le régime courant plutôt que l’historique complet de 20 ans :
- Moyenne glissante : $\$0.004$ (contre une moyenne historique de $$4.63$)
- Écart type glissant : $\$0.78$ (contre un écart type historique de $$2.75$)
- L’écart se négocie loin de sa moyenne de long terme
Value at Risk
La Value at Risk, ou VaR, est le seuil de perte du portefeuille pour un niveau de confiance donné. Je l’estime directement à partir de la distribution de Monte Carlo.
Calcul de la VaR :
# Calculate portfolio value for 60-day horizon
# Portfolio starts at 100_000 dollars and includes cash position
final_portfolio_value = portfolio_values[:, 60]
# VaR at different confidence levels
var_90 = np.percentile(final_portfolio_value, 10) # 90% confidence
var_95 = np.percentile(final_portfolio_value, 5) # 95% confidence
var_99 = np.percentile(final_portfolio_value, 1) # 99% confidence
Résultats de VaR à 60 jours :
| Niveau de confiance | Valeur du portefeuille à la VaR | VaR |
|---|---|---|
| 90% | $96,637 | $3,363 |
| 95% | $95,541 | $4,459 |
| 99% | $93,470 | $6,530 |
Garder une définition cohérente de la VaR
Une version antérieure mélangeait la valeur terminale du portefeuille à un percentile et l’ampleur de la perte en dollars. Ces valeurs ne sont pas directement comparables. J’ai retiré le tableau incohérent et gardé un seul ensemble de chiffres de VaR Monte Carlo et d’Expected Shortfall.

Expected Shortfall
L’Expected Shortfall, ou ES, mesure la perte moyenne parmi les résultats situés au-delà du seuil de VaR.
# ES: average portfolio value in the worst 5% of scenarios
final_portfolio_value = portfolio_values[:, 60]
var_threshold = np.percentile(final_portfolio_value, 5)
cvar_95 = final_portfolio_value[final_portfolio_value <= var_threshold].mean()
ES à 60 jours (95 %) : $94,270 (valeur moyenne du portefeuille dans les 5 % pires cas)
Dans les 5 % pires scénarios, la valeur moyenne du portefeuille tombe à $94,270. Cela représente une perte de $5,730, soit 5,7 % du capital initial. Le portefeuille a environ 200 % d’exposition brute. Cette perte de queue est donc plausible.
Drawdown maximal
Le drawdown maximal est la plus forte baisse entre un pic et un creux de la valeur du portefeuille le long d’une trajectoire simulée.
# Calculate maximum drawdown across all paths
cummax = np.maximum.accumulate(portfolio_values, axis=1)
drawdown = (portfolio_values - cummax) / cummax
max_dd = drawdown.min(axis=1)
# Average and worst-case drawdown
avg_max_drawdown = max_dd.mean() * 100
worst_drawdown = max_dd.min() * 100
Statistiques de drawdown maximal :
- Drawdown maximal moyen : -7.4%
- Pire drawdown : -21.0% (99e percentile)
- Drawdown maximal médian : -7.2%
Distribution de la valeur du portefeuille

Le graphique montre la distribution de la valeur du portefeuille à 60 jours sur 1 million de trajectoires simulées :
- VaR 95 % (5e percentile) : $95,541, donc 5 % des issues finissent en dessous de ce niveau
- Issue médiane : $100,468, soit environ un gain de 0,5 %
- 95e percentile : $105,383, soit environ un gain de 5,4 %
L’axe des x représente la valeur finale du portefeuille après 60 jours, à partir de $100,000.
Stratégie de trading à retour à la moyenne
Règles de la stratégie
Je trade l’écart avec une règle simple de retour à la moyenne fondée sur son z-score :
Signaux d’entrée :
- Long spread (acheter KO, vendre PEP) quand $z < -1.5$ (écart sous sa moyenne)
- Short spread (vendre KO, acheter PEP) quand $z > 1.5$ (écart au-dessus de sa moyenne)
Signaux de sortie :
- Fermer la position quand l’écart revient vers sa moyenne ($z$ franchit 0)
- Stop loss à $z = \pm 2.5$
Calcul du z-score :
$$z = \frac{S_t - \mu_S}{\sigma_S}$$où $S_t$ est l’écart courant, $\mu_S = 4.63$ est la moyenne de long terme et $\sigma_S = 2.75$ est l’écart type historique.
Implémentation de la stratégie
# Calculate z-scores for all paths
spread_all = ko_prices - beta * pep_prices
z_scores = (spread_all - spread_mean) / spread_std
# Entry conditions
long_entry = z_scores < -1.5 # Spread too low, buy spread
short_entry = z_scores > 1.5 # Spread too high, sell spread
# Exit conditions
exit_signal = np.abs(z_scores) < 0.2 # Near mean
stop_loss = np.abs(z_scores) > 2.5 # Risk management
# Track positions
position = np.zeros_like(z_scores)
pnl_strategy = np.zeros_like(z_scores)
for path in range(n_paths):
current_position = 0
entry_spread = 0
for day in range(1, n_days + 1):
# Check for entry signals
if current_position == 0:
if long_entry[path, day]:
current_position = 1
entry_spread = spread_all[path, day]
elif short_entry[path, day]:
current_position = -1
entry_spread = spread_all[path, day]
# Check for exit signals
elif exit_signal[path, day] or stop_loss[path, day]:
pnl_strategy[path, day] = current_position * (
spread_all[path, day] - entry_spread
)
current_position = 0
# Mark-to-market if still in position
elif current_position != 0:
pnl_strategy[path, day] = current_position * (
spread_all[path, day] - entry_spread
)
position[path, day] = current_position
Performance de la stratégie
Résultats à 60 jours (1 M de simulations) :
| Mesure | Valeur |
|---|---|
| Taux de gain | 70.1% |
| Gain moyen des gagnants | +$5,778 |
| Perte moyenne des perdants | -$3,732 |
| Gain maximal | +$34,214 |
| Perte maximale | -$26,966 |
| Ratio de Sharpe | 1.05 |
Calcul du ratio de Sharpe :
# Annualized Sharpe ratio for strategy
strategy_returns = strategy_pnl / initial_capital
mean_return = strategy_returns.mean()
std_return = strategy_returns.std()
sharpe_ratio = (mean_return / std_return) * np.sqrt(252 / 60)
Distribution du P&L de la stratégie

Le centre de la distribution des profits et pertes est positif :
- P&L médian : +$3,047 (rendement de 3,0 %)
- Probabilité de profit : 70.1%
- Intervalle de confiance à 95 % : environ [-$10k, +$15k]
Exemples de trajectoires de trading

Ces cinq trajectoires montrent le comportement de la règle :
- Trajectoires vertes : trades de retour à la moyenne réussis
- Trajectoires rouges : positions stoppées ou non rentables
- Points d’entrée : triangles aux seuils de z-score
- Points de sortie : cercles aux croisements de la moyenne ou aux stop losses
Backtest historique sur 2 ans
Période de test : octobre 2023 - octobre 2025
Je quitte ensuite le monde simulé et j’applique la règle aux prix réels de KO et PEP sur deux ans, soit 504 jours de bourse.
Paramètres du backtest :
Période des données : 11 octobre 2023 - 9 octobre 2025
Capital initial : $100,000
Règles de la stratégie : les mêmes que dans la partie 1 (seuils de z-score : entrée à ±1.5, sortie à 0, stop loss à ±2.5)
Implémentation
# Load 2 years of historical data
backtest_start = "2023-10-11"
backtest_end = "2025-10-09"
ko_backtest = yf.download(
"KO", start=backtest_start, end=backtest_end, auto_adjust=False
)["Adj Close"]
pep_backtest = yf.download(
"PEP", start=backtest_start, end=backtest_end, auto_adjust=False
)["Adj Close"]
# Calculate rolling statistics (60-day lookback)
lookback = 60
spread_backtest = ko_backtest - beta * pep_backtest
rolling_mean = spread_backtest.rolling(window=lookback).mean()
rolling_std = spread_backtest.rolling(window=lookback).std()
z_score_backtest = (spread_backtest - rolling_mean) / rolling_std
Résultats du backtest
Performance sur 2 ans :
| Mesure | Valeur |
|---|---|
| Rendement annualisé | -18.91% |
| Nombre de trades | 21 |
| Taux de gain | 28.6% |
| Drawdown maximal | ~34% |
Ce que le backtest change
Le taux de gain simulé et le résultat historique pointent dans des directions opposées. Cet écart est l’avertissement principal. La dynamique simulée n’a pas reproduit le comportement du marché qui comptait sur cet échantillon.
Ali G ferme le billet :