Cointégration: Test d’Engle-Granger pour identifier les paires
Spread: Résidu de la régression linéaire entre les deux prix
Z-score: Signal d’entrée quand |z| > threshold (ex: 1.5)
Half-life: Filtre sur la demi-vie de retour à la moyenne
Performance de référence
Sharpe ~0.8-1.2 (2020-2025) - marché neutre avec faible corrélation au marché.
Hypothèses à tester
Z-score threshold: 1.0, 1.5, 2.0
Lookback pour spread: 20, 50, 100 jours
Half-life max: 20, 30, 40 jours
Prérequis
Environnement Lean Research
Données ETF sectoriels
statsmodels pour tests de cointégration
Durée estimée: ~10 minutes
Note : Cette version simplifiée se concentre sur quelques paires pré-sélectionnées. L’implémentation complète utilise des modules custom pour la sélection dynamique des paires.
# Setup QuantBookfrom AlgorithmImports import*import numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom itertools import combinationsimport warningswarnings.filterwarnings('ignore')plt.style.use('seaborn-v0_8-darkgrid')plt.rcParams['figure.figsize'] = (14, 5)qb = QuantBook()print("QuantBook initialisé.")
QuantBook initialisé.
1. Chargement des données
On charge les données d’ETFs sectoriels pour la période 2020-2026.
Pivot de la série ‘close’ en DataFrame large, avec remapping des colonnes Symbol → ticker pour ETF-Pairs.
# Pivoter les donnéescloses = history['close'].unstack(level=0)symbol_to_ticker = {str(v): k for k, v in symbols.items()}closes.columns = [symbol_to_ticker.get(str(c), str(c)) for c in closes.columns]closes = closes.dropna()print(f"Période: {closes.index[0].date()} à {closes.index[-1].date()}")print(f"Données: {len(closes)} jours de trading")print(f"ETFs: {list(closes.columns)}")print(f"\nStatistiques des prix finaux:")for ticker in etf_tickers:if ticker in closes.columns: ret = (closes[ticker].iloc[-1] / closes[ticker].iloc[0] -1) *100print(f" {ticker}: {ret:+.1f}%")
Période: 2020-01-02 à 2025-12-31
Données: 1508 jours de trading
ETFs: ['AAPL', 'GOOGL', 'IWM', 'QQQ', 'SPY']
Statistiques des prix finaux:
SPY: +24.7%
QQQ: +48.9%
IWM: +35.4%
AAPL: +64.3%
GOOGL: +50.7%
2. Test de Cointégration
Le test d’Engle-Granger vérifie si deux séries temporelles partagent une relation à long terme.
def engle_granger_coint(y1, y2, significance=0.05):""" Test de cointégration d'Engle-Granger. Retourne: (is_cointegrated, pvalue, hedge_ratio, half_life) """from numpy.linalg import lstsq# Aligner les séries common_idx = y1.index.intersection(y2.index) y1_aligned = y1.loc[common_idx] y2_aligned = y2.loc[common_idx]iflen(y1_aligned) <50:returnFalse, 1.0, 1.0, 999# Régression OLS: y1 = hedge_ratio * y2 + residual X = y2_aligned.values.reshape(-1, 1) y = y1_aligned.values hedge_ratio, _, _, _ = lstsq(X, y, rcond=None) hedge_ratio = hedge_ratio[0]# Calculer le spread spread = y - hedge_ratio * y2_aligned.values# Test de Dickey-Fuller augmenté sur le spreadfrom statsmodels.tsa.stattools import adfuller adf_result = adfuller(spread, maxlag=1) pvalue = adf_result[1]# Calculer la demi-vie (half-life) du spread# Pour un processus OU: half_life = -ln(2) / ln(alpha) spread_lagged = spread[:-1] spread_current = spread[1:]iflen(spread_lagged) >10:# Régression: spread[t] = alpha * spread[t-1] + epsilon X_lag = spread_lagged.reshape(-1, 1) alpha, _, _, _ = lstsq(X_lag, spread_current, rcond=None) alpha = alpha[0]if0< alpha <1: half_life =-np.log(2) / np.log(alpha)else: half_life =999else: half_life =999 is_cointegrated = pvalue < significancereturn is_cointegrated, pvalue, hedge_ratio, half_life# Tester toutes les paires d'ETFsprint("Recherche de paires cointégrées...\n")pairs_results = []for etf1, etf2 in combinations(closes.columns, 2): is_coint, pval, hr, hl = engle_granger_coint( closes[etf1], closes[etf2], significance=0.05 )if is_coint and hl <50: # Filtre: half-life < 50 jours corr = closes[etf1].corr(closes[etf2]) pairs_results.append({'pair': f"{etf1}-{etf2}",'pvalue': pval,'hedge_ratio': hr,'half_life': hl,'correlation': corr })# Trier par p-valuepairs_results.sort(key=lambda x: x['pvalue'])print(f"Paires cointégrées trouvées: {len(pairs_results)}\n")print(f"{'Paire':<15}{'P-value':>10}{'Hedge Ratio':>12}{'Half-Life':>10}{'Corrélation':>10}")print("-"*70)for p in pairs_results[:10]:print(f"{p['pair']:<15}{p['pvalue']:>10.4f}{p['hedge_ratio']:>12.4f}{p['half_life']:>10.1f}j {p['correlation']:>10.2f}")
Recherche de paires cointégrées...
Paires cointégrées trouvées: 3
Paire P-value Hedge Ratio Half-Life Corrélation
----------------------------------------------------------------------
GOOGL-IWM 0.0000 9.3634 18.9j 0.98
QQQ-SPY 0.0000 0.8044 38.4j 0.97
AAPL-QQQ 0.0059 0.3838 44.3j 0.97
Interprétation: Cointégration
P-value < 0.05: La paire est cointégrée (relation stable à long terme)
Hedge Ratio: Nombre d’unités de l’ETF2 pour couvrir 1 unité de l’ETF1
Half-Life: Temps moyen pour que le spread retourne à sa moyenne
Corrélation: Corrélation prix (pas la même chose que cointégration!)
Important: Deux séries peuvent être corrélées sans être cointégrées (drift commun). La cointégration implique une relation d’équilibre à long terme.
3. Backtest Pairs Trading
Simulation de la stratégie avec: - Top 3 paires cointégrées - Entrée quand |z-score| > threshold - Sortie quand |z-score| < 0.5 - Position size pondérée par la volatilité