<< Sommaire QC | Précédent : QC-Py-Cloud-14-DualMomentum << | Suivant : QC-Py-Cloud-04-MeanReversion >>

QC-Py-Cloud-03 : Parite de Risque (Risk Parity)

< Classification de Texte | Suivant : RL DQN >

Niveau : Intermediaire | Duree estimee : 25 min | Kernel : Python 3

Objectifs

  • Comprendre le concept de parite de risque (Risk Parity / Risk Budgeting)
  • Implementer la ponderation par volatilite inverse (Inverse Volatility Weighting)
  • Gerer le rebalancement mensuel avec surveillance de derive
  • Deployer sur QC Cloud et comparer avec un portefeuille equally-weighted

Partie 1 : Théorie du Risk Parity

Le Risk Parity est une approche d’allocation developpee par Bridgewater Associates (Ray Dalio, 1996). Au lieu d’allouer le capital equitablement (1/N), on alloue le risque equitablement entre les classes d’actifs.

Pourquoi 1/N echoue : avec des capitaux egaux, l’actif le plus volatil domine le risque du portefeuille. Un portefeuille 20/20/20/20/20 ou DBC volatilise a 22,1% annuels contribue pres du double de risque de TLT qui volatilise a 12,4% — le capital est egal, le risque ne l’est pas. Le Risk Parity inverse la logique : on fixe la contribution au risque, et on laisse les poids s’en deduire.

Principe : Inverse Volatility Weighting

Pour N actifs, le poids de l’actif i est :

w_i = (1/sigma_i) / sum(1/sigma_j)

Ou sigma_i est la volatilite realisee de l’actif i sur une fenêtre glissante.

Cette formule est l’approximation la plus simple du Risk Parity : elle equalise les contributions au risque en supposant les correlations nulles ou uniformes. La Partie 4 montrera l’ecart que cette hypothese introduit, et la methode ERC qui la corrige — la demo ci-dessous en pose deja la base mesurable.

# Demonstration : calcul de poids Risk Parity
import numpy as np

assets = {"SPY": 16.5, "EFA": 18.2, "GLD": 15.8, "DBC": 22.1, "TLT": 12.4}

n = len(assets)
equal_weights = {k: 1/n for k in assets}

inv_vols = {k: 1/v for k, v in assets.items()}
total = sum(inv_vols.values())
rp_weights = {k: v/total for k, v in inv_vols.items()}

print("Allocation :  Equal-Weight  vs  Risk Parity")
print("-" * 45)
for asset in assets:
    ew = equal_weights[asset]
    rp = rp_weights[asset]
    print(f"  {asset:5s} :    {ew:5.1%}         {rp:5.1%}")

print()
ew_risk = {k: equal_weights[k] * assets[k] for k in assets}
rp_risk = {k: rp_weights[k] * assets[k] for k in assets}
print("Contribution au risque (%):")
print("-" * 45)
for asset in assets:
    print(f"  {asset:5s} :    {ew_risk[asset]:5.1f}%         {rp_risk[asset]:5.1f}%")
Allocation :  Equal-Weight  vs  Risk Parity
---------------------------------------------
  SPY   :    20.0%         19.9%
  EFA   :    20.0%         18.0%
  GLD   :    20.0%         20.8%
  DBC   :    20.0%         14.8%
  TLT   :    20.0%         26.5%

Contribution au risque (%):
---------------------------------------------
  SPY   :      3.3%           3.3%
  EFA   :      3.6%           3.3%
  GLD   :      3.2%           3.3%
  DBC   :      4.4%           3.3%
  TLT   :      2.5%           3.3%

La sortie chiffre exactement le mecanisme annonce en Partie 1. Cote allocation, l’ecart au 1/N reste modere (TLT passe de 20,0% a 26,5%, DBC recule de 20,0% a 14,8%) — ce sont des poids, on s’y attendait peu differents. Cote contribution au risque, la transformation est nette : en Equal-Weight, les contributions sont dispersees de 2,5% (TLT) a 4,4% (DBC) — DBC pese 1,8x le risque de TLT pour le meme capital. En Risk Parity, les cinq contributions convergent a 3,3% : chaque actif participe pour exactement un cinquieme du risque total. C’est la signature d’une allocation par le risque, pas par le capital.

Paramètres de la stratégie QC

ETF Classe Rôle dans le portefeuille
SPY Actions US Growth engine
EFA Actions internationales Diversification geographique
GLD Or Couverture inflation
DBC Matieres premières Exposition reelle
TLT Obligations long terme Safe haven / stabilisateur

Rebalancement mensuel avec trigger de derive a 5% (intra-mensuel).

# Code source de l'algorithme - pret pour deploiement QC Cloud
qc_code = '''# region imports
from AlgorithmImports import *
# endregion


class RiskParity(QCAlgorithm):
    """
    Risk Parity Strategy (Bridgewater-style simplifie).

    Edge: Equaliser la contribution au risque de chaque classe d'actifs
    plutot que la contribution en capital.
    Reference: Asness/Frazzini 2012 "Leverage Aversion and Risk Parity"

    Universe: SPY, EFA, GLD, DBC, TLT (5 classes d'actifs diversifiees)
    Signal: Poids = 1/volatilite(60j), normalises pour sommer a 1.0
    Rebalancement: Mensuel + trigger 5% de derive
    Periode: 2015-2026
    """

    # Parametres configurables
    TICKERS = ["SPY", "EFA", "GLD", "DBC", "TLT"]
    VOL_LOOKBACK = 60       # Jours pour la volatilite realisee
    REBALANCE_DAY = 1       # Jour du mois pour rebalancement mensuel
    DRIFT_THRESHOLD = 0.05  # Seuil de derive pour rebalancement intra-mensuel
    WARMUP_DAYS = 70        # Jours de warmup pour avoir 60j de donnees

    def initialize(self):
        self.set_start_date(2015, 1, 1)
        self.set_end_date(2026, 6, 30)  # fenetre figee (reproductibilite, fix D2 #10850)
        self.set_cash(100000)

        # Ajouter les ETFs en resolution quotidienne (suffisant pour mensuel)
        self.symbols = {}
        for ticker in self.TICKERS:
            symbol = self.add_equity(ticker, Resolution.DAILY).symbol
            self.symbols[ticker] = symbol

        # Indicateurs de volatilite (ecart-type 60j des rendements log)
        self.std_indicators = {}
        for ticker, symbol in self.symbols.items():
            # STD sur les log-returns: utiliser StandardDeviation sur les prix
            # On calculera manuellement via RollingWindow
            self.std_indicators[ticker] = self.STD(symbol, self.VOL_LOOKBACK, Resolution.DAILY)

        # Poids cibles actuels
        self.target_weights = {ticker: 1.0 / len(self.TICKERS) for ticker in self.TICKERS}

        # Warmup pour remplir les indicateurs
        self.set_warm_up(self.WARMUP_DAYS)

        # Scheduler: rebalancement mensuel le 1er jour de trading du mois
        self.schedule.on(
            self.date_rules.month_start("SPY"),
            self.time_rules.after_market_open("SPY", 30),
            self.rebalance
        )

        self.log("RiskParity initialized: 5 ETFs, 60-day vol, monthly rebalance")

    def on_data(self, data: Slice):
        """Verifier la derive des poids intra-mensuelle."""
        if self.is_warming_up:
            return

        if not self._indicators_ready():
            return

        # Verifier si les poids actuels ont derive au-dela du seuil
        if self._check_drift():
            self.log(f"Drift threshold exceeded, rebalancing intra-month")
            self._execute_rebalance()

    def rebalance(self):
        """Rebalancement mensuel principal."""
        if self.is_warming_up:
            return

        if not self._indicators_ready():
            self.log("Indicators not ready, skipping rebalance")
            return

        self._compute_target_weights()
        self._execute_rebalance()

    def _indicators_ready(self) -> bool:
        """Verifie que tous les indicateurs sont prets."""
        return all(ind.is_ready for ind in self.std_indicators.values())

    def _compute_target_weights(self):
        """Calcule les poids inversement proportionnels a la volatilite."""
        # Recuperer la volatilite de chaque actif (STD des prix)
        # La STD des prix n'est pas la bonne mesure - on veut la vol des rendements
        # On approxime: vol_rendements ~= STD(prix) / prix_moyen
        # Mais QuantConnect STD donne directement la STD des valeurs brutes
        # Pour avoir la vol des rendements: utiliser le ratio STD/prix actuel

        vols = {}
        for ticker, symbol in self.symbols.items():
            std_val = self.std_indicators[ticker].current.value
            current_price = self.securities[symbol].price

            if current_price > 0 and std_val > 0:
                # Volatilite relative (rendements)
                vols[ticker] = std_val / current_price
            else:
                vols[ticker] = 0.01  # Valeur par defaut si donnee manquante

        # Poids = 1/vol (inverse volatility weighting)
        inv_vols = {}
        for ticker, vol in vols.items():
            if vol > 0:
                inv_vols[ticker] = 1.0 / vol
            else:
                inv_vols[ticker] = 0.0

        total_inv_vol = sum(inv_vols.values())

        if total_inv_vol > 0:
            self.target_weights = {
                ticker: inv_vol / total_inv_vol
                for ticker, inv_vol in inv_vols.items()
            }
        else:
            # Fallback: poids egaux
            n = len(self.TICKERS)
            self.target_weights = {ticker: 1.0 / n for ticker in self.TICKERS}

        # Log les poids calcules
        weight_str = ", ".join(
            f"{t}:{w:.1%}" for t, w in sorted(self.target_weights.items())
        )
        self.log(f"Target weights: {weight_str}")

    def _check_drift(self) -> bool:
        """Verifie si un actif a derive au-dela du seuil par rapport a son poids cible."""
        if not self.portfolio.invested:
            return False

        total_value = self.portfolio.total_portfolio_value
        if total_value <= 0:
            return False

        for ticker, symbol in self.symbols.items():
            target = self.target_weights.get(ticker, 0.0)
            holding = self.portfolio[symbol]
            current_weight = holding.holdings_value / total_value

            if abs(current_weight - target) > self.DRIFT_THRESHOLD:
                return True

        return False

    def _execute_rebalance(self):
        """Execute le rebalancement vers les poids cibles."""
        for ticker, symbol in self.symbols.items():
            weight = self.target_weights.get(ticker, 0.0)
            self.set_holdings(symbol, weight)

        self.log(
            f"Rebalanced | "
            f"Portfolio: ${self.portfolio.total_portfolio_value:,.0f}"
        )

'''
print(f"Code source QC Cloud charge : RiskParity, {len(qc_code)} caracteres, "
      f"{len(qc_code.splitlines())} lignes, {len(['SPY','EFA','GLD','DBC','TLT'])} ETFs")
Code source QC Cloud charge : RiskParity, 6026 caracteres, 163 lignes, 5 ETFs

Architecture de l’algorithme

Composant Méthode Description
Indicateurs STD(60) Volatilite 60j par ETF
Poids cibles _compute_target_weights() 1/vol normalise
Detection derive _check_drift() Seuil 5% vs poids cible
Exécution _execute_rebalance() SetHoldings() vers cibles
Warmup SetWarmUp(70) Remplissage initial indicateurs

NOTE — approximation de volatilité : STD(60) s’applique aux prix bruts, pas aux rendements. La volatilité des rendements est approximée par std_prix / prix_actuel (cf. commentaires dans le code de l’algorithme). La mesure correcte serait la STD des log-rendements ; l’approximation est retenue pour sa simplicité avec l’indicateur QC natif.


Partie 2 : Deploiement via MCP QuantConnect

La cellule suivante ne s’execute pas comme une strategie : elle definit le code source complet dans une chaine qc_code et en verifie le chargement (sortie : « RiskParity, 6026 caracteres, 5 ETFs »). L’entrainement n’a rien a voir ici — c’est le backtest de la strategie complete qui se produit sur QuantConnect Cloud, via le serveur MCP dedie :

  1. Projet QC : le source est charge dans un projet QuantConnect ;
  2. Compilation Cloud (create_compile) : QC compile dans son propre environnement — indicateurs natifs STD, donnees ajustees ;
  3. Backtest (create_backtest puis read_backtest) : execution sur les donnees institutionnelles QC (dividendes, splits) — les metriques commentees en Partie 3 proviennent de cette execution Cloud.

La ligne de partage de la serie Cloud-native s’applique : le notebook enseigne et verifie les briques localement (poids, derive, volatilite), le Cloud execute la strategie entiere sur l’historique reel.

Exercice 1 : Detection de derive des poids

Le Risk Parity rebalance mensuellement mais verifie intra-mensuellement si les poids ont derive au-dela d’un seuil de 5%. Si oui, un rebalancement d’urgence est declenche.

Objectif : Implementez check_drift(target_weights, current_weights, threshold=0.05) qui detecte si un actif a derive au-dela du seuil.

Indices : - Indice : Le poids actuel = (quantite * prix_actuel) / valeur_portefeuille. - Indice : Si |poids_actuel - poids_cible| > seuil, il y a derive.

Methode : un seul passage sur les cinq actifs suffit — la derive est le max des ecarts absolus, compare au seuil. Les valeurs de test sont deja posees dans la cellule suivante : SPY a 0.24 pour une cible de 0.20 (derive de 4 points), les quatre autres restent proches de leurs cibles — la reponse attendue se joue sur la frontiere du seuil.

# Exercice 1 : Detection de derive des poids
# TODO etudiant : implementer check_drift(target, current, threshold=0.05) -> bool
# Indice : derive si |poids_actuel - poids_cible| > threshold pour au moins un actif

def check_drift(target_weights, current_weights, threshold=0.05):
    # TODO etudiant : comparer chaque poids actuel au poids cible
    return None  # TODO etudiant : retourner True si derive detectee

target = {"SPY": 0.20, "EFA": 0.20, "GLD": 0.20, "DBC": 0.20, "TLT": 0.20}
current = {"SPY": 0.24, "EFA": 0.18, "GLD": 0.19, "DBC": 0.21, "TLT": 0.18}
result_drift = None  # TODO etudiant : appeler check_drift
print("Exercice a completer : detection de derive")
Exercice a completer : detection de derive

Exercice 2 : Calcul de la volatilite realisee

La volatilite realisee sur 60 jours est l’indicateur cle du Risk Parity. Elle est calculee comme l’ecart-type des rendements quotidiens sur une fenêtre glissante, annualise par multiplication par sqrt(252).

Objectif : Implementez realized_vol(returns, window=60) qui retourne la volatilite annuelle realisee. Testez avec des rendements simules et comparez différentes fenêtres (21, 60, 126 jours).

Indices : - Indice : Vol_annualisee = std(rendements) * sqrt(252). - Indice : Une fenêtre plus courte reactit plus vite mais est plus bruitee.

Ce qu’on attend d’observer : les trois fenetres (21, 60, 126 jours) renvoient des volatilites du meme ordre — le processus simulate est stationnaire — mais avec une dispersion decroissante quand la fenetre s’allonge : 21 jours reactit a chaque accident du generateur, 126 jours les lisse tous. Le choix 60 jours de l’algorithme est un compromis, pas un optimum.

# Exercice 2 : Calcul de la volatilite realisee
# TODO etudiant : implementer realized_vol(returns, window=60) -> float
# Indice : vol_annualisee = std(rendements_fenetre) * sqrt(252)

import numpy as np

def realized_vol(returns, window=60):
    # TODO etudiant : prendre les derniers window rendements
    # TODO etudiant : calculer ecart-type et annualiser
    return None  # TODO etudiant : retourner la vol annualisee

np.random.seed(42)
sim_returns = np.random.normal(0.0003, 0.01, 500)
result_vol = None  # TODO etudiant : comparer window=21, 60, 126
print("Exercice a completer : volatilite realisee")
Exercice a completer : volatilite realisee

Partie 3 : Résultats et interpretation

Comparaison avec les benchmarks

Stratégie Sharpe CAGR MaxDD
60/40 (SPY/TLT) ~0.6 ~8% ~25%
Risk Parity (sans leverage) ~0.7 ~7% ~18%
Risk Parity (1.5x leverage) ~0.75 ~10% ~25%

Comment lire ces chiffres

  • Risk Parity sans leverage (Sharpe ~0.7, CAGR ~7%) : mieux ajuste du risque que le 60/40 (Sharpe ~0.6) mais rendement plus faible — sans levier, une allocation en grande partie obligations/produits defensifs ne peut pas egaler la course d’un portefeuille a 60% d’actions.
  • Le leverage 1.5x restaure le rendement (~10%) au prix du MaxDD (~25%) : c’est le contrat du Risk Parity reel — emprunter pour ramener le portefeuille defensive a l’agressivite du 60/40, en esperant une meilleure trajectoire ajustee du risque. Le tableau montre la limite du pari : la drawdown remonte au niveau du 60/40.
  • MaxDD ~18% sans leverage : la ligne ou le Risk Parity gagne nettement (-7 points vs 60/40) — la diversification du risque paie surtout dans les stress, ou les correlations montent et le 60/40 se comporte comme un seul grand bloc d’actions.

Points d’attention

  • Sans leverage, le Risk Parity a un rendement inferieur au 60/40
  • La derive des poids necessite une surveillance (trigger 5%)
  • Les correlations entre classes d’actifs changent en periode de stress
  • La fenêtre de volatilite (60j) est un paramètre sensible

Exercice 3 : Simulation du rebalancement Risk Parity

Un portefeuille Risk Parity rebalance mensuellement vers les poids cibles. Entre deux rebalancements, les prix bougent et les poids derivent.

Objectif : Simulez un portefeuille de 5 ETFs sur 12 mois. Recalculez les poids Risk Parity chaque mois en utilisant la volatilite realisee glissante.

Indices : - Indice : Chaque mois, calculez la vol sur 60j, puis les poids 1/vol. - Indice : Les poids changeront legerement chaque mois car la vol change.

Ce qu’on attend d’observer : des poids qui varient de quelques points de mois en mois — la volatilite realisee glissante est elle-meme bruitee — mais sans changement de regime : TLT reste surpondere, DBC sous-pondere, dans un corridor etroit autour des cibles de la Partie 1. Si un mois s’ecarte fortement, cherchez l’accident dans la serie de rendements simulee, pas dans la formule.

# Exercice 3 : Simulation du rebalancement Risk Parity
# TODO etudiant : simuler 12 mois de rebalancement Risk Parity
# Indice : chaque mois, recalculer vol(60j) puis poids 1/vol

import numpy as np

def simulate_rp_rebalance(returns_dict, n_months=12, vol_window=60):
    # returns_dict : {ticker: array de rendements quotidiens}
    # TODO etudiant : boucler sur les mois
    # TODO etudiant : recalculer poids Risk Parity chaque mois
    return None  # TODO etudiant : retourner historique des poids

result_rp_sim = None  # TODO etudiant : lancer la simulation
print("Exercice a completer : simulation rebalancement Risk Parity")
Exercice a completer : simulation rebalancement Risk Parity

Partie 4 : Au-dela de l’Inverse-Vol — ERC et PSR

La méthode Inverse Volatility Weighting (1/σ) que nous avons implementee est souvent appelee “Risk Parity” dans la pratique. Mais strictement parlant, ce n’est qu’une approximation qui ignore les correlations entre actifs.

Inverse-Vol vs ERC (Equal Risk Contribution)

Le vrai Risk Parity cherche a equaliser la contribution marginale au risque de chaque actif, en tenant compte de la matrice de covariance complete :

  • Contribution au risque de l’actif i = w_i × (Σ w)_i / σ_p
  • ERC : chaque actif contribue exactement 1/N du risque total

Inverse-Vol suppose implicitement que les correlations sont nulles ou uniformes. Quand c’est le cas, IVW ≈ ERC. Mais en pratique, les correlations changent — surtout en periode de stress ou les actifs tendent a corriger ensemble.

Probabilistic Sharpe Ratio (PSR)

Le PSR (Bailey & Lopez de Prado, 2014) repond a : “Ce Sharpe ratio est-il statistiquement significatif, ou juste du bruit ?”

Il ajuste le Sharpe observe pour : - La longueur de la periode de test (plus c’est court, plus c’est bruite) - L’asymetrie (skewness) des rendements - L’exces d’aplatissement (kurtosis)

Un PSR > 50% signifie que le Sharpe est plus probablement reel que du au hasard. C’est un garde-fou essentiel contre l’overfitting.

# Demo : Inverse-Vol vs ERC + calcul du PSR
import numpy as np
from scipy.stats import norm

np.random.seed(42)

# Parametres realistes pour 5 ETFs (volatilites annuelles en %)
vols = np.array([16.5, 18.2, 15.8, 22.1, 12.4])  # SPY, EFA, GLD, DBC, TLT
names = ["SPY", "EFA", "GLD", "DBC", "TLT"]
n_assets = len(vols)

# Matrice de correlation (simplifiee mais realiste)
# Actions correlées, or/obligations anti-corrélées
corr = np.array([
    [1.00, 0.85, 0.05, 0.40, -0.30],  # SPY
    [0.85, 1.00, 0.10, 0.35, -0.25],  # EFA
    [0.05, 0.10, 1.00, 0.20,  0.15],  # GLD
    [0.40, 0.35, 0.20, 1.00, -0.10],  # DBC
    [-0.30,-0.25, 0.15,-0.10,  1.00], # TLT
])

# Matrice de covariance annualisee
cov_matrix = np.outer(vols/100, vols/100) * corr

# --- Methode 1 : Inverse-Vol Weighting (notre implementation QC) ---
inv_vols = 1.0 / (vols / 100)
ivw_weights = inv_vols / inv_vols.sum()

# --- Methode 2 : ERC (iteratif, descente de gradient) ---
def compute_erc_weights(cov, max_iter=100, tol=1e-8):
    """Trouve les poids ERC par minimisation iterative."""
    n = cov.shape[0]
    w = np.ones(n) / n  # initialisation equal-weight
    for _ in range(max_iter):
        port_var = w @ cov @ w
        mrc = cov @ w / np.sqrt(port_var)  # marginal risk contribution
        rc = w * mrc  # risk contribution
        target_rc = rc.sum() / n  # egal pour tous
        # Ajustement simple
        grad = rc - target_rc
        w_new = w - 0.1 * grad / np.sum(np.abs(grad))
        w_new = np.maximum(w_new, 0.001)
        w_new = w_new / w_new.sum()
        if np.max(np.abs(w_new - w)) < tol:
            break
        w = w_new
    return w

erc_weights = compute_erc_weights(cov_matrix)

# Comparaison
print("=" * 65)
print("Comparaison : Inverse-Vol vs ERC (Equal Risk Contribution)")
print("=" * 65)
print(f"{'ETF':>5s}  {'Vol%':>6s}  {'IVW':>7s}  {'ERC':>7s}  {'Delta':>7s}")
print("-" * 45)
for i, name in enumerate(names):
    delta = erc_weights[i] - ivw_weights[i]
    print(f"  {name:>4s}  {vols[i]:5.1f}%  {ivw_weights[i]:6.1%}  {erc_weights[i]:6.1%}  {delta:+6.1%}")

# Verification : contribution au risque
port_var_ivw = ivw_weights @ cov_matrix @ ivw_weights
port_var_erc = erc_weights @ cov_matrix @ erc_weights
rc_ivw = ivw_weights * (cov_matrix @ ivw_weights) / port_var_ivw
rc_erc = erc_weights * (cov_matrix @ erc_weights) / port_var_erc

print(f"\nVolatilite portefeuille : IVW = {np.sqrt(port_var_ivw)*100:.1f}%  |  ERC = {np.sqrt(port_var_erc)*100:.1f}%")
print(f"Ecart max poids IVW vs ERC : {np.max(np.abs(ivw_weights - erc_weights)):.1%}")

print(f"\nContribution risque IVW : {[f'{rc:.1%}' for rc in rc_ivw/rc_ivw.sum()]}")
print(f"Contribution risque ERC : {[f'{rc:.1%}' for rc in rc_erc/rc_erc.sum()]}")

# --- Calcul du PSR ---
# Simulons des rendements Risk Parity sur 5 ans (1260 jours)
days = 1260
port_vol = np.sqrt(port_var_ivw)
daily_returns = np.random.normal(0.0001, port_vol/np.sqrt(252), days)
sharpe = np.mean(daily_returns) / np.std(daily_returns) * np.sqrt(252)

# Formule PSR (Bailey & Lopez de Prado 2014)
skew = float(np.mean(((daily_returns - daily_returns.mean()) / daily_returns.std())**3))
kurt = float(np.mean(((daily_returns - daily_returns.mean()) / daily_returns.std())**4))
n = len(daily_returns)
threshold = 0.0  # Sharpe minimum acceptable

psr_numerator = (sharpe - threshold) * np.sqrt(n - 1)
psr_denominator = np.sqrt(1 - skew * sharpe + (kurt - 1) / 4 * sharpe**2)
psr = norm.cdf(psr_numerator / psr_denominator) if psr_denominator > 0 else 0.5

print(f"\n--- Probabilistic Sharpe Ratio (PSR) ---")
print(f"Sharpe observe : {sharpe:.3f}")
print(f"Skewness : {skew:.2f} | Kurtosis : {kurt:.2f}")
print(f"Observations : {n} jours ({n/252:.1f} ans)")
print(f"PSR : {psr:.1%} {'(significatif)' if psr > 0.5 else '(non significatif)'}")
print(f"Reference : Bailey & Lopez de Prado (2014)")
=================================================================
Comparaison : Inverse-Vol vs ERC (Equal Risk Contribution)
=================================================================
  ETF    Vol%      IVW      ERC    Delta
---------------------------------------------
   SPY   16.5%   19.9%   16.0%   -3.9%
   EFA   18.2%   18.0%   13.7%   -4.3%
   GLD   15.8%   20.8%   20.2%   -0.6%
   DBC   22.1%   14.8%   12.2%   -2.6%
   TLT   12.4%   26.5%   37.9%  +11.4%

Volatilite portefeuille : IVW = 9.2%  |  ERC = 8.3%
Ecart max poids IVW vs ERC : 11.4%

Contribution risque IVW : ['25.3%', '25.9%', '19.0%', '23.4%', '6.3%']
Contribution risque ERC : ['17.6%', '17.5%', '22.3%', '18.8%', '23.8%']

--- Probabilistic Sharpe Ratio (PSR) ---
Sharpe observe : 0.888
Skewness : 0.08 | Kurtosis : 3.02
Observations : 1260 jours (5.0 ans)
PSR : 100.0% (significatif)
Reference : Bailey & Lopez de Prado (2014)

Interprétation du résultat : La comparaison montre la différence concrète entre Inverse-Vol (pondère seulement les volatilités individuelles) et ERC (égalise les contributions au risque, corrélations comprises). En ERC, chaque ETF contribue pour ~20 % du risque (17.6 % à 23.8 %), alors qu’en Inverse-Vol TLT ne contribue qu’à 6.3 % malgré 26.5 % de poids.

Pourquoi TLT monte à 37.9 % en ERC (+11.4 pts) : sa corrélation négative avec les actions (SPY : -0.30) réduit sa contribution marginale au risque total — l’algorithme peut donc lui donner plus de poids sans dégrader le risque. C’est l’avantage clé de l’ERC : il récompense la diversification, pas seulement la faible volatilité. Résultat : une volatilité portefeuille réduite (8.3 % vs 9.2 %).

PSR = 100 % : le Sharpe observé (0.888) reste significatif au seuil de Bailey & Lopez de Prado après ajustement pour le moment 3/4 (skewness 0.08, kurtosis 3.02 — quasi gaussien) sur 1260 observations. La performance n’est donc pas un artefact de queues épaisses sur ce jeu de données.


Conclusion

Le Risk Parity est une stratégie fondamentale en gestion quantitative : - Principe : allouer le risque, pas le capital - Implementation : poids proportionnels a 1/volatilite - Avantage : meilleure diversification du risque entre classes d’actifs - Limite : sans leverage, rendement inferieur au 60/40 - Depassement : l’ERC (Partie 4) corrige l’hypothese de correlations nulles de l’Inverse-Vol — TLT monte a 37,9% dans la demo des que sa correlation negative avec les actions est prise en compte

Concept Outil QC Utilisation
Volatilite STD(60) Indicateur QC natif
Poids 1/vol _compute_target_weights() Allocation dynamique
Derive _check_drift() Rebalancement intra-mensuel
Exécution SetHoldings() Rebalancement vers cibles

< Classification de Texte | Suivant : RL DQN >

Retour au sommet