<< Sommaire QC | Précédent : QC-Py-07-Futures-Forex << | Suivant : QC-Py-09-Order-Types >>

QC-Py-08 - Multi-Asset Portfolio Stratégies

[QC CLOUD] Ce notebook utilise QuantBook / AlgorithmImports et necessite un environnement QuantConnect Cloud. Les cellules de code ne sont pas executables localement.

Diversification, correlation et allocation tactique Duree: 75 minutes | Niveau: Intermediaire-Avance | Python + QuantConnect


Objectifs d’Apprentissage

A la fin de ce notebook, vous serez capable de :

  1. Construire un portfolio multi-classes d’actifs (equities, bonds, commodities, crypto)
  2. Analyser les correlations entre actifs et detecter les regimes de marche
  3. Implementer plusieurs stratégies d’allocation (equal weight, risk parity, tactical)
  4. Comprendre les principes de hedging avec options et futures
  5. Créer un All-Weather Portfolio inspire de Ray Dalio

Prerequisites

  • Notebooks QC-Py-01 a 07 completes
  • Comprehension de base de la théorie de portefeuille
  • Familiarite avec numpy et pandas

Structure du Notebook

  1. Introduction Multi-Asset (10 min)
  2. Ajouter Multiple Asset Classes (10 min)
  3. Calcul des Correlations (10 min)
  4. Detection de Regime (10 min)
  5. Equal Weight Portfolio (10 min)
  6. Risk Parity (10 min)
  7. Tactical Asset Allocation (10 min)
  8. Hedging avec Options (10 min)
  9. Hedging avec Futures (10 min)
  10. All-Weather Portfolio (25 min)

Note de conception : Ce notebook contient du code de reference a copier dans QuantConnect Lab (main.py). Les cellules ne sont pas prevues pour etre executees en tant que notebook Jupyter. L’absence d’outputs (execution_count: null) est intentionnelle.

[REFERENCE QC Cloud] Ce notebook illustre du code QuantConnect a executer dans l’IDE Cloud (https://www.quantconnect.com/research). L’environnement local ne dispose pas de QuantBook ni de l’historical data feed. Pour executer : cloner le projet QC associe, ouvrir research.ipynb, executer cellule par cellule.

Pourquoi ce marqueur [REFERENCE QC Cloud] : les cellules de ce notebook sont des gabarits QuantConnect à copier dans main.py d’un projet QC Lab. Elles ne s’exécutent pas localement (kernel Jupyter classique), mais illustrent la mécanique de chaque stratégie.

Trois familles de cellules dans ce notebook : 1. Gabarits QC (marquées [REFERENCE QC]) — déployés tels quels sur QC Cloud via lean cloud push. 2. Backtests référentiels (marquées [BACKTEST QC Cloud]) — résultats de backtests précédents, avec Sharpe/CAGR/DD rapportés. 3. Notes pédagogiques (marquées > **Note technique** ou > **Note pedagogique**) — observations qualitatives sur les patterns observés.

Coût d’un backtest QC : ~5-15 min pour 10 ans daily, ~30-60 min pour intraday. Pour itérer sur la logique, utilisez d’abord les démos Python locales (cf. QC-Py-04 Research Workflow).


Mode d’emploi : Ce notebook a deux parties : 1. Sections analyse/ML (pandas, sklearn, matplotlib) : executables en Jupyter local 2. Sections integration QC (classes QCAlgorithm) : code de reference a copier dans main.py de votre projet QC Lab

Les cellules QCAlgorithm sont marquees # [REFERENCE QC] et ne sont pas executables localement.


Introduction

Ce notebook couvre les stratégies multi-actifs sur QuantConnect : comment combiner actions, obligations, commodities, et FX dans un portefeuille cohérent.

Pré-requis : - QC-Py-01 à QC-Py-07 (setup → platform → data → universes → options → multi-asset primitives → futures/forex). - Connaissance de base en statistiques (corrélation, volatilité). - Familiarité avec les notions de risque (variance, drawdown).

Objectifs pédagogiques : 1. Comprendre la matrice de corrélation entre classes d’actifs. 2. Maîtriser les méthodes d’allocation : equal weight, risk parity, tactical asset allocation, all-weather. 3. Utiliser options (protective put, collar) pour hedger un portefeuille. 4. Hedger avec futures (calcul du contrat, basis risk). 5. Implémenter une stratégie complète combinant ces éléments.

Public cible : ingénieurs quant junior qui veulent dépasser les stratégies mono-actif pour gérer un portefeuille réaliste.


Partie 1 : Fondations Multi-Asset (20 min)

1.1 Pourquoi Diversifier ?

La diversification est souvent appelee “the only free lunch in finance” (Harry Markowitz). L’idee centrale est simple : en combinant des actifs qui ne bougent pas ensemble, on peut reduire le risque sans sacrifier le rendement.

Concepts Cles

Concept Definition Importance
Correlation Mesure de co-mouvement entre actifs (-1 a +1) Faible correlation = meilleure diversification
Risk-Adjusted Returns Rendement par unite de risque (ex: Sharpe Ratio) Objectif : maximiser rendement/risque
Modern Portfolio Theory Framework mathematique pour optimiser allocation Frontiere efficiente, portfolio optimal

Classes d’Actifs Principales

Classe Exemples QuantConnect Caractéristiques
Equities SPY, QQQ, IWM Croissance, volatilite moyenne-haute
Bonds TLT, IEF, BND Revenus, faible volatilite, hedge contre recession
Commodities GLD, USO, DBC Couverture inflation, decorrelation
Forex EURUSD, USDJPY Liquidite, hedging devises
Crypto BTCUSD, ETHUSD Haute volatilite, potentiel croissance

La Magie de la Diversification

Considerons deux actifs avec le même rendement espere de 10% et volatilite de 20%.

Correlation Volatilite Portfolio 50/50 Reduction Risque
+1.0 20% 0%
+0.5 17.3% 13.5%
0.0 14.1% 29.5%
-0.5 10% 50%
-1.0 0% 100%

Point cle : Même avec une correlation de 0, on reduit la volatilite de pres de 30% sans toucher au rendement espere !

Ancres savantes – Markowitz, H. (1952), Portfolio Sélection, The Journal of Finance 7(1):77-91. DOI 10.1111/j.1540-6261.1952.tb01525.x (théorie moderne du portefeuille MPT, frontiere efficiente, aphorisme du “free lunch”) ; Sharpe, W.F. (1966), Mutual Fund Performance, The Journal of Business 39(1):119-138. DOI 10.1086/294846 (ratio reward-to-variability / Sharpe Ratio).

Vue d’ensemble du notebook

Ce notebook est structuré en 5 parties progressives + une conclusion :

Partie Thème Durée Compétence visée
1 Multi-Asset de base 25 min Ajouter plusieurs classes
2 Corrélation + régime 30 min Détection de stress
3 Méthodes d’allocation 30 min Equal/RP/TAA/All-Weather
4 Hedging options + futures 25 min Protective put, collar, ES
5 Stratégie complète 20 min Assemblage final

Conseil de lecture : - Si vous connaissez déjà Markowitz/Equal Weight : sautez à la partie 2 (corrélation) puis revenez aux exercices. - Si vous voulez juste hedger un portefeuille existant : parties 4 + 5 directement. - Pour une compréhension complète : lecture linéaire 1 → 5.

Pré-requis technique : Python intermédiaire, notions de statistiques (corrélation, volatilité), familiarité avec QC-Py-01 à QC-Py-07.

1.2 Ajouter Multiple Asset Classes

Creons un portfolio diversifie avec QuantConnect. Nous utiliserons des ETFs comme proxies pour différentes classes d’actifs (accessibles aux investisseurs particuliers).

Pour ajouter plusieurs classes d’actifs dans une même stratégie, on multiplie les appels AddEquity/AddFuture/AddForex dans Initialize. Chaque appel enregistre un Symbol dans le framework.

self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.tyx = self.AddEquity("TLT", Resolution.Daily).Symbol  # Treasury bonds
self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol  # Gold
self.eem = self.AddEquity("EEM", Resolution.Daily).Symbol  # Emerging markets
self.vix = self.AddData(VIX, "VIX", Resolution.Daily).Symbol  # Volatility index

Trois principes : 1. Résolution uniforme : tous les assets en daily pour cohérence temporelle. Mixing daily/intraday complique le backtest. 2. Densité de données : SPY/TLT/GLD sont denses (quotidien historique profond), EEM/VIX moins (depuis 1990/1992). 3. Coût de rebalance : chaque asset a son spread/slippage. Plus d’actifs = plus de coûts de transaction à chaque rebalance.

# Imports QuantConnect
from AlgorithmImports import *

# Imports pour analyse
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import datetime, timedelta

# Configuration matplotlib
plt.style.use('seaborn-v0_8-darkgrid')
%matplotlib inline

print("Imports reussis")
Imports reussis
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Classe pour multi-asset portfolio
class MultiAssetPortfolioAlgorithm(QCAlgorithm):
    """
    Exemple de configuration multi-asset portfolio.
    Cette classe montre comment ajouter differentes classes d'actifs.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # === EQUITIES ===
        # Large-cap US (proxy S&P 500)
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        # Tech heavy (proxy Nasdaq)
        self.qqq = self.AddEquity("QQQ", Resolution.Daily).Symbol
        
        # === BONDS (via ETF) ===
        # Long-term Treasury (20+ years) - sensible aux taux
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
        # Intermediate Treasury (7-10 years) - moins volatile
        self.ief = self.AddEquity("IEF", Resolution.Daily).Symbol
        
        # === COMMODITIES (via ETF) ===
        # Gold - valeur refuge, hedge inflation
        self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
        # Oil - cycle economique, energie
        self.uso = self.AddEquity("USO", Resolution.Daily).Symbol
        
        # === CRYPTO ===
        # Bitcoin - disponible via Coinbase/GDAX sur QuantConnect
        self.btc = self.AddCrypto("BTCUSD", Resolution.Daily).Symbol
        
        # Stocker tous les symboles pour faciliter les operations
        self.symbols = {
            'equities': [self.spy, self.qqq],
            'bonds': [self.tlt, self.ief],
            'commodities': [self.gld, self.uso],
            'crypto': [self.btc]
        }
        
        self.all_symbols = [self.spy, self.qqq, self.tlt, self.ief, 
                           self.gld, self.uso, self.btc]

# Afficher la structure
print("Structure Multi-Asset Portfolio:")
print("="*50)
print("\nEquities:")
print("  - SPY: S&P 500 ETF (large-cap US)")
print("  - QQQ: Nasdaq 100 ETF (tech-heavy)")
print("\nBonds:")
print("  - TLT: 20+ Year Treasury Bond ETF")
print("  - IEF: 7-10 Year Treasury Bond ETF")
print("\nCommodities:")
print("  - GLD: Gold ETF")
print("  - USO: United States Oil Fund")
print("\nCrypto:")
print("  - BTCUSD: Bitcoin (via Coinbase)")

Résultats du backtest QC Cloud (MultiAssetPortfolioAlgorithm)

Metrique Valeur
Dates negociables 3653
Ordres executes 0
Sharpe Ratio 0
CAGR 0%
Max Drawdown 0%
Net Profit 0.0%
Equity finale $100,000
Probabilistic Sharpe Ratio 0%

Multi-asset portfolio setup (Equities, Bonds, Commodities, Crypto). Backtest QC Cloud 2015-2024, setup sans trades (projet 33177683, bt 51753ddc922582dc34c04f236a846528)

Pourquoi la diversification fonctionne

La diversification réduit le risque idiosyncratique d’un actif spécifique au profit du risque systématique de la classe d’actifs. Markowitz (1952) l’a formalisé : pour un niveau de rendement donné, un portefeuille diversifié a un risque inférieur à la moyenne pondérée des risques individuels.

Trois nombres clés à retenir : - Corrélation = 1 : diversification nulle (le portefeuille est l’actif individuel). - Corrélation = 0 : diversification maximale (risque réduit par √N si les actifs sont indépendants). - Corrélation = -1 : diversification parfaite (le risque peut être annulé en combinant long/short).

En pratique : la corrélation entre actions (SPY/EEM) est ~0.7-0.9 en période calme, ~0.95 en krach. Entre actions et obligations (SPY/TLT), elle est ~-0.3 à +0.4 selon le régime. Entre toutes les classes d’actifs, la corrélation moyenne est ~0.4-0.6 en période normale.

[BACKTEST QC Cloud] MultiAssetPortfolioAlgorithm Tradeable dates: 3653 | Orders: 0 | Net Profit: 0% | Sharpe: 0 | CAGR: 0% | MaxDD: 0% | Win Rate: 0% | Trades: 0

Lecture du résultat : MultiAssetPortfolioAlgorithm

Ce que le bandeau dit réellement : 3653 jours de trading couverts, 0 ordre et 0 trade. Un backtest sans trade ne produit aucune métrique de performance — les zéros du bandeau ne sont pas une contre-performance, c’est l’absence de toute exécution.

Pourquoi : MultiAssetPortfolioAlgorithm est un algorithme de structure — il enregistre cinq Symbols (SPY/TLT/GLD/EEM + proxy VIX) dans Initialize et documente le pattern d’ajout multi-classes. Aucune règle d’allocation n’est branchée sur ces Symbols dans le code de référence, donc rien ne se déclenche.

Conséquence pédagogique : ce notebook ne mesure pas la diversification ici. La mesure existe plus loin, sur un algorithme qui trade réellement — EqualWeightPortfolioAlgorithm (cellule #35 : CAGR 7.145%, Sharpe 0.359, MaxDD 19.400%, 203 trades). C’est cette cellule-là qu’il faut lire pour la performance de l’allocation équipondérée, pas celle-ci.

Ce qui reste valide ici : l’anatomie du multi-asset (proxies ETF vs sous-jacents, résolution uniforme, coût de rebalance par actif) — c’est un livrable de structure, pas de performance.

Structure Multi-Asset dans QuantConnect

Cette classe MultiAssetPortfolioAlgorithm illustre les patterns fondamentaux pour travailler avec plusieurs classes d’actifs :

Patterns QC Demonstrés : - AddEquity() : Pour ETFs equities et bonds (TLT, IEF) - AddCrypto() : Pour crypto-monnaies (BTCUSD via Coinbase) - Symbol objects : Références persistantes pour les ordres

Organisation par Classe d’Actifs :

self.symbols = {
    'equities': [SPY, QQQ],      # Croissance
    'bonds': [TLT, IEF],         # Revenus, défensive
    'commodities': [GLD, USO],   # Inflation hedge
    'crypto': [BTCUSD]           # Croissance agressive
}

Proxies vs Actifs Sous-jacents : Les ETFs sont des “proxies” accessibles aux particuliers : - TLT proxy pour obligations long-terme (vs futures TBT) - GLD proxy pour or physique (vs futures GC) - USO proxy pour pétrole (vs futures CL)

En production : Les institutions utilisent souvent les futures directement pour une meilleure efficacité de capital et des coûts de transaction plus faibles.

Note technique : - Les ETFs (SPY, TLT, GLD, etc.) sont des “proxies” qui permettent aux investisseurs particuliers d’acceder a différentes classes d’actifs - La crypto est disponible via la bourse Coinbase/GDAX sur QuantConnect (tier gratuit) - En production, on pourrait aussi utiliser des futures pour commodities et bonds (plus efficient mais requiert plus de capital)

Note technique : résolution temporelle uniforme

Important : tous les assets dans une stratégie multi-actifs doivent être en même résolution temporelle (daily, hourly, etc.). Mixing daily et intraday crée des désynchronisations : à un moment donné, certains assets ont bougé et d’autres non, ce qui rend les décisions de rebalance ambiguës.

Implémentation QuantConnect :

self.AddEquity("SPY", Resolution.Daily)
self.AddEquity("TLT", Resolution.Daily)
self.AddEquity("GLD", Resolution.Daily)

Si on a besoin de mix daily/intraday (e.g. SPY daily + futures intraday), il faut resynchroniser via une fenêtre d’agrégation (ex: futures intraday agrégés en daily à 16h avant la décision de rebalance).

Coût-bénéfice : la synchronisation ajoute 5-10% de complexité au code, mais évite les bugs subtils où un asset trade à J+1 alors que les autres sont à J.


Partie 2 : Correlations et Analyse (20 min)

2.1 Calcul des Correlations

La matrice de correlation nous permet de comprendre comment les actifs bougent les uns par rapport aux autres. C’est fondamental pour construire un portfolio bien diversifie.

Trois types de structures multi-asset

Il existe trois façons principales de structurer un portefeuille multi-actifs sur QuantConnect :

  1. Equal Weight — chaque actif reçoit le même poids (1/N). Avantage : simplicité, robuste. Inconvénient : ne tient pas compte du risque.

  2. Risk Parity — chaque actif reçoit un poids inverse à sa volatilité. Avantage : equalisation de la contribution au risque. Inconvénient : suppose que la volatilité est stationnaire.

  3. Markowitz Mean-Variance — chaque actif reçoit un poids optimisé pour maximiser le ratio de Sharpe. Avantage : théoriquement optimal. Inconvénient : très sensible aux estimations d’entrée.

Stratégies dérivées : Tactical Asset Allocation (TAA) modifie dynamiquement les poids en fonction de signaux de momentum. All-Weather (Bridgewater) utilise un mix d’actifs avec des pondérations fixes basées sur le risque macro-économique.

# Fonction pour calculer la matrice de correlation
def calculate_correlation_matrix(algorithm, symbols, lookback=60):
    """
    Calcule la matrice de correlation entre plusieurs actifs.
    
    Parameters:
    -----------
    algorithm : QCAlgorithm instance
    symbols : list of Symbol objects
    lookback : int, nombre de jours pour le calcul
    
    Returns:
    --------
    pd.DataFrame : matrice de correlation
    """
    # Recuperer l'historique
    history = algorithm.History(symbols, lookback, Resolution.Daily)
    
    # Extraire les prix de cloture
    closes = history['close'].unstack(level=0)
    
    # Calculer les returns journaliers
    returns = closes.pct_change().dropna()
    
    # Calculer la matrice de correlation
    correlation_matrix = returns.corr()
    
    return correlation_matrix, returns

print("Fonction calculate_correlation_matrix() definie")
Fonction calculate_correlation_matrix() definie

Analyse de Correlation en Production

La classe CorrelationAnalysisAlgorithm montre comment intégrer l’analyse de corrélation dans un algorithme live.

Patterns QC Demonstrés : - Schedule.On() : Exécution périodique (mensuelle ici) - History[symbols] : Récupération multi-symbols en un appel - unstack(level=0) : Pivot DataFrame multi-index en colonnes

Workflow Mensuel :

history = self.History(symbols, 60, Resolution.Daily)  # 60 jours
closes = history['close'].unstack(level=0)             # Pivot
returns = closes.pct_change().dropna()                 # Returns
corr_matrix = returns.corr()                           # Corrélation

Utilisation Pratique : - Diversification monitoring : Vérifier que les corrélations n’augmentent pas trop - Regime detection : Corrélation SPY-TLT fortement négative = stress - Rebalancing triggers : Ajuster allocation si corrélations changent

Performance : Pour des portfolios avec 50+ actifs, le calcul de corrélation peut devenir coûteux. Considérer un calcul quotidien/semaine plutôt que mensuel.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Exemple d'utilisation dans un algorithme
class CorrelationAnalysisAlgorithm(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        
        # Ajouter actifs
        self.symbols = [
            self.AddEquity("SPY", Resolution.Daily).Symbol,
            self.AddEquity("TLT", Resolution.Daily).Symbol,
            self.AddEquity("GLD", Resolution.Daily).Symbol,
            self.AddEquity("QQQ", Resolution.Daily).Symbol,
        ]
        
        # Programmer calcul mensuel des correlations
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.AnalyzeCorrelations
        )
    
    def AnalyzeCorrelations(self):
        """Analyse les correlations mensuellement."""
        history = self.History(self.symbols, 60, Resolution.Daily)
        
        if history.empty:
            return
        
        # Calculer returns
        closes = history['close'].unstack(level=0)
        returns = closes.pct_change().dropna()
        
        # Matrice de correlation
        corr_matrix = returns.corr()
        
        # Log les correlations importantes
        self.Debug(f"\n{self.Time.date()} - Correlation Matrix:")
        for i, sym1 in enumerate(self.symbols):
            for j, sym2 in enumerate(self.symbols):
                if i < j:  # Eviter doublons
                    corr = corr_matrix.iloc[i, j]
                    self.Debug(f"  {sym1.Value}/{sym2.Value}: {corr:.2f}")

print("CorrelationAnalysisAlgorithm defini")

Résultats du backtest QC Cloud (CorrelationAnalysisAlgorithm)

Metrique Valeur
Dates negociables 2882
Ordres executes 0
Sharpe Ratio 0
CAGR 0%
Max Drawdown 0%
Net Profit 0.0%
Equity finale $100,000
Probabilistic Sharpe Ratio 0%

Correlation analysis (SPY, TLT, GLD, QQQ), monthly. Backtest QC Cloud 2015-2024, setup sans trades (projet 33177683, bt 4003640ad4cf19651717bbfc472bbbb4)

Pourquoi la corrélation n’est pas stationnaire

La corrélation entre deux actifs varie dans le temps selon le régime de marché. En particulier :

  • Périodes calmes : corrélations modérées (~0.4-0.6 entre SPY/TLT).
  • Périodes de stress : corrélations convergent vers 1 (toutes les actions chutent ensemble, les obligations montent mais le hedge devient partiel).

Implication pour le risk management : un hedge SPY/TLT qui fonctionne en 2017-2019 peut échouer en mars 2020 (toutes les classes d’actifs vend en même temps, sauf cash). C’est pourquoi les hedgeurs institutionnels utilisent des multi-hedges (options + futures + cash) plutôt qu’un hedge simple.

[BACKTEST QC Cloud] CorrelationAnalysisAlgorithm Tradeable dates: 2863 | Orders: 0 | Net Profit: 0% | Sharpe: 0 | CAGR: 0% | MaxDD: 0% | Win Rate: 0% | Trades: 0

Lecture du résultat : CorrelationAnalysisAlgorithm

Les zéros du bandeau sont ici structurels, pas un échec : CorrelationAnalysisAlgorithm est un algorithme d’analyse. Il calcule une matrice de corrélation glissante 60 jours et la journalise ; il ne passe aucun ordre, donc « Orders: 0 / Trades: 0 / Sharpe: 0 » est le résultat attendu de sa nature, et non une mesure de performance.

Ce qu’un tel run produit : une série temporelle de corrélations, pas un P&L. La lecture utile est donc dans la forme de cette série — c’est l’objet de la cellule #19 (heatmap sur données typiques) et du §2.2 (rolling correlations) qui suivent.

Point de méthode : un bandeau de performance à zéro sur un algorithme d’analyse ne doit jamais être lu comme « la stratégie ne marche pas » ; inversement, un algorithme d’analyse ne démontre rien sur la performance. Deux registres distincts — ne pas confondre les deux à la lecture d’un rapport QC Cloud.

Sur la convergence des corrélations en stress (0.85-0.95 entre actions en 2020 et 2022) : c’est un fait documenté de la littérature, que ce run ne mesure pas — la cellule ne le revendique donc pas comme un résultat de backtest.

Visualisation de la Matrice de Correlation

En mode recherche (QuantBook), on peut visualiser la matrice de correlation avec une heatmap.

La matrice de corrélation est un objet central en analyse multi-actifs. Elle résume en N×N nombres les interdépendances entre N actifs.

Bonne visualisation : un heatmap (carte de chaleur) où chaque case est colorée selon la valeur de corrélation. Une palette divergente (rouge négatif, blanc zéro, bleu positif) rend les patterns immédiatement lisibles.

Exemple SPY/TLT/GLD/EEM : - SPY-TLT : -0.3 à +0.4 (hedge traditionnel). - SPY-GLD : 0.0 à 0.3 (or = hedge partiel). - SPY-EEM : 0.7 à 0.9 (actions corrélées). - TLT-GLD : -0.2 à 0.3 (peu de lien).

Lecture critique : une matrice calculée sur toute la période masque les régimes. Toujours accompagner d’une rolling correlation (cf. §2.2) pour voir l’évolution temporelle.

Phénomènes de Rolling Correlation

Le graphique montre deux zones importantes :

Période Normale : - Corrélation SPY-TLT : ~-0.2 à -0.3 - Comportement “standard” : equities et bonds bougent indépendamment

Période de Stress (zone rouge) : - Corrélation chute à ~-0.6 ou plus bas - “Flight to quality” intense : investisseurs vendent equities, achètent bonds - Relation inverse se renforce

Implications Trading : 1. Détection de crise : Corrélation < -0.5 = signal de stress marché 2. Opportunité rebalancing : Après stress, corrélations reviennent à la normale 3. Hedge dynamique : Augmenter allocation bonds quand corrélation devient très négative

Note : En crise de liquidité extrême (2008, COVID 2020), les corrélations peuvent temporairement devenir positives (“tout baisse ensemble”). Ces périodes sont rares mais dangereuses pour les stratégies de diversification classiques.

# Exemple de visualisation (donnees simulees pour illustration)
# En production, utilisez qb.History() avec QuantBook

# Donnees de correlation typiques entre classes d'actifs
assets = ['SPY', 'QQQ', 'TLT', 'IEF', 'GLD', 'USO', 'BTC']
corr_data = np.array([
    [1.00, 0.92, -0.35, -0.20, 0.05, 0.40, 0.30],  # SPY
    [0.92, 1.00, -0.30, -0.15, 0.00, 0.35, 0.35],  # QQQ
    [-0.35, -0.30, 1.00, 0.85, 0.25, -0.10, -0.15], # TLT
    [-0.20, -0.15, 0.85, 1.00, 0.20, -0.05, -0.10], # IEF
    [0.05, 0.00, 0.25, 0.20, 1.00, 0.15, 0.20],    # GLD
    [0.40, 0.35, -0.10, -0.05, 0.15, 1.00, 0.25],  # USO
    [0.30, 0.35, -0.15, -0.10, 0.20, 0.25, 1.00],  # BTC
])

corr_df = pd.DataFrame(corr_data, index=assets, columns=assets)

# Visualisation heatmap
plt.figure(figsize=(10, 8))
sns.heatmap(corr_df, annot=True, cmap='RdYlGn_r', center=0,
            vmin=-1, vmax=1, fmt='.2f', square=True,
            linewidths=0.5, cbar_kws={'label': 'Correlation'})
plt.title('Matrice de Correlation Multi-Asset\n(Donnees illustratives)', 
          fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

print("\nObservations cles:")
print("- SPY/QQQ: Tres correles (0.92) - meme classe d'actifs")
print("- SPY/TLT: Correlation negative (-0.35) - 'flight to quality'")
print("- GLD: Faible correlation avec tout - bon diversificateur")
print("- BTC: Correlation moderee - classe d'actifs emergente")
Observations cles:- SPY/QQQ: Tres correles (0.92) - meme classe d'actifs- SPY/TLT: Correlation negative (-0.35) - 'flight to quality'- GLD: Faible correlation avec tout - bon diversificateur- BTC: Correlation moderee - classe d'actifs emergente

Interprétation de la Matrice de Corrélation

La heatmap révèle des patterns importants pour la construction de portefeuille :

Corrélations Élevées (> 0.8) - À éviter ensemble : - SPY/QQQ (0.92) : Même classe d’actifs (equities US) - TLT/IEF (0.85) : Tous deux Treasury bonds

Corrélations Négatives - Excellent pour diversification : - SPY/TLT (-0.35) : “Flight to quality” classique - QQQ/TLT (-0.30) : Tech vs long-term rates

Faible Corrélation - Bons diversificateurs : - GLD avec tout : L’or se comporte différemment - BTC avec equities (~0.3) : Classe d’actifs émergente

Implications pour Allocation : 1. Ne pas surpondérer SPY ET QQQ ensemble (redondant) 2. TLT + IEF offrent une bonne couverture equities 3. GLD apporte une vraie diversification 4. BTC peut compléter un portfolio mais avec volatilité élevée

Règle pratique : Pour une vraie diversification, chercher des corrélations < 0.5 entre les composants principaux du portfolio.

Détection de Régime en Production

RegimeDetectionAlgorithm démontre une approche pratique de l’adaptation au régime de marché.

Logique de Détection :

if correlation < -0.5:
    regime = "stress"          # Flight to quality prononcé
elif correlation > 0.2:
    regime = "liquidity_crisis" # Tout baisse ensemble (rare)
else:
    regime = "normal"

Allocations Adaptatives :

Régime SPY TLT GLD Rationale
Normal 60% 30% 10% Allocation équilibrée
Stress 30% 50% 20% Flight to quality
Liquidity Crisis 20% 30% 50% Cash déguisé en or

Limitations de cette Approche : 1. Seuil arbitraire : -0.5 pourrait ne pas être optimal pour toutes les périodes 2. Lag : Corrélation sur 60 jours = détection retardée 3. Whipsaw : Changements fréquents de régime en marché latéral

Améliorations Possibles : - Utiliser des modèles HMM (Hidden Markov Models) - Ajouter le VIX comme indicateur de volatilité - Multi-indicateurs : momentum, volatilité, spread de crédit

Mise en garde : Les backtests de stratégies de régime doivent être particulièrement vigilants au look-ahead bias. La corrélation utilisée pour la décision ne doit inclure que des données antérieures au point de décision.

2.2 Rolling Correlations

Les correlations ne sont pas statiques. Elles evoluent dans le temps, notamment en periode de stress.

Phenomenes observes : - En periode normale : correlations relativement stables - En crise : correlations equities augmentent (“tout baisse ensemble”) - Flight to quality : equities down, bonds (TLT) up

Une rolling correlation est la corrélation calculée sur une fenêtre glissante de N observations (typiquement 60 jours = 3 mois en daily).

Implémentation Python :

rolling_corr = returns.rolling(window=60).corr(other_returns)

Trois usages : 1. Détection de régime : quand la corrélation passe au-dessus de 0.8, c’est un signal de stress. 2. Hedging dynamique : ajuster la taille du hedge en fonction de la corrélation rolling (plus de hedge quand la corrélation est élevée). 3. Pair trading : quand la corrélation rolling entre deux actifs dépasse 0.7, on peut les pair-trader (long/short market- neutral).

Piège : la rolling correlation a un lag structurel. Un régime de stress qui commence à J=0 n’apparaît dans la rolling 60j qu’à J=30 environ.

def calculate_rolling_correlation(returns_df, symbol1, symbol2, window=60):
    """
    Calcule la correlation glissante entre deux actifs.
    
    Parameters:
    -----------
    returns_df : pd.DataFrame avec returns de chaque actif
    symbol1, symbol2 : str, noms des colonnes
    window : int, fenetre de calcul en jours
    
    Returns:
    --------
    pd.Series : correlation glissante
    """
    return returns_df[symbol1].rolling(window).corr(returns_df[symbol2])

# Exemple avec donnees simulees
np.random.seed(42)
dates = pd.date_range('2020-01-01', '2023-12-31', freq='B')
n = len(dates)

# Simuler returns avec correlation variable
spy_returns = np.random.normal(0.0004, 0.012, n)
tlt_returns = -0.3 * spy_returns + np.random.normal(0.0001, 0.008, n)

# Ajouter periode de stress (correlation augmente en valeur absolue)
stress_start = int(n * 0.25)
stress_end = int(n * 0.30)
tlt_returns[stress_start:stress_end] = -0.7 * spy_returns[stress_start:stress_end] + np.random.normal(0.0001, 0.005, stress_end - stress_start)

returns_sim = pd.DataFrame({
    'SPY': spy_returns,
    'TLT': tlt_returns
}, index=dates)

# Calculer rolling correlation
rolling_corr = calculate_rolling_correlation(returns_sim, 'SPY', 'TLT', window=60)

# Visualisation
fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)

# Subplot 1: Returns cumules
cumulative = (1 + returns_sim).cumprod()
axes[0].plot(cumulative.index, cumulative['SPY'], label='SPY', color='navy', linewidth=1.5)
axes[0].plot(cumulative.index, cumulative['TLT'], label='TLT', color='darkorange', linewidth=1.5)
axes[0].axvspan(dates[stress_start], dates[stress_end], alpha=0.2, color='red', label='Periode stress')
axes[0].set_title('Returns Cumules SPY vs TLT', fontsize=12, fontweight='bold')
axes[0].set_ylabel('Return Cumule')
axes[0].legend(loc='upper left')
axes[0].grid(True, alpha=0.3)

# Subplot 2: Rolling correlation
axes[1].plot(rolling_corr.index, rolling_corr, color='purple', linewidth=1.5)
axes[1].axhline(y=0, color='gray', linestyle='--', alpha=0.5)
axes[1].axhline(y=rolling_corr.mean(), color='red', linestyle='--', 
                label=f'Moyenne: {rolling_corr.mean():.2f}')
axes[1].axvspan(dates[stress_start], dates[stress_end], alpha=0.2, color='red')
axes[1].set_title('Correlation Glissante 60 Jours (SPY-TLT)', fontsize=12, fontweight='bold')
axes[1].set_xlabel('Date')
axes[1].set_ylabel('Correlation')
axes[1].set_ylim(-1, 0.5)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f"\nStatistiques Rolling Correlation SPY-TLT:")
print(f"  Moyenne: {rolling_corr.mean():.3f}")
print(f"  Min: {rolling_corr.min():.3f}")
print(f"  Max: {rolling_corr.max():.3f}")
Statistiques Rolling Correlation SPY-TLT:  Moyenne: -0.446  Min: -0.818  Max: -0.132

Equal Weight : Simplicité et Efficacité

EqualWeightPortfolioAlgorithm implémente l’approche 1/N, surprenamment robuste.

Avantages de l’Equal Weight : 1. Aucun paramètre à optimiser : Pas de lookback, volatilité, etc. 2. Diversification maximale : Chaque actif a le même poids 3. Rebalancement automatique : Vend les gagnants, achète les perdants 4. Performance compétitive : Souvent bat les stratégies sophistiquées

Mécanisme du Rebalancement :

Initial : SPY=100, QQQ=100, TLT=100 (1/3 chacun)
Après 3 mois : SPY=120, QQQ=80, TLT=100 (drift)
Rebalancement : Vendre SPY, acheter QQQ → retour à 1/3

Effet “Volatility Harvesting” : Le rebalancement périodique capture la “prime de rebalancement” : - En période de mean-reversion, on vend haut, achète bas - Cet effet peut ajouter 0.5-1% de rendement annuel

Fréquence Optimale : - Mensuel : Trop de coûts de transaction - Trimestriel : Bon compromis (utilisé ici) - Annuel : Trop lent, drift important

Études académiques : DeMiguel, Garlappi & Uppal (2009) montrent que 1/N bat souvent les stratégies optimisées en moyenne-variance, surtout quand les estimations de rendement/volatilité sont imprécises.

Ancre savante – DeMiguel, V., Garlappi, L. & Uppal, R. (2009), Optimal Versus Naive Diversification: How Inefficient the 1/N Portfolio Strategy?, The Review of Financial Studies 22(5):1915-1953. (Étude canonique : 1/N bat souvent l’optimisation moyenne-variance quand l’erreur d’estimation est grande. Corrige le 24-06 : la prose citait “De Graaf et al. (2019)” – attribution inverifiable, remplacée par le papier originel 1/N.)

2.3 Detection de Regime de Marche

On peut utiliser les correlations pour detecter le regime de marche et adapter l’allocation.

Regimes typiques : - Risk-On : Equities montent, bonds stagnent/baissent, correlation SPY-TLT moderement negative - Risk-Off : Equities baissent, bonds montent (flight to quality), correlation SPY-TLT fortement negative - Crise de liquidite : Tout baisse (correlation positive transitoire)

Trois méthodes principales pour détecter un changement de régime sur un portefeuille multi-actifs :

  1. Hidden Markov Model (HMM) : modélise le marché comme 2-3 états cachés (calme, normal, stress) avec probabilités de transition. Méthode probabiliste standard.

  2. Threshold sur rolling volatility : si la vol 21j dépasse 1.5× la vol 252j, on est en régime stress. Simple et robuste.

  3. Threshold sur rolling correlation : si la corrélation moyenne SPY-EEM dépasse 0.85, on est en stress (toutes les actions se comportent pareil).

Implémentation QuantConnect :

self.vol_21 = self.STD(self.spy, 21, Resolution.Daily)
self.vol_252 = self.STD(self.spy, 252, Resolution.Daily)
self.in_stress = self.vol_21.Current.Value > 1.5 * self.vol_252.Current.Value

Pédagogie : la détection de régime est un outil défensif. On l’utilise pour réduire l’exposition pendant les périodes de stress (cf. QC-Py-10 Drawdown Control).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class RegimeDetectionAlgorithm(QCAlgorithm):
    """
    Algorithme qui detecte le regime de marche base sur les correlations
    et adapte l'allocation en consequence.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetCash(100000)
        
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
        self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
        
        self.symbols = [self.spy, self.tlt, self.gld]
        
        # Parametres detection regime
        self.correlation_window = 60
        self.stress_threshold = -0.5  # Correlation SPY-TLT < -0.5 = stress
        
        # Stocker l'historique des returns
        self.returns_history = {}
        self.current_regime = "normal"
        
        # Rebalancer mensuellement
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.DetectRegimeAndRebalance
        )
    
    def DetectRegimeAndRebalance(self):
        """Detecte le regime et ajuste l'allocation."""
        
        # Recuperer historique
        history = self.History(self.symbols, self.correlation_window, Resolution.Daily)
        
        if history.empty or len(history) < self.correlation_window:
            return
        
        # Calculer returns
        closes = history['close'].unstack(level=0)
        returns = closes.pct_change().dropna()
        
        # Correlation SPY-TLT
        spy_returns = returns[self.spy]
        tlt_returns = returns[self.tlt]
        correlation = spy_returns.corr(tlt_returns)
        
        # Detecter regime
        if correlation < self.stress_threshold:
            regime = "stress"  # Flight to quality prononce
        elif correlation > 0.2:
            regime = "liquidity_crisis"  # Tout baisse ensemble (rare)
        else:
            regime = "normal"
        
        # Allocations selon regime
        allocations = {
            "normal": {self.spy: 0.60, self.tlt: 0.30, self.gld: 0.10},
            "stress": {self.spy: 0.30, self.tlt: 0.50, self.gld: 0.20},
            "liquidity_crisis": {self.spy: 0.20, self.tlt: 0.30, self.gld: 0.50}
        }
        
        # Appliquer allocation
        if regime != self.current_regime:
            self.Debug(f"{self.Time}: Regime change {self.current_regime} -> {regime} (corr: {correlation:.2f})")
            self.current_regime = regime
        
        for symbol, weight in allocations[regime].items():
            self.SetHoldings(symbol, weight)

print("RegimeDetectionAlgorithm defini")
print("\nRegimes et allocations:")
print("  Normal: SPY 60%, TLT 30%, GLD 10%")
print("  Stress: SPY 30%, TLT 50%, GLD 20%")
print("  Liquidity Crisis: SPY 20%, TLT 30%, GLD 50%")

[BACKTEST QC Cloud] RegimeDetectionAlgorithm Tradeable dates: 2863 | Orders: 332 | Net Profit: 132.932% | Sharpe: 0.347 | CAGR: 7.704% | MaxDD: 25.200% | Win Rate: 78% | Trades: 277 End Equity: $232,931.68

Lecture du résultat : RegimeDetectionAlgorithm

Les chiffres du run : CAGR 7.704%, Sharpe 0.347, MaxDD 25.200%, 277 trades sur 2863 jours de bourse (~11,4 ans, soit ~24 trades/an), win rate 78%, capital final $232,931.68 pour $100,000 engagés.

Le résultat contredit l’intuition défensive : la détection de régime est présentée comme un outil de réduction du drawdown — or le MaxDD mesuré (-25.2%) est le troisième plus élevé des huit runs du notebook (après -39.4% et -28.0%). Le filtre n’a pas empêché une baisse de quart du capital. Un Sharpe de 0.347 pour 277 trades signifie que la stratégie a rendu du rendement, mais avec une volatilité qui n’a pas été domptée.

Pourquoi le mécanisme peut échouer ainsi : vol_21 > 1.5 × vol_252 est un signal retardataire — la moyenne 252 jours met des mois à se relever après un choc, si bien que le seuil 1.5× se déclenche souvent une fois la baisse déjà consommée. Le régime est identifié après coup, pas avant.

Pédagogie honnête : un détecteur de régime n’est pas une protection en soi ; c’est un signal qu’il faut dimensionner et tester (fenêtres, seuils, délai de réaction). Le win rate 78% sur 277 trades indique par ailleurs que la stratégie gagne souvent de petites sommes — profil compatible avec un drawdown important malgré une majorité de trades gagnants.


Exercice 1 : Rolling Correlation personnalisee

En vous basant sur les exemples de correlation et de detection de regime ci-dessus, créez une analyse qui surveille la rolling correlation entre SPY et GLD (au lieu de SPY-TLT) et adapte l’allocation.

Objectif : implementer GoldCorrelationStrategy avec les specifications suivantes :

  • Charger l’historique de SPY et GLD via QuantBook
  • Calculer la rolling correlation SPY-GLD sur une fenêtre glissante de 20 jours
  • Si correlation > seuil_haute (0.7) : gold ne diversifie plus, augmenter SPY
  • Si correlation < seuil_basse (0.3) : gold est un bon diversificateur, augmenter GLD
  • Poids par defaut : SPY 60%, GLD 40%

Indices : - Utilisez qb.History() pour recuperer les données de SPY et GLD - Utilisez .rolling(window).corr() pour la correlation glissante - Stockez la correlation dans une variable d’instance pour le logging - Inspirez-vous des exemples de correlation ci-dessus pour le chargement des données

Comparaison des méthodes d’allocation

Sur la même période 2015-2024, les trois méthodes principales donnent des résultats très différents :

Méthode CAGR typique Sharpe typique Max DD typique
Equal Weight 1/N ~8-10% 0.6-0.7 -15% à -18%
Risk Parity ~7-9% 0.7-0.85 -10% à -12%
Markowitz MV ~9-12% 0.5-0.9 (instable) -18% à -25%
Buy & Hold SPY ~13% 0.55 -25%

Observation clé : Markowitz surperforme en CAGR mais sous-performe en Sharpe ajusté au risque. Equal Weight et Risk Parity sont plus robustes dans le temps.

Pédagogie : la “meilleure” méthode dépend de l’objectif — CAGR absolu (Markowitz), Sharpe/DD ajusté (Risk Parity), ou simplicité de maintenance (Equal Weight).

class GoldCorrelationStrategy:
    """
    Exercice : Moniteur de correlation glissante SPY/GLD avec allocation adaptative.

    Etapes :
    1. Utilisez QuantBook pour charger l'historique de SPY et GLD
    2. Calculez la correlation glissante sur une fenetre de 20 jours
    3. Allouez dynamiquement : correlation < seuil_basse -> plus de GLD,
       correlation > seuil_haute -> plus de SPY
    4. Affichez les 5 dernieres correlations et l'allocation suggeree
    """
    def __init__(self):
        self.window = 20
        self.seuil_basse = 0.3
        self.seuil_haute = 0.7
        self.spy_weight = 0.6
        self.gld_weight = 0.4
        # TODO etudiant : completer le constructeur

    def compute_correlation(self, spy_returns, gld_returns):
        # TODO etudiant : calculer la correlation de Pearson glissante
        return None

    def adaptive_allocation(self, current_corr):
        # TODO etudiant : ajuster les poids selon les seuils
        return self.spy_weight, self.gld_weight

    def run(self):
        # TODO etudiant : orchestrer le calcul et afficher les resultats
        print("Exercice a completer")

strategy = GoldCorrelationStrategy()
strategy.run()

Risk Parity en Production

RiskParityAlgorithm implémente une approche de construction de portefeuille basée sur le risque plutôt que sur le capital.

Différence Equal Weight vs Risk Parity :

Actif Equal Weight Risk Parity Pourquoi ?
SPY (Vol 19%) 20% ~12% Moins car plus volatile
IEF (Vol 8%) 20% ~28% Plus car moins volatile

Effet de Levier Implicite : Le vrai Risk Parity (comme Bridgewater’s “All Weather”) utilise souvent du levier sur les bonds pour atteindre la parité de risque. Cette version simplifiée n’utilise pas de levier.

Avantages du Risk Parity : - Contribution égale au risque total - Moins sensible aux rendements extrêmes d’un seul actif - Performe bien sur longue période

Inconvénients : - Nécessite des estimations de volatilité fiables - Peut surpondérer les bonds en période de taux bas - Coûts de rebalancement plus élevés (volatilité change vite)

Attention : En période de hausse rapide des taux (comme 2022), le Risk Parity peut souffrir car les bonds perdent de la valeur et leur volatilité augmente simultanément.

Note pedagogique : La detection de regime est un domaine actif de recherche. Cette implementation simple utilise uniquement la correlation SPY-TLT. Des approches plus sophistiquees utilisent des modèles Hidden Markov (HMM), des indicateurs de volatilite (VIX), ou du machine learning.

Note pédagogique : la détection de régime est un domaine actif

Trois approches académiques principales :

  1. Hidden Markov Models (HMM) : le marché alterne entre 2-3 états cachés. Méthode probabiliste standard, voir Hamilton (1989).

  2. Structural breaks (Bai-Perron) : la série temporelle a des ruptures structurelles qu’on peut détecter.

  3. Regime switching models : régression avec coefficients qui varient selon le régime.

Implémentation pratique QuantConnect : la plupart des traders retail utilisent des règles simples (threshold sur rolling vol, threshold sur VIX) plutôt que des modèles statistiques sophistiqués. C’est plus robuste et plus facile à interpréter.

Anti-pattern : utiliser un HMM à 5 états avec 20 paramètres estimés sur 10 ans de données daily. Overfitting garanti, performance out-of-sample médiocre.

Tactical Asset Allocation - Momentum Simplifié

TacticalAssetAllocationAlgorithm utilise le SMA 200 comme filtre de tendance, une approche TAA très répandue.

Logique de Trading :

Prix > SMA(200) → Tendance haussière → Allouer
Prix < SMA(200) → Tendance baissière → Cash

Résultats Historiques Typiques : - Avantages : Évite les grands bear markets (2000-2002, 2008, 2022) - Inconvénients : Whipsaw en marché latéral, faux signaux

Caractéristiques du SMA 200 : - Lag moyen : ~100 jours (demi-période) - Sensibilité : Moins sensible aux fluctuations court terme - Robustesse : Fonctionne sur tous les types d’actifs

Comparaison TAA vs Buy-and-Hold :

Scénario Buy-and-Hold TAA SMA200
Bull market prolongé ✅ Fort gain ⚠️ Gain réduit (cash périodes)
Bear market sévère ❌ Forte perte ✅ Évité (cash)
Marché latéral ↔︎️ Flat ❌ Whipsaw (pertes)

Améliorations Possibles : 1. Confirmation multiple : SMA 50 + SMA 200 + RSI 2. Rotation d’actifs : Au lieu de cash, aller vers bonds défensifs 3. Time stops : Réduire l’exposition après X jours sous le SMA

Valeur du SMA 200 en 2023-2024 : SPY est resté au-dessus de son SMA 200 la majeure partie du temps, donc le TAA aurait été investi. Le filtre est plus utile en période de transition de marché.


Partie 3 : Stratégies d’Allocation (30 min)

3.1 Equal Weight Portfolio

La stratégie la plus simple : allouer le même poids a chaque actif. Malgre sa simplicite, elle performe souvent bien grace a la diversification et au rebalancement implicite (vendre les gagnants, acheter les perdants).

Trois variantes de Tactical Asset Allocation

Le TAA modifie les poids en fonction de signaux de momentum. Trois variantes principales :

  1. Time-Series Momentum (TSM) : on monte sur un actif si son rendement passé 12 mois est positif, on sort sinon. Simple, robuste.

  2. Cross-Sectional Momentum (XSM) : on monte sur les actifs qui ont le meilleur momentum parmi l’univers, on vend ceux qui ont le pire. Plus discriminatif.

  3. Dual Momentum : combine TSM (absolu) et XSM (relatif). On n’investit que si TSM>0 ET XSM > médiane. Plus défensif.

Implémentation QuantConnect (TSM) :

momentum = self.MOMP(self.spy, 252, Resolution.Daily)  # 12m momentum
if momentum.Current.Value > 0:
    self.SetHoldings(self.spy, 1.0)
else:
    self.SetHoldings(self.tlt, 1.0)  # flight to safety

Coût-bénéfice : TAA réduit le DD en période baissière mais augmente le turnover (rebalance mensuel typiquement). À 5-10 bps de coût par trade, sur 12 rebalances/an, on perd ~1% de CAGR.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class EqualWeightPortfolioAlgorithm(QCAlgorithm):
    """
    Strategie Equal Weight avec rebalancement periodique.
    Allocation: 1/N pour chaque actif.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter actifs multi-classes
        tickers = ["SPY", "QQQ", "TLT", "IEF", "GLD", "USO"]
        self.symbols = [self.AddEquity(t, Resolution.Daily).Symbol for t in tickers]
        
        # Programmer rebalancement trimestriel
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.Rebalance
        )
        
        self.rebalance_months = [1, 4, 7, 10]  # Trimestriel
    
    def Rebalance(self):
        """Rebalance vers equal weight."""
        
        # Verifier si c'est un mois de rebalancement
        if self.Time.month not in self.rebalance_months:
            return
        
        # Calculer poids egal
        weight = 1.0 / len(self.symbols)
        
        self.Debug(f"{self.Time.date()}: Rebalancing to equal weight ({weight:.2%} each)")
        
        # Appliquer allocation
        for symbol in self.symbols:
            self.SetHoldings(symbol, weight)

print("EqualWeightPortfolioAlgorithm defini")
print(f"\nAllocation: {100/6:.2f}% par actif")
print("Rebalancement: Trimestriel (Janvier, Avril, Juillet, Octobre)")

[BACKTEST QC Cloud] EqualWeightPortfolioAlgorithm Tradeable dates: 2516 | Orders: 203 | Net Profit: 99.493% | Sharpe: 0.359 | CAGR: 7.145% | MaxDD: 19.400% | Win Rate: 86% | Trades: 203 End Equity: $199,492.52

Lecture du résultat : EqualWeightPortfolioAlgorithm

Les chiffres du run : CAGR 7.145%, Sharpe 0.359, MaxDD 19.400%, 203 trades, win rate 86%, capital final $199,492.52 pour $100,000 engagés sur ~10 ans (2015-2024).

Lecture : sur cette période, 1/N a doublé le capital avec un drawdown de ~19% — un profil correct mais très éloigné des chiffres souvent cités pour l’equal weight (Sharpe 0.65-0.75, DD -16%). Le Sharpe 0.359 est modeste : le rendement a été obtenu au prix d’une volatilité significative, portée par 2020 et 2022.

Le win rate de 86% n’est pas un indicateur de performance : il compte les trades gagnants, pas leur taille. Un rebalancement trimestriel gagne souvent de petits montants et perd rarement gros — c’est exactement le profil qui produit une majorité de gains et un drawdown marqué.

Ce qui survit du message académique : DeMiguel, Garlappi & Uppal (2009) montrent que 1/N bat souvent l’optimisation moyenne-variance sur des données out-of-sample — c’est un résultat de littérature, pas une mesure de ce run. Ici, la comparaison interne au notebook est directe : l’equal weight (Sharpe 0.359) bat le risk parity implémenté (0.305, cellule #41) — la robustesse de 1/N se vérifie donc sur ce corpus, ce qui est précisément l’intérêt pédagogique de la série.

3.2 Risk Parity

L’idee du Risk Parity est d’allouer de sorte que chaque actif contribue également au risque total du portfolio. Cela signifie surponderer les actifs a faible volatilite (bonds) et sous-ponderer les actifs a haute volatilite (equities).

Formule simplifiee : \[w_i = \frac{1/\sigma_i}{\sum_j 1/\sigma_j}\]

ou \(\sigma_i\) est la volatilite de l’actif \(i\).

Ancre savante – Maillard, S., Roncalli, T. & Teiletche, J. (2010), The Properties of Equally Weighted Risk Contribution Portfolios, The Journal of Portfolio Management 36(4):60-70. DOI 10.3905/jpm.2010.36.4.060. (Formalisation du Risk Parity / contribution egale au risque.)

Le Risk Parity pondère chaque actif de façon à ce que sa contribution au risque total soit égale.

Exemple SPY/TLT/GLD (vol annualisée 16%/8%/15%) : - Inverse vol : 1/0.16=6.25, 1/0.08=12.5, 1/0.15=6.67 - Normalisation : 6.25/25.42=25%, 12.5/25.42=49%, 6.67/25.42=26%

→ TLT reçoit 49% du portefeuille (sa vol basse lui donne un poids élevé). SPY et GLD reçoivent ~25% chacun.

Implémentation QuantConnect :

weights = 1.0 / np.array([vol_spy, vol_tlt, vol_gld])
weights /= weights.sum()
self.SetHoldings(self.spy, weights[0])
self.SetHoldings(self.tlt, weights[1])
self.SetHoldings(self.gld, weights[2])

Avantage vs Equal Weight : tient compte du risque, donne plus de poids aux actifs défensifs (bonds) qui réduisent la variance du portefeuille.

Limite : suppose que la volatilité est stationnaire. Un krach où toutes les vols doublent en même temps ne change pas les poids — alors que c’est précisément le moment où le hedge serait utile.

Stratégies d’Options : Protective Put vs Collar

Protective Put (ProtectivePutAlgorithm) : - Structure : Long 100 SPY + Long 1 Put (5% OTM) - Objectif : Limiter les pertes à ~5% + prime - Coût : Prime du put (réduit le rendement espéré) - Best for : Protection à court terme, incertitude élevée

Collar (CollarStrategyAlgorithm) : - Structure : Long SPY + Long Put (5% OTM) + Short Call (5% OTM) - Objectif : Financer le put avec le call - Coût net : Souvent proche de zéro (prime call ~ prime put) - Trade-off : Gains limités à ~5% aussi

Payoff Comparé :

Scénario SPY Sans Hedge Protective Put Collar
-10% -10% ~-7% (put compense) ~-7%
0% 0% ~-2% (prime payée) ~0%
+10% +10% +8% (prime payée) +5% (call cap)

Quand Choisir Quoi : - Protective Put : Vous voulez protéger mais garder tout le potentiel de hausse - Collar : Vous êtes prêt à limiter le gain pour réduire le coût de la protection

Risque Important : En cas de gap down (ouverture -10%), le put peut ne pas offrir la protection attendue si le marché gape avant que vous puissiez ajuster.

def risk_parity_weights(returns_df, annualize=True):
    """
    Calcule les poids Risk Parity bases sur la volatilite inverse.
    
    Parameters:
    -----------
    returns_df : pd.DataFrame avec returns de chaque actif en colonnes
    annualize : bool, si True annualise la volatilite (252 jours)
    
    Returns:
    --------
    pd.Series : poids normalises
    """
    # Calculer volatilite
    volatilities = returns_df.std()
    
    if annualize:
        volatilities = volatilities * np.sqrt(252)
    
    # Inverse volatilite
    inv_vol = 1 / volatilities
    
    # Normaliser pour que somme = 1
    weights = inv_vol / inv_vol.sum()
    
    return weights, volatilities

# Demonstration avec donnees simulees
np.random.seed(42)
n_days = 252

# Simuler returns avec volatilites differentes
returns_demo = pd.DataFrame({
    'SPY': np.random.normal(0.0004, 0.012, n_days),   # Vol ~19%
    'QQQ': np.random.normal(0.0005, 0.015, n_days),   # Vol ~24%
    'TLT': np.random.normal(0.0001, 0.008, n_days),   # Vol ~13%
    'IEF': np.random.normal(0.0001, 0.005, n_days),   # Vol ~8%
    'GLD': np.random.normal(0.0002, 0.010, n_days),   # Vol ~16%
})

# Calculer poids
weights, vols = risk_parity_weights(returns_demo)

# Affichage
print("Risk Parity Weights:")
print("="*50)
result_df = pd.DataFrame({
    'Volatilite Ann.': vols.apply(lambda x: f"{x:.1%}"),
    'Poids Equal Weight': [f"{1/5:.1%}"] * 5,
    'Poids Risk Parity': weights.apply(lambda x: f"{x:.1%}")
})
print(result_df.to_string())

# Visualisation comparative
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Equal Weight
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
axes[0].pie([0.2]*5, labels=weights.index, autopct='%1.0f%%', colors=colors)
axes[0].set_title('Equal Weight', fontsize=12, fontweight='bold')

# Risk Parity
axes[1].pie(weights, labels=weights.index, autopct='%1.0f%%', colors=colors)
axes[1].set_title('Risk Parity', fontsize=12, fontweight='bold')

plt.tight_layout()
plt.show()

print("\nObservation: Risk Parity surpondere les actifs a faible volatilite (IEF, TLT)")
Risk Parity Weights:==================================================    Volatilite Ann. Poids Equal Weight Poids Risk ParitySPY           18.4%              20.0%             14.6%QQQ           23.9%              20.0%             11.3%TLT           12.7%              20.0%             21.3%IEF            7.4%              20.0%             36.3%GLD           16.4%              20.0%             16.4%Observation: Risk Parity surpondere les actifs a faible volatilite (IEF, TLT)

Hedging avec Futures : Calcul et Risques

FuturesHedgingAlgorithm montre comment hedger un portfolio equity avec des futures ES (S&P 500 E-mini).

Formule du Hedge Ratio :

Contracts = (Portfolio Value × β × Hedge Ratio) / (ES Price × 50)

Composants Clés : - β (Beta) : Sensibilité du portfolio à l’indice - β = 1 : Portfolio bouge comme l’indice - β > 1 : Portfolio plus volatile (tech-heavy) - β < 1 : Portfolio moins volatile (value stocks)

  • Hedge Ratio : Pourcentage du portfolio à hedger
    • 100% : Couverture totale
    • 50% : Couverture partielle (utilisé ici)
  • Multiplicateur ES : $50 par point
    • ES à 4000 = 1 contrat = $200,000 de valeur notionnelle

Exemple de Calcul :

Portfolio Value = $500,000
Beta = 1.2 (tech-heavy)
Hedge Ratio = 50%
ES Price = 4000

Contracts = -(500,000 × 1.2 × 0.5) / (4000 × 50)
          = -300,000 / 200,000
          = -1.5 → -1 ou -2 contracts

Avantages du Hedge Futures vs Options : - Coût récurrent : Juste les marges, pas de prime - Effet linéaire : Protection proportionnelle au mouvement - Liquidity : Futures très liquides

Risques Spécifiques : - Margin calls : Si le marché monte, le short ES perd et nécessite des fonds additionnels - Roll risk : Quand le contrat expire, il faut rouler vers le prochain (coût de carry) - Basis risk : ES peut ne pas parfaitement tracker le portfolio sous-jacent

Micro Futures : Pour les petits portfolios (<$100k), considérer les Micro E-mini (/MES) avec un multiplicateur de $5 au lieu de $50.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class RiskParityAlgorithm(QCAlgorithm):
    """
    Strategie Risk Parity: allouer en fonction de la volatilite inverse.
    Chaque actif contribue egalement au risque total.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter actifs
        tickers = ["SPY", "QQQ", "TLT", "IEF", "GLD"]
        self.symbols = [self.AddEquity(t, Resolution.Daily).Symbol for t in tickers]
        
        # Parametres
        self.lookback = 60  # Jours pour calcul volatilite
        
        # Rebalancement mensuel
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.RebalanceRiskParity
        )
    
    def RiskParityWeights(self, symbols, lookback=60):
        """
        Calcule les poids Risk Parity bases sur la volatilite historique.
        """
        # Recuperer historique
        history = self.History(symbols, lookback, Resolution.Daily)
        
        if history.empty:
            # Fallback: equal weight
            return {s: 1.0/len(symbols) for s in symbols}
        
        # Extraire closes et calculer returns
        closes = history['close'].unstack(level=0)
        returns = closes.pct_change().dropna()
        
        # Volatilite annualisee
        volatilities = returns.std() * np.sqrt(252)
        
        # Inverse volatilite
        inv_vol = 1 / volatilities
        
        # Normaliser
        weights = inv_vol / inv_vol.sum()
        
        # Convertir en dict avec Symbol comme cle
        return {symbol: weights[symbol] for symbol in symbols if symbol in weights.index}
    
    def RebalanceRiskParity(self):
        """Rebalance selon les poids Risk Parity."""
        
        weights = self.RiskParityWeights(self.symbols, self.lookback)
        
        if not weights:
            return
        
        self.Debug(f"{self.Time.date()}: Risk Parity Rebalancing")
        for symbol, weight in weights.items():
            self.SetHoldings(symbol, weight)
            self.Debug(f"  {symbol.Value}: {weight:.2%}")

print("RiskParityAlgorithm defini")

[BACKTEST QC Cloud] RiskParityAlgorithm Tradeable dates: 2516 | Orders: 536 | Net Profit: 73.812% | Sharpe: 0.305 | CAGR: 5.679% | MaxDD: 22.200% | Win Rate: 72% | Trades: 536 End Equity: $173,811.76

Lecture du résultat : RiskParityAlgorithm

Les chiffres du run : CAGR 5.679%, Sharpe 0.305, MaxDD 22.200%, 536 trades, win rate 72%, capital final $173,811.76.

Le risk parity est dominé par l’equal weight sur ce backtest, sur les trois axes à la fois :

Equal Weight (§3.1) Risk Parity (§3.2)
CAGR 7.145% 5.679%
Sharpe 0.359 0.305
MaxDD 19.400% 22.200%
Trades 203 536

Le risk parity rapporte moins, risque plus, et paie 2,6× plus de transactions (536 contre 203) — l’inverse du profil attendu (« meilleur Sharpe, plus faible DD »).

Pourquoi ce résultat est plausible plutôt qu’anecdotique : la version implémentée pondère par l’inverse de la volatilité mesurée sur une fenêtre de 60 jours et rebalance mensuellement. Sur 2015-2024, cette règle surpondère structurellement les obligations longues (TLT, faible vol) — et 2022 est l’année où obligations et actions ont baissé ensemble, précisément le scénario où la parité de risque souffre : le portefeuille était concentré sur l’actif qui perdait, au moment où sa volatilité montait.

Coût : la rotation rapide (536 ordres) est un facteur direct de friction. Un rebalancement trimestriel ou une fenêtre de vol plus longue aurait réduit ce coût — c’est un axe d’expérimentation pour l’exercice 2.

3.3 Tactical Asset Allocation (TAA)

La Tactical Asset Allocation ajuste dynamiquement les poids en fonction de signaux de marche. L’approche la plus simple utilise le momentum (SMA 200).

Règles : - Si prix > SMA 200 : actif en tendance haussiere -> allouer - Si prix < SMA 200 : actif en tendance baissiere -> cash (ou autre actif refuge)

Le TAA est un overlay sur une allocation stratégique. Plutôt que de maintenir des poids fixes (60% actions / 40% obligations), on ajuste dynamiquement en fonction de signaux.

Signaux courants : - Momentum (cf. §3.3 variantes) : surpondérer les actifs en tendance haussière. - Volatility : surpondérer les actifs à faible vol en période calme, sous-pondérer en stress. - Valuation : P/E ratio, yield curve, etc.

Fréquence de rebalance : mensuel (institutionnel), trimestriel (retail), annuel (lazy portfolios).

Implémentation type : - 60% actions + 40% obligations (stratégique). - Si momentum 12m SPY > 0 : 70% SPY / 30% TLT. - Si momentum 12m SPY < 0 : 40% SPY / 60% TLT (défensif).

Avantage : surperforme Buy & Hold sur le long terme en réduisant le DD. Inconvénient : turnover élevé, taxable (impositions sur les gains court terme).

All-Weather Portfolio : Philosophie et Implémentation

Allocation Originale de Ray Dalio : - 30% Equities (SPY) - Croissance économique - 40% Long-Term Bonds (TLT) - Protection contre déflation - 15% Intermediate Bonds (IEF) - Stabilité revenus - 7.5% Gold (GLD) - Protection contre inflation - 7.5% Commodities (DBC) - Protection contre inflation

Pourquoi cette Allocation ?

Scénario Économique Croissance Inflation Actifs Favorables Allocation All-Weather
Rising Growth ↑ - Equities, Corp Bonds 30% SPY
Falling Growth ↓ - Treasury Bonds 55% TLT+IEF
Rising Inflation - ↑ Gold, Commodities, TIPS 15% GLD+DBC
Falling Inflation - ↓ Equities, Treasuries Mix 30/55

Résultats Historiques Typiques (2010-2020) : - Sharpe Ratio : ~0.6-0.8 (meilleur que SPY seul) - Max Drawdown : ~15-20% (vs ~33% pour SPY) - CAGR : ~7-9% (vs ~13% pour SPY)

Avantages : - ✅ Performance décente dans tous les environnements - ✅ Faible volatilité - ✅ Peu de rebalancement nécessaire

Inconvénients : - ⚠️ Sous-performance en bull markets prolongés - ⚠️ Sensibilité aux taux d’intérêt (40% TLT) - ⚠️ Requiert un horizon long terme (5+ ans)

Note d’Implémentation : Le code AllWeatherPortfolioAlgorithm inclut un monitoring des corrélations avec alertes si la corrélation SPY-TLT change significativement (>0.3), ce qui pourrait indiquer un changement de régime nécessitant un ajustement.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class TacticalAssetAllocationAlgorithm(QCAlgorithm):
    """
    Tactical Asset Allocation avec signal momentum (SMA 200).
    Alloue uniquement aux actifs au-dessus de leur SMA 200.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter actifs
        tickers = ["SPY", "QQQ", "TLT", "GLD", "EFA"]  # EFA = MSCI EAFE
        self.symbols = []
        self.sma_indicators = {}
        
        for ticker in tickers:
            symbol = self.AddEquity(ticker, Resolution.Daily).Symbol
            self.symbols.append(symbol)
            
            # Creer indicateur SMA 200 pour chaque actif
            self.sma_indicators[symbol] = self.SMA(symbol, 200)
        
        # Warmup pour avoir les SMAs pretes
        self.SetWarmup(200)
        
        # Rebalancement mensuel
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.TacticalRebalance
        )
    
    def TacticalRebalance(self):
        """Rebalance tactiquement selon le signal momentum."""
        
        if self.IsWarmingUp:
            return
        
        # Determiner quels actifs sont au-dessus de leur SMA
        active_symbols = []
        
        for symbol in self.symbols:
            sma = self.sma_indicators[symbol]
            
            if not sma.IsReady:
                continue
            
            current_price = self.Securities[symbol].Price
            
            if current_price > sma.Current.Value:
                active_symbols.append(symbol)
                self.Debug(f"  {symbol.Value}: ACTIVE (price {current_price:.2f} > SMA {sma.Current.Value:.2f})")
            else:
                self.Debug(f"  {symbol.Value}: INACTIVE (price {current_price:.2f} < SMA {sma.Current.Value:.2f})")
        
        # Calculer allocation
        if len(active_symbols) > 0:
            weight = 1.0 / len(active_symbols)
        else:
            weight = 0  # 100% cash si aucun actif actif
        
        self.Debug(f"{self.Time.date()}: TAA Rebalance - {len(active_symbols)} active assets")
        
        # Appliquer allocation
        for symbol in self.symbols:
            if symbol in active_symbols:
                self.SetHoldings(symbol, weight)
            else:
                self.Liquidate(symbol)  # Vendre si sous SMA

print("TacticalAssetAllocationAlgorithm defini")
print("\nLogique:")
print("  - Prix > SMA(200) -> Allouer (momentum positif)")
print("  - Prix < SMA(200) -> Cash (eviter tendance baissiere)")

Résultats du backtest QC Cloud (TacticalAssetAllocationAlgorithm)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.231
CAGR 5.765%
Max Drawdown 22.900%
Net Profit 75.2%
Equity finale $175,154
Probabilistic Sharpe Ratio 1.872%

Tactical asset allocation (SMA200 momentum) with SPY/QQQ/TLT/GLD/EFA. Backtest QC Cloud 2015-2024 (projet 33177683, bt 9d5d5f326db8a8aec24594018fe0c0ee). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10)


Exercice 2 : Risk Parity personnalise avec contraintes

En vous basant sur les exemples d’equal weight, risk parity et TAA ci-dessus, créez une fonction qui calcule les poids d’un portfolio selon une approche Risk Parity modifiée avec contraintes pratiques.

Objectif : implementer ConstrainedRiskParity avec les specifications suivantes :

  • Calculer la volatilite annualisee de chaque actif sur les 60 derniers jours
  • Les poids de base sont l’inverse de la volatilite (risk parity classique)
  • Contrainte 1 : aucun poids ne peut depasser 40% (cap)
  • Contrainte 2 : aucun poids ne peut etre inferieur a 5% (floor)
  • Contrainte 3 : la somme des poids doit etre exactement 1.0
  • Afficher les poids finaux et verifier que toutes les contraintes sont respectees

Indices : - returns.std() * np.sqrt(252) pour la volatilite annualisee - Utilisez np.clip(poids, floor, cap) puis renormalisez - returns_df.dot(weights) pour calculer les returns du portfolio - Inspirez-vous de la fonction risk_parity_weights de la section 3.2

Résultat attendu : une fonction qui prend un DataFrame de returns et retourne un dictionnaire {symbol: weight} avec les poids contraints.

def constrained_risk_parity(returns_df, cap=0.40, floor=0.05, annualize=True):
    """
    Exercice : Risk Parity avec contraintes pratiques.

    Etapes :
    1. Calculer la volatilite annualisee de chaque actif
    2. Calculer les poids inverses (1/vol) comme base
    3. Appliquer np.clip pour respecter cap et floor
    4. Renormaliser pour que la somme = 1.0
    5. Verifier et afficher les contraintes

    Parameters :
    - returns_df : DataFrame des returns journaliers
    - cap : poids maximum par actif (defaut 40%)
    - floor : poids minimum par actif (defaut 5%)
    """
    # TODO etudiant : calculer les volatilites annualisees
    vols = None  # remplacer par returns_df.std() * np.sqrt(252)

    # TODO etudiant : calculer les poids inverses
    inv_vol = None  # remplacer par 1 / vols

    # TODO etudiant : normaliser
    raw_weights = None  # remplacer par inv_vol / inv_vol.sum()

    # TODO etudiant : appliquer les contraintes avec np.clip
    constrained = None  # utiliser np.clip(raw_weights, floor, cap)

    # TODO etudiant : renormaliser pour somme = 1.0
    final_weights = None  # constrained / constrained.sum()

    # TODO etudiant : verifier les contraintes et afficher
    print("Exercice a completer")
    return None

# Test avec les donnees simulees de la section Risk Parity
result = constrained_risk_parity(returns_demo)
if result is not None:
    for sym, w in result.items():
        print(f"  {sym}: {w:.2%}")

[BACKTEST QC Cloud] TacticalAssetAllocationAlgorithm Tradeable dates: 2516 | Orders: 391 | Net Profit: 96.509% | Sharpe: 0.318 | CAGR: 6.983% | MaxDD: 22.300% | Win Rate: 77% | Trades: 391 End Equity: $196,508.59

Lecture du résultat : TacticalAssetAllocationAlgorithm

Les chiffres du run : CAGR 6.983%, Sharpe 0.318, MaxDD 22.300%, 391 trades, win rate 77%, capital final $196,508.59.

Le TAA ne surperforme pas le Buy & Hold sur cette période : avec un CAGR de 6.983%, la stratégie rend moins que le S&P 500 seul (profil ~13% cité en cellule #28) et son drawdown reste à -22.3% — il n’a donc pas rempli sa promesse défensive de « réduction vs Buy & Hold ». Le filtre SMA 200 a manqué de réactivité là où il devait servir.

Pourquoi : le signal momentum est retardataire par construction (SMA 200 = demi-période de lag ~100 jours). Sur 2020, il sort après le krach et rentre après le rebond — le whipsaw coûte deux fois. Les 391 trades sur 10 ans sont la trace de ces allers-retours.

Ce que la cellule enseigne vraiment : un filtre de tendance n’est ni gratuit ni automatiquement protecteur. Sa valeur dépend de la période et du coût de friction ; ici, la friction (391 ordres) et le retard du signal ont mangé le bénéfice espéré. C’est un résultat négatif utile : il justifie la comparaison systématique des variantes (TSM/XSM/Dual) plutôt que l’adoption d’un filtre par défaut.

Avantages TAA : - Evite les grandes baisses (bear markets) - Suit les tendances de fond

Inconvenients : - “Whipsaw” en marche lateraux (faux signaux) - Late entry/exit (le SMA est un indicateur retardataire)

Avantages du TAA

Trois avantages principaux du Tactical Asset Allocation :

  1. Réduction du DD : sortir en période baissière protège le capital psychologique. Un DD de -25% → retour à 0% demande +33% de gain, alors qu’un DD de -12% → retour à 0% demande seulement +14%.

  2. Capture de momentum : les marchés ont tendance à persister à court-moyen terme (12 mois). Être long pendant les phases haussières et sortir pendant les phases baissières capte cette tendance.

  3. Discipline : le signal mécanique élimine l’émotion (“cette fois c’est différent”). On suit le modèle même quand l’intuition dit autre chose.

Coût caché : le signal momentum lag la réalité. Au moment où le signal dit “sort”, la chute a déjà eu lieu. On protège le capital futur, pas le capital présent.


Partie 4 : Hedging (20 min)

4.1 Hedging avec Options

Les options permettent de proteger un portfolio contre les baisses tout en conservant le potentiel de hausse.

Stratégies principales : - Protective Put : Acheter un put pour limiter les pertes - Collar : Protective put + covered call (finance le put avec le call)

Trois stratégies de hedging avec options

Le hedging avec options permet de protéger un portefeuille sans le liquider. Trois stratégies principales :

  1. Protective Put : acheter un put OTM sur le portefeuille. Coût : la prime du put (1-3% du notionnel). Bénéfice : protection en cas de baisse au-delà du strike.

  2. Collar : vendre un call OTM + acheter un put OTM. Coût net proche de zéro (prime call finance la prime put). Bénéfice : protection mais plafond de gain (call vendu).

  3. Put Spread : acheter un put ITM + vendre un put plus OTM. Coût réduit, protection réduite à un range de strikes.

Comparaison : Protective Put = protection pure (coût élevé), Collar = protection gratuite (gain plafonné), Put Spread = compromis.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class ProtectivePutAlgorithm(QCAlgorithm):
    """
    Strategie Protective Put: long equity + long put pour protection.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter l'equity
        equity = self.AddEquity("SPY", Resolution.Minute)
        equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.spy = equity.Symbol
        
        # Ajouter options sur SPY
        option = self.AddOption("SPY", Resolution.Minute)
        self.option_symbol = option.Symbol
        
        # Filtre options: puts OTM, expiration 30-60 jours
        option.SetFilter(lambda universe: universe
            .IncludeWeeklys()
            .Strikes(-10, -2)   # OTM puts (strikes sous le prix actuel)
            .Expiration(30, 60))  # 30-60 jours d'expiration
        
        self.put_contract = None
    
    def OnData(self, data):
        # Verifier si on a des options disponibles
        if not self.Portfolio[self.spy].Invested:
            # Acheter SPY
            self.SetHoldings(self.spy, 0.90)  # 90% en SPY
        
        # Si pas de put actif, en acheter un
        if self.put_contract is None or not self.Portfolio[self.put_contract].Invested:
            self.BuyProtectivePut(data)
    
    def BuyProtectivePut(self, data):
        """Achete un put de protection."""
        
        # Recuperer la chaine d'options
        chain = data.OptionChains.get(self.option_symbol)
        
        if chain is None:
            return
        
        # Filtrer pour puts uniquement
        puts = [x for x in chain if x.Right == OptionRight.Put]
        
        if len(puts) == 0:
            return
        
        # Selectionner le put le plus proche de 5% OTM
        underlying_price = self.Securities[self.spy].Price
        target_strike = underlying_price * 0.95  # 5% OTM
        
        # Trouver le put le plus proche du target
        put = min(puts, key=lambda x: abs(x.Strike - target_strike))
        
        # Acheter le put
        qty = int(self.Portfolio[self.spy].Quantity / 100)  # 1 option = 100 shares
        if qty > 0:
            self.MarketOrder(put.Symbol, qty)
            self.put_contract = put.Symbol
            self.Debug(f"{self.Time}: Bought protective put - Strike: {put.Strike}, Expiry: {put.Expiry}")

print("ProtectivePutAlgorithm defini")
print("\nStrategic Protective Put:")
print("  - Long 100 shares SPY")
print("  - Long 1 put SPY (5% OTM, 30-60 DTE)")
print("  - Protection: pertes limitees a ~5% + prime du put")

Résultats du backtest QC Cloud (ProtectivePutAlgorithm)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio -0.657
CAGR 0.907%
Max Drawdown 6.400%
Net Profit 9.4%
Equity finale $109,449
Probabilistic Sharpe Ratio 0.315%

Protective put (long SPY + long put 5% OTM), options, minute resolution. Backtest QC Cloud 2015-2024 (projet 33177683, bt c5bbbe1d538472f6f491b462e6b458ff). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10)

[BACKTEST QC Cloud] ProtectivePutAlgorithm Tradeable dates: 2516 | Orders: 15 | Net Profit: 9.462% | Sharpe: -0.657 | CAGR: 0.907% | MaxDD: 6.400% | Win Rate: 12% | Trades: 15 End Equity: $109,461.61

Lecture du résultat : ProtectivePutAlgorithm

Les chiffres du run : CAGR 0.907%, Sharpe -0.657, MaxDD 6.400%, 15 trades seulement, win rate 12%, capital final $109,461.61.

Le résultat est double, et les deux moitiés comptent :

  1. La protection a fonctionné sur le drawdown : MaxDD -6.4%, le plus faible des huit runs du notebook — aucune autre stratégie testée ici ne descend aussi peu. Le put a rempli son rôle de plancher.
  2. Elle a détruit le rendement : CAGR 0.907% (taux annualisé ; +9.462% sur la décennie) et un Sharpe négatif (-0.657) — le portefeuille a rendu moins qu’un placement sans risque, tout en payant la prime. Le win rate de 12% (2 trades gagnants sur 15) dit la même chose : la prime versée a dominé.

Ce que le nombre de trades impose : 15 trades en 10 ans, c’est un échantillon trop petit pour conclure à une loi générale de l’assurance optionnelle. Ce que ce run établit, c’est le coût d’une prime systématique dans un marché haussier : payer une assurance pendant 10 ans de hausse coûte le rendement, même quand l’assurance est rarement appelée.

Pédagogie : le protective put est un arbitrage explicite rendement contre plancher, pas une amélioration. Ici il achète un DD de -6.4% en payant ~12 points de CAGR par rapport au Buy & Hold. La question utile n’est pas « est-ce que ça protège ? » (oui) mais « à quel prix, et pour quel horizon ? ».

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class CollarStrategyAlgorithm(QCAlgorithm):
    """
    Strategie Collar: Protective Put + Covered Call.
    La prime du call finance (partiellement) le put.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Setup equity et options
        equity = self.AddEquity("SPY", Resolution.Minute)
        equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        self.spy = equity.Symbol
        
        option = self.AddOption("SPY", Resolution.Minute)
        self.option_symbol = option.Symbol
        
        # Filtre: strikes autour du prix actuel
        option.SetFilter(lambda universe: universe
            .IncludeWeeklys()
            .Strikes(-5, 5)
            .Expiration(30, 60))
        
        self.collar_active = False
    
    def OnData(self, data):
        # Etablir position equity si necessaire
        if not self.Portfolio[self.spy].Invested:
            shares = int(self.Portfolio.Cash * 0.9 / self.Securities[self.spy].Price)
            shares = (shares // 100) * 100  # Arrondir a 100
            if shares >= 100:
                self.MarketOrder(self.spy, shares)
        
        # Mettre en place le collar si pas encore actif
        if not self.collar_active and self.Portfolio[self.spy].Quantity >= 100:
            self.SetupCollar(data)
    
    def SetupCollar(self, data):
        """Met en place le collar: long put + short call."""
        
        chain = data.OptionChains.get(self.option_symbol)
        if chain is None:
            return
        
        underlying_price = self.Securities[self.spy].Price
        
        # Separer puts et calls
        puts = [x for x in chain if x.Right == OptionRight.Put]
        calls = [x for x in chain if x.Right == OptionRight.Call]
        
        if len(puts) == 0 or len(calls) == 0:
            return
        
        # Put: 5% OTM (protection)
        put_target = underlying_price * 0.95
        selected_put = min(puts, key=lambda x: abs(x.Strike - put_target))
        
        # Call: 5% OTM (cap le gain)
        call_target = underlying_price * 1.05
        selected_call = min(calls, key=lambda x: abs(x.Strike - call_target))
        
        # Nombre de contrats
        qty = int(self.Portfolio[self.spy].Quantity / 100)
        
        if qty > 0:
            # Acheter put (protection)
            self.MarketOrder(selected_put.Symbol, qty)
            # Vendre call (finance le put, mais cap le gain)
            self.MarketOrder(selected_call.Symbol, -qty)
            
            self.collar_active = True
            self.Debug(f"{self.Time}: Collar established")
            self.Debug(f"  Long Put: Strike {selected_put.Strike}, Premium {selected_put.LastPrice:.2f}")
            self.Debug(f"  Short Call: Strike {selected_call.Strike}, Premium {selected_call.LastPrice:.2f}")

print("CollarStrategyAlgorithm defini")
print("\nStrategie Collar:")
print("  - Long SPY")
print("  - Long Put 5% OTM (protection downside)")
print("  - Short Call 5% OTM (finance le put, cap le gain)")
print("\nResultat: Pertes limitees a ~5%, Gains limites a ~5%")

Résultats du backtest QC Cloud (CollarStrategyAlgorithm)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.485
CAGR 10.605%
Max Drawdown 28.000%
Net Profit 174.0%
Equity finale $273,998
Probabilistic Sharpe Ratio 9.179%

Collar strategy (long SPY + long put 5% OTM + short call 5% OTM), options, minute resolution. Backtest QC Cloud 2015-2024 (projet 33177683, bt 4d27e53f85d9f456431e8d4cebb9c712). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10)

[BACKTEST QC Cloud] CollarStrategyAlgorithm Tradeable dates: 2516 | Orders: 5 | Net Profit: 174.221% | Sharpe: 0.485 | CAGR: 10.605% | MaxDD: 28.000% | Win Rate: 50% | Trades: 5 End Equity: $274,220.80

Lecture du résultat : CollarStrategyAlgorithm

Les chiffres du run : CAGR 10.605%, Sharpe 0.485, MaxDD 28.000%, 5 trades sur 2516 jours, win rate 50%, capital final $274,220.80.

Deux constats qui nuancent la promesse du collar :

  • Le drawdown n’est pas plafonné : MaxDD -28%, le deuxième plus élevé des huit runs du notebook — très loin du « -10% » attendu d’un collar. Le call vendu plafonne le gain, mais n’empêche pas la valeur du portefeuille de descendre quand la protection n’est pas en place au bon moment.
  • 5 trades en 10 ans : l’échantillon est trop petit pour lire les métriques comme une propriété de la stratégie. Un CAGR de 10.6% sur 5 décisions relève de la trajectoire d’un marché, pas d’une preuve d’efficacité.

Ce qui reste vrai du mécanisme : le collar finance le put par le call — le coût net reste proche de zéro, et c’est sa vraie différence avec le protective put (cellule #53, Sharpe -0.657). Sur un marché haussier, ce financement se paie par le plafond de gain ; sur ce run il a néanmoins laissé 10.6% de CAGR, contre 0.9% pour le put seul.

À retenir : comparer collar et protective put sur ce notebook donne un contraste net — le put achète un plancher très bas (DD -6.4%) en sacrifiant tout le rendement ; le collar conserve du rendement mais ne garantit pas le plancher. Aucun des deux n’est « gratuit ».


Exercice 3 : Mini All-Weather personnalise

En vous basant sur le All-Weather Portfolio de Ray Dalio et les stratégies d’allocation vues precedemment, créez une version simplifiee et personnalisee du All-Weather.

Objectif : implementer MiniAllWeather avec les specifications suivantes :

  • Choisir 4 actifs maximum parmi ceux disponibles (SPY, QQQ, TLT, IEF, GLD, USO)
  • Définir une allocation cible avec au moins 2 classes d’actifs différentes
  • Implementer un filtre de regime simple : si prix SPY < SMA(100), reduire l’exposition equities de 50%
  • Simuler un rebalancement trimestriel avec ajustement selon le regime detecte
  • Afficher un resume de l’allocation et du regime detecte

Indices : - Le filtre SMA peut se calculer avec une moyenne mobile simple sur les données historiques - Pour le regime, comparer le prix actuel a la valeur du SMA - Les mois trimestriels sont [1, 4, 7, 10] - Utilisez un dictionnaire pour les allocations cibles (ex: {"SPY": 0.40, "TLT": 0.30})

Résultat attendu : une classe MiniAllWeather avec initialisation, detection de regime, ajustement d’allocation et affichage du resume.

class MiniAllWeather:
    """
    Exercice : Mini All-Weather Portfolio avec filtre de regime.

    Etapes :
    1. Choisir 4 actifs maximum (au moins 2 classes differentes)
    2. Definir les allocations cibles
    3. Implementer le filtre SMA(100) sur SPY
    4. Implementer le rebalancement trimestriel avec ajustement de regime
    5. Afficher le resume allocation + regime
    """
    def __init__(self):
        # TODO etudiant : choisir les actifs et definir les allocations cibles
        self.target_allocations = {}  # ex: {'SPY': 0.40, 'TLT': 0.30, 'GLD': 0.15, 'IEF': 0.15}
        self.rebalance_months = [1, 4, 7, 10]
        self.sma_period = 100

    def detect_regime(self, current_price, sma_value):
        """
        Detecte le regime base sur la position du prix vs SMA.
        Retourne 'bull' ou 'defensif'.
        """
        # TODO etudiant : implementer la detection
        return "bull"

    def compute_adjusted_allocations(self, regime):
        """
        Ajuste les allocations selon le regime detecte.
        En defensif : reduire equities de 50%.
        """
        # TODO etudiant : implementer l'ajustement
        return self.target_allocations

    def rebalance_summary(self):
        """Affiche le resume de l'allocation et du regime."""
        # TODO etudiant : implementer l'affichage
        print("Exercice a completer")

portfolio = MiniAllWeather()
portfolio.rebalance_summary()

4.2 Hedging avec Futures

Les futures permettent de hedger un portfolio equity en vendant des contrats futures sur indice (ex: ES pour S&P 500).

Calcul du Hedge Ratio : \[\text{Contracts} = \frac{\text{Portfolio Value} \times \beta}{\text{Futures Value}}\]

ou : - \(\beta\) = beta du portfolio vs l’indice (mesure la sensibilite) - Futures Value = Prix du future x multiplicateur (50 pour ES)

Le hedging avec futures est moins cher que le hedging avec options (pas de prime à payer), mais introduit un basis risk.

Exemple : portefeuille \(100k SPY, β=1.0, future ES à 4500\), multiplicateur 50$. - N = (1.0 × 100000) / (4500 × 50) = 0.44 contrats.

Basis risk : le future ne suit pas parfaitement le sous-jacent. L’écart (basis) varie dans le temps, créant un P&L de hedging imparfait. Pendant les stress, le basis peut diverger fortement (future surperforme ou sous-performe le cash).

Coût-bénéfice : - Pas de prime initiale (contrairement aux puts). - Mark-to-market daily (appels de marge). - Efficacité imparfaite due au basis risk.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class FuturesHedgingAlgorithm(QCAlgorithm):
    """
    Hedging d'un portfolio equity avec futures ES (S&P 500 E-mini).
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(500000)  # Plus de capital pour marges futures
        
        # Portfolio equity
        self.equities = [
            self.AddEquity("AAPL", Resolution.Daily).Symbol,
            self.AddEquity("MSFT", Resolution.Daily).Symbol,
            self.AddEquity("GOOGL", Resolution.Daily).Symbol,
            self.AddEquity("AMZN", Resolution.Daily).Symbol,
        ]
        
        # Futures ES (S&P 500 E-mini)
        self.es = self.AddFuture(Futures.Indices.SP500EMini, Resolution.Daily)
        self.es.SetFilter(0, 90)  # Contrats expirant dans 0-90 jours
        
        # Parametres hedging
        self.portfolio_beta = 1.2  # Beta estime du portfolio (tech-heavy = beta > 1)
        self.es_multiplier = 50    # 1 point ES = $50
        self.hedge_ratio = 0.5     # Hedger 50% du portfolio
        
        self.hedge_active = False
        self.hedge_contract = None
        
        # Programmer rebalancement
        self.Schedule.On(
            self.DateRules.MonthStart("AAPL"),
            self.TimeRules.AfterMarketOpen("AAPL", 30),
            self.RebalanceHedge
        )
    
    def OnData(self, data):
        # Etablir position equity si necessaire
        if not any(self.Portfolio[s].Invested for s in self.equities):
            weight = 0.8 / len(self.equities)  # 80% en equities, 20% reserve pour marges
            for symbol in self.equities:
                self.SetHoldings(symbol, weight)
    
    def CalculateHedgeContracts(self):
        """
        Calcule le nombre de contrats ES a shorter pour hedger le portfolio.
        """
        # Valeur du portfolio equity
        equity_value = sum(self.Portfolio[s].HoldingsValue for s in self.equities)
        
        if equity_value <= 0:
            return 0
        
        # Valeur a hedger
        value_to_hedge = equity_value * self.hedge_ratio
        
        # Ajuster par le beta
        beta_adjusted_value = value_to_hedge * self.portfolio_beta
        
        # Trouver le contrat ES actif
        if self.hedge_contract is None:
            return 0
        
        # Valeur d'un contrat ES
        es_price = self.Securities[self.hedge_contract].Price
        es_contract_value = es_price * self.es_multiplier
        
        # Nombre de contrats (negatif car on short)
        contracts = -int(beta_adjusted_value / es_contract_value)
        
        return contracts
    
    def RebalanceHedge(self):
        """Rebalance la position de hedge."""
        
        # Trouver le contrat ES front-month
        for chain in self.CurrentSlice.FutureChains:
            contracts = list(chain.Value)
            if len(contracts) > 0:
                # Prendre le contrat le plus proche de l'expiration
                front_month = sorted(contracts, key=lambda x: x.Expiry)[0]
                self.hedge_contract = front_month.Symbol
                break
        
        if self.hedge_contract is None:
            self.Debug("No ES contract found for hedging")
            return
        
        # Calculer contracts necessaires
        target_contracts = self.CalculateHedgeContracts()
        current_contracts = int(self.Portfolio[self.hedge_contract].Quantity)
        
        # Ajuster position
        delta = target_contracts - current_contracts
        
        if delta != 0:
            self.MarketOrder(self.hedge_contract, delta)
            self.Debug(f"{self.Time.date()}: Hedge adjusted by {delta} contracts")
            self.Debug(f"  Portfolio Value: ${sum(self.Portfolio[s].HoldingsValue for s in self.equities):,.0f}")
            self.Debug(f"  ES Contracts: {target_contracts}")

print("FuturesHedgingAlgorithm defini")
print("\nParametres de hedge:")
print(f"  Portfolio Beta: 1.2 (tech-heavy)")
print(f"  ES Multiplier: $50 par point")
print(f"  Hedge Ratio: 50% du portfolio")
print("\nFormule: Contracts = (Portfolio Value * Beta * Hedge Ratio) / (ES Price * 50)")

Résultats du backtest QC Cloud (FuturesHedgingAlgorithm)

Metrique Valeur
Dates negociables 2579
Sharpe Ratio 0.821
CAGR 24.030%
Max Drawdown 39.400%
Net Profit 761.5%
Equity finale $4,307,621
Probabilistic Sharpe Ratio 29.549%

Futures hedging (AAPL/MSFT/GOOGL/AMZN + ES short hedge), daily resolution. Backtest QC Cloud 2015-2024, capital initial $500,000 (marges futures); NP eleve reflete la hausse tech 2015-2024 (projet 33177683, bt c75add1e8ae039d9f3544e249ccc185e). Net Profit / Equity finale deduits du CAGR verifie (End = $500,000 x (1+CAGR)^10)

[BACKTEST QC Cloud] FuturesHedgingAlgorithm Tradeable dates: 2579 | Orders: 4 | Net Profit: 763.061% | Sharpe: 0.821 | CAGR: 24.030% | MaxDD: 39.400% | Win Rate: 0% | Trades: 4 End Equity: $4,315,307.37

Lecture du résultat : FuturesHedgingAlgorithm

Les chiffres du run, et ce qu’ils ne peuvent pas dire : CAGR 24.030%, Sharpe 0.821, MaxDD 39.400%, Net Profit 763.061%, capital final $4,315,307.37 — mais 4 trades sur 2579 jours et un win rate de 0%.

Pourquoi ce bandeau n’est pas une preuve de performance : quatre trades en dix ans ne constituent pas un échantillon. Un win rate de 0% affiché à côté d’un profit de 763% signale un problème de comptage des trades fermés, pas une stratégie gagnante par construction. Les métriques de ce run décrivent une trajectoire (celle d’un portefeuille equity partiellement couvert par un short ES pendant une décennie haussière), elles ne mesurent pas la qualité du hedge.

Le drawdown contredit la promesse du hedge : -39.4%, le plus élevé du notebook. Un hedge censé réduire le drawdown qui produit le pire drawdown de la série est un résultat à lire tel quel : le short ES réduit l’exposition en moyenne, il ne protège pas les points bas.

Ce que le notebook enseigne donc honnêtement : sur les quatre mécanismes de couverture testés (put, collar, futures, filtre de régime), un seul réduit réellement le drawdown — le protective put, à -6.4% — et il le paie d’un Sharpe négatif. Les trois autres affichent un drawdown de 22% à 39%, c’est-à-dire au niveau ou au-dessus des stratégies d’allocation non couvertes du même notebook (equal weight 19.4%, TAA 22.3%). Un hedge se juge donc sur le couple (rendement, drawdown) mesuré sur un échantillon suffisant, jamais sur le mécanisme annoncé — et son coût dépend du régime de marché traversé.

Note importante sur les Futures : - Les futures requierent des marges (capital immobilise) - Le multiplicateur ES est $50 (1 contrat ES = $50 x prix SP500) - Un contrat ES mini (/ES) represente ~$200k de valeur notionnelle - Pour les petits portfolios, considerer les micro futures (/MES, multiplicateur $5)

Note importante sur les Futures

Trois pièges courants avec les futures en hedging :

  1. Contango/Backwardation : si le future est en contango (prix future > spot), le roll est négatif — on perd de l’argent à chaque rollover. C’est le cas typique pour les commodities (CL, NG).

  2. Mark-to-market daily : un mouvement adverse de 5% du future génère un appel de marge de $5000 sur 1 contrat ES. Si le compte n’a pas assez de cash, margin call = liquidation forcée.

  3. Expiry : les futures expirent. Il faut rouler la position 5-15 jours avant l’expiry (cf. QC-Py-07 Futures/Forex).

Recommandation : utiliser les futures uniquement avec un compte suffisamment capitalisé (>$50k) pour absorber les appels de marge sans liquidation forcée.


Partie 5 : All-Weather Portfolio (25 min)

5.1 Concept du All-Weather Portfolio

Le All-Weather Portfolio est une stratégie developpee par Ray Dalio (Bridgewater Associates). L’idee est de créer un portfolio qui performe dans tous les environnements economiques.

Les 4 Scénarios Economiques :

Environnement Croissance Inflation Actifs Favorables
Rising Growth Hausse - Equities, Corporate Bonds, Commodities
Falling Growth Baisse - Treasury Bonds, TIPS
Rising Inflation - Hausse Commodities, TIPS, Gold
Falling Inflation - Baisse Equities, Treasury Bonds

Allocation Originale Dalio : - 30% Actions (SPY) - 40% Obligations Long Terme (TLT) - 15% Obligations Moyen Terme (IEF) - 7.5% Or (GLD) - 7.5% Commodities (DBC ou GSG)

# Visualisation de l'allocation All-Weather
allocations = {
    'Equities (SPY)': 30,
    'Long-Term Bonds (TLT)': 40,
    'Intermediate Bonds (IEF)': 15,
    'Gold (GLD)': 7.5,
    'Commodities (DBC)': 7.5
}

# Couleurs par classe d'actifs
colors = ['#1f77b4', '#ff7f0e', '#ffbb78', '#ffd700', '#8c564b']

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# Pie chart
axes[0].pie(allocations.values(), labels=allocations.keys(), autopct='%1.1f%%',
            colors=colors, explode=[0.02]*5, shadow=True)
axes[0].set_title('All-Weather Portfolio\n(Ray Dalio)', fontsize=14, fontweight='bold')

# Bar chart par classe
classes = ['Equities', 'Bonds', 'Commodities']
class_weights = [30, 55, 15]  # 30% eq, 55% bonds (40+15), 15% commodities (7.5+7.5)
class_colors = ['#1f77b4', '#ff7f0e', '#8c564b']

bars = axes[1].bar(classes, class_weights, color=class_colors, edgecolor='black')
axes[1].set_ylabel('Allocation (%)', fontsize=12)
axes[1].set_title('Allocation par Classe d\'Actifs', fontsize=14, fontweight='bold')
axes[1].set_ylim(0, 60)

# Ajouter valeurs sur les barres
for bar, val in zip(bars, class_weights):
    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
                 f'{val}%', ha='center', fontsize=12, fontweight='bold')

plt.tight_layout()
plt.show()

print("\nPhilosophie All-Weather:")
print("  - Surponderation bonds (55%) : stabilite, revenus reguliers")
print("  - Equities modere (30%) : croissance long terme")
print("  - Commodities (15%) : protection inflation, diversification")
Philosophie All-Weather:  - Surponderation bonds (55%) : stabilite, revenus reguliers  - Equities modere (30%) : croissance long terme  - Commodities (15%) : protection inflation, diversification

5.2 Implementation Complete

Cette section finale assemble toutes les briques du notebook dans une stratégie cohérente multi-actifs.

Architecture cible : 1. Allocation stratégique : 60% actions / 30% obligations / 10% commodities (vol-weights via Risk Parity). 2. Overlay TAA : momentum 12m sur chaque classe → ajustement +/-10% des poids. 3. Hedging : collar 5% OTM sur la portion actions. 4. Risk management : drawdown control à -15% → réduction d’exposition 50%.

Ce que le backtest de cette architecture donne réellement (cellule #69, AllWeatherPortfolioAlgorithm, sortie committée) : CAGR 4.867%, Sharpe 0.198, MaxDD 23.500%, 159 trades. La version assemblée testée ici ne reproduit donc pas le profil espéré — et l’écart n’est pas un détail de calibrage : il vient de la partie obligataire longue (40% TLT) sur une décennie qui contient la remontée de taux de 2022.

Lire ce résultat comme un point de départ, pas comme un échec : les quatre briques sont posées et chaque levier est modifiable (fenêtre de vol, fréquence de rebalance, seuil de drawdown). La cellule #71 récapitule ce que le notebook établit et ce qu’il laisse ouvert ; les exercices 1 à 3 fournissent le cadre pour tester vos propres variantes.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
class AllWeatherPortfolioAlgorithm(QCAlgorithm):
    """
    All-Weather Portfolio inspire de Ray Dalio.
    
    Allocation:
    - 30% Equities (SPY)
    - 40% Long-Term Bonds (TLT)
    - 15% Intermediate Bonds (IEF)
    - 7.5% Gold (GLD)
    - 7.5% Commodities (DBC)
    
    Features:
    - Rebalancement trimestriel
    - Risk monitoring (volatilite, correlations)
    - Alertes si correlations changent significativement
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # === CONFIGURATION PORTFOLIO ===
        self.target_allocations = {}
        
        # Equities
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.target_allocations[self.spy] = 0.30
        
        # Long-Term Bonds
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
        self.target_allocations[self.tlt] = 0.40
        
        # Intermediate Bonds
        self.ief = self.AddEquity("IEF", Resolution.Daily).Symbol
        self.target_allocations[self.ief] = 0.15
        
        # Gold
        self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol
        self.target_allocations[self.gld] = 0.075
        
        # Commodities (DBC = Invesco DB Commodity Index)
        self.dbc = self.AddEquity("DBC", Resolution.Daily).Symbol
        self.target_allocations[self.dbc] = 0.075
        
        self.symbols = list(self.target_allocations.keys())
        
        # === PARAMETRES RISK MONITORING ===
        self.correlation_lookback = 60  # Jours pour calcul correlation
        self.correlation_threshold = 0.3  # Seuil alerte changement correlation
        self.baseline_correlations = {}  # Correlations de reference
        
        # === REBALANCEMENT TRIMESTRIEL ===
        self.rebalance_months = [1, 4, 7, 10]  # Janvier, Avril, Juillet, Octobre
        
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.MonthlyCheck
        )
        
        # === WARMUP ===
        self.SetWarmup(self.correlation_lookback)
    
    def MonthlyCheck(self):
        """Verification mensuelle: rebalancement et monitoring."""
        
        if self.IsWarmingUp:
            return
        
        # Risk monitoring (tous les mois)
        self.MonitorRisk()
        
        # Rebalancement (trimestriel)
        if self.Time.month in self.rebalance_months:
            self.Rebalance()
    
    def Rebalance(self):
        """Rebalance vers les allocations cibles."""
        
        self.Debug(f"\n{'='*50}")
        self.Debug(f"{self.Time.date()}: QUARTERLY REBALANCE")
        self.Debug(f"{'='*50}")
        
        total_value = self.Portfolio.TotalPortfolioValue
        self.Debug(f"Portfolio Value: ${total_value:,.2f}")
        
        # Calculer drift (ecart par rapport a la cible)
        self.Debug(f"\nAllocations (Current -> Target):")
        
        for symbol in self.symbols:
            current_value = self.Portfolio[symbol].HoldingsValue
            current_weight = current_value / total_value if total_value > 0 else 0
            target_weight = self.target_allocations[symbol]
            drift = current_weight - target_weight
            
            self.Debug(f"  {symbol.Value}: {current_weight:.1%} -> {target_weight:.1%} (drift: {drift:+.1%})")
            
            # Appliquer allocation cible
            self.SetHoldings(symbol, target_weight)
        
        self.Debug(f"\nRebalance complete.")
    
    def MonitorRisk(self):
        """Surveille les metriques de risque et alerte si changement significatif."""
        
        # Recuperer historique
        history = self.History(self.symbols, self.correlation_lookback, Resolution.Daily)
        
        if history.empty or len(history) < self.correlation_lookback:
            return
        
        # Calculer returns et correlations
        closes = history['close'].unstack(level=0)
        returns = closes.pct_change().dropna()
        
        # Volatilite portfolio
        weights = np.array([self.target_allocations[s] for s in self.symbols])
        portfolio_returns = returns.dot(weights)
        portfolio_vol = portfolio_returns.std() * np.sqrt(252)
        
        # Correlation SPY-TLT (indicateur cle)
        spy_tlt_corr = returns[self.spy].corr(returns[self.tlt])
        
        # Matrice de correlation complete
        corr_matrix = returns.corr()
        
        # Stocker comme baseline si premier calcul
        key = "spy_tlt"
        if key not in self.baseline_correlations:
            self.baseline_correlations[key] = spy_tlt_corr
            self.Debug(f"{self.Time.date()}: Baseline SPY-TLT correlation set to {spy_tlt_corr:.2f}")
        
        # Verifier changement significatif
        baseline = self.baseline_correlations[key]
        change = abs(spy_tlt_corr - baseline)
        
        if change > self.correlation_threshold:
            self.Debug(f"\n*** ALERT: SPY-TLT correlation changed significantly ***")
            self.Debug(f"  Baseline: {baseline:.2f}")
            self.Debug(f"  Current: {spy_tlt_corr:.2f}")
            self.Debug(f"  Change: {change:.2f}")
            
            # Envoyer notification (en production, email/SMS/etc.)
            self.Notify.Email("trader@example.com", 
                             "All-Weather Alert: Correlation Shift",
                             f"SPY-TLT correlation changed from {baseline:.2f} to {spy_tlt_corr:.2f}")
            
            # Mettre a jour baseline
            self.baseline_correlations[key] = spy_tlt_corr
        
        # Log mensuel
        self.Debug(f"\n{self.Time.date()}: RISK MONITOR")
        self.Debug(f"  Portfolio Vol (ann.): {portfolio_vol:.1%}")
        self.Debug(f"  SPY-TLT Correlation: {spy_tlt_corr:.2f}")
    
    def OnEndOfAlgorithm(self):
        """Resume final."""
        self.Debug(f"\n{'='*50}")
        self.Debug("ALL-WEATHER PORTFOLIO - FINAL SUMMARY")
        self.Debug(f"{'='*50}")
        self.Debug(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
        self.Debug(f"Total Return: {(self.Portfolio.TotalPortfolioValue / 100000 - 1) * 100:.2f}%")

print("AllWeatherPortfolioAlgorithm defini")

Résultats du backtest QC Cloud (AllWeatherPortfolioAlgorithm)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.2
CAGR 4.889%
Max Drawdown 23.500%
Net Profit 61.2%
Equity finale $161,176
Probabilistic Sharpe Ratio 3.029%

All-Weather portfolio (Dalio-inspired: SPY/TLT/IEF/GLD/DBC), quarterly rebalance + risk monitoring. Backtest QC Cloud 2015-2024 (projet 33177683, bt c5a9fb0fa4f70816fcc476420bb64457). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10)

[BACKTEST QC Cloud] AllWeatherPortfolioAlgorithm Tradeable dates: 2516 | Orders: 159 | Net Profit: 60.899% | Sharpe: 0.198 | CAGR: 4.867% | MaxDD: 23.500% | Win Rate: 83% | Trades: 159 End Equity: $160,898.51

Lecture du résultat : AllWeatherPortfolioAlgorithm

Les chiffres du run : CAGR 4.867%, Sharpe 0.198, MaxDD 23.500%, 159 trades, win rate 83%, capital final $160,898.51 pour $100,000 engagés sur ~10 ans.

Le profil « le plus défensif du notebook » n’est pas ce que le backtest montre :

All-Weather (§5) Equal Weight (§3.1)
CAGR 4.867% 7.145%
Sharpe 0.198 0.359
MaxDD 23.500% 19.400%

L’All-Weather rend moins que l’equal weight, avec un Sharpe plus faible et un drawdown plus élevé. Sur cette période, surpondérer les obligations longues (40% TLT) n’a pas produit la stabilité attendue : 2022 a vu actions et obligations baisser ensemble, et un portefeuille à 55% de duration en a subi le plein effet.

Pourquoi la philosophie reste valide malgré ce run : la construction de Dalio raisonne par régimes macroéconomiques (croissance/inflation, hausse/baisse) et cherche une allocation qui ne s’effondre dans aucun des quatre. Cette logique ne garantit pas un meilleur Sharpe sur une fenêtre donnée — elle vise la robustesse entre régimes. 2015-2024 contient deux régimes obligataires opposés (taux nuls puis remontée brutale), ce qui est précisément le stress test qui pénalise la version simplifiée testée ici.

Ce que le notebook démontre donc : l’allocation par régime est un choix de philosophie, pas un gain automatique. La comparaison interne est sans ambiguïté — sur ce corpus, 1/N fait mieux sur les trois axes. Comprendre pourquoi (duration, corrélation actions-obligations non stationnaire) est l’objectif pédagogique de cette section.

# Resume de la strategie All-Weather
print("="*60)
print("ALL-WEATHER PORTFOLIO - RESUME")
print("="*60)

print("\n1. ALLOCATION CIBLE:")
print("   - SPY (Equities):      30%")
print("   - TLT (Long Bonds):    40%")
print("   - IEF (Int. Bonds):    15%")
print("   - GLD (Gold):          7.5%")
print("   - DBC (Commodities):   7.5%")

print("\n2. REBALANCEMENT:")
print("   - Frequence: Trimestriel (Jan, Avr, Jul, Oct)")
print("   - Methode: Reequilibrer vers allocations cibles")

print("\n3. RISK MONITORING:")
print("   - Volatilite portfolio (annualisee)")
print("   - Correlation SPY-TLT (indicateur cle)")
print("   - Alerte si changement > 0.3 en correlation")

print("\n4. AVANTAGES:")
print("   - Robuste dans differents environnements economiques")
print("   - Faible volatilite (surponderation bonds)")
print("   - Diversification reelle (actifs decorrelated)")

print("\n5. INCONVENIENTS:")
print("   - Performance moderate en bull markets")
print("   - Sensible aux taux d'interet (40% TLT)")
print("   - Surponderation historiquement en bonds")

Conclusion et Prochaines Étapes

Recapitulatif

Dans ce notebook, nous avons couvert :

  1. Fondations Multi-Asset :
    • Pourquoi diversifier (correlation, risk-adjusted returns)
    • Classes d’actifs (equities, bonds, commodities, crypto)
    • Comment ajouter différentes classes dans QuantConnect
  2. Analyse des Correlations :
    • Matrice de correlation statique
    • Rolling correlations pour detecter les changements
    • Detection de regime (normal, stress, liquidity crisis)
  3. Stratégies d’Allocation :
    • Equal Weight (simplicite, diversification naturelle)
    • Risk Parity (contribution egale au risque)
    • Tactical Asset Allocation (momentum SMA 200)
  4. Hedging :
    • Protective puts et collars (options)
    • Hedge avec futures ES (beta-adjusted)
  5. All-Weather Portfolio :
    • Philosophie Ray Dalio
    • Implementation complete avec risk monitoring

Ce que les backtests committés établissent (chiffres des bandeaux, cellules #27 à #69) :

Stratégie CAGR Sharpe MaxDD Trades
Equal Weight (1/N) 7.145% 0.359 19.400% 203
Risk Parity 5.679% 0.305 22.200% 536
TAA momentum 6.983% 0.318 22.300% 391
Detection de regime 7.704% 0.347 25.200% 277
Protective Put 0.907% -0.657 6.400% 15
Collar 10.605% 0.485 28.000% 5
Futures Hedging 24.030% 0.821 39.400% 4
All-Weather 4.867% 0.198 23.500% 159

Le résultat le plus instructif du corpus est un résultat négatif : sur cette période (2015-2024), aucune des stratégies d’allocation sophistiquées ne bat l’equal weight, et parmi les mécanismes de couverture un seul réduit réellement le drawdown — le protective put, à -6.4% — en payant un Sharpe négatif ; les trois autres affichent 22% à 39%, au niveau ou au-dessus des allocations non couvertes. Les deux runs à très petit échantillon (collar : 5 trades, futures : 4 trades) ne portent aucune conclusion statistique.

Points Cles a Retenir

Concept Point Cle
Diversification Correlation < 1 = reduction risque sans perte de rendement
Risk Parity Allouer par contribution au risque, pas par capital
TAA Momentum simple (SMA 200) peut eviter les bear markets
Hedging Options = protection avec upside, Futures = hedge lineaire
All-Weather Surponderer bonds pour stabilite dans tous regimes

Limitations

  • ETFs comme proxies : Les ETFs ont des frais et tracking error vs actifs sous-jacents
  • Correlations non-stationnaires : Les correlations changent, surtout en crise
  • Covariance estimation : Difficile avec peu de données (regime shifts)
  • Leverage implicite Risk Parity : Pour vraie parite, faudrait leverager les bonds
  • Echantillons courts sur le hedging : collar (5 trades) et futures (4 trades) ne soutiennent aucune conclusion statistique
  • Deux bandeaux sans performance : MultiAssetPortfolioAlgorithm (#8) et CorrelationAnalysisAlgorithm (#16) n’ont pas trade — les zeros y sont structurels

Limites assumées : les backtests exécutés dans ce notebook sont référentiels (paramètres standards, période 2015-2024). Pour vos propres portefeuilles, vous devrez optimiser les poids sur votre univers d’actifs et horizon d’investissement.

Prochaines Étapes

Notebook Suivant : QC-Py-09

Nous couvrirons des sujets plus avances comme : - Optimisation de portefeuille (Mean-Variance, Black-Litterman) - Factor investing - Portfolio rebalancing avance

Branchement série : - Suivant : QC-Py-09 Order Types — utilise les méthodes d’allocation de ce notebook pour déterminer quand et comment passer les ordres. - Complémentaire : QC-Py-10 Risk Portfolio Management — applique les techniques de hedging vues ici dans un cadre risk-aware (position sizing, stop-loss, drawdown control).

Pour aller plus loin : - Bridgewater All-Weather : papier Dalio sur la philosophie d’allocation par régime macro. - Markowitz original : “Portfolio Selection” (1952) — base théorique de la diversification. - DeMiguel et al. (2009) : “Optimal Versus Naive Diversification” — pourquoi 1/N bat les méthodes sophistiquées out-of-sample.

Ressources Complementaires

Références académiques

  • DeMiguel, V., Garlappi, L. & Uppal, R. (2009). Optimal Versus Naive Diversification: How Inefficient the 1/N Portfolio Strategy? The Review of Financial Studies 22(5):1915-1953.
  • Maillard, S., Roncalli, T. & Teiletche, J. (2010). The Properties of Equally Weighted Risk Contribution Portfolios. The Journal of Portfolio Management 36(4):60-70. DOI 10.3905/jpm.2010.36.4.060.
  • Markowitz, H. (1952). Portfolio Sélection. The Journal of Finance 7(1):77-91. DOI 10.1111/j.1540-6261.1952.tb01525.x.
  • Sharpe, W.F. (1966). Mutual Fund Performance. The Journal of Business 39(1):119-138. DOI 10.1086/294846.

Notebook complete. Pret pour QC-Py-09.

Retour au sommet