Analyser la combinaison de deux stratégies alpha complementaires via le QC Algorithm Framework: - EMA-Cross Alpha: Mean-reversion rapide sur 5 tech stocks (EMA20 > EMA50) - TrendStocks Alpha: Trend-following a double confirmation sur 15 actions diversifiees (Prix > SMA200 AND EMA20 > EMA50)
Pivot de la série ‘close’ en DataFrame large, avec remapping des colonnes Symbol → ticker pour Framework_Composite_EMATrend.
# Pivoter les donnees pour avoir un DataFrame de prixcloses = history['close'].unstack(level=0)# Renommer les colonnessymbol_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"Periode: {closes.index[0].date()} a {closes.index[-1].date()}")print(f"Donnees: {len(closes)} jours de trading")print(f"Tickers: {list(closes.columns)}")
Periode: 2015-01-02 a 2025-12-31
Donnees: 2766 jours de trading
Tickers: ['AAPL', 'GOOGL', 'SPY']
2. Implementation des signaux Alpha
On implemente la logique des deux AlphaModels dans un format vectorise pour le backtest.
Signaux EMA-Cross (derniers 5 jours):
AAPL MSFT GOOGL AMZN NVDA
time
2025-12-24 13:00:00 0 NaN 1 NaN NaN
2025-12-26 16:00:00 0 NaN 1 NaN NaN
2025-12-29 16:00:00 0 NaN 1 NaN NaN
2025-12-30 16:00:00 0 NaN 1 NaN NaN
2025-12-31 16:00:00 0 NaN 1 NaN NaN
Signaux TrendStocks (derniers 5 jours):
AAPL MSFT GOOGL AMZN NVDA JPM V MA UNH JNJ XOM \
time
2025-12-24 13:00:00 0 NaN 0 NaN NaN NaN NaN NaN NaN NaN NaN
2025-12-26 16:00:00 0 NaN 0 NaN NaN NaN NaN NaN NaN NaN NaN
2025-12-29 16:00:00 0 NaN 0 NaN NaN NaN NaN NaN NaN NaN NaN
2025-12-30 16:00:00 0 NaN 0 NaN NaN NaN NaN NaN NaN NaN NaN
2025-12-31 16:00:00 0 NaN 0 NaN NaN NaN NaN NaN NaN NaN NaN
CVX HD PG KO
time
2025-12-24 13:00:00 NaN NaN NaN NaN
2025-12-26 16:00:00 NaN NaN NaN NaN
2025-12-29 16:00:00 NaN NaN NaN NaN
2025-12-30 16:00:00 NaN NaN NaN NaN
2025-12-31 16:00:00 NaN NaN NaN NaN
Interpretation: Signaux Alpha
EMA-Cross: Signal = 1 quand EMA20 > EMA50 (momentum court-terme)
TrendStocks: Signal = 1 quand Prix > SMA200 ET EMA20 > EMA50 (confirmation double)
Les 5 tech stocks (AAPL, MSFT, GOOGL, AMZN, NVDA) apparaissent dans les deux signaux, creant une synergie potentielle.
3. Backtest du Composite
Fonction de backtest vectorise combinant les deux stratégies avec allocation variable.
def backtest_composite(closes, ema_signals, trend_signals, ema_alloc=0.40, trend_alloc=0.60, ema_rebal_freq=1, trend_rebal_freq=5):""" Backtest du composite avec allocation variable. Args: ema_alloc: Allocation a EMA-Cross (ex: 0.40 = 40%) trend_alloc: Allocation a TrendStocks (ex: 0.60 = 60%) ema_rebal_freq: Frequence rebalance EMA (1 = daily) trend_rebal_freq: Frequence rebalance Trend (5 = weekly) """ returns_df = closes.pct_change() portfolio_values = [1.0] warmup =200 ema_counter =0 trend_counter =0# Tracking des positions ema_holdings =set() trend_holdings =set()for i inrange(warmup, len(closes)):# Mise a jour EMA-Cross holdings (daily) ema_counter +=1if ema_counter >= ema_rebal_freq: ema_counter =0 ema_holdings =set()for t in ema_tickers:if t in ema_signals.columns and ema_signals[t].iloc[i] ==1: ema_holdings.add(t)# Mise a jour TrendStocks holdings (weekly) trend_counter +=1if trend_counter >= trend_rebal_freq: trend_counter =0 trend_holdings =set()for t in trend_tickers:if t in trend_signals.columns and trend_signals[t].iloc[i] ==1: trend_holdings.add(t)# Calcul du return du portefeuille port_return =0.0# EMA-Cross contributioniflen(ema_holdings) >0: weight_per_stock = ema_alloc /len(ema_holdings)for t in ema_holdings:if t in returns_df.columns: port_return += weight_per_stock * returns_df[t].iloc[i]# TrendStocks contributioniflen(trend_holdings) >0: weight_per_stock = trend_alloc /len(trend_holdings)for t in trend_holdings:if t in returns_df.columns: port_return += weight_per_stock * returns_df[t].iloc[i] portfolio_values.append(portfolio_values[-1] * (1+ port_return))# Calcul des metriques 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) -1if years >0else0 vol = np.std(returns) * np.sqrt(252) iflen(returns) >1else0 sharpe = (cagr -0.03) / vol if vol >0.001else0 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] }print("Fonction de backtest composite definie.")
Fonction de backtest composite definie.
4. Test des allocations
On teste différentes allocations EMA/Trend de 30/70 a 70/30.
# Test differentes allocationsallocations = [ (0.30, 0.70, "EMA30/Trend70"), (0.40, 0.60, "EMA40/Trend60"), (0.50, 0.50, "EMA50/Trend50"), (0.60, 0.40, "EMA60/Trend40"), (0.70, 0.30, "EMA70/Trend30"),]results = {}print(f"{'Allocation':<20}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}{'Vol':>8}")print("-"*55)for ema_alloc, trend_alloc, name in allocations: r = backtest_composite(closes, ema_signals, trend_signals, ema_alloc=ema_alloc, trend_alloc=trend_alloc) results[name] = rprint(f"{name:<20}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}{r['vol']:>7.1%}")# Trouver la meilleure allocationbest_alloc =max(results.items(), key=lambda x: x[1]['sharpe'])print(f"\nMeilleure allocation: {best_alloc[0]} (Sharpe={best_alloc[1]['sharpe']:.3f})")
fig, axes = plt.subplots(1, 2, figsize=(16, 5))# Gauche: Comparaison des allocationsax = axes[0]for name, r in results.items(): ax.plot(r['cum'].values, label=f"{name} (S={r['sharpe']:.2f})", linewidth=1.5)ax.set_title('Allocation EMA/Trend', fontsize=12, fontweight='bold')ax.set_ylabel('Valeur du portefeuille')ax.legend(fontsize=9)ax.grid(True, alpha=0.3)# Droite: Composite vs Strategies individuellesax = axes[1]ax.plot(ema_result['values'][200:], label=f"EMA-Cross (S={ema_result['sharpe']:.2f})", linewidth=1.5)ax.plot(trend_result['values'][200:], label=f"TrendStocks (S={trend_result['sharpe']:.2f})", linewidth=1.5)ax.plot(best_alloc[1]['cum'].values, label=f"Composite {best_alloc[0]} (S={best_alloc[1]['sharpe']:.2f})", linewidth=2, linestyle='--')ax.set_title('Synergie Composite', fontsize=12, fontweight='bold')ax.set_ylabel('Valeur du portefeuille')ax.legend(fontsize=9)ax.grid(True, alpha=0.3)plt.tight_layout()plt.savefig('composite_ematrend_analysis.png', dpi=150, bbox_inches='tight')plt.show()print("Graphique sauvegarde.")
Graphique sauvegarde.
7. Analyse de la synergie des signaux
# Analyser la correlation des signaux sur les tech stocks (overlap)tech_overlap = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA"]# Compter les accords entre les deux strategiesagreement = pd.DataFrame(index=ema_signals.index, columns=tech_overlap)for t in tech_overlap:if t in ema_signals.columns and t in trend_signals.columns: agreement[t] = (ema_signals[t] == trend_signals[t]) & (ema_signals[t] ==1)# Pourcentage d'accord par stockagreement_pct = agreement.mean() *100print("Accord EMA-Cross / TrendStocks sur les tech stocks:")print(f"{'Stock':<10}{'Accord (%)':>12}")print("-"*25)for t in tech_overlap:if t in agreement_pct.index:print(f"{t:<10}{agreement_pct[t]:>11.1f}%")avg_agreement = agreement_pct.mean()print(f"\nAccord moyen: {avg_agreement:.1f}%")print(f"\nInterpretation: Quand les deux strategies sont d'accord sur un tech stock,\n"f"ce stock recoit une allocation double dans le composite.")
Accord EMA-Cross / TrendStocks sur les tech stocks:
Stock Accord (%)
-------------------------
AAPL 34.2%
MSFT 0.0%
GOOGL 42.3%
AMZN 0.0%
NVDA 0.0%
Accord moyen: 15.3%
Interpretation: Quand les deux strategies sont d'accord sur un tech stock,
ce stock recoit une allocation double dans le composite.
8. Conclusions et recommandations
Resume
Metrique
EMA-Cross
TrendStocks
Composite optimal
Sharpe
(a remplir)
(a remplir)
(a remplir)
CAGR
(a remplir)
(a remplir)
(a remplir)
Max DD
(a remplir)
(a remplir)
(a remplir)
Allocation recommandee
Allocation: [a remplir avec la meilleure allocation]
Prochaines étapes
Deployer sur QC cloud avec l’allocation optimale
Backtester sur différentes periodes (2015-2020, 2020-2026)
Tester la robustesse: variations de paramètres EMA (10/40, 15/45, 25/55)