# 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
<< Sommaire QC | Précédent : QC-Py-13-Alpha-Models << | Suivant : QC-Py-14b-Liquidity-Execution-Costs >>
[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
A la fin de ce notebook, vous serez capable de :
| 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.pyde votre projet QuantConnect Lab. Ne tentez pas de faire “Run All” – les importsAlgorithmImportsn’existent pas en Jupyter local.Pour demarrer un projet QC : copiez un
main.pydepuisprojects/oupartner-course-quant-trading/templates/, puis adaptez-le.
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)
| 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 reussis
Le Portfolio Construction Model recoit les Insights generes par l’Alpha Model et les transforme en Portfolio Targets (allocations cibles).
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
| 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 |
| 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")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.)
Le EqualWeightingPortfolioConstructionModel attribue un poids egal a tous les actifs avec des insights actifs. C’est l’equivalent d’un portefeuille equipondere.
\[w_i = \frac{direction_i}{\sum |direction_j|}\]
Ou : - \(w_i\) = poids de l’actif i - \(direction_i\) = +1 (Up), -1 (Down), 0 (Flat)
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), orResolution.Weeklyn’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 enExpiry.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
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 | 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 |
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")Le InsightWeightingPortfolioConstructionModel utilise les proprietes des Insights pour determiner les poids :
\[w_i = \frac{direction_i \times magnitude_i \times confidence_i}{\sum_j |direction_j \times magnitude_j \times confidence_j|}\]
| 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
| Situation | Recommandation |
|---|---|
| Alpha Model genere magnitude/confidence | InsightWeighting |
| Signaux tous equivalents | EqualWeighting |
| Modèle ML avec probabilites | InsightWeighting |
| Signaux discretionnaires | EqualWeighting |
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
| 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
rebalancingPeriodetportfolioBiasduMeanVarianceOptimizationPortfolioConstructionModelont ete supprimes des versions recentes de Lean ; (2) après correction de l’API, leConstantAlphaModelemet desInsightsansmagnitude, 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/portfolioBiassupprimes) et leMeanVarianceOptimizationPortfolioConstructionModelrefuse lesInsightsansmagnitude. L’obtention de metriques reelles necessiterait de redisigner le modèle Alpha (hors scope #3367). Realite verifiee, pas de valeurs inventees.
| 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 |
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")Risk Parity est une approche d’allocation qui egalise la contribution au risque de chaque actif plutot que les montants investis.
\[w_i = \frac{1/\sigma_i}{\sum_j 1/\sigma_j}\]
Ou \(\sigma_i\) est la volatilite de l’actif i.
| 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 bloctry/except: continueautour 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.
L’allocation multi-stratégies permet de combiner plusieurs Alpha Models et d’allouer le capital a chaque strategy individuellement.
| 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% |
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"]
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|}\]
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%
L’Exécution Model recoit les PortfolioTargets et les transforme en ordres d’achat/vente. Il gere :
| 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 estInsight.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).
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 :
source_modelalpha_allocations : Dict {“EMACross”: 0.40, “TrendStocks”: 0.60}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
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%")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)
L’analyse des projets QuantConnect révèle des patterns critiques pour les composites de stratégies.
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 |
| 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).
Pour qu’un composite soit réussi, les composantes doivent être orthogonales :
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
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
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é |
| Modèle | Vitesse | Contrôle Prix | Impact Marche | Complexite |
|---|---|---|---|---|
| Immediate | Rapide | Aucun | Eleve | Simple |
| VWAP | Lent | Moyen | Faible | Moyenne |
| StdDev | Variable | Eleve | Faible | Moyenne |
Créer un Exécution Model personnalise qui :
# [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).
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
Le Risk Management Model intercepte les PortfolioTargets et les ajuste selon des règles de risque. Il peut :
| 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:
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
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 :
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
| 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 |
Construire une stratégie complete utilisant tous les composants du Framework :
# [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) combineMomentumAlphaModelFrameworketRiskParityPortfolioConstructionFramework, qui appellent tous deuxalgorithm.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.
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 :
CoarseFilter : Réduit à 200 candidats liquidesFineFilter : Sélectionne les 30 plus grandes capitalisationsParamè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
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"]
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 |
| 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 |
Notebook complete. Vous maitrisez maintenant les composants avances du Framework QuantConnect.