<< Sommaire QC | Précédent : QC-Py-13-Alpha-Models << | Suivant : QC-Py-14b-Liquidity-Execution-Costs >>

QC-Py-14 - Portfolio Construction et Exécution Models

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

Transformer les Insights en positions optimales et les executer efficacement Duree: 90 minutes | Niveau: Intermediaire-Avance | Python + QuantConnect


Objectifs d’Apprentissage

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

  1. Comprendre le rôle du Portfolio Construction Model dans le Framework
  2. Utiliser les modèles EqualWeighting et InsightWeighting
  3. Implementer un MeanVarianceOptimization Portfolio Model
  4. Créer un Custom Portfolio Construction Model (Risk Parity)
  5. Implementer un Multi-Strategy Portfolio Construction (alpha-level allocation)
  6. Maitriser les Exécution Models (Immediate, VWAP)
  7. Implementer un Smart Exécution Model avec contrôle du slippage
  8. Utiliser les Risk Management Models integres et composites
  9. Construire une stratégie Framework complete avec tous les composants

Prerequis

  • Notebooks QC-Py-01 a QC-Py-12 completes
  • Comprehension du Framework QuantConnect (Alpha, Universe Sélection)
  • Notions de base en optimisation de portefeuille

Structure du Notebook

Partie Sujet Duree
1 Rôle du Portfolio Construction Model 10 min
2 EqualWeightingPortfolioConstructionModel 8 min
3 InsightWeightingPortfolioConstructionModel 10 min
4 MeanVarianceOptimizationPortfolioConstructionModel 12 min
5 Custom Portfolio Construction Model (Risk Parity) 15 min
5.5 Multi-Strategy Portfolio Construction (Alpha-Level) 15 min
6 Exécution Models (Immediate, VWAP) 18 min
7 Custom Smart Exécution Model 12 min
8 Risk Management Models 15 min
9 Stratégie Framework Complete 20 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.

[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.


Mode d’emploi : Ce notebook est un document de reference (non executable). Le code QCAlgorithm presente ici est a copier dans main.py de votre projet QuantConnect Lab. Ne tentez pas de faire “Run All” – les imports AlgorithmImports n’existent pas en Jupyter local.

Pour demarrer un projet QC : copiez un main.py depuis projects/ ou partner-course-quant-trading/templates/, puis adaptez-le.



Introduction : Le Framework QuantConnect

Le Framework QuantConnect decompose une stratégie de trading en composants modulaires. Ce notebook se concentre sur les 3 derniers etages de la pipeline :

Pipeline du Framework QuantConnect:

1. Universe Sélection Model
   |
   v
2. Alpha Model (genere des Insights)
   |
   v
3. PORTFOLIO CONSTRUCTION MODEL  <-- Ce notebook
   |  Transforme Insights -> PortfolioTargets
   v
4. RISK MANAGEMENT MODEL         <-- Ce notebook
   |  Ajuste les targets selon les risques
   v
5. EXÉCUTION MODEL               <-- Ce notebook
      Execute les targets (ordres)

Pourquoi separer ces composants ?

Composant Responsabilite Avantage de la separation
Alpha Model Generer des signaux (Insights) Focus sur la prediction
Portfolio Construction Allocation des poids Testable independamment
Risk Management Contraintes de risque Règle de risque reutilisable
Exécution Placement des ordres Optimisation des couts
# Imports QuantConnect
from AlgorithmImports import *

# Imports pour analyse
import numpy as np
import pandas as pd
from datetime import datetime, timedelta

print("Imports reussis")
Imports reussis

Partie 1 : Rôle du Portfolio Construction Model (10 min)

Qu’est-ce qu’un Portfolio Construction Model ?

Le Portfolio Construction Model recoit les Insights generes par l’Alpha Model et les transforme en Portfolio Targets (allocations cibles).

Flux de données

Alpha Model                    Portfolio Construction          Exécution
    |                                  |
    +-- Insight(AAPL, Up, 0.8)  -->   |                         |
    +-- Insight(MSFT, Up, 0.6)  -->   +-- PortfolioTarget(AAPL, 0.40)
    +-- Insight(TSLA, Down, 0.9) -->  +-- PortfolioTarget(MSFT, 0.30)
                                      +-- PortfolioTarget(TSLA, -0.20)
                                            |
                                            v
                                      Ordres d'achat/vente

Proprietes d’un Insight

Propriete Type Description
Symbol Symbol L’actif concerne
Direction InsightDirection Up, Down, ou Flat
Magnitude float Force attendue du mouvement
Confidence float Niveau de confiance (0-1)
Period TimeSpan Duree de validite de l’insight
Weight float Poids optionnel suggere

Proprietes d’un PortfolioTarget

Propriete Type Description
Symbol Symbol L’actif concerne
Quantity decimal Nombre d’actions cibles
Tag string Commentaire optionnel
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Structure de base d'un Portfolio Construction Model
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class BasicPortfolioConstructionModel(PortfolioConstructionModel):
    """
    Template de base pour un Portfolio Construction Model.
    Herite de PortfolioConstructionModel et implemente CreateTargets.
    """
    
    def __init__(self):
        """Initialisation du modele."""
        pass
    
    def CreateTargets(self, algorithm, insights):
        """
        Methode principale : transforme les Insights en PortfolioTargets.
        
        Args:
            algorithm: QCAlgorithm - acces aux donnees et portfolio
            insights: List[Insight] - insights actifs generes par l'Alpha Model
        
        Returns:
            List[PortfolioTarget] - allocations cibles
        """
        targets = []
        
        for insight in insights:
            # Logique de construction de portfolio
            # ...
            pass
        
        return targets
    
    def OnSecuritiesChanged(self, algorithm, changes):
        """
        Appele quand l'univers change.
        Permet de nettoyer les positions sur les titres retires.
        """
        for security in changes.RemovedSecurities:
            # Optionnel: fermer les positions retirees
            pass

print("Template BasicPortfolioConstructionModel defini")
print("\nMethodes principales:")
print("  - __init__(): Initialisation")
print("  - CreateTargets(): Insights -> PortfolioTargets")
print("  - OnSecuritiesChanged(): Gestion des changements d'univers")

Les Portfolio Construction Models integres

QuantConnect fournit plusieurs modèles pre-implementes :

Modèle Description Cas d’usage
EqualWeightingPortfolioConstructionModel Même poids pour tous les insights actifs Simple, pas de biais
InsightWeightingPortfolioConstructionModel Ponderation par magnitude/confidence Exploiter la conviction
MeanVarianceOptimizationPortfolioConstructionModel Optimisation Markowitz Maximum Sharpe
BlackLittermanOptimizationPortfolioConstructionModel Black-Litterman avec views Combine marche + alpha
NullPortfolioConstructionModel Ne fait rien Desactive le composant

Ancre savante – Black, F. & Litterman, R. (1991), Asset Allocation: Combining Investor Views with Market Equilibrium, The Journal of Fixed Income 1(2):7-18. DOI 10.3905/jfi.1991.408013. (Black-Litterman : combine views investisseur + equilibre de marche.)


Partie 2 : EqualWeightingPortfolioConstructionModel (8 min)

Le modèle le plus simple

Le EqualWeightingPortfolioConstructionModel attribue un poids egal a tous les actifs avec des insights actifs. C’est l’equivalent d’un portefeuille equipondere.

Formule

\[w_i = \frac{direction_i}{\sum |direction_j|}\]

Ou : - \(w_i\) = poids de l’actif i - \(direction_i\) = +1 (Up), -1 (Down), 0 (Flat)

Exemple

Avec 4 insights actifs (3 Up, 1 Down) : - AAPL (Up): +25% - MSFT (Up): +25% - GOOGL (Up): +25% - TSLA (Down): -25%

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Utilisation de EqualWeightingPortfolioConstructionModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class EqualWeightExample(QCAlgorithm):
    """
    Exemple d'utilisation du EqualWeightingPortfolioConstructionModel.
    Tous les actifs avec insights actifs ont le meme poids.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter les actifs
        self.AddEquity("SPY", Resolution.Daily)
        self.AddEquity("QQQ", Resolution.Daily)
        self.AddEquity("IWM", Resolution.Daily)
        self.AddEquity("TLT", Resolution.Daily)
        
        # === PORTFOLIO CONSTRUCTION MODEL ===
        # EqualWeighting : tous les insights actifs ont le meme poids
        self.SetPortfolioConstruction(
            EqualWeightingPortfolioConstructionModel()
        )
        
        # Execution immediate
        self.SetExecution(ImmediateExecutionModel())
        
        # Alpha Model simple (RSI)
        self.SetAlpha(RsiAlphaModel())
        
        self.Log("EqualWeightingPortfolioConstructionModel configure")
    
    def OnData(self, data):
        pass

print("EqualWeightExample defini")
print("\nCaracteristiques:")
print("  - Poids identique pour tous les insights actifs")
print("  - Direction Up -> poids positif")
print("  - Direction Down -> poids negatif (short)")
print("  - Simple et sans biais")

Résultat Backtest QC Cloud (Cellule 10) – corrige #3367 : metriques reelles QC Cloud

Metrique Valeur
Statut Completed
Sharpe Ratio -0.111
CAGR 0.071%
Max Drawdown 39.400%
Probabilistic Sharpe Ratio 0.019%

Correctif #3367 : le bloc précédent affichait Sharpe 3.372 / CAGR 89.49% avec End Equity $116993.28 – des valeurs fabriquees et internement contradictoires (une End Equity de $116993.28 sur 10 ans = +1.58% de CAGR, incompatible avec le CAGR de 89.49% affiche). Metriques reelles ci-dessus obtenues par redeploiement de l’algorithme verbatim sur QC Cloud (projet 33072091, 2015-2024, $100k). Les champs Sortino / Total Orders / Trades / Win Rate / Total Fees sont omis car non fournis de maniere fiable par l’API read_backtest (toujours 0 / “-”).

Backtest ID: a764d2faa2e942a0feb3e552da0d3fdc

Ancre savante – Bailey, D.H. & Lopez de Prado, M. (2012), The Sharpe Ratio Efficient Frontier, Journal of Risk 15(2):3-44. DOI 10.21314/JOR.2012.259. (Probabilistic Sharpe Ratio, corrige du biais de l’echantillon non-IID.)

On définit ici le modèle de construction du portefeuille en spécifiant les pondérations cibles et les contraintes d’optimisation.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# EqualWeighting avec options de rebalancement
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class EqualWeightWithRebalance(QCAlgorithm):
    """
    EqualWeighting avec rebalancement periodique.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Actifs
        symbols = ["SPY", "QQQ", "IWM", "TLT", "GLD"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        # Portfolio Construction avec rebalancement hebdomadaire
        self.SetPortfolioConstruction(
            EqualWeightingPortfolioConstructionModel(
                rebalance=Expiry.EndOfWeek  # Rebalancer chaque semaine
            )
        )
        
        # Alternatives de rebalancement:
        # rebalance=Resolution.Daily     # Quotidien
        # rebalance=timedelta(days=30)   # Mensuel
        # rebalance=Expiry.EndOfMonth    # Fin de mois
        # rebalance=lambda time: time.day == 1  # Premier du mois
        
        self.SetExecution(ImmediateExecutionModel())
        self.SetAlpha(ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=7)))
        
        self.Log("EqualWeighting with weekly rebalancing configured")

print("EqualWeightWithRebalance defini")
print("\nOptions de rebalancement:")
print("  - Resolution.Daily")
print("  - Expiry.EndOfWeek")
print("  - timedelta(days=30)")
print("  - Expiry.EndOfMonth")
print("  - Custom lambda function")

Résultat Backtest QC Cloud (Cellule 12) – corrige #3367 : idiome de rebalancement corrige + metriques reelles

Metrique Valeur
Statut Completed
Sharpe Ratio 0.49
CAGR 9.704%
Max Drawdown 25.700%
Probabilistic Sharpe Ratio 11.723%

Correctif #3367 : le code de reference de la cellule précédente utilisait EqualWeightingPortfolioConstructionModel(rebalance=Resolution.Weekly), or Resolution.Weekly n’existe pas (l’enum Resolution ne contient que Tick / Second / Minute / Hour / Daily) – la stratégie echouait a l’initialisation (Runtime Error, cf. correctif #3522). L’idiome est corrige en Expiry.EndOfWeek (rebalancement hebdomadaire aligne sur la fin de semaine). Le backtest aboutit desormais : metriques reelles ci-dessus obtenues par redeploiement de l’algorithme corrige sur QC Cloud (projet 33072122, 2015-2024, $100k). Les champs Sortino / Total Orders / Trades / Win Rate / Total Fees sont omis car non fournis de maniere fiable par l’API read_backtest. Pour memoire, le bloc d’origine affichait Sharpe 1.912 / CAGR 36.493% / End Equity $107938.48 – des valeurs fabriquees et internement contradictoires (End Equity $107,938 sur 10 ans = +0.77% de CAGR, incompatible avec 36.493%).

Backtest ID: 62cb1805751563f907002edd3a9fddde

Interpretation : EqualWeighting avec rebalancement

Le paramètre rebalance contrôle la frequence a laquelle le portfolio est reequilibre :

Options de rebalancement : - Resolution.Daily : Reequilibrage quotidien (plus reactive, plus de couts) - Resolution.Weekly : Hebdomadaire (bon compromis) - Resolution.Monthly : Mensuel (moins de couts, moins reactive) - Expiry.EndOfMonth : Dernier jour du mois - lambda : Fonction custom (ex: lambda time: time.day == 1 pour le 1er de chaque mois)

Impact sur la performance : - Rebalancement frequent : Suit mieux les signaux, mais augmente les couts de transaction - Rebalancement rare : Moins de couts, mais peut rater des changements de regime

Cas d’usage : - Stratégies momentum : Rebalancement mensuel ou hebdomadaire - Stratégies mean reversion : Quotidien ou hebdomadaire - Buy and hold : Pas de rebalancement (ou annuel)


Avantages et limites de EqualWeighting

Avantages Limites
Simple a comprendre Ignore la conviction (confidence)
Pas de biais Ignore la magnitude attendue
Robuste Peut surponderer des signaux faibles
Diversifie naturellement Pas d’optimisation du risque

Exercice 1 : Allocation equal-weight avec rebalancement conditionnel

L’EqualWeightingPortfolioConstructionModel rebalance a intervalle fixe. Une variante intelligente ne rebalance que si l’ecart par rapport aux poids cibles depasse un seuil de tolerance.

Objectif : Implementer un PCM equal-weight qui ne rebalance que si le drift depasse 5%.

Règles : - Poids cible : 1/N pour chaque actif - Ne rebalance que si max(|w_actuel - w_cible|) > 0.05 pour au moins un actif - Loggez chaque decision (rebalance ou skip) avec la raison - Testez avec 4 actifs sur une periode de 1 an

Indices : - # Indice : portfolio[symbol] donne le holding actuel, portfolio.TotalPortfolioValue la valeur totale - # Indice : weight_actuel = holding.Quantity * holding.Price / total_value

# [REFERENCE QC] Exercice 1 : Equal-weight avec rebalancement conditionnel
# TODO etudiant : Implementer un PCM equal-weight avec seuil de drift
# Indice : Comparer poids actuels vs cible (1/N), rebalancer si drift > 5%
# Etape 1 : Definir la classe ConditionalEqualWeightPCM
# Etape 2 : Calculer les poids actuels du portfolio
# Etape 3 : Verifier si le drift depasse le seuil de 5%
# Etape 4 : Creer ou non les targets en fonction du drift

result = None  # TODO etudiant : remplacer par le modele equal-weight conditionnel
print("Exercice a completer")

Partie 3 : InsightWeightingPortfolioConstructionModel (10 min)

Ponderation par l’Insight

Le InsightWeightingPortfolioConstructionModel utilise les proprietes des Insights pour determiner les poids :

  • Magnitude : Force attendue du mouvement
  • Confidence : Niveau de conviction

Formule

\[w_i = \frac{direction_i \times magnitude_i \times confidence_i}{\sum_j |direction_j \times magnitude_j \times confidence_j|}\]

Exemple

Symbol Direction Magnitude Confidence Score Poids
AAPL Up (+1) 0.10 0.80 0.080 40%
MSFT Up (+1) 0.05 0.60 0.030 15%
GOOGL Up (+1) 0.08 0.70 0.056 28%
TSLA Down (-1) 0.06 0.55 -0.033 -17%
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# InsightWeightingPortfolioConstructionModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class InsightWeightExample(QCAlgorithm):
    """
    Utilisation du InsightWeightingPortfolioConstructionModel.
    Ponderation basee sur la magnitude et la confidence des insights.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Actifs
        self.AddEquity("SPY", Resolution.Daily)
        self.AddEquity("QQQ", Resolution.Daily)
        self.AddEquity("IWM", Resolution.Daily)
        self.AddEquity("TLT", Resolution.Daily)
        
        # === PORTFOLIO CONSTRUCTION MODEL ===
        # InsightWeighting : poids proportionnels a magnitude * confidence
        self.SetPortfolioConstruction(
            InsightWeightingPortfolioConstructionModel()
        )
        
        self.SetExecution(ImmediateExecutionModel())
        
        # Alpha Model personnalise qui genere des insights avec magnitude/confidence
        self.SetAlpha(MomentumAlphaModel())
        
        self.Log("InsightWeightingPortfolioConstructionModel configure")

print("InsightWeightExample defini")
print("\nCaracteristiques:")
print("  - Poids = Direction * Magnitude * Confidence")
print("  - Insights avec forte conviction -> poids plus eleves")
print("  - Permet d'exploiter la qualite des signaux")

Résultat Backtest QC Cloud (Cellule 16)

Metrique Valeur
Statut Completed
Sharpe Ratio 0
Sortino Ratio 0
CAGR 0%
Net Profit 0%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0%
End Equity $100000
Total Fees $0
Note InsightWeighting with MomentumAlpha - no trades executed (insights likely not generated)

Backtest ID:

On calcule les pondérations optimales des actifs en minimisant le risque pour un rendement cible donné.

# Alpha Model personnalise avec magnitude et confidence
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MomentumAlphaModel(AlphaModel):
    """
    Alpha Model qui genere des insights avec magnitude et confidence
    basees sur le momentum.
    """
    
    def __init__(self, lookback=20):
        self.lookback = lookback
        self.securities = []
    
    def Update(self, algorithm, data):
        insights = []
        
        for symbol in self.securities:
            if not data.ContainsKey(symbol):
                continue
            
            # Recuperer historique
            history = algorithm.History(symbol, self.lookback, Resolution.Daily)
            if history.empty or len(history) < self.lookback:
                continue
            
            # Calculer momentum
            returns = history['close'].pct_change().dropna()
            momentum = returns.mean()
            volatility = returns.std()
            
            if volatility == 0:
                continue
            
            # Direction basee sur le momentum
            direction = InsightDirection.Up if momentum > 0 else InsightDirection.Down
            
            # Magnitude = momentum absolu (normalise)
            magnitude = min(abs(momentum) * 10, 0.2)  # Cap a 20%
            
            # Confidence = Sharpe ratio normalise (0 a 1)
            sharpe = momentum / volatility if volatility > 0 else 0
            confidence = min(max(abs(sharpe) / 3, 0.1), 1.0)  # Entre 0.1 et 1.0
            
            # Generer l'insight
            insight = Insight.Price(
                symbol,
                timedelta(days=5),
                direction,
                magnitude,
                confidence
            )
            insights.append(insight)
            
            algorithm.Debug(f"{symbol.Value}: mom={momentum:.4f}, dir={direction}, mag={magnitude:.3f}, conf={confidence:.2f}")
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            self.securities.append(security.Symbol)
        for security in changes.RemovedSecurities:
            if security.Symbol in self.securities:
                self.securities.remove(security.Symbol)

print("MomentumAlphaModel defini")
print("\nGenere des insights avec:")
print("  - Direction: basee sur le signe du momentum")
print("  - Magnitude: momentum absolu normalise")
print("  - Confidence: Sharpe ratio normalise")
MomentumAlphaModel definiGenere des insights avec:  - Direction: basee sur le signe du momentum  - Magnitude: momentum absolu normalise  - Confidence: Sharpe ratio normalise

Quand utiliser InsightWeighting ?

Situation Recommandation
Alpha Model genere magnitude/confidence InsightWeighting
Signaux tous equivalents EqualWeighting
Modèle ML avec probabilites InsightWeighting
Signaux discretionnaires EqualWeighting

Partie 4 : MeanVarianceOptimizationPortfolioConstructionModel (12 min)

Optimisation de Markowitz

Le MeanVarianceOptimizationPortfolioConstructionModel utilise la théorie moderne du portefeuille (MPT) pour optimiser l’allocation :

\[\max_w \left( w^T \mu - \frac{\lambda}{2} w^T \Sigma w \right)\]

Ou : - \(w\) = vecteur des poids - \(\mu\) = rendements attendus (derives des insights) - \(\Sigma\) = matrice de covariance - \(\lambda\) = aversion au risque

Options disponibles

Paramètre Options Description
rebalancingPeriod Resolution/Expiry Frequence de rebalancement
portfolioBias Long, Short, LongShort Contrainte de direction
lookback int Jours pour estimer la covariance
resolution Resolution Resolution des données historiques

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, optimisation Mean-Variance) ; Sharpe, W.F. (1966), Mutual Fund Performance, The Journal of Business 39(1):119-138. DOI 10.1086/294846 (ratio de Sharpe / reward-to-variability, objectif Maximum Sharpe).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# MeanVarianceOptimizationPortfolioConstructionModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
from Portfolio.MeanVarianceOptimizationPortfolioConstructionModel import *

class MeanVarianceOptimizationExample(QCAlgorithm):
    """
    Utilisation du MeanVarianceOptimization pour optimiser le portfolio.
    Maximise le Sharpe Ratio sous contraintes.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Actifs diversifies
        symbols = ["SPY", "QQQ", "IWM", "TLT", "GLD", "EFA", "EEM"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        # === MEAN-VARIANCE OPTIMIZATION ===
        self.SetPortfolioConstruction(
            MeanVarianceOptimizationPortfolioConstructionModel(
                rebalancingPeriod=Expiry.EndOfMonth,  # Rebalancement mensuel
                portfolioBias=PortfolioBias.Long,     # Long only
                lookback=60,                           # 60 jours d'historique
                resolution=Resolution.Daily
            )
        )
        
        # Execution
        self.SetExecution(ImmediateExecutionModel())
        
        # Alpha Model simple
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=30))
        )
        
        self.Log("MeanVarianceOptimizationPortfolioConstructionModel configure")
        self.Log("  Rebalancement: Fin de mois")
        self.Log("  Biais: Long only")
        self.Log("  Lookback: 60 jours")

print("MeanVarianceOptimizationExample defini")
print("\nParametres:")
print("  - rebalancingPeriod: Expiry.EndOfMonth")
print("  - portfolioBias: PortfolioBias.Long")
print("  - lookback: 60 jours")

Résultat Backtest QC Cloud (MeanVarianceOptimizationExample) – corrige (#3367, stratégie non executable sur la version Lean actuelle, projet 33048058)

Metrique Valeur
Statut Runtime Error (stratégie non executable sur Lean actuel)

Correctif #3367 : le bloc précédent affichait Net Profit +7.791% / End Equity $107,790.97 / Total Fees $326.83 – mais avec Trades = 0, une contradiction interne (zero trade implique zero fill, zero frais, zero profit). Les valeurs etaient fabriquees.

Verification QC Cloud (3 tentatives, projet 33048058) : la stratégie ne s’execute pas sur la version Lean actuelle. Deux causes : (1) derive d’API – les arguments rebalancingPeriod et portfolioBias du MeanVarianceOptimizationPortfolioConstructionModel ont ete supprimes des versions recentes de Lean ; (2) après correction de l’API, le ConstantAlphaModel emet des Insight sans magnitude, que le modèle MeanVariance refuse au runtime (“does not accept None as Insight.magnitude”). L’obtention de metriques reelles necessiterait de redisigner le modèle Alpha (hors scope #3367, qui corrige les metriques fabriquees sans reinventer les algos). Les valeurs ci-dessus documentent la realite verifiee plutot que d’inventer des chiffres.

On génère les ordres de rééquilibrage nécessaires pour aligner le portefeuille réel sur les pondérations cibles.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Options de PortfolioBias
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
from Portfolio.MeanVarianceOptimizationPortfolioConstructionModel import *

class MeanVarianceLongShort(QCAlgorithm):
    """
    MeanVariance avec positions Long et Short.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        symbols = ["SPY", "QQQ", "IWM", "TLT", "GLD"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        # === LONG-SHORT OPTIMIZATION ===
        self.SetPortfolioConstruction(
            MeanVarianceOptimizationPortfolioConstructionModel(
                rebalancingPeriod=Resolution.Weekly,
                portfolioBias=PortfolioBias.LongShort,  # Autorise les shorts
                lookback=90,
                resolution=Resolution.Daily
            )
        )
        
        self.SetExecution(ImmediateExecutionModel())
        self.SetAlpha(MomentumAlphaModel())
        
        self.Log("MeanVariance Long-Short configure")

print("MeanVarianceLongShort defini")
print("\nOptions PortfolioBias:")
print("  - PortfolioBias.Long: Positions longues uniquement")
print("  - PortfolioBias.Short: Positions courtes uniquement")
print("  - PortfolioBias.LongShort: Les deux")

Résultat Backtest QC Cloud (MeanVarianceLongShort) – corrige (#3367, stratégie non executable sur la version Lean actuelle, projet 33048059)

Metrique Valeur
Statut Runtime Error (stratégie non executable sur Lean actuel)

Correctif #3367 : le bloc précédent affichait Net Profit +12.797% / End Equity $112,797.12 / Total Fees $209.1 – mais avec Trades = 0, une contradiction interne. Les valeurs etaient fabriquees.

Verification QC Cloud (projet 33048059) : même diagnostic que MeanVarianceOptimizationExample ci-dessus – derive d’API Lean (rebalancingPeriod/portfolioBias supprimes) et le MeanVarianceOptimizationPortfolioConstructionModel refuse les Insight sans magnitude. L’obtention de metriques reelles necessiterait de redisigner le modèle Alpha (hors scope #3367). Realite verifiee, pas de valeurs inventees.

Avantages et limites de MeanVariance

Avantages Limites
Optimise le ratio risque/rendement Sensible aux estimations de covariance
Base théorique solide (MPT) Peut generer des positions concentrees
Prend en compte les correlations Plus lent (optimisation numérique)
Configurable (Long/Short) Necessite suffisamment d’historique

Exercice 2 : Optimisation Mean-Variance avec contraintes sectorielles

L’optimisation Mean-Variance classique ne tient pas compte de la concentration sectorielle. On ajoute des contraintes pour limiter l’exposition a un seul secteur.

Objectif : Implementer un PCM Mean-Variance avec contrainte sectorielle max 30% par secteur.

Règles : - Utilisez 5 actifs repartis en 3 secteurs (Technology, Healthcare, Energy) - Contrainte : poids total par secteur <= 30% - Contrainte supplementaire : poids individuel entre 2% et 35% - Affichez les poids optimaux et la allocation par secteur

Indices : - # Indice : Utilisez scipy.optimize.minimize avec des contraintes lineaires - # Indice : Definissez les contraintes comme {'type': 'ineq', 'fun': lambda w: 0.30 - sum(w[tech_indices])}

# [REFERENCE QC] Exercice 2 : Mean-Variance avec contraintes sectorielles
# TODO etudiant : Implementer l'optimisation MV avec limites sectorielles
# Indice : scipy.optimize.minimize + contraintes lineaires par secteur
# Etape 1 : Definir la matrice de covariance et les rendements attendus
# Etape 2 : Definir les contraintes (somme=1, secteurs, bornes individuelles)
# Etape 3 : Resoudre l'optimisation
# Etape 4 : Afficher les poids optimaux et la repartition sectorielle

result = None  # TODO etudiant : remplacer par l'optimisation Mean-Variance contrainte
print("Exercice a completer")

Partie 5 : Custom Portfolio Construction Model - Risk Parity (15 min)

Qu’est-ce que Risk Parity ?

Risk Parity est une approche d’allocation qui egalise la contribution au risque de chaque actif plutot que les montants investis.

Principe

  • Equal Weight : Mêmes montants en dollars -> risque domine par les actifs volatils
  • Risk Parity : Même contribution au risque -> poids inverses a la volatilite

Formule simplifiee (Inverse Volatility)

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

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

Exemple

Actif Volatilite 1/Vol Poids
Actions (SPY) 20% 5 27.8%
Obligations (TLT) 10% 10 55.6%
Or (GLD) 15% 6.67 16.7%

Ancres savantes – Qian, E. (2005), Risk Parity Portfolios: Efficient Portfolios Through True Diversification, PanAgora Asset Management Research (origine praticien du Risk Parity) ; 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 : egaliser la contribution au risque de chaque actif).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Custom Risk Parity Portfolio Construction Model
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
import numpy as np

class RiskParityPortfolioConstruction(PortfolioConstructionModel):
    """
    Portfolio Construction Model basé sur Risk Parity.
    Alloue les poids inversement proportionnels a la volatilite.
    """
    
    def __init__(self, rebalance_period=30, lookback=60):
        """
        Args:
            rebalance_period: Jours entre chaque rebalancement
            lookback: Jours d'historique pour calculer la volatilite
        """
        self.rebalance_period = rebalance_period
        self.lookback = lookback
        self.last_rebalance = datetime.min
    
    def CreateTargets(self, algorithm, insights):
        """
        Cree les PortfolioTargets basés sur Risk Parity.
        
        Args:
            algorithm: QCAlgorithm
            insights: Liste des insights actifs
        
        Returns:
            Liste de PortfolioTarget
        """
        targets = []
        
        # Verifier si c'est le moment de rebalancer
        if (algorithm.Time - self.last_rebalance).days < self.rebalance_period:
            return targets
        
        self.last_rebalance = algorithm.Time
        
        # Filtrer les insights actifs avec direction Up ou Down
        active_insights = [i for i in insights 
                          if i.Direction != InsightDirection.Flat]
        
        if len(active_insights) == 0:
            return targets
        
        # Calculer les volatilites pour chaque symbole
        volatilities = {}
        
        for insight in active_insights:
            symbol = insight.Symbol
            
            # Recuperer historique
            history = algorithm.History(symbol, self.lookback, Resolution.Daily)
            
            if history.empty or len(history) < self.lookback * 0.8:
                continue
            
            try:
                # Calculer returns et volatilite annualisee
                returns = history['close'].pct_change().dropna()
                vol = returns.std() * np.sqrt(252)
                
                if vol > 0:
                    volatilities[symbol] = vol
                    algorithm.Debug(f"{symbol.Value}: Vol = {vol:.2%}")
            except:
                continue
        
        if len(volatilities) == 0:
            return targets
        
        # Calculer poids par volatilite inverse (Risk Parity)
        inv_vols = {s: 1.0 / v for s, v in volatilities.items()}
        total_inv_vol = sum(inv_vols.values())
        
        # Creer les targets
        for insight in active_insights:
            symbol = insight.Symbol
            
            if symbol not in inv_vols:
                continue
            
            # Poids de base (Risk Parity)
            base_weight = inv_vols[symbol] / total_inv_vol
            
            # Ajuster selon la direction de l'insight
            direction = 1 if insight.Direction == InsightDirection.Up else -1
            final_weight = direction * base_weight
            
            # Creer le target
            targets.append(PortfolioTarget(symbol, final_weight))
            algorithm.Debug(f"Target {symbol.Value}: {final_weight:.2%}")
        
        algorithm.Debug(f"Risk Parity: {len(targets)} targets generes")
        return targets
    
    def OnSecuritiesChanged(self, algorithm, changes):
        """Gerer les changements d'univers."""
        for security in changes.RemovedSecurities:
            if security.Invested:
                algorithm.Liquidate(security.Symbol)

print("RiskParityPortfolioConstruction defini")
print("\nCaracteristiques:")
print("  - Poids = 1/Volatilite (normalise)")
print("  - Actifs moins volatils -> poids plus eleves")
print("  - Contribution au risque egale pour tous les actifs")

On implémente la logique d’exécution des ordres en tenant compte des coûts de transaction et de la liquidité.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Utilisation du Risk Parity Portfolio Construction Model
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class RiskParityAlgorithm(QCAlgorithm):
    """
    Strategie utilisant Risk Parity pour l'allocation.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Actifs avec volatilites differentes
        symbols = ["SPY", "QQQ", "TLT", "GLD", "IEF"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        # === RISK PARITY PORTFOLIO CONSTRUCTION ===
        self.SetPortfolioConstruction(
            RiskParityPortfolioConstruction(
                rebalance_period=30,  # Mensuel
                lookback=60            # 60 jours d'historique
            )
        )
        
        self.SetExecution(ImmediateExecutionModel())
        
        # Alpha: Long sur tous les actifs
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=30))
        )
        
        self.Log("Risk Parity Algorithm initialized")

print("RiskParityAlgorithm defini")
print("\nAllocation typique Risk Parity:")
print("  - Obligations (faible vol) -> poids eleves (~40-50%)")
print("  - Or (vol moyenne) -> poids moyens (~15-20%)")
print("  - Actions (haute vol) -> poids faibles (~20-30%)")

Résultats Backtest QC Cloud - RiskParityAlgorithm (2015-2024, $100,000 initial)

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

Le modèle Risk Parity custom s’execute sur la fenêtre complete 2015-2024 sans erreur mais ne genere aucun ordre : dans CreateTargets, le bloc try/except: continue autour du calcul de volatilite (algorithm.History(...) + history['close'].pct_change()) avale silencieusement chaque symbole, donc aucun target actionnable n’est jamais construit. Le caption d’origine (“insights not generated in Q1 2023”) etait doublement faux : la fenêtre est 2015-2024 (2516 dates), et la cause reelle est ce echec silencieux du PCM, non une absence d’insights.

Backtest QC Cloud 2015-2024 (projet 33177683, bt 10708ff1d88e293034cd073a385cee1e). 0 ordre execute, $100,000 inchanges.


Partie 5.5 : Custom Multi-Strategy Portfolio Construction (15 min)

Qu’est-ce que l’allocation multi-stratégies ?

L’allocation multi-stratégies permet de combiner plusieurs Alpha Models et d’allouer le capital a chaque strategy individuellement.

Différence avec Risk Parity

Aspect Risk Parity Multi-Strategy PCM
Niveau d’allocation Par actif (symbol-level) Par strategy (alpha-level)
Critere de poids Volatilite inverse Fixe par le trader
Cas d’usage Diversification des actifs Combinaison de stratégies
Exemple SPY 20%, TLT 50% EMA-Cross 40%, Trend 60%

Pourquoi utiliser Multi-Strategy PCM ?

  1. Combiner des stratégies complementaires : Momentum + Mean Reversion
  2. Contrôler l’exposition par strategy : Limiter le capital alloue a chaque strategy
  3. Créer des meta-stratégies : Portfolio de stratégies automatise
  4. Backtesting granulaire : Analyser la contribution de chaque strategy

Architecture

flowchart TD
    EMA["EMA-Cross Alpha (40% slice)"] --> MS["MultiStrategyPCM<br/>(Capital Router)"]
    TS["TrendStocks Alpha (60% slice)"] --> MS
    MS --> EI["EMA Insights"]
    MS --> TI["Trend Insights"]
    EI --> EW["Equal Weighting<br/>(intra-alpha)"]
    TI --> EW
    EW --> PT["Portfolio Targets"]

Formule d’allocation

Pour chaque strategy \(\alpha\), le capital alloue est :

\[W_{\alpha} = \text{allocation}_{\alpha}\]

Et pour chaque actif \(i\) dans la strategy \(\alpha\) :

\[w_{i,\alpha} = W_{\alpha} \times \frac{direction_i}{\sum |direction_j|}\]

Exemple concret

Allocation : EMA-Cross 40%, TrendStocks 60%

Strategy Capital Slice Insights actifs Poids par insight
EMA-Cross 40% 3 Up (SPY, QQQ, IWM) 13.33% chacun
TrendStocks 60% 2 Up (SPY, QQQ) 30% chacun

Résultat final : - SPY : 13.33% (EMA) + 30% (Trend) = 43.33% - QQQ : 13.33% (EMA) + 30% (Trend) = 43.33% - IWM : 13.33% (EMA) + 0% (Trend) = 13.33%


Partie 6 : Exécution Models (18 min)

Rôle de l’Exécution Model

L’Exécution Model recoit les PortfolioTargets et les transforme en ordres d’achat/vente. Il gere :

  • Le type d’ordre (Market, Limit, Stop)
  • Le timing de l’exécution
  • La gestion du slippage
  • Le splitting des gros ordres

Exécution Models integres

Modèle Description Cas d’usage
ImmediateExecutionModel Ordres Market immediats Simple, liquid markets
VolumeWeightedAveragePriceExecutionModel VWAP exécution Gros ordres, minimiser impact
StandardDeviationExecutionModel Ordres Limit bases sur std Reduire le slippage
NullExecutionModel Ne fait rien Desactive l’exécution
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Utilisation de MultiStrategyPCM avec CompositeAlphaModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MultiStrategyCompositeAlgorithm(QCAlgorithm):
    """
    Strategie composite combinant EMA-Cross et TrendStocks.
    
    Allocation cible:
    - EMA-Cross Alpha : 40% du capital
    - TrendStocks Alpha : 60% du capital
    
    Universe: SPY, QQQ, IWM, TLT, GLD (overlap intentionnel)
    """
    
    def Initialize(self):
        # === CONFIGURATION ===
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # === UNIVERSE ===
        # EMA-Cross universe
        ema_tickers = ["SPY", "QQQ", "IWM"]
        
        # TrendStocks universe (overlap avec EMA-Cross)
        trend_tickers = ["SPY", "QQQ", "TLT", "GLD"]
        
        for ticker in set(ema_tickers + trend_tickers):
            self.AddEquity(ticker, Resolution.Daily)
        
        # === ALPHA MODELS (CompositeAlphaModel) ===
        self.SetAlpha(CompositeAlphaModel(
            EMACrossAlpha(ema_tickers, fast_period=20, slow_period=50),
            TrendStocksAlpha(trend_tickers, ema_fast=20, ema_slow=50, sma_trend=200)
        ))
        
        # === PORTFOLIO CONSTRUCTION (Multi-Strategy) ===
        self.SetPortfolioConstruction(MultiStrategyPCM(
            alpha_allocations={
                "EMACross": 0.40,    # 40% pour EMA-Cross
                "TrendStocks": 0.60,  # 60% pour TrendStocks
            },
            rebalance=timedelta(days=7)  # Rebalancement hebdomadaire
        ))
        
        # === EXECUTION ===
        self.SetExecution(ImmediateExecutionModel())
        
        # === RISK MANAGEMENT ===
        self.SetRiskManagement(MaximumDrawdownPercentPerSecurity(0.10))
        
        self.Log("="*60)
        self.Log("MULTI-STRATEGY COMPOSITE ALGORITHM INITIALIZED")
        self.Log("="*60)
        self.Log("Allocation: EMA-Cross 40%, TrendStocks 60%")
        self.Log("Universe: SPY, QQQ, IWM, TLT, GLD")
        self.Log("Rebalancement: Hebdomadaire")
        self.Log("="*60)


# === ALPHA MODEL 1: EMA-Cross ===
class EMACrossAlpha(AlphaModel):
    """Alpha Model: EMA Crossover (simple momentum)."""
    
    def __init__(self, tickers, fast_period=20, slow_period=50):
        super().__init__()
        self.name = "EMACross"
        self.tickers = tickers
        self.fast_period = fast_period
        self.slow_period = slow_period
        self.symbols = {}
        self.fast_emas = {}
        self.slow_emas = {}
        self._last_month = -1
    
    def Update(self, algorithm, data):
        # Emission mensuelle
        if algorithm.Time.month == self._last_month:
            return []
        self._last_month = algorithm.Time.month
        
        if algorithm.IsWarmingUp:
            return []
        
        insights = []
        period = timedelta(days=31)
        
        for ticker in self.tickers:
            symbol = self.symbols.get(ticker)
            if symbol is None:
                continue
            
            fast = self.fast_emas.get(symbol)
            slow = self.slow_emas.get(symbol)
            
            if fast is None or slow is None:
                continue
            
            if not fast.IsReady or not slow.IsReady:
                continue
            
            # Signal: Fast EMA > Slow EMA -> UP
            if fast.Current.Value > slow.Current.Value:
                insights.append(Insight.Price(
                    symbol, period, InsightDirection.UP,
                    weight=None,  # Pas de weight explicite -> egal intra-strategy
                    source_model=self.name
                ))
            else:
                insights.append(Insight.Price(
                    symbol, period, InsightDirection.FLAT,
                    source_model=self.name
                ))
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            ticker = security.Symbol.Value
            if ticker in self.tickers:
                self.symbols[ticker] = security.Symbol
                self.fast_emas[security.Symbol] = algorithm.EMA(
                    security.Symbol, self.fast_period, Resolution.Daily
                )
                self.slow_emas[security.Symbol] = algorithm.EMA(
                    security.Symbol, self.slow_period, Resolution.Daily
                )


# === ALPHA MODEL 2: TrendStocks ===
class TrendStocksAlpha(AlphaModel):
    """Alpha Model: Trend Following (Price > SMA200 AND EMA20 > EMA50)."""
    
    def __init__(self, tickers, ema_fast=20, ema_slow=50, sma_trend=200):
        super().__init__()
        self.name = "TrendStocks"
        self.tickers = tickers
        self.ema_fast = ema_fast
        self.ema_slow = ema_slow
        self.sma_trend = sma_trend
        self.symbols = {}
        self.fast_emas = {}
        self.slow_emas = {}
        self.sma200s = {}
        self._last_week = -1
    
    def Update(self, algorithm, data):
        # Emission hebdomadaire (plus reactif que EMA-Cross)
        week_number = algorithm.Time.isocalendar()[1]
        if week_number == self._last_week:
            return []
        self._last_week = week_number
        
        if algorithm.IsWarmingUp:
            return []
        
        insights = []
        period = timedelta(days=7)  # Duree plus courte pour rebalancement rapide
        
        for ticker in self.tickers:
            symbol = self.symbols.get(ticker)
            if symbol is None:
                continue
            
            fast = self.fast_emas.get(symbol)
            slow = self.slow_emas.get(symbol)
            sma200 = self.sma200s.get(symbol)
            
            if fast is None or slow is None or sma200 is None:
                continue
            
            if not fast.IsReady or not slow.IsReady or not sma200.IsReady:
                continue
            
            price = algorithm.Securities[symbol].Price
            
            # Signal: Price > SMA200 AND Fast EMA > Slow EMA -> UP
            if price > sma200.Current.Value and fast.Current.Value > slow.Current.Value:
                insights.append(Insight.Price(
                    symbol, period, InsightDirection.UP,
                    weight=None,
                    source_model=self.name
                ))
            else:
                insights.append(Insight.Price(
                    symbol, period, InsightDirection.FLAT,
                    source_model=self.name
                ))
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            ticker = security.Symbol.Value
            if ticker in self.tickers:
                self.symbols[ticker] = security.Symbol
                self.fast_emas[security.Symbol] = algorithm.EMA(
                    security.Symbol, self.ema_fast, Resolution.DAILY
                )
                self.slow_emas[security.Symbol] = algorithm.EMA(
                    security.Symbol, self.ema_slow, Resolution.DAILY
                )
                self.sma200s[security.Symbol] = algorithm.SMA(
                    security.Symbol, self.sma_trend, Resolution.DAILY
                )


print("MultiStrategyCompositeAlgorithm defini")
print("\nArchitecture:")
print("  1. CompositeAlphaModel combine EMACrossAlpha + TrendStocksAlpha")
print("  2. MultiStrategyPCM alloue 40% a EMACross, 60% a TrendStocks")
print("  3. Chaque strategy distribue egalement ses insights")
print("\nExemple d'allocation:")
print("  Si EMACross: SPY/UP, QQQ/UP, IWM/UP")
print("     TrendStocks: SPY/UP, QQQ/UP, TLT/FLAT, GLD/FLAT")
print("  Resultat:")
print("    SPY: 13.33% (40%/3) + 30% (60%/2) = 43.33%")
print("    QQQ: 13.33% (40%/3) + 30% (60%/2) = 43.33%")
print("    IWM: 13.33% (40%/3) + 0% = 13.33%")
print("    TLT: 0% + 0% = 0%")
print("    GLD: 0% + 0% = 0%")

Résultats Backtest QC Cloud - MultiStrategyCompositeAlgorithm (2015-2024, $100,000 initial)

Runtime Error (backtest QC Cloud, bt 0d2d674a75f9874d5747dfee7c3b87e5). La stratégie de reference combine EMACrossAlpha + TrendStocksAlpha via CompositeAlphaModel, MultiStrategyPCM 40/60, rebalancement hebdomadaire, puis Runtime Error a 2015-04-01 : Insight.Price(symbol, period, InsightDirection.UP, weight=None, source_model=self.name) leve une TypeError - les alpha models du notebook utilisent des kwargs INVALIDES (weight=, source_model=). L’API QC est Insight.Price(symbol, period, direction, magnitude=, conviction=, source=). Bug reel dans le code de reference (cellule EMACrossAlpha/TrendStocksAlpha), non un artifice. Le caption d’origine (“warmup period exceeds Q1 2023”) etait faux : la fenêtre est 2015-2024 (2516 dates) et l’arret vient de cette TypeError, non d’un warmup.. Le backtest s’arrete prematurement ; les statistiques partielles sur la periode anterieure a l’erreur ne sont pas un résultat 10y significatif et ne sont pas committees (G.2 : aucune metrique non-verifiable). La table fabrique d’origine (“Q1 2023”, Sharpe 0, 0 trade) est supprimee.

Backtest QC Cloud 2015-2024 (projet 33177683, bt 0d2d674a75f9874d5747dfee7c3b87e5).

Interpretation : Multi-Strategy Composite Algorithm

Cet exemple complet montre comment combiner deux Alpha Models avec allocation au niveau des stratégies via MultiStrategyPCM.

Architecture du composite :

┌─────────────────────────────────────────────────────────────┐
│                    COMPOSITE ALPHA MODEL                    │
├──────────────────────┬──────────────────────────────────────┤
│   EMACrossAlpha      │      TrendStocksAlpha                 │
│   (40% allocation)   │      (60% allocation)                │
├──────────────────────┼──────────────────────────────────────┤
│ • Mensuel            │ • Hebdomadaire                       │
│ • EMA20 > EMA50      │ • Price > SMA200 AND EMA20 > EMA50   │
│ • Universe: 3 stocks │ • Universe: 4 stocks                 │
└──────────┬───────────┴──────────────┬───────────────────────┘
           │                          │
           └──────────┬───────────────┘
                      │
                      v
          ┌───────────────────────┐
          │   MultiStrategyPCM    │
          │  (Capital Router)     │
          │  EMA: 40%, Trend: 60% │
          └───────────┬───────────┘
                      │
                      v
          ┌───────────────────────┐
          │   Equal Weighting     │
          │  (intra-strategy)     │
          └───────────┬───────────┘
                      │
                      v
              Portfolio Targets

Points clés de l’implémentation :

  1. CompositeAlphaModel : Combine les deux Alpha Models
    • Chaque Alpha génère ses insights indépendamment
    • Les insights sont tagués avec source_model
  2. MultiStrategyPCM : Alloue le capital par stratégie
    • alpha_allocations : Dict {“EMACross”: 0.40, “TrendStocks”: 0.60}
    • Vérifie que la somme = 100%
    • Distribue également au sein de chaque stratégie
  3. Overlap intentionnel : SPY et QQQ apparaissent dans les deux univers
    • EMA-Cross : SPY, QQQ, IWM (3 stocks)
    • TrendStocks : SPY, QQQ, TLT, GLD (4 stocks)
    • Résultat : SPY et QQQ reçoivent plus de poids (agrégation)

Exemple d’allocation calculée :

État : EMA-Cross génère 3 insights UP, TrendStocks génère 2 insights UP

EMA-Cross (40%):
  - SPY: 40% / 3 = 13.33%
  - QQQ: 40% / 3 = 13.33%
  - IWM: 40% / 3 = 13.33%

TrendStocks (60%):
  - SPY: 60% / 2 = 30%
  - QQQ: 60% / 2 = 30%
  - TLT: 0% (FLAT)
  - GLD: 0% (FLAT)

Résultat final:
  - SPY: 13.33% + 30% = 43.33%
  - QQQ: 13.33% + 30% = 43.33%
  - IWM: 13.33%
  - TLT: 0%
  - GLD: 0%

Avantages de cette approche : - Combinaison de temporalités : Mensuel + Hebdomadaire - Diversification des signaux : Momentum simple + Trend confirmé - Flexibilité : Facile d’ajuster les allocations



Exercice 3 : Risk Parity manuel avec contraintes

Le Risk Parity attribue un poids inversement proportionnel a la volatilite de chaque actif. En pratique, on ajoute des contraintes (poids min/max, concentration max).

Objectif : Implementer un modèle Risk Parity avec contraintes dans un algorithme QCAlgorithm.

Règles : - Créez un custom PortfolioConstructionModel qui calcule les poids Risk Parity - Contraintes : poids minimum 5%, poids maximum 40%, somme = 100% - Utilisez la volatilite sur 63 jours (3 mois) comme mesure de risque - Comparez avec l’EqualWeightingPortfolioConstructionModel sur 3 actifs

Indices : - # Indice : Poids Risk Parity = (1/vol_i) / sum(1/vol_j) puis clippez entre 0.05 et 0.40 - # Indice : self.Securities[symbol].VolatilityModel.Volatility pour obtenir la volatilite

# [REFERENCE QC] Exercice 3 : Risk Parity avec contraintes
# TODO etudiant : Implementer un PCM Risk Parity avec poids min/max
# Indice : 1/vol normalise, puis clip [0.05, 0.40], renormaliser
# Etape 1 : Definir la classe CustomRiskParityPCM heritant de PortfolioConstructionModel
# Etape 2 : Calculer les poids bruts (inverse volatilite)
# Etape 3 : Appliquer les contraintes min/max et renormaliser
# Etape 4 : Creer les targets de portfolio pour chaque insight

result = None  # TODO etudiant : remplacer par le modele Risk Parity contraint
print("Exercice a completer")
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# MultiStrategyPCM - Portfolio Construction Model multi-strategies
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MultiStrategyPCM(PortfolioConstructionModel):
    """
    Portfolio Construction Model pour allocation multi-strategies.
    
    Alloue une tranche de capital fixe a chaque Alpha Model (strategy),
    puis distribue egalement les insights au sein de chaque strategy.
    
    Exemple: EMA-Cross 40%, TrendStocks 60%
             -> Chaque insight EMA recoit 40% / N_ema
             -> Chaque insight Trend recoit 60% / N_trend
    """
    
    def __init__(self, alpha_allocations, rebalance=timedelta(days=7)):
        """
        Args:
            alpha_allocations: Dict[str, float] - Allocation par strategy
                {"EMACross": 0.40, "TrendStocks": 0.60}
            rebalance: timedelta - Frequence de rebalancement
        """
        super().__init__()
        self.alpha_allocations = alpha_allocations
        self.set_rebalancing_func(lambda dt: dt + rebalance)
        
        # Verification que la somme = 100%
        total = sum(alpha_allocations.values())
        if not (0.99 <= total <= 1.01):
            raise ValueError(f"Allocations must sum to 1.0, got {total}")
    
    def determine_target_percent(self, active_insights):
        """
        Determine les poids cibles pour chaque insight.
        
        Cette methode est appelee automatiquement par le Framework
        a chaque rebalancement.
        
        Args:
            active_insights: List[Insight] - Insights actifs de tous les Alpha Models
        
        Returns:
            Dict[Insight, float] - Mapping insight -> poids cible
        """
        result = {}
        
        # Grouper les insights par Alpha Model (source_model)
        by_alpha = {}
        for insight in active_insights:
            source = insight.source_model or "Unknown"
            if source not in by_alpha:
                by_alpha[source] = []
            by_alpha[source].append(insight)
        
        # Pour chaque strategy, allouer sa tranche de capital
        for alpha_name, insights in by_alpha.items():
            capital_slice = self.alpha_allocations.get(alpha_name, 0)
            
            if capital_slice == 0:
                continue
            
            # Filtrer les insights actifs (non-Flat)
            active = [i for i in insights if i.direction != InsightDirection.FLAT]
            
            if len(active) == 0:
                continue
            
            # Verifier si les insights ont des poids explicites
            has_weights = any(i.weight is not None and i.weight > 0 for i in active)
            
            if has_weights:
                # Allocation ponderee par les weights des insights
                total_weight = sum(i.weight or 0 for i in active)
                for insight in active:
                    weight_fraction = (insight.weight or 0) / total_weight
                    result[insight] = insight.direction * weight_fraction * capital_slice
            else:
                # Allocation egale entre les insights de cette strategy
                per_symbol = capital_slice / len(active)
                for insight in active:
                    result[insight] = insight.direction * per_symbol
        
        return result
    
    def OnSecuritiesChanged(self, algorithm, changes):
        """Gerer les changements d'univers."""
        for security in changes.RemovedSecurities:
            if security.Invested:
                algorithm.Liquidate(security.Symbol)


print("MultiStrategyPCM defini")
print("\nCaracteristiques:")
print("  - Allocation par strategy (alpha-level)")
print("  - Distribution egale intra-strategy")
print("  - Supporte les insights avec ou sans weights")
print("  - Verifie que la somme des allocations = 100%")

Interpretation : MultiStrategyPCM

Le Multi-Strategy Portfolio Construction Model permet d’allouer le capital au niveau des stratégies (alpha-level) plutôt qu’au niveau des actifs individuels.

Architecture à deux niveaux : 1. Inter-strategy : Allocation fixe entre les Alpha Models (ex: EMA 40%, Trend 60%) 2. Intra-strategy : Distribution égale des insights au sein de chaque stratégie

Formule d’allocation :

Poids_final(actif_i, strategy_α) = Allocation_α × (1 / N_insights_α)

Exemple concret : - EMA-Cross (40%) : 3 insights actifs → 13.33% par insight - TrendStocks (60%) : 2 insights actifs → 30% par insight - SPY reçoit : 13.33% + 30% = 43.33%

Avantages : - Combinaison de stratégies complémentaires - Contrôle précis de l’exposition par stratégie - Backtesting granulaire par stratégie

Cas d’usage : - Stratégies multi-Alpha (composite) - Allocation tactique entre approches - Meta-stratégies (portefeuille de stratégies)


Étude de Cas : Composites Réussis vs Échoués

L’analyse des projets QuantConnect révèle des patterns critiques pour les composites de stratégies.

Le problème fondamental : manque d’orthogonalité

Composite échoué : MomentumSector + RegimeSwitching (Sharpe 0.144)

Aspect Détail
Univers partagé SPY, IEF, GLD (les deux stratégies)
Résultat Pas de diversification réelle
Problème RegimeSwitching défensif + Momentum agressif = faible Sharpe

Composite réussi : FamaFrench + AllWeather (Sharpe 0.588)

Aspect Détail
Univers distincts Factor ETFs (VLUE, MTUM…) vs Traditionnels (SPY, IEF, GLD)
Résultat Diversification réelle
Succès Exposition factorielle + allocation macro

Tableau comparatif

Composite Universes Sharpe Réussite ?
Momentum + Regime SPY/QQQ/IEF/GLD (partagé) 0.144 ❌ Non
FamaFrench + AllWeather Factor ETFs + Traditionnels 0.588 ✅ Oui
EMA + Trend (TP6) SPY/QQQ/IWM (partagé) 0.867 ✅ Oui*

*Le composite EMA+Trend fonctionne car les stratégies ont des logiques temporelles différentes (mensuel vs hebdomadaire).

Leçon clé

Pour qu’un composite soit réussi, les composantes doivent être orthogonales :

  1. Univers différents : Actifs distincts ou classes d’actifs différentes
  2. Logiques différentes : Momentum vs Mean Reversion, ou temporalités différentes
  3. Pas de redondance : Chaque stratégie apporte quelque chose d’unique

Check-list pour créer un composite réussi


Deux Execution Models integres : Immediate et VWAP

Les deux blocs [REFERENCE QC] ci-dessous mettent cote a cote les extremites du spectre d’execution, a copier dans l’IDE QuantConnect :

  • ImmediateExecutionModel — le plus simple : chaque cible de portefeuille est executee immediatement par ordre Market, sans decoupage (univers SPY/QQQ/IWM/TLT en Resolution.Daily) ;
  • VolumeWeightedAveragePriceExecutionModel — l’ordre est decoupe sur la journee pour suivre le prix moyen pondere par les volumes ; il exige des donnees Resolution.Minute (le second bloc passe SPY/QQQ/IWM en Minute pour cette raison).

Le compromis est celui de toute l’execution : certitude et simplicite du fill immediat contre impact de marche reduit par l’etalement.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# ImmediateExecutionModel - Le plus simple
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class ImmediateExecutionExample(QCAlgorithm):
    """
    Utilisation du ImmediateExecutionModel.
    Execute immediatement avec des ordres Market.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        symbols = ["SPY", "QQQ", "IWM", "TLT"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # === IMMEDIATE EXECUTION MODEL ===
        # Execute immediatement tous les targets avec des Market Orders
        self.SetExecution(ImmediateExecutionModel())
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=7))
        )
        
        self.Log("ImmediateExecutionModel configure")

print("ImmediateExecutionExample defini")
print("\nCaracteristiques ImmediateExecutionModel:")
print("  - Ordres Market executes immediatement")
print("  - Simple et rapide")
print("  - Pas de controle sur le prix d'execution")
print("  - Peut subir du slippage sur gros volumes")
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# VolumeWeightedAveragePriceExecutionModel (VWAP)
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class VWAPExecutionExample(QCAlgorithm):
    """
    Utilisation du VolumeWeightedAveragePriceExecutionModel.
    Split les ordres sur la journee pour suivre le VWAP.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        symbols = ["SPY", "QQQ", "IWM"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Minute)  # Resolution Minute requise
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # === VWAP EXECUTION MODEL ===
        # Split les ordres sur la periode pour suivre le VWAP
        self.SetExecution(
            VolumeWeightedAveragePriceExecutionModel()
        )
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=7))
        )
        
        self.Log("VolumeWeightedAveragePriceExecutionModel configure")

print("VWAPExecutionExample defini")
print("\nCaracteristiques VWAPExecutionModel:")
print("  - Decoupe les gros ordres en petites tranches")
print("  - Execute proportionnellement au volume du marche")
print("  - Minimise l'impact de marche")
print("  - Necessite Resolution.Minute ou plus fin")

Résultat Backtest QC Cloud (Cellule 40) – corrige #3367 : metriques reelles QC Cloud

Metrique Valeur
Statut Completed
Sharpe Ratio 0.498
CAGR 13.017%
Max Drawdown 34.200%
Probabilistic Sharpe Ratio 6.881%

Correctif #3367 : le bloc précédent affichait Sharpe 1.56 / CAGR 43.447% avec End Equity $109263.57 – des valeurs fabriquees et internement contradictoires (une End Equity de $109263.57 sur 10 ans = +0.89% de CAGR, incompatible avec le CAGR de 43.447% affiche). Metriques reelles ci-dessus obtenues par redeploiement de l’algorithme verbatim sur QC Cloud (projet 33072197, 2015-2024, $100k). Les champs Sortino / Total Orders / Trades / Win Rate / Total Fees sont omis car non fournis de maniere fiable par l’API read_backtest (toujours 0 / “-”).

Backtest ID: ca4aff2968957c672d2629e2ef152e38

Interpretation : VWAP Exécution Model

Le Volume-Weighted Average Price (VWAP) Exécution Model est conçu pour minimiser l’impact de marché lors de l’exécution de gros ordres.

Fonctionnement : 1. Découpage temporel : L’ordre est fractionné sur la période d’exécution 2. Profil volumétrique : Chaque tranche est exécutée proportionnellement au volume du marché 3. Lissage : Évite les pics de volume qui déplaceraient le prix

Avantages : - Réduit l’impact sur le prix (slippage) - Exécution proche du prix moyen de la journée - Idéal pour les ordres de taille importante

Inconvénients : - Nécessite une résolution fine (Minute ou Hour) - Plus lent que l’exécution immédiate - Peut manquer des opportunités de prix intraday

Cas d’usage : - Grosses positions (plus de 1% du volume quotidien) - ETFs liquides (SPY, QQQ) avec volumes élevés - Stratégies qui minimisent les coûts de transaction

Comparaison : - Immediate : Rapide mais impact élevé - VWAP : Plus lent mais impact faible - StdDev : Contrôle du prix via limit orders


On évalue les écarts de suivi (tracking error) entre le portefeuille construit et le portefeuille cible théorique.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# StandardDeviationExecutionModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class StdDevExecutionExample(QCAlgorithm):
    """
    Utilisation du StandardDeviationExecutionModel.
    Place des ordres Limit bases sur les deviations standard.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        symbols = ["SPY", "QQQ", "IWM"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Minute)
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # === STANDARD DEVIATION EXECUTION ===
        # Place des Limit Orders a price +/- N*std
        self.SetExecution(
            StandardDeviationExecutionModel(
                period=60,                # 60 periodes pour calculer std
                deviations=2              # 2 deviations standard
            )
        )
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=7))
        )
        
        self.Log("StandardDeviationExecutionModel configure")

print("StdDevExecutionExample defini")
print("\nCaracteristiques StandardDeviationExecutionModel:")
print("  - Place des Limit Orders")
print("  - Prix Limit = Price - N * StdDev (pour achats)")
print("  - Attend des conditions favorables")
print("  - Peut ne pas etre execute si le marche s'eloigne")

Résultat Backtest QC Cloud (Cellule 42) – corrige #3367 : metriques reelles QC Cloud

Metrique Valeur
Statut Completed
Sharpe Ratio 0.502
CAGR 13.112%
Max Drawdown 34.100%
Probabilistic Sharpe Ratio 7.067%

Correctif #3367 : le bloc précédent affichait Sharpe 1.944 / CAGR 53.066% avec End Equity $111018.75 – des valeurs fabriquees et internement contradictoires (une End Equity de $111018.75 sur 10 ans = +1.05% de CAGR, incompatible avec le CAGR de 53.066% affiche). Metriques reelles ci-dessus obtenues par redeploiement de l’algorithme verbatim sur QC Cloud (projet 33072217, 2015-2024, $100k). Les champs Sortino / Total Orders / Trades / Win Rate / Total Fees sont omis car non fournis de maniere fiable par l’API read_backtest (toujours 0 / “-”).

Backtest ID: 7208ae3f65e7bcfaf627a5b989e55d45

Interpretation : StandardDeviation Exécution Model

Le StandardDeviation Exécution Model utilise des Limit Orders avec des prix basés sur les écarts types pour contrôler le slippage.

Mécanisme : 1. Calcul de la volatilité : StdDev sur period barres (60 par défaut) 2. Prix limite : - Achat : Prix - N × StdDev (ex: prix actuel - 2σ) - Vente : Prix + N × StdDev (ex: prix actuel + 2σ) 3. Attente d’exécution : L’ordre reste jusqu’à ce que le prix atteigne la limite

Paramètres clés : - period : Nombre de barres pour calculer la volatilité (60 par défaut) - deviations : Nombre d’écarts types pour le prix limite (2 par défaut)

Avantages : - Contrôle précis du prix d’exécution - Réduit le slippage négatif - S’adapte automatiquement à la volatilité du marché

Inconvénients : - Risque de non-exécution si le marché s’éloigne - Plus lent que les Market Orders - Nécessite une résolution fine (Minute)

Cas d’usage : - Marchés volatils où le slippage est coûteux - Stratégies sensibles au prix d’exécution - Actifs avec volatilité variable

Comparaison rapide :

Modèle Type d’ordre Vitesse Risque non-exécution
Immediate Market Immédiate Nul
VWAP Market (slicé) Progressif Faible
StdDev Limit Variable Élevé

Comparaison des Exécution Models

Modèle Vitesse Contrôle Prix Impact Marche Complexite
Immediate Rapide Aucun Eleve Simple
VWAP Lent Moyen Faible Moyenne
StdDev Variable Eleve Faible Moyenne

Partie 7 : Custom Smart Exécution Model (12 min)

Objectif

Créer un Exécution Model personnalise qui :

  1. Verifie le spread avant d’executer
  2. Utilise Market Orders si le spread est acceptable
  3. Utilise Limit Orders si le spread est trop large
  4. Contrôle le slippage maximum
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Smart Execution Model avec controle du spread
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class SmartExecutionModel(ExecutionModel):
    """
    Execution Model intelligent qui adapte le type d'ordre
    selon le spread bid-ask.
    """
    
    def __init__(self, max_slippage_percent=0.001):
        """
        Args:
            max_slippage_percent: Slippage maximum acceptable (0.001 = 0.1%)
        """
        self.max_slippage = max_slippage_percent
        self.targets_collection = PortfolioTargetCollection()
    
    def Execute(self, algorithm, targets):
        """
        Execute les PortfolioTargets.
        
        Args:
            algorithm: QCAlgorithm
            targets: IEnumerable[PortfolioTarget]
        """
        # Mettre a jour la collection de targets
        self.targets_collection.AddRange(targets)
        
        for target in self.targets_collection:
            symbol = target.Symbol
            
            # Verifier que le security existe
            if symbol not in algorithm.Securities:
                continue
            
            security = algorithm.Securities[symbol]
            
            # Calculer la quantite a trader
            current_qty = algorithm.Portfolio[symbol].Quantity
            
            # Calculer target quantity depuis le poids
            target_value = algorithm.Portfolio.TotalPortfolioValue * target.Quantity
            target_qty = int(target_value / security.Price) if security.Price > 0 else 0
            
            delta = target_qty - current_qty
            
            # Ne rien faire si pas de changement significatif
            if abs(delta) < 1:
                continue
            
            # Calculer le spread
            bid_price = security.BidPrice
            ask_price = security.AskPrice
            
            if bid_price <= 0 or ask_price <= 0:
                # Pas de quote disponible -> Market Order
                algorithm.MarketOrder(symbol, delta)
                algorithm.Debug(f"{symbol.Value}: Market Order (no quotes) - {delta} shares")
                continue
            
            spread = ask_price - bid_price
            spread_pct = spread / security.Price
            
            algorithm.Debug(f"{symbol.Value}: Spread = {spread_pct:.4%} (max: {self.max_slippage:.4%})")
            
            # Decision: Market ou Limit?
            if spread_pct < self.max_slippage * 2:
                # Spread acceptable -> Market Order
                algorithm.MarketOrder(symbol, delta)
                algorithm.Debug(f"{symbol.Value}: Market Order - {delta} shares")
            else:
                # Spread trop large -> Limit Order
                if delta > 0:  # Achat
                    limit_price = bid_price + spread * 0.3  # Bid + 30% du spread
                else:  # Vente
                    limit_price = ask_price - spread * 0.3  # Ask - 30% du spread
                
                algorithm.LimitOrder(symbol, delta, limit_price)
                algorithm.Debug(f"{symbol.Value}: Limit Order @ ${limit_price:.2f} - {delta} shares")
        
        # Nettoyer les targets executes
        self.targets_collection.ClearFulfilled(algorithm)

print("SmartExecutionModel defini")
print("\nLogique:")
print("  1. Calcule le spread bid-ask")
print("  2. Si spread < 2x max_slippage -> Market Order")
print("  3. Sinon -> Limit Order a prix avantageux")
print("  4. Reduit le slippage sur marches peu liquides")

On finalise la stratégie de construction de portefeuille en intégrant les contraintes réglementaires et les limites de position.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Utilisation du SmartExecutionModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class SmartExecutionAlgorithm(QCAlgorithm):
    """
    Algorithme utilisant le SmartExecutionModel.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Actifs (certains plus liquides que d'autres)
        symbols = ["SPY", "QQQ", "IWM", "VNQ", "EEM"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Minute)
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # === SMART EXECUTION MODEL ===
        self.SetExecution(
            SmartExecutionModel(max_slippage_percent=0.001)  # Max 0.1% slippage
        )
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=7))
        )
        
        self.Log("SmartExecutionAlgorithm initialized")
        self.Log("  Max slippage: 0.1%")

print("SmartExecutionAlgorithm defini")
print("\nAvantages:")
print("  - Adapte le type d'ordre au marche")
print("  - Reduit les couts de transaction")
print("  - Plus efficace sur actifs moins liquides")

Résultats Backtest QC Cloud - SmartExecutionAlgorithm (2015-2024, $100,000 initial)

Runtime Error (backtest QC Cloud, bt d068e02425474a84268c99d6fd989816). La stratégie de reference utilise SmartExecutionModel (spread-aware) + EqualWeightingPCM + ConstantAlphaModel sur SPY/QQQ/IWM/VNQ/EEM en resolution Minute, puis Runtime Error a ~81% du backtest (vers 2022). Le caption d’origine (“120920 orders, 0 filled trades, Q1 2023”) n’est PAS reproduit par l’exécution verbatim : la fenêtre est 2015-2024 complete (2516 dates, non Q1 2023) et le backtest tombe en erreur avant la fin. La ligne/message d’erreur exact n’est pas remonte par l’API read_backtest pour cette erreur tardive (visible sur la page de résultats QC Cloud, bt d068e024). Note : le chiffre 120920 ordres du caption d’origine n’a pas pu etre reproduit - signale au coordinateur (ai-01) qui avait marque cell 63 comme nuance (120920 suppose reel).. Le backtest s’arrete prematurement ; les statistiques partielles sur la periode anterieure a l’erreur ne sont pas un résultat 10y significatif et ne sont pas committees (G.2 : aucune metrique non-verifiable). La table fabrique d’origine (“Q1 2023”, Sharpe 0, 0 trade) est supprimee.

Backtest QC Cloud 2015-2024 (projet 33177683, bt d068e02425474a84268c99d6fd989816).

Interpretation : SmartExecutionModel

Ce modèle d’exécution intelligent adapte le type d’ordre en fonction des conditions de marché :

Logique de décision : 1. Calcul du spread : spread_pct = (ask - bid) / price 2. Seuil de décision : 2 × max_slippage (0.2% par défaut) 3. Market Order : Si spread < seuil (marche liquide) 4. Limit Order : Si spread ≥ seuil (marche peu liquide)

Avantages : - Réduit le slippage sur les actifs moins liquides - Adapte automatiquement la stratégie d’exécution - Équilibre vitesse (Market) et prix (Limit)

Cas d’usage : - ETFs liquides (SPY, QQQ) → généralement Market Order - Small caps ou ETFs sectoriels → souvent Limit Order - Marchés volatils → Limit Orders plus fréquents



Partie 8 : Risk Management Models (15 min)

Rôle du Risk Management Model

Le Risk Management Model intercepte les PortfolioTargets et les ajuste selon des règles de risque. Il peut :

  • Reduire les positions qui depassent un seuil
  • Liquider les positions avec trop de pertes
  • Limiter l’exposition par secteur

Risk Models integres

Modèle Description
MaximumDrawdownPercentPerSecurity Liquide si drawdown > X% par position
MaximumUnrealizedProfitPercentPerSecurity Take profit si gain > X%
MaximumSectorExposureRiskManagementModel Limite l’exposition par secteur
TrailingStopRiskManagementModel Trailing stop automatique
NullRiskManagementModel Desactive le risk management
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# MaximumDrawdownPercentPerSecurity
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class DrawdownRiskExample(QCAlgorithm):
    """
    Exemple avec MaximumDrawdownPercentPerSecurity.
    Liquide automatiquement si perte > 5% sur une position.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        symbols = ["SPY", "QQQ", "AAPL", "MSFT", "TSLA"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        
        # === RISK MANAGEMENT: MAX DRAWDOWN 5% ===
        self.SetRiskManagement(
            MaximumDrawdownPercentPerSecurity(0.05)  # Liquide si perte > 5%
        )
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=30))
        )
        
        self.Log("MaximumDrawdownPercentPerSecurity(0.05) configure")

print("DrawdownRiskExample defini")
print("\nFonctionnement:")
print("  - Surveille le drawdown de chaque position")
print("  - Si une position perd > 5% depuis son max -> liquidation")
print("  - Protection automatique contre les grosses pertes")

Résultat Backtest QC Cloud (Cellule 51) – corrige #3367 : metriques reelles QC Cloud

Metrique Valeur
Statut Completed
Sharpe Ratio 0.859
CAGR 25.928%
Max Drawdown 37.600%
Probabilistic Sharpe Ratio 32.962%

Correctif #3367 : le bloc précédent affichait Sharpe 5.664 / CAGR 215.072% avec End Equity $132551.14 – des valeurs fabriquees et internement contradictoires (une End Equity de $132551.14 sur 10 ans = +2.86% de CAGR, incompatible avec le CAGR de 215.072% affiche). Metriques reelles ci-dessus obtenues par redeploiement de l’algorithme verbatim sur QC Cloud (projet 33072072, 2015-2024, $100k). Les champs Sortino / Total Orders / Trades / Win Rate / Total Fees sont omis car non fournis de maniere fiable par l’API read_backtest (toujours 0 / “-”).

Backtest ID: 815a39ff4580e56a821100b2f2fe5ed4

On vérifie la cohérence des pondérations finales et on génère le rapport de construction du portefeuille.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# MaximumSectorExposureRiskManagementModel
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class SectorExposureRiskExample(QCAlgorithm):
    """
    Exemple avec MaximumSectorExposureRiskManagementModel.
    Limite l'exposition par secteur a 20%.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Mix de secteurs
        symbols = ["AAPL", "MSFT", "GOOGL",  # Tech
                   "JPM", "BAC",              # Finance
                   "JNJ", "PFE",              # Healthcare
                   "XOM", "CVX"]              # Energy
        
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        
        # === RISK MANAGEMENT: MAX 20% PAR SECTEUR ===
        self.SetRiskManagement(
            MaximumSectorExposureRiskManagementModel(0.20)  # Max 20% par secteur
        )
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=30))
        )
        
        self.Log("MaximumSectorExposureRiskManagementModel(0.20) configure")

print("SectorExposureRiskExample defini")
print("\nFonctionnement:")
print("  - Classifie les positions par secteur")
print("  - Si un secteur depasse 20% -> reduit les positions")
print("  - Assure la diversification sectorielle")

Résultat Backtest QC Cloud (Cellule 53)

Metrique Valeur
Statut Completed
Sharpe Ratio -0.583
Sortino Ratio -0.733
CAGR -1.535%
Net Profit -0.379%
Total Orders 59
Trades 25
Win Rate 88.0%
Max Drawdown 6.8%
End Equity $99620.92
Total Fees $59.62

Backtest ID:

Interpretation : SectorExposureRiskExample

Ce modèle de risque assure la diversification sectorielle automatique :

Fonctionnement : 1. Classification : Regroupe les positions par secteur (Tech, Finance, Healthcare…) 2. Calcul exposition : Somme des poids pour chaque secteur 3. Réduction : Si un secteur dépasse 20%, réduit les positions concernées 4. Protection : Empêche la concentration excessive

Avantages : - Diversification automatique sans gestion manuelle - Protection contre les risques sectoriels spécifiques - Adaptatif aux changements d’univers

Cas d’usage : - Stratégies long-only sur actions - Univers large avec plusieurs secteurs - Protection contre “bêta sectoriel”

Note : Nécessite des données fondamentales pour classifier les secteurs.


# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# CompositeRiskManagementModel - Combiner plusieurs risk models
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class CompositeRiskExample(QCAlgorithm):
    """
    Exemple avec CompositeRiskManagementModel.
    Combine plusieurs risk models.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        symbols = ["SPY", "QQQ", "AAPL", "MSFT", "GOOGL", "TSLA", "JPM", "JNJ"]
        for symbol in symbols:
            self.AddEquity(symbol, Resolution.Daily)
        
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        
        # === COMPOSITE RISK MANAGEMENT ===
        # Combine plusieurs regles de risque
        self.SetRiskManagement(
            CompositeRiskManagementModel(
                MaximumDrawdownPercentPerSecurity(0.05),     # Stop loss 5%
                MaximumSectorExposureRiskManagementModel(0.20)  # Max 20% par secteur
            )
        )
        
        self.SetAlpha(
            ConstantAlphaModel(InsightType.Price, InsightDirection.Up, timedelta(days=30))
        )
        
        self.Log("CompositeRiskManagementModel configure:")
        self.Log("  - MaximumDrawdownPercentPerSecurity(0.05)")
        self.Log("  - MaximumSectorExposureRiskManagementModel(0.20)")

print("CompositeRiskExample defini")
print("\nAvantages du CompositeRiskManagementModel:")
print("  - Applique plusieurs regles simultanement")
print("  - Chaque regle est evaluee independamment")
print("  - Protection multi-niveaux")

Résultat Backtest QC Cloud (Cellule 56) – corrige #3367 : metriques reelles QC Cloud

Metrique Valeur
Statut Completed
Sharpe Ratio 0.635
CAGR 11.092%
Max Drawdown 20.200%
Probabilistic Sharpe Ratio 27.935%

Correctif #3367 : le bloc précédent affichait Sharpe 2.355 / CAGR 42.005% avec End Equity $108992.9 – des valeurs fabriquees et internement contradictoires (une End Equity de $108992.9 sur 10 ans = +0.87% de CAGR, incompatible avec le CAGR de 42.005% affiche). Metriques reelles ci-dessus obtenues par redeploiement de l’algorithme verbatim sur QC Cloud (projet 33072157, 2015-2024, $100k). Les champs Sortino / Total Orders / Trades / Win Rate / Total Fees sont omis car non fournis de maniere fiable par l’API read_backtest (toujours 0 / “-”).

Backtest ID: 74b4d31aa87c2773f5be9ef32cb71a63

Interpretation : CompositeRiskManagementModel

Le CompositeRiskManagementModel permet de combiner plusieurs règles de gestion du risque en un seul modèle. Chaque règle est évaluée indépendamment et les ajustements sont appliqués cumulativement.

Architecture multi-niveaux :

                    PortfolioTargets
                           │
                           ▼
            ┌──────────────────────────────┐
            │   CompositeRiskManagement    │
            │  (Combine plusieurs modèles) │
            └──────────┬───────────────────┘
                       │
         ┌─────────────┴─────────────┐
         ▼                           ▼
┌──────────────────┐      ┌──────────────────┐
│ MaxDrawdown 5%   │      │ MaxSector 20%    │
│ (par position)   │      │ (par secteur)    │
└──────────────────┘      └──────────────────┘
         │                           │
         └─────────────┬─────────────┘
                       ▼
              Targets Ajustés

Règles combinées dans l’exemple :

  1. MaximumDrawdownPercentPerSecurity(0.05)
    • Surveille le drawdown de chaque position
    • Liquide si perte > 5% depuis le plus haut
    • Protection contre les pertes excessives
  2. MaximumSectorExposureRiskManagementModel(0.20)
    • Classifie les positions par secteur
    • Limite l’exposition à 20% par secteur
    • Assure la diversification sectorielle

Ordre d’évaluation : - Les modèles sont évalués dans l’ordre de leur ajout - Chaque modèle peut ajuster les targets - Le dernier modèle voit les targets déjà ajustés

Avantages : - Protection multi-niveaux : Plusieurs couches de sécurité - Flexibilité : Ajoutez/retirez des règles facilement - Personnalisation : Combinez n’importe quels risk models

Autres Risk Models disponibles : - MaximumUnrealizedProfitPercentPerSecurity : Take profit automatique - TrailingStopRiskManagementModel : Trailing stop dynamique - NullRiskManagementModel : Désactive le risk management

Cas d’usage : - Stratégies long-only avec univers large - Protection contre les risques spécifiques (drawdown + concentration) - Portefeuilles multi-sectoriels


Resume des Risk Management Models

Modèle Paramètre Effet
MaximumDrawdownPercentPerSecurity(0.05) 5% Liquide si perte > 5%
MaximumUnrealizedProfitPercentPerSecurity(0.10) 10% Take profit si gain > 10%
MaximumSectorExposureRiskManagementModel(0.20) 20% Max 20% par secteur
TrailingStopRiskManagementModel(0.03) 3% Trailing stop 3%
CompositeRiskManagementModel(...) Multiple Combine plusieurs models

Partie 9 : Stratégie Framework Complete (20 min)

Objectif

Construire une stratégie complete utilisant tous les composants du Framework :

  1. Universe Sélection : Fine Fundamental (Volume + Market Cap)
  2. Alpha Model : Momentum avec magnitude et confidence
  3. Portfolio Construction : Risk Parity
  4. Exécution : Smart Exécution avec contrôle du spread
  5. Risk Management : Composite (Drawdown + Sector)
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Strategie Framework Complete
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
import numpy as np

class FullFrameworkAlgorithm(QCAlgorithm):
    """
    Strategie complete utilisant tous les composants du Framework:
    
    1. Universe: Coarse + Fine (Large Cap, High Volume)
    2. Alpha: Momentum avec magnitude/confidence
    3. Portfolio: Risk Parity (volatilite inverse)
    4. Execution: Smart (spread-aware)
    5. Risk: Composite (Drawdown + Sector)
    """
    
    def Initialize(self):
        # === CONFIGURATION ===
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe settings
        self.UniverseSettings.Resolution = Resolution.Daily
        self.num_stocks = 30
        
        # === 1. UNIVERSE SELECTION ===
        self.AddUniverse(self.CoarseFilter, self.FineFilter)
        
        # === 2. ALPHA MODEL ===
        self.SetAlpha(MomentumAlphaModelFramework(lookback=60))
        
        # === 3. PORTFOLIO CONSTRUCTION ===
        self.SetPortfolioConstruction(
            RiskParityPortfolioConstructionFramework(rebalance_period=30, lookback=60)
        )
        
        # === 4. EXECUTION ===
        self.SetExecution(
            SmartExecutionModelFramework(max_slippage_percent=0.001)
        )
        
        # === 5. RISK MANAGEMENT ===
        self.SetRiskManagement(
            CompositeRiskManagementModel(
                MaximumDrawdownPercentPerSecurity(0.05),
                MaximumSectorExposureRiskManagementModel(0.25)
            )
        )
        
        self.Log("="*60)
        self.Log("FULL FRAMEWORK ALGORITHM INITIALIZED")
        self.Log("="*60)
        self.Log("Universe: Coarse + Fine (Top 30 by Market Cap)")
        self.Log("Alpha: Momentum (60 days)")
        self.Log("Portfolio: Risk Parity")
        self.Log("Execution: Smart (spread-aware)")
        self.Log("Risk: Max 5% DD per security, Max 25% sector exposure")
        self.Log("="*60)
    
    # === UNIVERSE SELECTION ===
    def CoarseFilter(self, coarse):
        """Premier filtre: liquidite et donnees fondamentales."""
        filtered = [x for x in coarse 
                    if x.HasFundamentalData
                    and x.Price > 10
                    and x.DollarVolume > 10000000]
        
        sorted_by_volume = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        return [x.Symbol for x in sorted_by_volume[:200]]  # 200 candidats
    
    def FineFilter(self, fine):
        """Deuxieme filtre: Market Cap."""
        filtered = [x for x in fine if x.MarketCap > 5e9]  # > $5B Market Cap
        sorted_by_cap = sorted(filtered, key=lambda x: x.MarketCap, reverse=True)
        
        selected = sorted_by_cap[:self.num_stocks]
        self.Log(f"Universe: {len(selected)} stocks selected")
        
        return [x.Symbol for x in selected]
    
    def OnData(self, data):
        """Le Framework gere tout automatiquement."""
        pass
    
    def OnEndOfAlgorithm(self):
        """Resume final."""
        self.Log("\n" + "="*60)
        self.Log("FINAL SUMMARY")
        self.Log("="*60)
        self.Log(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
        total_return = (self.Portfolio.TotalPortfolioValue / self.StartingCash - 1) * 100
        self.Log(f"Total Return: {total_return:.2f}%")
        self.Log("="*60)


# === ALPHA MODEL ===
class MomentumAlphaModelFramework(AlphaModel):
    """Alpha Model basé sur le momentum."""
    
    def __init__(self, lookback=60):
        self.lookback = lookback
        self.securities = []
    
    def Update(self, algorithm, data):
        insights = []
        
        for symbol in self.securities:
            if not data.ContainsKey(symbol):
                continue
            
            history = algorithm.History(symbol, self.lookback, Resolution.Daily)
            if history.empty or len(history) < self.lookback * 0.8:
                continue
            
            try:
                returns = history['close'].pct_change().dropna()
                momentum = returns.mean()
                volatility = returns.std()
                
                if volatility == 0:
                    continue
                
                direction = InsightDirection.Up if momentum > 0 else InsightDirection.Down
                magnitude = min(abs(momentum) * 10, 0.2)
                sharpe = momentum / volatility
                confidence = min(max(abs(sharpe) / 3, 0.1), 1.0)
                
                insight = Insight.Price(symbol, timedelta(days=30), direction, magnitude, confidence)
                insights.append(insight)
            except:
                continue
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            if security.Symbol not in self.securities:
                self.securities.append(security.Symbol)
        for security in changes.RemovedSecurities:
            if security.Symbol in self.securities:
                self.securities.remove(security.Symbol)


# === PORTFOLIO CONSTRUCTION ===
class RiskParityPortfolioConstructionFramework(PortfolioConstructionModel):
    """Portfolio Construction basé sur Risk Parity."""
    
    def __init__(self, rebalance_period=30, lookback=60):
        self.rebalance_period = rebalance_period
        self.lookback = lookback
        self.last_rebalance = datetime.min
    
    def CreateTargets(self, algorithm, insights):
        targets = []
        
        if (algorithm.Time - self.last_rebalance).days < self.rebalance_period:
            return targets
        
        self.last_rebalance = algorithm.Time
        active_insights = [i for i in insights if i.Direction != InsightDirection.Flat]
        
        if len(active_insights) == 0:
            return targets
        
        volatilities = {}
        for insight in active_insights:
            symbol = insight.Symbol
            history = algorithm.History(symbol, self.lookback, Resolution.Daily)
            
            if history.empty or len(history) < self.lookback * 0.8:
                continue
            
            try:
                returns = history['close'].pct_change().dropna()
                vol = returns.std() * np.sqrt(252)
                if vol > 0:
                    volatilities[symbol] = vol
            except:
                continue
        
        if len(volatilities) == 0:
            return targets
        
        inv_vols = {s: 1.0 / v for s, v in volatilities.items()}
        total_inv_vol = sum(inv_vols.values())
        
        for insight in active_insights:
            symbol = insight.Symbol
            if symbol not in inv_vols:
                continue
            
            base_weight = inv_vols[symbol] / total_inv_vol
            direction = 1 if insight.Direction == InsightDirection.Up else -1
            final_weight = direction * base_weight
            
            targets.append(PortfolioTarget(symbol, final_weight))
        
        algorithm.Log(f"Risk Parity: {len(targets)} targets")
        return targets
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.RemovedSecurities:
            if security.Invested:
                algorithm.Liquidate(security.Symbol)


# === EXECUTION MODEL ===
class SmartExecutionModelFramework(ExecutionModel):
    """Execution Model intelligent avec controle du spread."""
    
    def __init__(self, max_slippage_percent=0.001):
        self.max_slippage = max_slippage_percent
        self.targets_collection = PortfolioTargetCollection()
    
    def Execute(self, algorithm, targets):
        self.targets_collection.AddRange(targets)
        
        for target in self.targets_collection:
            symbol = target.Symbol
            
            if symbol not in algorithm.Securities:
                continue
            
            security = algorithm.Securities[symbol]
            current_qty = algorithm.Portfolio[symbol].Quantity
            target_value = algorithm.Portfolio.TotalPortfolioValue * target.Quantity
            target_qty = int(target_value / security.Price) if security.Price > 0 else 0
            delta = target_qty - current_qty
            
            if abs(delta) < 1:
                continue
            
            bid_price = security.BidPrice
            ask_price = security.AskPrice
            
            if bid_price <= 0 or ask_price <= 0:
                algorithm.MarketOrder(symbol, delta)
                continue
            
            spread_pct = (ask_price - bid_price) / security.Price
            
            if spread_pct < self.max_slippage * 2:
                algorithm.MarketOrder(symbol, delta)
            else:
                spread = ask_price - bid_price
                limit_price = bid_price + spread * 0.3 if delta > 0 else ask_price - spread * 0.3
                algorithm.LimitOrder(symbol, delta, limit_price)
        
        self.targets_collection.ClearFulfilled(algorithm)


print("FullFrameworkAlgorithm defini avec tous les composants")
print("\nComposants:")
print("  1. Universe: CoarseFilter + FineFilter")
print("  2. Alpha: MomentumAlphaModelFramework")
print("  3. Portfolio: RiskParityPortfolioConstructionFramework")
print("  4. Execution: SmartExecutionModelFramework")
print("  5. Risk: CompositeRiskManagementModel")

Résultat Backtest QC Cloud (Cellule 59) – corrige #3367 : backtest non aboutissant + statistiques fabriquees

Metrique Valeur
Statut Backtest non aboutissant (O(n^2) History-per-bar sur univers 30 titres)

Correctif #3367 : le bloc précédent affichait un Statut “Runtime Error” MAIS avec des statistiques completes (Sharpe -1.137 / Sortino -1.273 / 296 trades / Win Rate 52.0%) – une contradiction : un backtest en Runtime Error ne produit pas de Sharpe / Sortino / Win Rate valides. Ces statistiques etaient fabriquees. Le FullFrameworkAlgorithm (cellule précédente) combine MomentumAlphaModelFramework et RiskParityPortfolioConstructionFramework, qui appellent tous deux algorithm.History(symbol, lookback) par titre a chaque barre, sur un univers Coarse/Fine de 30 titres – le même pattern O(n^2) que le MomentumFramework de QC-Py-13 (verifie bloque ~9h avant arret manuel via la console QC). Ce backtest n’a deliberement pas ete relance ici pour ne pas bloquer le node QC ~9h. Les metriques reelles necessiteraient un redesign des modèles (indicateurs Momentum/RateOfChange enregistres en O(n)) – hors scope #3367. Cette note documente la realite verifiee plutot que de conserver des chiffres fabriques.

Interpretation : FullFrameworkAlgorithm

Cet exemple complet démontre une stratégie Framework complète utilisant les 5 composants modulaires de QuantConnect.

Vue d’ensemble de l’architecture :

┌─────────────────────────────────────────────────────────────────┐
│                     FULL FRAMEWORK ALGORITHM                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  1. UNIVERSE SÉLECTION                                          │
│     ├─ CoarseFilter: Volume > $10M, Price > $10                 │
│     └─ FineFilter: Market Cap > $5B → Top 30 stocks            │
│                                                                   │
│  2. ALPHA MODEL (MomentumAlphaModelFramework)                    │
│     ├─ Lookback: 60 jours                                       │
│     ├─ Direction: Signe du momentum                              │
│     ├─ Magnitude: |momentum| × 10 (max 20%)                     │
│     └─ Confidence: |Sharpe| / 3 (0.1 à 1.0)                     │
│                                                                   │
│  3. PORTFOLIO CONSTRUCTION (RiskParity)                          │
│     ├─ Rebalancement: Mensuel (30 jours)                        │
│     └─ Poids: 1/Volatilité (égalise le risque)                  │
│                                                                   │
│  4. EXÉCUTION (SmartExecutionModelFramework)                     │
│     ├─ Max slippage: 0.1%                                       │
│     ├─ Market Order: Si spread < 0.2%                           │
│     └─ Limit Order: Sinon à prix amélioré                       │
│                                                                   │
│  5. RISK MANAGEMENT (Composite)                                  │
│     ├─ MaxDrawdown: 5% par position                             │
│     └─ MaxSectorExposure: 25% par secteur                        │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘

Flux de données complet :

Universe (30 stocks)
        │
        ▼
┌──────────────────┐
│  Alpha Model     │ → Insights avec direction, magnitude, confidence
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Portfolio Const. │ → Targets pondérés par 1/volatilité (Risk Parity)
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ Risk Management  │ → Targets ajustés (DD < 5%, Sector < 25%)
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│    Exécution     │ → Market ou Limit ordres selon spread
└──────────────────┘

Points clés de l’implémentation :

  1. Universe Sélection dynamique
    • CoarseFilter : Réduit à 200 candidats liquides
    • FineFilter : Sélectionne les 30 plus grandes capitalisations
    • Actualisation mensuelle automatique
  2. Alpha avec métadonnées riches
    • Les insights incluent magnitude et confidence
    • Permet une pondération sophistiquée dans le PCM
    • Le Sharpe ratio normalisé détermine la confiance
  3. Risk Parity pour la construction
    • Calcule la volatilité de chaque actif sur 60 jours
    • Poids inversement proportionnels à la volatilité
    • Contribution au risque égale pour tous les actifs
  4. Exécution adaptative
    • Vérifie le spread bid-ask avant chaque ordre
    • Market Order sur marchés liquides
    • Limit Order avec prix amélioré sinon
  5. Protection multi-niveaux
    • Niveau position : Stop loss à 5%
    • Niveau portefeuille : Max 25% par secteur

Paramètres optimisables :

Paramètre Valeur Impact
num_stocks 30 Diversification vs concentration
lookback 60 jours Stabilité des signaux
rebalance_period 30 jours Coûts de transaction vs réactivité
max_slippage 0.1% Tolérance au slippage
max_drawdown 5% Niveau de stop loss
max_sector 25% Concentration sectorielle

Avantages de l’approche Framework : - Modularité : Chaque composant peut être testé indépendamment - Réutilisabilité : Les modèles peuvent être combinés différemment - Maintenabilité : Facile de modifier un composant sans toucher aux autres - Testabilité : Backtesting granulaire par composant


Architecture de la stratégie complete

flowchart TD
    UF["Universe Filter (Coarse + Fine)<br/>Coarse: Vol > $10M, Price > $10<br/>Fine: Market Cap > $5B -> 30 stocks"] --> AM["Alpha Model (Momentum)<br/>Momentum 60 jours -> Direction, Magnitude, Confidence"]
    AM --> PC["Portfolio Constr. (Risk Parity)<br/>1/volatilite -> Poids equalises en risque"]
    PC --> RM["Risk Management (Composite)<br/>Max DD 5%, Max Sector 25% -> Targets ajustes"]
    RM --> EX["Exécution (Smart)<br/>Market si spread OK, Limit si spread large -> Ordres"]

Conclusion et Prochaines Étapes

Recapitulatif

Dans ce notebook, nous avons appris a :

Composant Modèles Points Cles
Portfolio Construction EqualWeighting, InsightWeighting, MeanVariance, Custom Transforme Insights -> Targets
Exécution Immediate, VWAP, StdDev, Smart Custom Gere le type d’ordre et le timing
Risk Management MaxDrawdown, SectorExposure, Composite Protege le portefeuille

Tableau de choix des modèles

Situation Portfolio Model Exécution Model Risk Model
Simple, liquide EqualWeighting Immediate MaxDrawdown
Signaux sophistiques InsightWeighting Immediate Composite
Optimisation risque MeanVariance VWAP SectorExposure
Multi-asset class Risk Parity Smart Composite

Points Cles a Retenir

  1. Separation des responsabilites : Chaque modèle a un rôle précis
  2. Modularite : Facile de changer un composant sans affecter les autres
  3. Custom models : Heriter des classes de base pour implementer votre logique
  4. CompositeRiskModel : Combiner plusieurs règles de risque
  5. Smart Exécution : Adapter le type d’ordre au marche

Ressources Complementaires


Notebook complete. Vous maitrisez maintenant les composants avances du Framework QuantConnect.

References academiques

  • Bailey, D.H. & Lopez de Prado, M. (2012). The Sharpe Ratio Efficient Frontier. Journal of Risk 15(2):3-44. DOI 10.21314/JOR.2012.259.
  • Black, F. & Litterman, R. (1991). Asset Allocation: Combining Investor Views with Market Equilibrium. The Journal of Fixed Income 1(2):7-18. DOI 10.3905/jfi.1991.408013.
  • 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.
  • Qian, E. (2005). Risk Parity Portfolios: Efficient Portfolios Through True Diversification. PanAgora Asset Management Research.
  • Sharpe, W.F. (1966). Mutual Fund Performance. The Journal of Business 39(1):119-138. DOI 10.1086/294846.
Retour au sommet