Analyser la stratégie TrendStocks (double confirmation SMA200 + EMA20/50) sur 15 actions diversifiées via QuantBook, pour valider les résultats avant déploiement.
Pivot de la série ‘close’ en DataFrame large, avec remapping des colonnes Symbol → ticker pour TrendStocks-Alpha.
# 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"Tickers: {list(closes.columns)}")
Période: 2015-01-02 à 2025-12-31
Données: 2766 jours de trading
Tickers: ['AAPL', 'GOOGL']
2. Implémentation du signal TrendStocks
def compute_trend_stocks_signals(closes, tickers, ema_fast=20, ema_slow=50, sma_trend=200):"""Génère les signaux TrendStocks (double confirmation).""" signals = pd.DataFrame(index=closes.index, columns=tickers)for ticker in tickers:if ticker notin closes.columns:continue price = closes[ticker] ema_f = price.ewm(span=ema_fast, adjust=False).mean() ema_s = price.ewm(span=ema_slow, adjust=False).mean() sma_200 = price.rolling(sma_trend).mean()# Double confirmation: prix > SMA200 ET EMA20 > EMA50 price_above_sma = price > sma_200 ema_bullish = ema_f > ema_s signals[ticker] = (price_above_sma & ema_bullish).astype(int)return signals# Signaux avec paramètres par défautsignals = compute_trend_stocks_signals(closes, tickers)print("Signaux TrendStocks (derniers 5 jours):")print(signals.iloc[-5:])print(f"\nNombre moyen de positions: {signals.sum(axis=1).mean():.1f} sur {len(tickers)}")
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
Nombre moyen de positions: 0.8 sur 15
Interprétation: Signal TrendStocks
Signal = 1: Double confirmation (trend long-terme + momentum court-terme)
Signal = 0: Une des deux conditions n’est pas remplie
La double confirmation réduit les faux signaux par rapport à EMA-Cross simple.
3. Backtest de la stratégie TrendStocks
def backtest_trend_stocks(closes, signals, rebal_freq=5):"""Backtest TrendStocks equally-weighted.""" returns_df = closes.pct_change() portfolio_values = [1.0] warmup =250 counter =0 holdings = []for i inrange(warmup, len(closes)):# Mise à jour holdings (hebdomadaire) counter +=1if counter >= rebal_freq: counter =0 holdings = [t for t in signals.columns if signals[t].iloc[i] ==1]elif i == warmup: holdings = [t for t in signals.columns if signals[t].iloc[i] ==1]# Calcul du return port_return =0.0iflen(holdings) >0: weight_per_stock =1.0/len(holdings)for t in 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))# 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) -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 définie.")
Fonction de backtest définie.
4. Test des périodes EMA
# Test différentes périodes EMAema_pairs = [ (15, 45, "EMA15/45"), (20, 50, "EMA20/50"), (25, 55, "EMA25/55"),]results = {}print(f"{'Période EMA':<12}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}{'Vol':>8}")print("-"*50)for fast, slow, name in ema_pairs: sig = compute_trend_stocks_signals(closes, tickers, ema_fast=fast, ema_slow=slow) r = backtest_trend_stocks(closes, sig) results[name] = rprint(f"{name:<12}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}{r['vol']:>7.1%}")best_ema =max(results.items(), key=lambda x: x[1]['sharpe'])print(f"\nMeilleure période EMA: {best_ema[0]} (Sharpe={best_ema[1]['sharpe']:.3f})")
Période EMA Sharpe CAGR MaxDD Vol
--------------------------------------------------
EMA15/45 0.478 10.0% -25.5% 14.7%
EMA20/50 0.281 7.2% -20.9% 14.9%
EMA25/55 0.199 6.3% -26.5% 16.8%
Meilleure période EMA: EMA15/45 (Sharpe=0.478)
5. Test de la période SMA200
# Test différentes périodes SMAsma_periods = [150, 200, 250]print(f"{'Période SMA':<12}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}")print("-"*40)sma_results = {}for sma_p in sma_periods: sig = compute_trend_stocks_signals(closes, tickers, sma_trend=sma_p) r = backtest_trend_stocks(closes, sig) sma_results[sma_p] = rprint(f"SMA{sma_p:<9}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}")best_sma =max(sma_results.items(), key=lambda x: x[1]['sharpe'])print(f"\nMeilleure période SMA: {best_sma[0]} (Sharpe={best_sma[1]['sharpe']:.3f})")
Période SMA Sharpe CAGR MaxDD
----------------------------------------
SMA150 0.276 7.1% -22.6%
SMA200 0.281 7.2% -20.9%
SMA250 0.324 7.8% -20.3%
Meilleure période SMA: 250 (Sharpe=0.324)
6. Test du rebalancement
# Test fréquences de rebalancementrebal_freqs = [ (1, "Daily"), (5, "Weekly"), (21, "Monthly"),]print(f"{'Rebalancement':<12}{'Sharpe':>8}{'CAGR':>8}{'MaxDD':>8}")print("-"*40)sig = compute_trend_stocks_signals(closes, tickers)rebal_results = {}for freq, name in rebal_freqs: r = backtest_trend_stocks(closes, sig, rebal_freq=freq) rebal_results[name] = rprint(f"{name:<12}{r['sharpe']:>8.3f}{r['cagr']:>7.1%}{r['max_dd']:>7.1%}")