Objectif
Analyser la stratégie EMA crossover sur un panier d’actions tech.
Stratégie
Univers : AAPL, MSFT, GOOGL, AMZN, NVDA
Signal : EMA fast > EMA slow (cross haussier)
Allocation : Equal-weight des actions en signal
Rebalance : Quotidien
Max positions : 5 (toutes les actions)
Hypothèses à tester
Période EMA: (10/40), (20/50), (20/60)
Allocation: equal-weight vs momentum-weighted
Univers: 5 stocks vs 10 stocks diversifiés
Prérequis
Environnement Lean Research
Données actions US
Durée estimée: ~8 minutes
Note : Cette stratégie diffère de EMA-Cross-Index par la diversification intra-sectorielle (5 actions au lieu de 1 ETF SPY).
# Setup QuantBook
from AlgorithmImports import *
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore' )
plt.style.use('seaborn-v0_8-darkgrid' )
plt.rcParams['figure.figsize' ] = (14 , 5 )
qb = QuantBook()
print ("QuantBook initialisé." )
1. Chargement des données
On charge les données actions pour la période 2010-2026.
# Univers Tech Stocks
tickers = ["AAPL" , "MSFT" , "GOOGL" , "AMZN" , "NVDA" ]
symbols = {}
for ticker in tickers:
symbols[ticker] = qb.add_equity(ticker, Resolution.DAILY).symbol
# Charger l'historique (2010-2026 pour multi-regime)
start = datetime(2010 , 1 , 1 )
end = datetime(2026 , 1 , 1 )
history = qb.history(list (symbols.values()), start, end, Resolution.DAILY)
print (f"Données chargées: { len (history)} lignes" )
Données chargées: 8048 lignes
Pivot de la série ‘close’ en DataFrame large, avec remapping des colonnes Symbol → ticker pour EMA-Cross-Stocks.
# Pivoter les données
closes = 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"Actions: { list (closes.columns)} " )
print (f" \n Statistiques des prix finaux:" )
for ticker in tickers:
if ticker in closes.columns:
ret = (closes[ticker].iloc[- 1 ] / closes[ticker].iloc[0 ] - 1 ) * 100
print (f" { ticker} : { ret:+.1f} %" )
Période: 2010-01-04 à 2025-12-31
Données: 4024 jours de trading
Actions: ['AAPL', 'GOOGL']
Statistiques des prix finaux:
AAPL: +1753.9%
GOOGL: +558.2%
2. Calcul des EMA
Calcul des moyennes mobiles exponentielles fast et slow.
def compute_ema(closes, period):
"""Calcule l'EMA pour une période donnée."""
return closes.ewm(span= period, adjust= False ).mean()
# EMA avec paramètres par défaut (20/50)
ema_fast = compute_ema(closes, 20 )
ema_slow = compute_ema(closes, 50 )
print ("EMA Fast (20) - Derniers 5 jours:" )
print (ema_fast.iloc[- 5 :])
print (f" \n EMA Slow (50) - Derniers 5 jours:" )
print (ema_slow.iloc[- 5 :])
EMA Fast (20) - Derniers 5 jours:
AAPL GOOGL
time
2025-12-24 13:00:00 122.15 2062.52
2025-12-26 16:00:00 122.15 2062.52
2025-12-29 16:00:00 122.15 2062.52
2025-12-30 16:00:00 122.15 2062.52
2025-12-31 16:00:00 122.15 2062.52
EMA Slow (50) - Derniers 5 jours:
AAPL GOOGL
time
2025-12-24 13:00:00 122.15 2062.52
2025-12-26 16:00:00 122.15 2062.52
2025-12-29 16:00:00 122.15 2062.52
2025-12-30 16:00:00 122.15 2062.52
2025-12-31 16:00:00 122.15 2062.52
Interprétation: EMA Crossover
EMA fast > EMA slow : Tendance haussière (signal long)
EMA fast < EMA slow : Tendance baissière (signal exit)
Cross : Changement de tendance potentiel
Multi-stock : Diversification réduit le risque spécifique
3. Backtest Multi-Stock EMA
Simulation de la stratégie avec: - EMA 20/50 crossover - Equal-weight allocation - Rebalance quotidien - Max 95% investi
def backtest_ema_cross_stocks(closes, ema_fast, ema_slow, max_invested= 0.95 ):
"""
Backtest Multi-Stock EMA Crossover.
Retourne les métriques de performance.
"""
portfolio_values = [1.0 ]
current_positions = {} # ticker -> weight
warmup = max (ema_fast.shape[1 ], ema_slow.shape[1 ]) # Wait for slowest EMA
for i in range (warmup, len (closes)):
# Find stocks with bullish EMA cross
bullish = []
for ticker in closes.columns:
if ema_fast[ticker].iloc[i] > ema_slow[ticker].iloc[i]:
bullish.append(ticker)
# Equal weight allocation
target_weight = max_invested / max (len (bullish), 1 ) if bullish else 0
current_positions = {ticker: target_weight for ticker in bullish}
# Calculate return
daily_returns = closes.iloc[i] / closes.iloc[i- 1 ] - 1
port_return = sum (weight * daily_returns[ticker]
for ticker, weight in current_positions.items())
portfolio_values.append(portfolio_values[- 1 ] * (1 + port_return))
# Métriques
returns = np.diff(portfolio_values) / np.array(portfolio_values[:- 1 ])
cum_returns = pd.Series(portfolio_values[1 :], index= closes.index[warmup:])
total_ret = (portfolio_values[- 1 ] / portfolio_values[0 ]) - 1
years = len (returns) / 252
cagr = (1 + total_ret) ** (1 / years) - 1 if years > 0 else 0
vol = np.std(returns) * np.sqrt(252 ) if len (returns) > 1 else 0
sharpe = (cagr - 0.03 ) / vol if vol > 0.001 else 0
running_max = cum_returns.expanding().max ()
drawdown = (cum_returns - running_max) / running_max
max_dd = drawdown.min ()
return {
'cum' : cum_returns,
'sharpe' : sharpe,
'cagr' : cagr,
'max_dd' : max_dd,
'vol' : vol,
'final_value' : portfolio_values[- 1 ]
}
result = backtest_ema_cross_stocks(closes, ema_fast, ema_slow)
print (f"Performance Multi-Stock EMA:" )
print (f" Sharpe: { result['sharpe' ]:.3f} " )
print (f" CAGR: { result['cagr' ]:.1%} " )
print (f" Max DD: { result['max_dd' ]:.1%} " )
print (f" Vol: { result['vol' ]:.1%} " )
Performance Multi-Stock EMA:
Sharpe: 0.886
CAGR: 18.6%
Max DD: -21.6%
Vol: 17.6%
4. Test des Périodes EMA
# Test différentes paires EMA
ema_pairs = [
((10 , 40 ), "EMA10/40" ),
((20 , 50 ), "EMA20/50" ),
((20 , 60 ), "EMA20/60" ),
]
print (f" { 'Période EMA' :<12} { 'Sharpe' :>8} { 'CAGR' :>8} { 'MaxDD' :>8} " )
print ("-" * 40 )
ema_results = {}
for (fast, slow), name in ema_pairs:
ema_f = compute_ema(closes, fast)
ema_s = compute_ema(closes, slow)
r = backtest_ema_cross_stocks(closes, ema_f, ema_s)
ema_results[name] = r
print (f" { name:<12} { r['sharpe' ]:>8.3f} { r['cagr' ]:>7.1%} { r['max_dd' ]:>7.1%} " )
best_ema = max (ema_results.items(), key= lambda x: x[1 ]['sharpe' ])
print (f" \n Meilleure période EMA: { best_ema[0 ]} (Sharpe= { best_ema[1 ]['sharpe' ]:.3f} )" )
Période EMA Sharpe CAGR MaxDD
----------------------------------------
EMA10/40 1.064 21.1% -18.3%
EMA20/50 0.886 18.6% -21.6%
EMA20/60 0.892 19.0% -19.0%
Meilleure période EMA: EMA10/40 (Sharpe=1.064)
5. Test d’Allocation Momentum-Weighted
def backtest_ema_momentum_weighted(closes, ema_fast, ema_slow, max_invested= 0.95 ):
"""
Backtest avec allocation momentum-weighted.
Le poids est proportionnel à la distance entre EMAs.
"""
portfolio_values = [1.0 ]
warmup = max (ema_fast.shape[1 ], ema_slow.shape[1 ])
for i in range (warmup, len (closes)):
# Find bullish stocks and compute momentum strength
bullish_strength = {}
for ticker in closes.columns:
fast_val = ema_fast[ticker].iloc[i]
slow_val = ema_slow[ticker].iloc[i]
if fast_val > slow_val:
# Strength = % distance above slow EMA
strength = (fast_val / slow_val) - 1
bullish_strength[ticker] = max (0 , strength)
if not bullish_strength:
portfolio_values.append(portfolio_values[- 1 ])
continue
# Normalize weights
total_strength = sum (bullish_strength.values())
weights = {k: (v / total_strength) * max_invested
for k, v in bullish_strength.items()}
# Calculate return
daily_returns = closes.iloc[i] / closes.iloc[i- 1 ] - 1
port_return = sum (weights[ticker] * daily_returns[ticker]
for ticker in weights)
portfolio_values.append(portfolio_values[- 1 ] * (1 + port_return))
# Métriques
returns = np.diff(portfolio_values) / np.array(portfolio_values[:- 1 ])
cum_returns = pd.Series(portfolio_values[1 :], index= closes.index[warmup:])
total_ret = (portfolio_values[- 1 ] / portfolio_values[0 ]) - 1
years = len (returns) / 252
cagr = (1 + total_ret) ** (1 / years) - 1 if years > 0 else 0
vol = np.std(returns) * np.sqrt(252 ) if len (returns) > 1 else 0
sharpe = (cagr - 0.03 ) / vol if vol > 0.001 else 0
running_max = cum_returns.expanding().max ()
drawdown = (cum_returns - running_max) / running_max
max_dd = drawdown.min ()
return {
'cum' : cum_returns,
'sharpe' : sharpe,
'cagr' : cagr,
'max_dd' : max_dd,
'vol' : vol,
'final_value' : portfolio_values[- 1 ]
}
# Comparaison Equal-weight vs Momentum-weighted
ew_result = backtest_ema_cross_stocks(closes, ema_fast, ema_slow)
mw_result = backtest_ema_momentum_weighted(closes, ema_fast, ema_slow)
print (f" { 'Allocation' :<15} { 'Sharpe' :>8} { 'CAGR' :>8} { 'MaxDD' :>8} " )
print ("-" * 43 )
print (f" { 'Equal-weight' :<15} { ew_result['sharpe' ]:>8.3f} { ew_result['cagr' ]:>7.1%} { ew_result['max_dd' ]:>7.1%} " )
print (f" { 'Momentum-weight' :<15} { mw_result['sharpe' ]:>8.3f} { mw_result['cagr' ]:>7.1%} { mw_result['max_dd' ]:>7.1%} " )
Allocation Sharpe CAGR MaxDD
-------------------------------------------
Equal-weight 0.886 18.6% -21.6%
Momentum-weight 0.931 19.8% -21.4%
6. Comparaison avec SPY B&H
# Charger SPY pour comparaison
spy = qb.add_equity("SPY" , Resolution.DAILY).symbol
spy_history = qb.history(spy, start, end, Resolution.DAILY)
spy_close = spy_history['close' ]
# Aligner les dates
warmup = 50
spy_values = spy_close.iloc[warmup:] / spy_close.iloc[warmup]
# Métriques SPY
spy_ret = spy_values.pct_change().dropna()
spy_cagr = (spy_values.iloc[- 1 ] ** (252 / len (spy_values))) - 1
spy_vol = spy_ret.std() * np.sqrt(252 )
spy_sharpe = (spy_cagr - 0.03 ) / spy_vol
spy_dd = (spy_values / spy_values.cummax() - 1 ).min ()
print ("=== Comparaison vs SPY B&H ===" )
print (f" { 'Stratégie' :<20} { 'CAGR' :>10} { 'Sharpe' :>10} { 'MaxDD' :>10} " )
print ("-" * 53 )
print (f" { 'Multi-Stock EMA' :<20} { result['cagr' ]:>9.1%} { result['sharpe' ]:>10.3f} { result['max_dd' ]:>9.1%} " )
print (f" { 'SPY B&H' :<20} { spy_cagr:>9.1%} { spy_sharpe:>10.3f} { spy_dd:>9.1%} " )
=== Comparaison vs SPY B&H ===
Stratégie CAGR Sharpe MaxDD
-----------------------------------------------------
Multi-Stock EMA 18.6% 0.886 -21.6%
SPY B&H 9.6% 0.454 -33.7%
7. Visualisation des résultats
fig, axes = plt.subplots(1 , 2 , figsize= (16 , 5 ))
# Gauche: EMA periods comparison
ax = axes[0 ]
for name, r in ema_results.items():
ax.plot(r['cum' ].values, label= f" { name} (S= { r['sharpe' ]:.2f} )" , linewidth= 1.5 )
ax.plot(spy_values.values, label= 'SPY B&H' , linestyle= '--' , alpha= 0.5 )
ax.set_title('Période EMA optimale' , fontsize= 12 , fontweight= 'bold' )
ax.set_ylabel('Valeur du portefeuille' )
ax.legend(fontsize= 8 )
ax.grid(True , alpha= 0.3 )
# Droite: Equal vs Momentum weighted
ax = axes[1 ]
ax.plot(ew_result['cum' ].values, label= f"Equal-weight (S= { ew_result['sharpe' ]:.2f} )" , linewidth= 1.5 )
ax.plot(mw_result['cum' ].values, label= f"Momentum-weight (S= { mw_result['sharpe' ]:.2f} )" , linewidth= 1.5 )
ax.plot(spy_values.values, label= 'SPY B&H' , linestyle= '--' , alpha= 0.5 )
ax.set_title('Méthode d \' allocation' , fontsize= 12 , fontweight= 'bold' )
ax.set_ylabel('Valeur du portefeuille' )
ax.legend(fontsize= 8 )
ax.grid(True , alpha= 0.3 )
plt.tight_layout()
plt.savefig('ema_cross_stocks_analysis.png' , dpi= 150 , bbox_inches= 'tight' )
plt.show()
print ("Graphique sauvegardé." )
8. Analyse de la Diversification
Calculons la corrélation entre les 5 actions pour comprendre la diversification.
# Matrice de corrélation
returns = closes.pct_change().dropna()
corr_matrix = returns.corr()
print ("Matrice de corrélation:" )
print (corr_matrix.round (2 ))
# Corrélation moyenne
avg_corr = corr_matrix.values[np.triu_indices_from(corr_matrix.values, k= 1 )].mean()
print (f" \n Corrélation moyenne: { avg_corr:.2f} " )
if avg_corr > 0.8 :
print (" \n ⚠️ Corrélation élevée - diversification limitée" )
elif avg_corr > 0.5 :
print (" \n ✓ Corrélation modérée - diversification acceptable" )
else :
print (" \n ✓ Corrélation faible - bonne diversification" )
Matrice de corrélation:
AAPL GOOGL
AAPL 1.00 0.53
GOOGL 0.53 1.00
Corrélation moyenne: 0.53
✓ Corrélation modérée - diversification acceptable
9. Conclusions et recommandations
Résumé
Période EMA
(à remplir)
Allocation
(à remplir)
Sharpe
(à remplir)
CAGR
(à remplir)
Corrélation moyenne
(à remplir)
Verdict
Si Sharpe > 0.9: Déployer avec les paramètres optimaux
Points forts Multi-Stock EMA
Diversification : 5 actions réduit le risque spécifique
Simplicité : Signaux EMA clairs et interprétables
Adaptativité : Chaque action a son propre cycle
Limitations
Corrélation élevée : Actions tech souvent corrélées
Whipsaws : EMA crossover peut générer beaucoup de trades
Sector concentration : 100% tech = concentration sectorielle
Prochaines étapes
Déployer sur QC cloud avec les paramètres optimaux
Tester avec un univers multi-sectoriel (tech + finance + healthcare)
Ajouter un filtre de tendance macro (SMA200 du marché)
Optimiser la fréquence de rebalance (hebdomadaire vs quotidien)
Retour au sommet