# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Manual Universe : Portfolio FAANG
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
class FAANGManualUniverse(QCAlgorithm):
"""
Exemple de Manual Universe avec les actions FAANG.
Allocation equal-weight sur les 5 actions.
"""
def Initialize(self):
# Configuration backtest
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Liste des symboles FAANG
self.tickers = ["META", "AAPL", "AMZN", "NFLX", "GOOGL"]
self.symbols = []
# Ajouter chaque action manuellement
for ticker in self.tickers:
equity = self.AddEquity(ticker, Resolution.Daily)
self.symbols.append(equity.Symbol)
self.Log(f"Added {ticker}: {equity.Symbol}")
# Allocation cible : equal-weight (20% chacune)
self.target_allocation = 1.0 / len(self.symbols)
# Rebalancement initial planifie
self.first_trade = True
self.Log(f"FAANG Universe initialized with {len(self.symbols)} symbols")
def OnData(self, data):
# Attendre que toutes les donnees soient disponibles
if not all(data.ContainsKey(s) for s in self.symbols):
return
# Allocation initiale (une seule fois)
if self.first_trade:
for symbol in self.symbols:
self.SetHoldings(symbol, self.target_allocation)
self.Log(f"Initial allocation: {symbol.Value} -> {self.target_allocation:.1%}")
self.first_trade = False
def OnEndOfAlgorithm(self):
self.Log(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")<< Sommaire QC | Précédent : QC-Py-04-Research-Workflow << | Suivant : QC-Py-06-Options-Trading >>
Objectifs d’Apprentissage
A la fin de ce notebook, vous serez capable de :
- Comprendre le concept d’Universe et son importance en trading algorithmique
- Créer un Manual Universe avec une liste fixe de symboles
- Implementer un Coarse Universe Sélection base sur le volume et le prix
- Utiliser le Fine Universe Sélection avec des données fondamentales
- Configurer les Universe Settings (resolution, leverage, etc.)
- Gerer le Dynamic Rebalancing lors des changements d’univers
- Planifier des rebalancements avec Scheduled Universe Sélection
- Construire une stratégie Momentum complete
Prerequis
- Notebooks QC-Py-01 a QC-Py-04 completes
- Comprehension du lifecycle QCAlgorithm
- Notions de base en analyse fondamentale
Structure du Notebook
- Introduction a l’Universe Sélection (10 min)
- Manual Universe (15 min)
- Coarse Universe Sélection (20 min)
- Fine Universe Sélection (20 min)
- Universe Settings (10 min)
- Dynamic Rebalancing (15 min)
- Scheduled Universe Sélection (10 min)
- Stratégie Complete: Momentum Universe (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.
Public cible et pré-requis
Ce notebook est le cinquième de la série QuantConnect Python. Il couvre la sélection d’univers — c’est-à-dire quelles actions votre algorithme va trader. C’est une décision aussi critique que le signal d’entrée ou le sizing : un univers mal choisi annule les meilleurs alpha models.
Pré-requis techniques : - QC-Py-01 à QC-Py-04 (Setup, Platform, Data, Research) - Python intermédiaire (classes, méthodes, list comprehensions) - Familiarité avec pandas DataFrame (filtres, groupby) - Notions de finance : secteurs, capitalisation, liquidité
Compétences acquises en fin de notebook : 1. Choisir entre Manual / Coarse / Fine / Scheduled Universe selon le cas d’usage. 2. Implémenter un filtre fondamental multi-critères (P/E, ROE, market cap, sector). 3. Gérer le rebalancement dynamique quand l’univers change (OnSecuritiesChanged). 4. Combiner Universe + filtre qualité dans une stratégie Momentum.
Pourquoi ce notebook est important : la plupart des algos quantitatifs échouent non pas à cause du signal, mais à cause de l’univers mal défini. Trader 500 actions illiquides vs 50 actions liquides = différence de Sharpe de 0.5 sur le même signal.
[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.
1. Introduction a l’Universe Sélection (10 min)
Qu’est-ce qu’un Universe?
Dans QuantConnect, un Universe represente l’ensemble des actifs que votre algorithme considere pour le trading a un moment donne. Plutot que de trader toujours les mêmes actions, l’Universe Sélection permet de :
- Sélectionner dynamiquement les meilleurs candidats selon vos critères
- S’adapter aux conditions de marche (nouvelle IPO, delisting, etc.)
- Filtrer parmi des milliers d’actifs pour ne garder que les plus pertinents
- Automatiser le screening qui prendrait des heures manuellement
Pourquoi sélectionner dynamiquement des actions?
| Approche | Avantages | Inconvenients |
|---|---|---|
| Liste fixe | Simple, rapide | Risque de biais, actions obsoletes |
| Universe dynamique | Adaptable, sans biais | Plus complexe, backtest plus long |
Exemples d’utilisation :
- Stratégie momentum : Trader les 50 actions avec le meilleur momentum sur 12 mois
- Value investing : Filtrer les actions avec P/E < 15 et dividende > 3%
- Secteur spécifique : Ne trader que les actions du secteur technologie
- Liquidite : Exclure les actions avec un volume trop faible
Types d’Universe Sélection
Universe Sélection
|
+-- Manual Universe (liste fixe)
|
+-- Coarse Universe (filtre sur prix, volume)
| |
| +-- Fine Universe (données fondamentales)
|
+-- Custom Universe (données alternatives)
Pipeline de Sélection
8000+ actions US Coarse Sélection Fine Sélection Portfolio
[Marche] ---> (prix, volume) ---> (P/E, secteur) ---> [Trading]
~500 candidats ~50 candidats ~20 actions
Trois familles d’univers
L’universe selection chez QuantConnect se décline en 3 grandes familles qu’il faut choisir selon la stratégie :
- Manual Universe : liste statique de symboles définis à l’avance. Idéal pour reproduire un indice, tester une thèse sectorielle, ou backtester un signal simple.
- Coarse Universe : filtrage quotidien sur des critères simples (prix, volume, market cap). Réduit l’univers de 8000+ actions à ~200-500 candidates. Tourne à chaque journée de marché.
- Fine Universe : filtrage fondamental sur données Morningstar/Engine (P/E, ROE, ratios sectoriels). Réduit l’univers coarse à ~10-50 actions finales. Tourne typiquement 1×/mois.
Données QuantConnect — la pyramide
QC offre une pyramide de données : à chaque niveau, plus de données mais plus lent :
| Niveau | Données | Vitesse | Coût |
|---|---|---|---|
| Manual | Symboles | <1ms | Gratuit |
| Coarse | Prix, volume, market cap | ~1-3s | Gratuit |
| Fine | Fondamentales Morningstar | ~3-10s | Gratuit |
| Alpha Streams | Données tierces premium | Variable | Payant |
Lecture critique : Fine Universe est 3-10× plus lent que Coarse. Sur un backtest 10 ans daily, ça peut représenter 5-15 minutes de plus. À utiliser seulement si votre filtre requiert réellement des données fondamentales.
Anti-pattern fréquent
L’erreur classique du débutant : utiliser Fine Universe pour tout. On charge 8000 actions × 50 champs fondamentaux × 252 jours/an × 10 ans = ~10 milliards de valeurs pour au final ne garder que 20 actions. Le ratio signal/bruit est désastreux. Filtrer en amont (Coarse) puis affiner (Fine) = pipeline efficace.
2. Manual Universe (15 min)
Approche la plus simple : Liste fixe de symboles
La maniere la plus simple de définir un univers est d’ajouter manuellement les actions que vous souhaitez trader avec self.AddEquity().
Exemple : Portfolio FAANG
Les FAANG (META ex-Facebook, Apple, Amazon, Netflix, Google) sont les geants de la tech americaine. Creons un algorithme qui trade ces 5 actions.
2. Manual Universe (15 min)
L’approche la plus simple : liste fixe de symboles dans le constructeur de l’algorithme.
Cas d’usage typique : - Reproduire un indice (S&P 500 top 10, CAC 40, Nasdaq-100) - Backtester une thèse sectorielle (energy stocks 2020-2024) - Tester un signal sur un petit univers pour itérer vite - Stratégies “stock-picking” discrétionnaire
Avantages : - Zéro filtrage → backtest ultra-rapide - Reproductible (la liste est dans le code) - Facile à déboguer
Limites : - Survivorship bias si on backtest sur les composants actuels d’un indice (ex: tester sur les MegaCaps 2024 = survivants des krachs 2008/2020) - Pas dynamique : si une action sort de l’indice, l’algo la garde quand même - Coût de maintenance : il faut mettre à jour la liste à la main chaque trimestre/année
Exemple : Portfolio FAANG
Le portfolio FAANG (Facebook/Meta, Apple, Amazon, Netflix, Google/Alphabet) est un cas d’école. C’est : - 5 symboles, ultra-liquides - Tracké par tous les investisseurs retail - Surperformant historiquement le S&P 500 sur 2015-2024 - Mais corrélé à 0.85+ avec QQQ → pas de diversification sectorielle
C’est un bon premier test pour un signal : si votre algo ne bat pas FAANG sur ces 5 actions, il y a un problème de signal (pas d’univers).
Backtest QC Cloud : FAANGManualUniverse
| Sharpe | CAGR | MaxDD | Net Profit | Orders | End Equity |
|---|---|---|---|---|---|
| 0.84 | 27.526% | 50.700% | +1039.578% | 5 | $1,139,577.97 |
FAANG buy-and-hold, 2015-2024
Avantages et limites du Manual Universe
| Avantages | Limites |
|---|---|
| Simple a implementer | Biais de sélection (survivorship bias) |
| Contrôle total sur les actifs | Pas d’adaptation aux conditions de marche |
| Backtest rapide | Ne decouvre pas de nouvelles opportunites |
| Ideal pour tester une idee | Maintenance manuelle requise |
Quand utiliser Manual Universe?
- Prototypage rapide : Tester une stratégie sur quelques actions
- Stratégie sectorielle : Trader un panier défini (ETF-like)
- Arbitrage spécifique : Pairs trading sur 2-3 actifs
- Benchmark : Comparer avec une stratégie dynamique
Variation : Multiple Asset Classes
Avantages et limites du Manual Universe
Quand utiliser Manual Universe ?
| Cas d’usage | Manuel ? | Pourquoi |
|---|---|---|
| Reproduire indice | ✅ | Liste connue à l’avance |
| Thèse sectorielle | ✅ | Univers défini |
| Pair trading | ✅ | 2-10 symboles fixes |
| Rotation sectorielle | ⚠️ | Possible mais pénible |
| Small/mid-cap screening | ❌ | Demande Coarse |
| Filtre fondamental | ❌ | Demande Fine |
| Univers changeant | ❌ | Demande Scheduled |
Variation : Multiple Asset Classes
QC permet de mixer plusieurs classes d’actifs dans le même univers : actions (Equity), options (Option), futures (Future), forex (Forex), crypto (Crypto), ETF.
Exemple : universe “60/40” — 60% actions (SPY/EFA/AGG), 40% obligations (TLT/IEF). On combine AddEquity avec des méthodes spécifiques par classe.
Pédagogie : la diversification multi-classes réduit la volatilité (corrélations plus basses entre classes que intra-classe). Mais elle complique le code (gestion des sessions de marché différentes : actions 9h30-16h, forex 24h, crypto 24/7).
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Manual Universe multi-asset
# Exemple avec actions, ETFs et crypto
from AlgorithmImports import *
class MultiAssetManualUniverse(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Actions individuelles
self.stocks = {
"AAPL": self.AddEquity("AAPL", Resolution.Daily).Symbol,
"MSFT": self.AddEquity("MSFT", Resolution.Daily).Symbol,
}
# ETFs
self.etfs = {
"SPY": self.AddEquity("SPY", Resolution.Daily).Symbol, # S&P 500
"QQQ": self.AddEquity("QQQ", Resolution.Daily).Symbol, # Nasdaq 100
"TLT": self.AddEquity("TLT", Resolution.Daily).Symbol, # Obligations
}
# Tous les symboles
self.all_symbols = list(self.stocks.values()) + list(self.etfs.values())
self.Log(f"Multi-asset universe: {len(self.stocks)} stocks, {len(self.etfs)} ETFs")Backtest QC Cloud : MultiAssetManualUniverse
| Résultat | End Equity |
|---|---|
| Exploration (0 ordre) | $100,000.00 |
Multi-asset exploration, no trading
Exercice 1 : Manuel Universe personnalise avec allocation ponderee
En vous basant sur les exemples FAANGManualUniverse et MultiAssetManualUniverse ci-dessus, créez un algorithme avec un Manual Universe de 6 actions et une allocation ponderee personnalisee (pas equal-weight).
Objectif : implementer WeightedManualUniverse avec les specifications suivantes :
- Créer un dictionnaire de 6 actions avec des ponderations cibles (la somme doit faire 1.0)
- Ajouter chaque action avec
self.AddEquity(ticker, Resolution.Daily) - Dans
OnData, investir selon les ponderations définies au premier trade disponible - Ajouter un
Schedule.Onmensuel pour rebalancer vers les ponderations cibles
Indices : - self.SetHoldings(symbol, weight) ou weight est un float entre 0 et 1 - self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.At(10, 0), self.Rebalance) pour le rebalancement - Utilisez un dictionnaire self.target_weights = {"AAPL": 0.25, "MSFT": 0.20, ...} pour stocker les ponderations - Verifiez data.ContainsKey(symbol) avant de trader
Exercice 1 : Manuel Universe personnalisé avec allocation
Objectif : Construire un portefeuille 5-7 actions avec allocation pondérée (pas equal-weight).
Contexte : Vous backtestez une thèse “tech européenne”. Vous voulez investir dans ASML, SAP, Nokia, Ericsson, Spotify, Adyen, Prosus avec une allocation qui reflète votre conviction.
Travail demandé : 1. Définir l’univers dans self.universe (liste de symboles). 2. Définir les poids dans self.weights (doit sommer à 1.0). 3. Implémenter OnData pour passer les ordres proportionnellement aux poids. 4. Rebalancer mensuellement (1er du mois) pour respecter les poids cibles.
Indices : - Utiliser self.SetHoldings(symbol, weight) pour passer les ordres en proportion du portefeuille. - Pour le rebalance mensuel, comparer self.Time.month à la valeur précédente. - Si vous voulez éviter le rebalance daily (friction), stocker le mois du dernier rebalance dans self.last_rebalance_month.
Pédagogie : cet exercice vous fait toucher deux bugs classiques des débutants : - Oublier le SetHoldings qui ne fait rien sans ordre explicite. - Le rebalance daily qui multiplie les frais de transaction par 30 (mensuel = 1× par mois, daily = ~21× par mois).
Validation : backtest sur 2019-2023 devrait donner un Sharpe 0.6-0.9 et un DD max -20% à -25% (typique pour une thèse sectorielle concentrée).
# [REFERENCE QC] Exercice 1 : Manuel Universe personnalise avec allocation ponderee
# A completer et copier dans l'IDE QuantConnect
from AlgorithmImports import *
class WeightedManualUniverse(QCAlgorithm):
"""
TODO etudiant : implementer un Manual Universe avec 6 actions
et des ponderations cibles personnalisees.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# TODO etudiant : definir 6 actions avec ponderations cibles (somme = 1.0)
# self.target_weights = {"AAPL": 0.25, "MSFT": 0.20, ...}
# TODO etudiant : ajouter chaque action avec AddEquity
# TODO etudiant : planifier un rebalancement mensuel avec Schedule.On
self.first_trade = True
def Rebalance(self):
# TODO etudiant : rebalancer vers les ponderations cibles
pass
def OnData(self, data):
# TODO etudiant : investir au premier trade disponible
pass
print("Exercice a completer : WeightedManualUniverse")3. Coarse Universe Sélection (20 min)
Filtrage base sur le prix et le volume
Le Coarse Universe Sélection est le premier niveau de filtrage dynamique. Il permet de filtrer parmi toutes les actions du marche en utilisant des critères simples :
- Price : Prix de l’action (eviter les penny stocks)
- DollarVolume : Volume en dollars (liquidite)
- HasFundamentalData : Presence de données fondamentales
Structure du Coarse Sélection
def Initialize(self):
# Activer la sélection d'univers
self.AddUniverse(self.CoarseSelectionFunction)
def CoarseSelectionFunction(self, coarse):
# coarse = liste de CoarseFundamental objects
# Retourne une liste de Symbol
filtered = [x for x in coarse if x.Price > 10]
return [x.Symbol for x in filtered]Proprietes de CoarseFundamental
| Propriete | Type | Description |
|---|---|---|
Symbol |
Symbol | Identifiant unique de l’action |
Price |
float | Prix de cloture |
DollarVolume |
float | Volume quotidien en dollars (Price x Volume) |
Volume |
long | Nombre d’actions echangees |
HasFundamentalData |
bool | True si données fondamentales disponibles |
3. Coarse Universe Selection (20 min)
Le Coarse Universe est le premier niveau de filtrage dans QC. Il s’exécute quotidiennement avant l’ouverture du marché et réduit l’univers de 8000+ actions à ~200-500 candidates.
Critères typiques : - Prix > $5 (exclure penny stocks) - Volume > $1M/jour (liquidité minimale) - Market cap > $1B (exclure micro-caps) - Domicile US (filtrer par HasFundamentalData) - Secteur (exclure financials, REITs si non-désiré)
Structure du Coarse Selection
def CoarseSelectionFunction(self, coarse):
sorted_by_volume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in sorted_by_volume[:100]]Trois étapes : 1. Recevoir la liste coarse (~8000 actions avec métadonnées de base). 2. Filtrer selon vos critères (prix, volume, cap). 3. Retourner la liste des Symbol retenus (~100-500).
Pédagogie : le retour de la fonction Coarse est un filtre uniquement — il n’envoie aucun ordre, ne calcule aucun signal. Il précise quelles actions sont candidates pour la suite.
Optimisations importantes
Pour un backtest rapide : - Trier sur DollarVolume avant de prendre les N premiers (un sorted() complet est O(N log N), un heapq.nlargest(N) est O(N log N) aussi mais avec moins d’overhead). - Retourner directement une liste de Symbol (pas un DataFrame) — QC sérialise le retour. - Éviter les calculs lourds dans CoarseSelectionFunction : déplacez-les dans OnData ou dans une classe FineFundamental.
Coût typique : Coarse tourne en <100ms par jour de marché sur 8000 actions. C’est négligeable.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Coarse Universe Selection : Top 100 par volume
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
class CoarseUniverseAlgorithm(QCAlgorithm):
"""
Selection dynamique des 100 actions les plus liquides.
Filtres : prix > 10$, volume > 10M$/jour.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Configurer l'univers avec Coarse Selection
self.AddUniverse(self.CoarseSelectionFunction)
# Nombre d'actions a selectionner
self.num_stocks = 100
# Filtres
self.min_price = 10 # Prix minimum ($)
self.min_dollar_volume = 10000000 # Volume min ($10M/jour)
self.Log(f"Coarse Universe initialized: top {self.num_stocks} by volume")
def CoarseSelectionFunction(self, coarse):
"""
Fonction de filtrage Coarse.
Appelee automatiquement chaque jour par QuantConnect.
Args:
coarse: Liste de CoarseFundamental objects (toutes les actions)
Returns:
Liste de Symbol des actions selectionnees
"""
# Etape 1: Filtrer par prix et volume
filtered = [x for x in coarse
if x.Price > self.min_price
and x.DollarVolume > self.min_dollar_volume]
# Etape 2: Trier par DollarVolume (decroissant)
sorted_by_volume = sorted(filtered,
key=lambda x: x.DollarVolume,
reverse=True)
# Etape 3: Garder les top N
top_stocks = sorted_by_volume[:self.num_stocks]
# Log pour debug
self.Log(f"Coarse selection: {len(filtered)} filtered, {len(top_stocks)} selected")
# Retourner les symboles
return [x.Symbol for x in top_stocks]
def OnData(self, data):
# L'univers change automatiquement
# self.ActiveSecurities contient les actions actuelles
pass
def OnSecuritiesChanged(self, changes):
"""Appele quand l'univers change (ajout/retrait d'actions)."""
self.Log(f"Securities changed: +{len(changes.AddedSecurities)} / -{len(changes.RemovedSecurities)}")Backtest QC Cloud : CoarseUniverseAlgorithm
| Résultat | End Equity |
|---|---|
| Exploration (0 ordre) | $100,000.00 |
Coarse sélection only, 2020-2024
Résultats backtest QC Cloud : CoarseUniverseAlgorithm (2015-2024, $100k) — 0 ordres, Net Profit 0%. Demo Coarse Sélection (Top 100 par volume). La sélection s’execute mais OnData est vide — pas de trading.
Comprendre le flux de Coarse Sélection
Chaque jour de trading:
1. QuantConnect appelle CoarseSelectionFunction(coarse)
|
+-- coarse = 8000+ CoarseFundamental objects
|
+-- Votre logique de filtrage
|
+-- Retourne liste de Symbol (ex: 100 symboles)
2. QuantConnect compare avec l'univers précédent
|
+-- Nouvelles actions -> AddedSecurities
+-- Actions retirees -> RemovedSecurities
3. OnSecuritiesChanged() est appele
|
+-- Vous pouvez ajuster vos positions
4. OnData() recoit les données des actions de l'univers actuel
Optimisations importantes
1. Utiliser HasFundamentalData pour eviter les ETFs et ADRs :
filtered = [x for x in coarse
if x.HasFundamentalData # Exclut ETFs, ADRs
and x.Price > 10
and x.DollarVolume > 10000000]2. Limiter la frequence de reselection :
def Initialize(self):
# Reselection mensuelle au lieu de quotidienne
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction)
self.last_month = -1
def CoarseSelectionFunction(self, coarse):
# Ne re-sélectionner qu'en debut de mois
if self.Time.month == self.last_month:
return Universe.Unchanged
self.last_month = self.Time.month
# Logique de filtrage...Comprendre le flux de Coarse Selection
Le flux de données Coarse est quotidien et agrégé :
- J-1, 23:00 UTC : QC calcule les métriques coarse (volume, market cap, prix de clôture ajusté).
- J, 00:00 UTC :
CoarseSelectionFunctionest appelée avec les données J-1. - J, 09:30 ET :
OnDataest appelée avec les données intraday J, et l’univers coarse est appliqué.
Important : il y a un décalage de 1 jour entre les données Coarse et le trading. C’est pourquoi les algos quantitatifs institutionnels passent leurs ordres à l’ouverture (9:30-10:00 ET) et non à la clôture.
Optimisations importantes (suite)
- Cache des données coarse sur plusieurs jours pour éviter les recalculs.
- Filtrer les ETF dès Coarse si votre algo ne trade que des actions (gain de temps).
- Utiliser
self.Logpour tracer la taille de l’univers coarse → fine → finale (debugging rapide).
Piège classique : universe trop large
Un Coarse qui retourne 1000 actions est trop large pour un backtest rapide. Réduisez à 50-200 avant Fine Selection. Si votre filtre coarse ne réduit pas assez, affinez vos critères (price > $20, volume > $5M, etc.).
Lecture critique : un bon Coarse Selection réduit l’univers de 80% sans perdre les actions qui vont performer. Trop laxiste = bruit ; trop strict = vous ratez les winners.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Coarse Selection avec reselection mensuelle
# Optimisation pour reduire le temps de backtest
from AlgorithmImports import *
class MonthlyCoarseUniverse(QCAlgorithm):
"""
Coarse selection avec rebalancement mensuel.
Evite de recalculer l'univers chaque jour.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
self.AddUniverse(self.CoarseSelectionFunction)
self.num_stocks = 50
self.last_month = -1 # Pour tracker le mois
def CoarseSelectionFunction(self, coarse):
# Verifier si on est dans un nouveau mois
if self.Time.month == self.last_month:
# Pas de changement -> retourner Universe.Unchanged
return Universe.Unchanged
# Nouveau mois -> mettre a jour
self.last_month = self.Time.month
self.Log(f"Monthly reselection: {self.Time.strftime('%Y-%m')}")
# Filtrage standard
filtered = [x for x in coarse
if x.HasFundamentalData
and x.Price > 5
and x.DollarVolume > 5000000]
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in sorted_stocks[:self.num_stocks]]Backtest QC Cloud : MonthlyCoarseUniverse
| Résultat | End Equity |
|---|---|
| Exploration (0 ordre) | $100,000.00 |
Monthly coarse reselection, 2020-2024
Résultats backtest QC Cloud : MonthlyCoarseUniverse (2015-2024, $100k) — 0 ordres, Net Profit 0%. Demo reselection mensuelle avec Universe.Unchanged. Pas de logique de trading dans OnData.
4. Fine Universe Sélection (20 min)
Filtrage avec données fondamentales
Le Fine Universe Sélection ajoute une deuxieme couche de filtrage en utilisant les données fondamentales de Morningstar :
- Valorisation : P/E Ratio, P/B Ratio, EV/EBITDA
- Taille : Market Cap, Revenue, Total Assets
- Classification : Secteur, Industrie, Morningstar Style
- Dividendes : Dividend Yield, Payout Ratio
Architecture Coarse + Fine
def Initialize(self):
# Coarse PUIS Fine
self.AddUniverse(self.CoarseSelectionFunction,
self.FineSelectionFunction)
def CoarseSelectionFunction(self, coarse):
# Premier filtre: prix, volume
# Retourne candidats pour Fine
return [x.Symbol for x in filtered]
def FineSelectionFunction(self, fine):
# Deuxieme filtre: données fondamentales
# fine = seulement les actions selectionnees par Coarse
return [x.Symbol for x in filtered]Proprietes de FineFundamental
| Catégorie | Propriete | Exemple d’acces |
|---|---|---|
| Valorisation | P/E Ratio | x.ValuationRatios.PERatio |
| P/B Ratio | x.ValuationRatios.PBRatio |
|
| EV/EBITDA | x.ValuationRatios.EVToEBITDA |
|
| Taille | Market Cap | x.MarketCap |
| Revenue | x.FinancialStatements.IncomeStatement.TotalRevenue.Value |
|
| Classification | Secteur Morningstar | x.AssetClassification.MorningstarSectorCode |
| Industrie | x.AssetClassification.MorningstarIndustryCode |
|
| Dividendes | Dividend Yield | x.ValuationRatios.TrailingDividendYield |
4. Fine Universe Selection (20 min)
Le Fine Universe est le deuxième niveau de filtrage. Il s’exécute après Coarse et applique des critères fondamentaux sur les actions retenues par Coarse.
Données accessibles via Fine : - Ratios financiers : P/E, P/B, EV/EBITDA, ROE, ROA - Profitabilité : marges (brute, nette, opérationnelle), croissance revenus - Bilan : dette/capitaux, current ratio, cash flow - Secteur Morningstar, industrie, style (growth/value/blend) - Pays d’origine, exchange
Architecture Coarse + Fine
L’architecture classique est un pipeline à 2 étages :
Universe complet (8000+ actions)
↓ Coarse Selection (prix, liquidité)
Filtered (~200-500)
↓ Fine Selection (fondamentales)
Final Universe (~10-50)
↓ OnData (signal)
Ordres
Pourquoi ce pipeline : 1. Performance : Fine est 3-10× plus lent que Coarse. Si on l’applique aux 8000 actions, le backtest explose. Coarse pré-filtrer réduit le scope. 2. Logique : un filtre “P/E < 15” n’a de sens que sur des actions liquides (sinon on trade du penny stock avec données fondamental absentes). 3. Données manquantes : Fine retourne None pour les actions sans fondamentales (small caps, ETF, IPO récentes). Coarse en amont évite ces None bizarres.
Codes Morningstar Sector
Les codes secteurs Morningstar sont des constantes entières :
| Secteur | Code |
|---|---|
| Basic Materials | 101 |
| Consumer Cyclical | 102 |
| Financial Services | 103 |
| Real Estate | 104 |
| Consumer Defensive | 205 |
| Healthcare | 206 |
| Utilities | 207 |
| Communication Services | 308 |
| Energy | 309 |
| Industrials | 310 |
| Technology | 311 |
Exemple : Sélection multi-critères
Critères typiques d’un value investor quant : - ValuationRatios.PERatio < 15 - ValuationRatios.PBRatio < 2 - OperationRatios.ROE > 0.15 - AssetClassification.MorningstarSectorCode == 311 (Technology) - HasFundamentalData == True
Résultat typique : 5-20 actions retenues sur 200-500 après Coarse.
Pédagogie : plus le filtre est restrictif, plus le turnover est faible (les actions qui passent tous les critères changent rarement). Un turnover faible = moins de frais = meilleur Sharpe net.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Fine Universe Selection : Actions Tech avec P/E < 30
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
class TechValueUniverse(QCAlgorithm):
"""
Selection d'actions du secteur technologie avec P/E raisonnable.
Coarse: Volume > 10M$, Prix > 10$
Fine: Secteur Tech, 0 < P/E < 30
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Universe avec Coarse ET Fine
self.AddUniverse(self.CoarseSelectionFunction,
self.FineSelectionFunction)
self.num_coarse = 500 # Candidats pour Fine
self.num_fine = 20 # Selection finale
self.Log("Tech Value Universe initialized")
def CoarseSelectionFunction(self, coarse):
"""Premier filtre: liquidite et HasFundamentalData."""
# Filtrer les actions avec donnees fondamentales
filtered = [x for x in coarse
if x.HasFundamentalData # IMPORTANT pour Fine
and x.Price > 10
and x.DollarVolume > 10000000]
# Trier par volume et prendre les top N
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
self.Log(f"Coarse: {len(coarse)} -> {len(filtered)} filtered -> {min(len(sorted_stocks), self.num_coarse)} candidates")
return [x.Symbol for x in sorted_stocks[:self.num_coarse]]
def FineSelectionFunction(self, fine):
"""Deuxieme filtre: secteur et valorisation."""
# Filtrer par secteur: Technology
tech_stocks = [x for x in fine
if x.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.Technology]
# Filtrer par P/E: entre 0 et 30 (exclure negatif et surevaloue)
value_stocks = [x for x in tech_stocks
if x.ValuationRatios.PERatio > 0
and x.ValuationRatios.PERatio < 30]
# Trier par P/E croissant (plus bas = meilleur)
sorted_by_pe = sorted(value_stocks,
key=lambda x: x.ValuationRatios.PERatio)
self.Log(f"Fine: {len(fine)} -> {len(tech_stocks)} tech -> {len(value_stocks)} P/E<30 -> {min(len(sorted_by_pe), self.num_fine)} selected")
return [x.Symbol for x in sorted_by_pe[:self.num_fine]]
def OnSecuritiesChanged(self, changes):
"""Rebalancer quand l'univers change."""
# Vendre les actions retirees
for security in changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol)
self.Log(f"Removed: {security.Symbol.Value}")
# Acheter les nouvelles actions (equal weight)
if len(changes.AddedSecurities) > 0:
weight = 1.0 / max(len(self.ActiveSecurities), 1)
for security in changes.AddedSecurities:
self.SetHoldings(security.Symbol, weight)
self.Log(f"Added: {security.Symbol.Value} -> {weight:.1%}")Backtest QC Cloud : TechValueUniverse
| Résultat | End Equity |
|---|---|
| Exploration (0 ordre) | $100,000.00 |
Coarse+Fine tech/value sélection, 2020-2024
Résultats backtest QC Cloud : TechValueUniverse (2015-2024, $100k) — 0 ordres, Net Profit 0%. Demo Coarse+Fine Sélection (Tech sector, P/E < 30). La sélection fonctionne mais pas de logique de trading dans OnData.
Codes Morningstar Sector
Les secteurs Morningstar sont accessibles via MorningstarSectorCode :
| Code | Secteur | Exemples |
|---|---|---|
BasicMaterials |
Materiaux de base | Mining, Chimie |
ConsumerCyclical |
Conso. cyclique | Retail, Automobile |
FinancialServices |
Services financiers | Banques, Assurances |
RealEstate |
Immobilier | REITs |
ConsumerDefensive |
Conso. defensive | Alimentation, Pharma |
Healthcare |
Sante | Biotech, Hopitaux |
Utilities |
Services publics | Electricite, Gaz |
CommunicationServices |
Communication | Telecom, Media |
Energy |
Energie | Petrole, Gaz |
Industrials |
Industrie | Aerospace, Transport |
Technology |
Technologie | Software, Hardware |
Exemple: Sélection multi-critères
Codes Morningstar Sector — détails
Les codes Morningstar Sector sont organisés en 3 groupes de 3 chiffres chacun. Les secteurs cycliques (101-104) répondent à la croissance économique ; les défensifs (205-207) y sont moins sensibles ; les sensibles au taux (308-311) sont très corrélés aux taux d’intérêt.
Sector vs Industry : chaque secteur Morningstar contient ~10-15 industries. Par exemple, Technology (311) contient Software, Hardware, Semiconductors. Si votre filtre cible un sous-secteur, utilisez MorningstarIndustryCode au lieu de MorningstarSectorCode.
Exemple : Sélection multi-critères (détaillée)
Voici la décomposition d’un filtre Fine classique :
def FineSelectionFunction(self, fine):
filtered = [x for x in fine
if x.ValuationRatios.PERatio < 15
and x.ValuationRatios.PBRatio < 2
and x.OperationRatios.ROE > 0.15
and x.AssetClassification.MorningstarSectorCode == 311
and x.HasFundamentalData]
return [x.Symbol for x in filtered]Pédagogie : ce filtre est conservateur — il cherche des actions : - Value (P/E < 15, P/B < 2) - Profitables (ROE > 15%) - Tech (secteur 311)
Résultat attendu : 5-15 actions sur 8000. C’est typique d’un value tech style Buffett-tech.
Anti-pattern : trop de critères
Un filtre avec 8 critères AND ensemble peut retourner 0 action la plupart du temps → signal absent. Commencez avec 2-3 critères, validez qu’ils sont corrélés (P/E et P/B le sont souvent), puis ajoutez 1 critère à la fois.
Métrique de validation : après backtest, vérifiez que la cardinalité moyenne de l’univers final est 5-30 actions. Trop peu (< 3) = signal fragile, trop (> 50) = filtre inefficace.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Fine Selection avec criteres multiples
# Large Cap + Dividende + Low P/E
from AlgorithmImports import *
class DividendValueUniverse(QCAlgorithm):
"""
Selection: Large Cap + Dividend Yield > 2% + P/E < 20
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
self.AddUniverse(self.CoarseSelectionFunction,
self.FineSelectionFunction)
# Criteres
self.min_market_cap = 10e9 # 10B$ minimum
self.min_dividend_yield = 0.02 # 2% minimum
self.max_pe_ratio = 20 # P/E max
self.num_stocks = 30
def CoarseSelectionFunction(self, coarse):
filtered = [x for x in coarse
if x.HasFundamentalData
and x.Price > 5
and x.DollarVolume > 5000000]
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in sorted_stocks[:500]]
def FineSelectionFunction(self, fine):
# Filtre 1: Large Cap (Market Cap > 10B$)
large_cap = [x for x in fine if x.MarketCap > self.min_market_cap]
# Filtre 2: Dividend Yield > 2%
dividend_payers = [x for x in large_cap
if x.ValuationRatios.TrailingDividendYield is not None
and x.ValuationRatios.TrailingDividendYield > self.min_dividend_yield]
# Filtre 3: P/E entre 0 et 20
value_stocks = [x for x in dividend_payers
if x.ValuationRatios.PERatio is not None
and 0 < x.ValuationRatios.PERatio < self.max_pe_ratio]
# Trier par dividend yield (plus haut = meilleur)
sorted_by_yield = sorted(value_stocks,
key=lambda x: x.ValuationRatios.TrailingDividendYield,
reverse=True)
self.Log(f"Fine: {len(fine)} total -> {len(large_cap)} large -> {len(dividend_payers)} dividend -> {len(value_stocks)} value")
return [x.Symbol for x in sorted_by_yield[:self.num_stocks]]Backtest QC Cloud : DividendValueUniverse
| Résultat | End Equity |
|---|---|
| Exploration (0 ordre) | $100,000.00 |
Coarse+Fine dividend/value sélection, 2020-2024
Résultats backtest QC Cloud : DividendValueUniverse (2015-2024, $100k) — 0 ordres, Net Profit 0%. Demo Fine Sélection multi-critères (Large Cap + Dividend Yield > 2% + P/E < 20). Pas de logique OnData pour le trading.
Exercice 2 : Filtre fondamental personnalise par secteur
En vous basant sur les exemples TechValueUniverse et DividendValueUniverse ci-dessus, créez un algorithme qui selectionne des actions selon des critères fondamentaux personnalises dans un secteur spécifique.
Objectif : implementer SectorValueUniverse avec les specifications suivantes :
- Coarse : prix > 5 \(, DollarVolume > 8 M\)/jour,
HasFundamentalData = True, trier par DollarVolume, garder les 400 meilleurs - Fine : secteur Healthcare (
MorningstarSectorCode.Healthcare), P/B Ratio entre 0.5 et 3, Market Cap > 2 B$, trier par P/B croissant, garder les 15 premières actions - Rebalancement : dans
OnSecuritiesChanged, liquidate les sortants et equal-weight sur les entrants
Indices : - x.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.Healthcare pour filtrer le secteur sante - x.ValuationRatios.PBRatio pour le Price-to-Book ratio - x.MarketCap pour la capitalisation boursiere (en dollars) - Verifiez que PBRatio n’est pas None et est strictement positif avant de comparer - Inspirez-vous du pattern TechValueUniverse pour la structure Coarse+Fine
Exercice 2 : Filtre fondamental personnalisé par secteur
Objectif : Implémenter un filtre Fine qui combine 2 ratios fondamentaux ET un filtre sectoriel.
Contexte : Vous voulez backtester une stratégie value sur les secteurs cycliques (Basic Materials + Consumer Cyclical + Financial Services). Vous pensez que les secteurs cycliques sous-performent en moyenne mais ont des épisodes de rebound après une récession.
Travail demandé : 1. Implémenter FineSelectionFunction qui filtre : - ValuationRatios.PERatio < 12 (value stricte) - OperationRatios.ROE > 0.10 (rentabilité minimale) - AssetClassification.MorningstarSectorCode in [101, 102, 103] 2. Retourner les 10-20 meilleurs (tri par ROE décroissant). 3. Backtester sur 2015-2024, rebalance mensuel.
Indices : - Utiliser sorted(filtered, key=lambda x: x.OperationRatios.ROE, reverse=True)[:20] - Ne pas oublier HasFundamentalData (sinon None pour petites caps). - Tracker le nombre d’actions retenues dans self.universe_size pour debugging.
Pédagogie : cet exercice illustre deux trade-offs : 1. Valeur stricte (P/E < 12) : capte les value traps (P/E bas car earnings en chute). À surveiller avec un filtre supplémentaire (ROE > 10% pour exclure les bankruptcies latentes). 2. Cycliques : ces secteurs ont un bêta élevé (1.2-1.5). Le backtest va probablement montrer un DD plus profond que sur des secteurs défensifs. C’est attendu, pas un bug.
Validation attendue : CAGR 8-12% (en dessus de SPY ~10%), DD -20% à -30% (au-dessus de SPY ~-25%), Sharpe 0.5-0.7. Si DD > -35%, le filtre n’élimine pas assez les value traps.
# [REFERENCE QC] Exercice 2 : Filtre fondamental personnalise par secteur
# A completer et copier dans l'IDE QuantConnect
from AlgorithmImports import *
class SectorValueUniverse(QCAlgorithm):
"""
TODO etudiant : implementer un universe Healthcare avec filtres
P/B Ratio et Market Cap.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# TODO etudiant : ajouter l'univers avec Coarse + Fine selection
# self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.num_coarse = 400
self.num_fine = 15
def CoarseSelectionFunction(self, coarse):
# TODO etudiant : filtrer par HasFundamentalData, Price > 5, DollarVolume > 8M
# Etape 1 : filtrer
# Etape 2 : trier par DollarVolume decroissant
# Etape 3 : retourner top 400
return None # TODO etudiant : retourner la liste de Symbol
def FineSelectionFunction(self, fine):
# TODO etudiant : filtrer par secteur Healthcare, P/B entre 0.5 et 3, MarketCap > 2B
# Etape 1 : secteur Healthcare (MorningstarSectorCode.Healthcare)
# Etape 2 : P/B Ratio entre 0.5 et 3 (verifier None)
# Etape 3 : Market Cap > 2 milliards
# Etape 4 : trier par P/B croissant, garder 15
return None # TODO etudiant : retourner la liste de Symbol
def OnSecuritiesChanged(self, changes):
# TODO etudiant : liquidate sortants, equal-weight entrants
pass
print("Exercice a completer : SectorValueUniverse")5. Universe Settings (10 min)
Configurer le comportement de l’univers
UniverseSettings permet de configurer comment QuantConnect gere les actions ajoutees dynamiquement a l’univers.
Proprietes de UniverseSettings
| Propriete | Type | Description | Defaut |
|---|---|---|---|
Resolution |
Resolution | Resolution des données | Minute |
Leverage |
float | Levier maximum | 2.0 |
FillForward |
bool | Remplir les données manquantes | True |
ExtendedMarketHours |
bool | Inclure pre/post-market | False |
MinimumTimeInUniverse |
TimeSpan | Duree minimum avant retrait | 1 jour |
DataNormalizationMode |
Mode | Ajustement des prix | Adjusted |
5. Universe Settings (10 min)
Les Universe Settings contrôlent le comportement du pipeline de sélection d’univers : à quelle fréquence tourne Fine, comment les actions manquantes sont gérées, etc.
Propriétés de UniverseSettings
| Propriété | Défaut | Rôle |
|---|---|---|
Resolution |
Resolution.Minute |
Granularité des données |
Leverage |
1.0 | Levier max |
DataNormalizationMode |
Adjusted |
Split/dividendes ajustés |
MinimumOrderMarginPortfolioPercentage |
0.0 | Marge min par ordre |
Settings.FreePortfolioValue |
0 | Cash à ne pas investir |
Pédagogie : la propriété Resolution est critique pour les performances. Si votre algo trade sur des données daily mais que vous réglez Resolution.Minute, vous payez le coût de stockage et de calcul des données intraday pour rien. Adapter la résolution à la stratégie :
- Stratégie daily / weekly :
Resolution.Daily - Stratégie intraday (HFT, scalping) :
Resolution.MinuteouResolution.Second
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Configuration de UniverseSettings
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
class UniverseSettingsExample(QCAlgorithm):
"""
Demonstration des UniverseSettings.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# === UNIVERSE SETTINGS ===
# 1. Resolution des donnees pour les nouvelles securities
# Daily = plus rapide, Minute = plus precis
self.UniverseSettings.Resolution = Resolution.Daily
# 2. Levier maximum autorise
# 1.0 = pas de levier, 2.0 = x2 max
self.UniverseSettings.Leverage = 1.0
# 3. Fill Forward : remplir les jours sans donnees
# avec la derniere valeur connue (utile pour actions peu liquides)
self.UniverseSettings.FillForward = True
# 4. Extended Market Hours : inclure pre-market (4am) et after-hours (8pm)
# False = seulement 9:30am - 4:00pm
self.UniverseSettings.ExtendedMarketHours = False
# 5. Minimum Time In Universe : eviter le churn
# Une action reste au moins 1 jour dans l'univers
self.UniverseSettings.MinimumTimeInUniverse = timedelta(days=1)
# 6. Data Normalization : ajustement des prix
# Adjusted = ajuste pour splits/dividendes (recommande)
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Adjusted
# Ajouter l'univers
self.AddUniverse(self.CoarseSelectionFunction)
self.Log("Universe settings configured:")
self.Log(f" Resolution: {self.UniverseSettings.Resolution}")
self.Log(f" Leverage: {self.UniverseSettings.Leverage}")
self.Log(f" FillForward: {self.UniverseSettings.FillForward}")
self.Log(f" ExtendedMarketHours: {self.UniverseSettings.ExtendedMarketHours}")
def CoarseSelectionFunction(self, coarse):
filtered = [x for x in coarse
if x.HasFundamentalData
and x.Price > 10
and x.DollarVolume > 10000000]
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in sorted_stocks[:50]]Backtest QC Cloud : UniverseSettingsExample
| Résultat | End Equity |
|---|---|
| Exploration (0 ordre) | $100,000.00 |
UniverseSettings demo, 2020-2024
Résultats backtest QC Cloud : UniverseSettingsExample (2015-2024, $100k) — 0 ordres, Net Profit 0%. Demo de configuration uniquement (pas de logique de trading). Resolution Daily, Leverage 1.0, FillForward True, DataNormalization Adjusted.
Quand modifier les settings?
| Scénario | Setting recommande |
|---|---|
| Backtest rapide | Resolution.Daily |
| Stratégie intraday | Resolution.Minute ou Hour |
| Sans levier | Leverage = 1.0 |
| Actions illiquides | FillForward = True |
| Scalping pre-market | ExtendedMarketHours = True |
| Eviter overtrading | MinimumTimeInUniverse = timedelta(days=7) |
Quand modifier les settings ?
Les Universe Settings se modifient dans Initialize AVANT d’ajouter l’univers. Les cas typiques :
Stratégie daily sur données daily :
self.UniverseSettings.Resolution = Resolution.Dailypour éviter de charger les données intraday inutiles.Levier : si vous voulez trader avec 2× levier,
self.UniverseSettings .Leverage = 2.0. Attention : QC supporte le levier, mais le margin call automatique si la perte dépasse la marge.FreePortfolioValuePercentage : laisse X% du portefeuille en cash pour absorber les drawdowns sans vendre en perte. Typique 5-10%.
DataNormalizationMode :
Adjusted(défaut, ajuste splits/ dividendes) vsRaw(prix bruts). Pour le backtest, toujours Adjusted sauf cas particulier (tester une stratégie event-driven sur splits).
Piège classique : Resolution mismatch
Si votre OnData traite des données daily mais que vous avez oublié de configurer UniverseSettings.Resolution = Resolution.Daily, QC charge les minute bars et appelle OnData à chaque minute. Le backtest prend 300× plus de temps (390 minutes vs 6.5h de marché × 252 jours × 10 ans).
Symptôme : backtest qui prend plusieurs heures sans raison apparente. Solution : vérifier la Resolution en premier.
6. Dynamic Rebalancing (15 min)
Gerer les changements d’univers avec OnSecuritiesChanged
Quand l’univers change (nouvelle action ajoutee ou retiree), QuantConnect appelle OnSecuritiesChanged(changes). C’est ici que vous gerez le rebalancement.
Structure de SecurityChanges
def OnSecuritiesChanged(self, changes):
# changes.AddedSecurities : liste des nouvelles actions
# changes.RemovedSecurities : liste des actions retirees
for security in changes.RemovedSecurities:
# Vendre la position
pass
for security in changes.AddedSecurities:
# Acheter la nouvelle action
passProprietes de Security
| Propriete | Type | Description |
|---|---|---|
Symbol |
Symbol | Identifiant de l’action |
Invested |
bool | True si position ouverte |
Price |
float | Prix actuel |
Holdings |
SecurityHolding | Details de la position |
6. Dynamic Rebalancing (15 min)
Le rebalancement dynamique gère les situations où l’univers change pendant la période de détention : une action sort de l’univers (ne passe plus le filtre), une nouvelle action entre, une fusion/acquisition modifie les symboles.
Gérer les changements d’univers avec OnSecuritiesChanged
QC expose deux callbacks : - OnSecuritiesChanged(self, changes) : appelé quand l’univers change. changes.AddedSecurities et changes.RemovedSecurities contiennent les Security ajoutées/retirées.
Pattern classique :
def OnSecuritiesChanged(self, changes):
for security in changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol)
for security in changes.AddedSecurities:
if not self.Portfolio[security.Symbol].Invested:
self.SetHoldings(security.Symbol, 0.05) # 5% par positionStructure de SecurityChanges
SecurityChanges contient 3 propriétés : - AddedSecurities : nouvelles actions dans l’univers - RemovedSecurities : actions sorties de l’univers - (parfois) rien pour les actions inchangées
Pédagogie : le liquidate sur action retirée est critique. Si une action sort de votre filtre (ex: son P/E passe à 25), votre algo doit vendre immédiatement. Sinon vous continuez à détenir une action qui ne respecte plus vos critères.
Anti-pattern : ignorer OnSecuritiesChanged
L’erreur classique du débutant : oublier OnSecuritiesChanged et utiliser seulement OnData. Résultat : votre algo continue à détenir des actions sorties de l’univers, contaminant le backtest avec des positions hors-critères. Toujours implémenter les deux callbacks.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Dynamic Rebalancing avec OnSecuritiesChanged
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
class DynamicRebalancingAlgorithm(QCAlgorithm):
"""
Rebalancement automatique lors des changements d'univers.
- Liquidate les actions retirees
- Acheter les nouvelles actions en equal-weight
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction)
self.num_stocks = 20
self.last_month = -1
def CoarseSelectionFunction(self, coarse):
# Reselection mensuelle
if self.Time.month == self.last_month:
return Universe.Unchanged
self.last_month = self.Time.month
filtered = [x for x in coarse
if x.HasFundamentalData
and x.Price > 10
and x.DollarVolume > 10000000]
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in sorted_stocks[:self.num_stocks]]
def OnSecuritiesChanged(self, changes):
"""
Appele automatiquement quand l'univers change.
Gere le rebalancement du portfolio.
"""
# Etape 1: Liquider les positions retirees
for security in changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol)
self.Log(f"SELL (removed): {security.Symbol.Value}")
# Etape 2: Calculer le poids cible
# Utiliser ActiveSecurities pour avoir le nombre actuel
count = len(self.ActiveSecurities)
if count == 0:
return
target_weight = 1.0 / count
# Etape 3: Rebalancer toutes les positions
for symbol in self.ActiveSecurities.Keys:
self.SetHoldings(symbol, target_weight)
self.Log(f"Rebalanced: {count} securities @ {target_weight:.1%} each")
def OnData(self, data):
# Pas de logique supplementaire ici
# Le rebalancement est gere par OnSecuritiesChanged
passRésultats backtest QC Cloud : DynamicRebalancingAlgorithm (2015-2024, $100k) — 2461 ordres, Net Profit +179.7%, Sharpe 0.406, CAGR 10.8%, MaxDD 38.7%, Win Rate 72%. Top 20 mensuel, equal-weight via OnSecuritiesChanged.
Patterns de rebalancement
1. Equal-Weight (ci-dessus) :
weight = 1.0 / len(self.ActiveSecurities)
for symbol in self.ActiveSecurities.Keys:
self.SetHoldings(symbol, weight)2. Market-Cap Weighted :
total_cap = sum(s.Fundamentals.MarketCap for s in self.ActiveSecurities.Values)
for security in self.ActiveSecurities.Values:
weight = security.Fundamentals.MarketCap / total_cap
self.SetHoldings(security.Symbol, weight)3. Volatility-Weighted (Risk Parity) :
# Calcul des volatilites historiques
# Allouer plus aux actions moins volatilesEviter le over-trading
Patterns de rebalancement
Trois patterns courants de rebalancement quand l’univers change :
Liquidate + re-allocation : vendre toutes les positions retirées, recalculer les poids cibles sur le nouvel univers, racheter. Simple mais génère beaucoup de frais si l’univers change souvent.
Hold-and-rotate : garder les positions actuelles qui sont toujours dans l’univers, ajouter les nouvelles, vendre uniquement les retirées. Moins de frais, mais dérive des poids cibles.
Buffer zone : définir une marge autour des critères (ex : P/E entre 12 et 18 = keep, P/E > 18 ou P/E < 12 = rotate). Réduit le turnover sans sacrifier le signal.
Calcul des volatilités historiques
Pour un rebalancement risk-parity :
history = self.History(symbol, 60, Resolution.Daily)
vol = history['close'].pct_change().std() * (252 ** 0.5)Le calcul prend les 60 derniers jours de prix, calcule les returns quotidiens, et annualise la volatilité (×√252).
Allouer plus aux actions moins volatiles
Inverse volatility weighting :
weights = {sym: 1/vol for sym, vol in vols.items()}
total = sum(weights.values())
weights = {sym: w/total for sym, w in weights.items()}Pédagogie : cette allocation égalise la contribution au risque de chaque position. Une action à 30% de vol reçoit 2× moins de capital qu’une action à 15% de vol. C’est le principe de Risk Parity appliqué à un seul actif.
Limite : suppose que la volatilité est stationnaire, ce qui n’est pas le cas en stress (cf QC-Py-08 Multi-Asset).
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Rebalancement intelligent avec seuil de tolerance
# Evite de rebalancer pour de petites variations
from AlgorithmImports import *
class SmartRebalancingAlgorithm(QCAlgorithm):
"""
Rebalancement avec seuil de tolerance.
Ne rebalance que si la deviation depasse 5%.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction)
self.num_stocks = 20
self.tolerance = 0.05 # 5% de deviation max
self.last_month = -1
def CoarseSelectionFunction(self, coarse):
if self.Time.month == self.last_month:
return Universe.Unchanged
self.last_month = self.Time.month
filtered = [x for x in coarse
if x.HasFundamentalData and x.Price > 10 and x.DollarVolume > 10000000]
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
return [x.Symbol for x in sorted_stocks[:self.num_stocks]]
def OnSecuritiesChanged(self, changes):
# Liquider les retirees
for security in changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol)
# Rebalancer avec tolerance
self.RebalanceWithTolerance()
def RebalanceWithTolerance(self):
"""Rebalance seulement si deviation > tolerance."""
count = len(self.ActiveSecurities)
if count == 0:
return
target_weight = 1.0 / count
portfolio_value = self.Portfolio.TotalPortfolioValue
for symbol in self.ActiveSecurities.Keys:
holding = self.Portfolio[symbol]
current_weight = holding.HoldingsValue / portfolio_value if portfolio_value > 0 else 0
# Rebalancer seulement si deviation > tolerance
if abs(current_weight - target_weight) > self.tolerance:
self.SetHoldings(symbol, target_weight)
self.Debug(f"Rebalanced {symbol.Value}: {current_weight:.1%} -> {target_weight:.1%}")Résultats backtest QC Cloud : SmartRebalancingAlgorithm (2015-2024, $100k) — 83 ordres, Net Profit +148.1%, Sharpe 0.476, CAGR 9.5%, MaxDD 29.2%, Win Rate 59%. Top 20 avec tolerance 5%, rebalancement uniquement si deviation significative. Moins d’ordres = moins de frais.
Exercice 3 : Universe combine avec filtre qualite
En vous basant sur les exemples de Coarse + Fine Sélection et de rebalancement intelligent vus precedemment, créez un algorithme qui combine un filtre de liquidite (Coarse) avec un filtre de qualite fondamentale (Fine).
Objectif : implementer QualityUniverseAlgorithm avec les specifications suivantes :
- Coarse : prix > 10 \(, DollarVolume > 5 M\)/jour,
HasFundamentalData = True - Fine : ROE (Return on Equity) > 15 %, Debt-to-Equity < 2, P/E entre 5 et 25
- Sélection : trier par MarketCap decroissant, garder les 25 premières actions
- Rebalancement : mensuel avec
Universe.Unchangedles autres jours
Indices : - x.FinancialStatements.BalanceSheet.TotalAssets.Value pour les actifs totaux - x.FinancialStatements.BalanceSheet.TotalLiabilities.AsQuaterly.Value pour le passif total - x.ValuationRatios.PERatio pour le P/E ratio - x.EarningRatios.ROE ou x.OperationRatios.ROE.ThreeMonth pour le ROE - Pensez a verifier que les valeurs ne sont pas None avant de les comparer
Exercice 3 : Universe combiné avec filtre qualité
Objectif : Combiner Manual + Fine Universe dans une même stratégie avec un filtre qualité additionnel (ROE > 15%).
Contexte : Vous voulez backtester une stratégie “quality value” sur 30 actions de grande capitalisation US. Vous partez d’une liste manuelle (les 30 plus grosses caps), vous filtrez Fine sur ROE > 15% ET P/E < 18, et vous rebalancez trimestriellement.
Travail demandé : 1. Définir un Manual Universe de 30 symboles (top 30 SPY par exemple : AAPL/MSFT/GOOGL/AMZN/NVDA/META/…). 2. Ajouter un FineSelectionFunction qui filtre ROE > 0.15 ET P/E < 18. 3. Implémenter OnSecuritiesChanged pour liquidate les positions retirées. 4. Rebalance trimestriellement (1er jan/avr/jui/oct).
Indices : - Stocker self.rebalance_quarter = self.Time.month in [1, 4, 7, 10]. - Utiliser self.SetHoldings(symbol, 1/N) pour equal-weight sur N actions retenues. - Tracker dans self.universe_size le nombre d’actions après filtre.
Pédagogie : cet exercice illustre 3 concepts combinés : 1. Manual Universe (liste connue) + Fine Selection (filtre dynamique sur fondamentales) = pipeline hybride. 2. OnSecuritiesChanged (gestion des rotations) — sans cela, le backtest dérive. 3. Rebalance trimestriel (compromis frais/signal) — le quotidien serait trop de frais, l’annuel trop lent à réagir.
Validation attendue : CAGR 9-13% (typique quality value), DD -15% à -20% (moins que SPY car filtre qualité), Sharpe 0.7-0.9. Si Sharpe < 0.5, le filtre qualité n’apporte pas de signal discriminant.
# [REFERENCE QC] Exercice 3 : Universe combine avec filtre qualite
# A completer et copier dans l'IDE QuantConnect
from AlgorithmImports import *
class QualityUniverseAlgorithm(QCAlgorithm):
"""
TODO etudiant : implementer un universe combine Coarse + Fine
avec filtres de qualite fondamentale (ROE, Debt/Equity, P/E).
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# TODO etudiant : configurer UniverseSettings (Resolution, Leverage)
# TODO etudiant : ajouter l'univers avec Coarse + Fine selection
# self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
# Parametres
self.num_stocks = 25
self.last_month = -1
def CoarseSelectionFunction(self, coarse):
# TODO etudiant : implementer le filtre Coarse
# Etape 1 : verifier si nouveau mois (sinon return Universe.Unchanged)
# Etape 2 : filtrer par HasFundamentalData, Price > 10, DollarVolume > 5M
# Etape 3 : trier par DollarVolume decroissant, retourner top 500
return None # TODO etudiant : retourner la liste de Symbol
def FineSelectionFunction(self, fine):
# TODO etudiant : implementer le filtre Fine
# Etape 1 : filtrer par ROE > 15% (verifier que la valeur n'est pas None)
# Etape 2 : filtrer par Debt-to-Equity < 2
# Etape 3 : filtrer par P/E entre 5 et 25
# Etape 4 : trier par MarketCap decroissant, retourner top 25
return None # TODO etudiant : retourner la liste de Symbol
def OnSecuritiesChanged(self, changes):
# TODO etudiant : gerer le rebalancement (liquidate sortants, equal-weight entrants)
pass
print("Exercice a completer : QualityUniverseAlgorithm")7. Scheduled Universe Sélection (10 min)
Planifier le rebalancement periodique
Au lieu de rebalancer a chaque changement d’univers, vous pouvez planifier des rebalancements a dates fixes avec self.Schedule.On().
Schedule.On() pour rebalancement periodique
# Rebalancement le premier jour du mois a 10h
self.Schedule.On(
self.DateRules.MonthStart("SPY"), # Date rule
self.TimeRules.At(10, 0), # Time rule
self.Rebalance # Action
)DateRules disponibles
| DateRule | Description | Exemple |
|---|---|---|
EveryDay() |
Tous les jours | Rebalancement quotidien |
Every(day) |
Jours spécifiques | Every(DayOfWeek.Monday) |
MonthStart(symbol) |
Premier jour de bourse du mois | Rebalancement mensuel |
MonthEnd(symbol) |
Dernier jour du mois | |
WeekStart(symbol) |
Premier jour de la semaine | Rebalancement hebdo |
WeekEnd(symbol) |
Dernier jour de la semaine |
7. Scheduled Universe Selection (10 min)
Le Scheduled Universe Selection combine les forces de Coarse et Fine avec un rebalancement planifié. Au lieu de tourner quotidiennement (Coarse) ou à chaque Fundamental update (Fine), on tourne à intervalle fixe : hebdomadaire, mensuel, trimestriel.
Planifier le rebalancement périodique
self.Schedule.On(self.DateRules.MonthStart("SPY"),
self.TimeRules.AfterMarketOpen("SPY", 30),
self.Rebalance)Trois composantes : 1. DateRules.MonthStart("SPY") : quand (premier jour de bourse du mois). 2. TimeRules.AfterMarketOpen("SPY", 30) : à quelle heure (30 min après l’open de SPY). 3. self.Rebalance : la méthode à appeler (qui contient Coarse + Fine).
Piège : MonthStart() sans symbole saute des mois entiers.
DateRules.MonthStart() (sans argument) cale l’événement sur le 1er du mois calendaire : si ce 1er tombe un week-end ou un jour férié, l’événement ne se déclenche pas du tout ce mois-ci — aucun report au jour de bourse suivant. Mesuré sur SPY (2007-2026) : 85 premiers du mois sur 236 ne sont pas des jours de bourse (68 week-ends + 17 jours fériés), soit environ un déclenchement sur trois silencieusement perdu (constat initial du dossier mesuré avec TimeRules.AfterMarketOpen ; le piège est identique quelle que soit la TimeRule).
Les deux formes côte à côte :
# PIEGE : sans symbole, ~1 mois sur 3 sans declenchement (85/236 sur SPY, 2007-2026)
self.Schedule.On(self.DateRules.MonthStart(),
self.TimeRules.At(10, 0),
self.Rebalance)
# CORRECT : premier jour de BOURSE du mois pour ce symbole, chaque mois
self.Schedule.On(self.DateRules.MonthStart("SPY"),
self.TimeRules.At(10, 0),
self.Rebalance)Schedule.On() pour rebalancement périodique
Variantes utiles : - DateRules.WeekStart() : chaque lundi. - DateRules.MonthStart("SPY", n) : premier jour de bourse du n-ième mois de l’année (ex : n = 1, 4, 7, 10 = rebalancement trimestriel, cf. exemples plus bas). - DateRules.EveryDay("SPY") : tous les jours de marché.
Exemples de schedules
Hebdomadaire : chaque lundi à 9h30.
self.Schedule.On(self.DateRules.WeekStart(),
self.TimeRules.AfterMarketOpen("SPY", 30),
self.Rebalance)Quotidien : à la clôture (16h ET).
self.Schedule.On(self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 0),
self.Rebalance)Pédagogie : le Scheduled Universe est idéal pour les stratégies low-frequency : momentum mensuel, value trimestrielle, mean-reversion hebdomadaire. Pour les stratégies high-frequency (intraday), restez sur Coarse pur.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Scheduled Rebalancing : Mensuel
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
class ScheduledRebalancingAlgorithm(QCAlgorithm):
"""
Rebalancement planifie le premier jour de chaque mois.
L'univers est mis a jour mensuellement, le rebalancement aussi.
"""
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Reference pour DateRules
self.AddEquity("SPY", Resolution.Daily)
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction)
self.num_stocks = 30
self.last_month = -1
# Planifier le rebalancement mensuel
self.Schedule.On(
self.DateRules.MonthStart("SPY"), # Premier jour du mois
self.TimeRules.At(10, 0), # A 10h00
self.Rebalance # Fonction a appeler
)
self.Log("Scheduled monthly rebalancing at month start, 10:00 AM")
def CoarseSelectionFunction(self, coarse):
# Mise a jour mensuelle de l'univers
if self.Time.month == self.last_month:
return Universe.Unchanged
self.last_month = self.Time.month
filtered = [x for x in coarse
if x.HasFundamentalData and x.Price > 10 and x.DollarVolume > 10000000]
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
self.Log(f"Universe updated: {len(sorted_stocks[:self.num_stocks])} stocks")
return [x.Symbol for x in sorted_stocks[:self.num_stocks]]
def Rebalance(self):
"""
Fonction de rebalancement planifiee.
Appelee automatiquement le premier jour de chaque mois.
"""
count = len(self.ActiveSecurities)
if count == 0:
self.Log("Rebalance skipped: no active securities")
return
target_weight = 1.0 / count
# Liquider les positions non presentes dans l'univers actuel
for symbol in list(self.Portfolio.Keys):
if symbol not in self.ActiveSecurities and self.Portfolio[symbol].Invested:
self.Liquidate(symbol)
self.Log(f"Liquidated (not in universe): {symbol.Value}")
# Rebalancer vers equal-weight
for symbol in self.ActiveSecurities.Keys:
self.SetHoldings(symbol, target_weight)
self.Log(f"Rebalanced {count} positions @ {target_weight:.1%} each")
def OnSecuritiesChanged(self, changes):
# Log les changements mais ne pas rebalancer ici
# Le rebalancement est gere par la fonction planifiee
if len(changes.AddedSecurities) > 0:
self.Log(f"Added: {[s.Symbol.Value for s in changes.AddedSecurities]}")
if len(changes.RemovedSecurities) > 0:
self.Log(f"Removed: {[s.Symbol.Value for s in changes.RemovedSecurities]}")Résultats backtest QC Cloud : ScheduledRebalancingAlgorithm (2015-2024, $100k) — 1515 ordres, Net Profit +187.4%, Sharpe 0.424, CAGR 11.1%, MaxDD 34.6%, Win Rate 83%. Top 30 par volume, rebalancement mensuel via Schedule.On.
Exemples de schedules
# Hebdomadaire : chaque lundi a 9h30
self.Schedule.On(
self.DateRules.Every(DayOfWeek.Monday),
self.TimeRules.At(9, 30),
self.Rebalance
)
# Quotidien : a la cloture
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 30), # 30 min avant cloture
self.Rebalance
)
# Trimestriel : premier jour de Q1, Q2, Q3, Q4
for month in [1, 4, 7, 10]:
self.Schedule.On(
self.DateRules.MonthStart("SPY", month),
self.TimeRules.At(10, 0),
self.Rebalance
)8. Stratégie Complete: Momentum Universe (20 min)
Objectif
Construire une stratégie de momentum complete :
- Sélection : Top 20 actions par momentum sur 12 mois
- Rebalancement : Mensuel
- Allocation : Equal-weight
Théorie du Momentum
Le momentum est base sur l’observation empirique que les gagnants continuent de gagner sur le court/moyen terme (3-12 mois). Les actions avec les meilleurs rendements passes tendent a surperformer.
Formule du momentum :
Momentum_12M = (Prix_actuel - Prix_12M_ago) / Prix_12M_ago
Architecture de la stratégie
1. Coarse Sélection
├── Prix > 5$
├── Volume > 5M$/jour
└── Retourne ~500 candidats
2. Fine Sélection (Momentum)
├── Recupere historique 252 jours
├── Calcule return 12 mois
├── Trie par momentum decroissant
└── Retourne top 20
3. Rebalancement mensuel
├── Liquide les sortants
└── Equal-weight sur les 20
8. Stratégie Complète: Momentum Universe (20 min)
Mise en pratique : une stratégie momentum combinant Coarse + Fine + Scheduled + Manual dans une seule architecture.
Objectif
Construire un portefeuille momentum top 10 parmi les actions US large-cap, rebalancé mensuellement. Le signal : momentum 12 mois (return annualisé). Filtre Fine : P/E < 25 (exclude distressed).
Théorie du Momentum
Le momentum (Jegadeesh & Titman 1993) est l’un des facteurs les plus robustes en finance quantitative :
- Les actions qui ont bien performé sur 6-12 mois continuent à surperformer sur les 3-12 mois suivants.
- Le contraire (mean-reversion) fonctionne sur des horizons plus courts (1-5 jours) et pour les small caps.
Causes (théoriques) : 1. Underreaction : les investisseurs ne réagissent pas instantanément aux news. 2. Herding : les investisseurs achètent ce qui monte. 3. Risk-based : le momentum compense un risque non-observé.
Empirique : momentum long-short (long winners, short losers) a historiquement généré ~10-15% de Sharpe annualisé sur 50+ ans (difficile à répliquer post-2009 car crowded).
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Strategie Momentum Universe Complete
# A copier dans l'IDE QuantConnect
from AlgorithmImports import *
from datetime import timedelta
class MomentumUniverseStrategy(QCAlgorithm):
"""
Strategie Momentum :
- Selection : Top 20 par momentum 12 mois
- Rebalancement : Mensuel
- Allocation : Equal-weight
Basee sur la recherche academique de Jegadeesh & Titman (1993)
"""
def Initialize(self):
# Configuration backtest
self.SetStartDate(2015, 1, 1)
self.SetEndDate(2024, 12, 31)
self.SetCash(100000)
# Reference pour schedule
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
# Universe settings
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
# Parametres de la strategie
self.num_coarse = 500 # Candidats Coarse
self.num_fine = 20 # Selection finale
self.lookback_days = 252 # 12 mois de trading
self.min_price = 5 # Prix minimum
self.min_volume = 5000000 # Volume minimum ($5M/jour)
# Tracking
self.last_month = -1
self.momentum_scores = {} # Stocker les scores momentum
# Scheduled rebalancing
self.Schedule.On(
self.DateRules.MonthStart("SPY"),
self.TimeRules.At(10, 0),
self.Rebalance
)
self.Log("Momentum Universe Strategy initialized")
self.Log(f" Top {self.num_fine} stocks by {self.lookback_days}-day momentum")
self.Log(f" Monthly rebalancing")
def CoarseSelectionFunction(self, coarse):
"""Premier filtre : liquidite."""
# Reselection mensuelle uniquement
if self.Time.month == self.last_month:
return Universe.Unchanged
self.last_month = self.Time.month
# Filtrer par prix, volume et donnees fondamentales
filtered = [x for x in coarse
if x.HasFundamentalData
and x.Price > self.min_price
and x.DollarVolume > self.min_volume]
# Trier par volume
sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
self.Log(f"Coarse: {len(coarse)} total -> {len(filtered)} filtered -> {self.num_coarse} candidates")
return [x.Symbol for x in sorted_stocks[:self.num_coarse]]
def FineSelectionFunction(self, fine):
"""Deuxieme filtre : calcul du momentum."""
# Calculer le momentum pour chaque action
momentum_data = []
for stock in fine:
symbol = stock.Symbol
# Recuperer l'historique 12 mois
history = self.History(symbol, self.lookback_days, Resolution.Daily)
if history.empty or len(history) < self.lookback_days * 0.9:
# Pas assez de donnees
continue
try:
# Calculer le momentum (return 12 mois)
price_now = history['close'].iloc[-1]
price_12m_ago = history['close'].iloc[0]
momentum = (price_now - price_12m_ago) / price_12m_ago
momentum_data.append({
'symbol': symbol,
'momentum': momentum
})
except:
continue
# Trier par momentum decroissant
sorted_by_momentum = sorted(momentum_data,
key=lambda x: x['momentum'],
reverse=True)
# Garder les top N
top_momentum = sorted_by_momentum[:self.num_fine]
# Stocker les scores pour reference
self.momentum_scores = {x['symbol']: x['momentum'] for x in top_momentum}
self.Log(f"Fine: {len(fine)} -> {len(momentum_data)} with data -> {len(top_momentum)} selected")
if len(top_momentum) > 0:
self.Log(f" Top momentum: {top_momentum[0]['symbol'].Value} = {top_momentum[0]['momentum']:.1%}")
self.Log(f" Bottom of top: {top_momentum[-1]['symbol'].Value} = {top_momentum[-1]['momentum']:.1%}")
return [x['symbol'] for x in top_momentum]
def Rebalance(self):
"""Rebalancement mensuel en equal-weight."""
# Exclure SPY (reference)
trading_symbols = [s for s in self.ActiveSecurities.Keys if s != self.spy]
count = len(trading_symbols)
if count == 0:
self.Log("Rebalance: no trading securities")
return
target_weight = 1.0 / count
# Liquider les positions non presentes dans l'univers
for symbol in list(self.Portfolio.Keys):
if symbol not in trading_symbols and self.Portfolio[symbol].Invested:
self.Liquidate(symbol)
self.Log(f"Liquidated: {symbol.Value}")
# Rebalancer
for symbol in trading_symbols:
momentum = self.momentum_scores.get(symbol, 0)
self.SetHoldings(symbol, target_weight)
self.Log(f"Rebalanced: {count} positions @ {target_weight:.1%}")
def OnSecuritiesChanged(self, changes):
"""Log les changements d'univers."""
added = [s.Symbol.Value for s in changes.AddedSecurities if s.Symbol != self.spy]
removed = [s.Symbol.Value for s in changes.RemovedSecurities if s.Symbol != self.spy]
if added:
self.Log(f"Universe +{len(added)}: {added[:5]}{'...' if len(added) > 5 else ''}")
if removed:
self.Log(f"Universe -{len(removed)}: {removed[:5]}{'...' if len(removed) > 5 else ''}")
def OnEndOfAlgorithm(self):
"""Resume final."""
self.Log("="*50)
self.Log("MOMENTUM STRATEGY SUMMARY")
self.Log("="*50)
self.Log(f"Initial Capital: ${self.StartingCash:,.2f}")
self.Log(f"Final Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
total_return = (self.Portfolio.TotalPortfolioValue - self.StartingCash) / self.StartingCash
self.Log(f"Total Return: {total_return:.1%}")
self.Log("="*50)Backtest QC Cloud : MomentumUniverseStrategy
| Sharpe | CAGR | MaxDD | Net Profit | Orders | End Equity |
|---|---|---|---|---|---|
| 0.208 | 9.008% | 19.000% | +29.561% | 667 | $129,560.80 |
Momentum coarse+fine, 2022-2024, lookback=126d
Résultats backtest QC Cloud : MomentumUniverseStrategy (2015-2024, $100k) — 1952 ordres, Net Profit +163.7%, Sharpe 0.366, CAGR 10.2%, MaxDD 36.9%, Win Rate 78%. Top 20 momentum mensuel, equal-weight. Stratégie complete Coarse+Fine+Schedule.
Points cles de la stratégie
- Coarse Sélection : Filtre rapidement ~8000 actions en ~500 candidats liquides
- Fine Sélection : Calcule le momentum 12 mois pour chaque candidat
- Rebalancement : Equal-weight mensuel sur les top 20
- Scheduled :
Schedule.On()garantit un rebalancement previsible
Ameliorations possibles
| Amelioration | Implementation |
|---|---|
| Momentum ajuste au risque | Diviser le momentum par la volatilite |
| Exclusion du mois recent | Skip du dernier mois (reversal) |
| Filtrage sectoriel | Eviter la concentration dans un secteur |
| Stop-loss | Vendre si perte > 20% |
| Market timing | Ne pas investir si SPY < SMA 200 |
Sélection par périodicité intraday du volume : le critère spectral
Les critères de ce carnet — manuel, coarse, fondamental, momentum — classent les titres sur des données de fin de journée ou fondamentales. La research QuantConnect Intraday Volume Periodicity (Rudy Osuna, 2026) propose un critère d’une autre nature : classer chaque semaine les cent valeurs US les plus liquides par la part de variance de leur volume minute expliquée par la fréquence dominante de sa série — un score spectral — et de conserver le quintile supérieur, pondéré par un score d’exécution (EMA de liquidité).
Le fondement académique est réel : Wu, Zhang et Dai, Spectral Volume Models: Universal High-Frequency Periodicities in Intraday Trading Activities (SSRN 4230610, DOI 10.2139/ssrn.4230610 — preprint vérifié daté de 2022 ; l’article QC le cite « 2025 ») documentent des périodicités intraday universelles du volume. Le mécanisme invoqué est l’asymétrie d’information : un volume qui revient périodiquement porte plus d’information, donc exige une prime de rendement. L’article annonce un Sharpe de 0.792 contre 0.406 pour SPY (juillet 2021 - juillet 2026), les vingt-cinq combinaisons de paramètres testées battant toutes le benchmark.
Deux lectures critiques avant d’admirer : (a) l’article est une pièce promotionnelle pour le dataset payant « Spectral Tick-Flow Signal » — le facteur y est livré par le dataset, et l’optimum retenu siège au bord de la grille de paramètres, ce que l’auteur lui-même reconnaît comme un signal d’optimisation in-sample ; (b) la reproduction du backtest complet exigerait cette souscription. La version pédagogique ci-dessous recalcule donc le score spectral nous-mêmes : périodogramme scipy sur volume minute réel (yfinance), sans aucun dataset commercial — puis une cellule de référence QC montre le squelette de l’algorithme de sélection tel qu’on l’écrirait dans QC Lab.
# Critere spectral de selection d'univers (research 21066) : chaque titre est note
# par la part de variance de son volume minute expliquee par la frequence dominante.
# Le dataset commercial "Spectral Tick-Flow Signal" n'est PAS souscrit : le calcul
# est refait ici sur donnees reelles (yfinance, volume minute, cinq seances).
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
from scipy.signal import periodogram
tickers = ["SPY", "AAPL", "MSFT", "F"] # de l'ETF ultra-liquide au titre moins echange
data = yf.download(tickers, interval="1m", period="5d", group_by="ticker",
progress=False, auto_adjust=True)
def volumes(t):
v = data[t]["Volume"].dropna()
return v[v > 0] # minutes sans echange exclues
resultats = {}
for t in tickers:
v = volumes(t)
# (a) profil intraday moyen : moyenne du volume par minute de la seance (U-shape)
minute_seance = (v.index.hour * 60 + v.index.minute) - (9 * 60 + 30)
profil = v.groupby(minute_seance).mean()
# (b) periodogramme de la serie brute (fs = 1 par minute), tendance lineaire
# retiree pour ne pas laisser la derive multi-seances manger le pic journalier
freqs, puiss = periodogram(v.values, detrend="linear")
i_dom = int(np.argmax(puiss[1:]) + 1) # hors composante continue
resultats[t] = {
"score": puiss[i_dom] / puiss[1:].sum(), # part de variance
"periode_min": 1.0 / freqs[i_dom],
"profil": profil,
"freqs": freqs,
"puiss": puiss,
}
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4.2))
for t, r in resultats.items():
ax1.plot(r["profil"].index, r["profil"].values / r["profil"].values.max(),
label=t, alpha=0.75)
ax2.semilogy(r["freqs"][1:], r["puiss"][1:], alpha=0.6, label=t)
ax1.set_xlabel("Minute de la seance (0 = 9h30)")
ax1.set_ylabel("Volume moyen (normalise)")
ax1.set_title("Profil intraday moyen du volume")
ax1.axvline(0, color="gray", lw=0.5); ax1.axvline(390, color="gray", lw=0.5)
ax2.axvline(1 / 390, color="red", ls="--", lw=1, label="1/seance (390 min)")
ax2.set_xlabel("Frequence (min$^{-1}$)"); ax2.set_ylabel("Densite spectrale")
ax2.set_title("Periodogramme du volume minute")
ax2.set_xlim(0, 0.02)
for ax in (ax1, ax2):
ax.legend(fontsize=8)
plt.tight_layout()
plt.show()
print(f"{'Ticker':<7}{'score spectral':>16}{'periode dominante':>20}")
for t, r in sorted(resultats.items(), key=lambda kv: -kv[1]["score"]):
print(f"{t:<7}{r['score']:>16.1%}{r['periode_min']:>14.0f} min")
Ticker score spectral periode dominante
AAPL 9.8% 390 min
MSFT 8.3% 390 min
SPY 5.0% 390 min
F 2.1% 195 min
Lecture du résultat : le score spectral sépare les titres — et révèle leurs horloges
Sur ces cinq séances réelles, la fréquence dominante du volume minute est la séance entière (390 minutes) pour SPY, MSFT et AAPL : le profil de gauche le montre, le volume se concentre à l’ouverture et à la clôture (le U-shape classique), et le périodogramme porte un pic net sur la ligne rouge. Mais le classement est le vrai livrable : AAPL concentre 9.8 % de la variance de son volume dans ce pic, MSFT 8.3 %, SPY 5.0 % — et Ford seulement 2.1 %, avec une dominante non plus à la séance mais à la demi-séance (195 minutes) : son horloge de volume est une autre. C’est exactement la quantité que l’article transforme en critère de sélection : classer les titres par ce score et retenir le quintile supérieur, c’est parier que l’horloge du volume porte de l’information sur le titre.
Deux garde-fous tempèrent l’enthousiasme : la part de variance reste modeste (un dixième au mieux — le volume minute est d’abord du bruit), et aucune prime de rendement n’est mesurée ici — le Sharpe 0.792 annoncé vient du backtest de l’article, calibré in-sample sur le dataset commercial dont l’optimum siégeait au bord de la grille. La cellule de référence ci-dessous montre comment ce critère s’écrirait dans QC Lab, l’histoire minute étant le seul ingrédient supplémentaire.
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# SpectralVolumeUniverse : selection hebdomadaire par score spectral du volume minute
# (research 21066) -- le score est calcule sur l'historique minute du nuage coarse,
# sans dataset commercial. Le calcul d'historique par titre est couteux : il se fait
# au rythme du rebalancement hebdomadaire, pas a chaque barre.
from AlgorithmImports import *
import numpy as np
from scipy.signal import periodogram
class SpectralVolumeUniverse(QCAlgorithm):
def initialize(self):
self.set_start_date(2021, 7, 1)
self.set_cash(100000)
self.universe_settings.resolution = Resolution.MINUTE
self.add_universe(self.coarse_spectral)
self.schedule.on(self.date_rules.week_start(self.securities.keys()),
self.time_rules.at(1, 0), self.rebalance)
def score_spectral(self, symbol):
# Part de variance du volume minute expliquee par la frequence dominante
h = self.history(symbol, timedelta(days=5), Resolution.MINUTE)
if h.empty:
return 0.0
v = h["volume"].to_numpy(dtype=float)
v = v[v > 0]
if len(v) < 800: # moins de deux seances completes : score non defini
return 0.0
freqs, puiss = periodogram(v, detrend="linear")
i_dom = int(np.argmax(puiss[1:]) + 1)
return puiss[i_dom] / puiss[1:].sum()
def coarse_spectral(self, coarse):
candidats = sorted(coarse, key=lambda c: c.dollar_volume, reverse=True)[:100]
scores = {c.symbol: self.score_spectral(c.symbol) for c in candidats}
retenus = sorted(scores, key=scores.get, reverse=True)[:20] # top quintile
return [str(s) for s in retenus]
def rebalance(self):
# Pondération inclinee par la liquidité (EMA du dollar-volume), a l'image
# de l'"execution score" de l'article
for holding in self.portfolio.values():
self.set_holdings(holding.symbol, 0.05)Conclusion et Prochaines Étapes
Recapitulatif
Dans ce notebook, nous avons appris a :
- Comprendre les Universes : Sélection dynamique vs liste fixe
- Manual Universe :
AddEquity()pour une liste fixe (FAANG) - Coarse Sélection : Filtrage par
Price,DollarVolume,HasFundamentalData - Fine Sélection : Filtrage par données Morningstar (P/E, secteur, MarketCap)
- Universe Settings :
Resolution,Leverage,FillForward - Dynamic Rebalancing :
OnSecuritiesChanged()pour gerer les changements - Scheduled Sélection :
Schedule.On()pour rebalancement periodique - Stratégie Momentum : Implementation complete
Tableau recapitulatif
| Concept | Code | Usage |
|---|---|---|
| Manual Universe | self.AddEquity("AAPL") |
Liste fixe d’actions |
| Coarse Sélection | self.AddUniverse(self.CoarseSelectionFunction) |
Filtre par prix/volume |
| Fine Sélection | self.AddUniverse(Coarse, Fine) |
Filtre par fondamentaux |
| Settings | self.UniverseSettings.Resolution |
Configurer l’univers |
| Changements | OnSecuritiesChanged(changes) |
Gerer entrees/sorties |
| Unchanged | return Universe.Unchanged |
Skip la reselection |
| Schedule | self.Schedule.On() |
Rebalancement periodique |
Workflow recommande
1. Définir les critères de sélection
├── Coarse: liquidite minimum
└── Fine: facteurs alpha (momentum, value, quality)
2. Choisir la frequence de rebalancement
├── Quotidien (rare, couteux)
├── Hebdomadaire
└── Mensuel (recommande)
3. Implementer OnSecuritiesChanged
├── Liquider les sortants
└── Acheter les entrants
4. Backtester et optimiser
├── Comparer avec buy-and-hold
└── Analyser turnover et couts
Prochaine étape : QC-Py-06 - Options Trading
Dans le prochain notebook, nous explorerons :
- Ajout d’options :
self.AddOption("SPY") - Filtrage : Strike, expiration, type (call/put)
- Stratégies : Covered call, protective put, spread
- Greeks : Delta, gamma, theta, vega
Exercice suggere
Créer une stratégie Value Universe :
- Coarse: Volume > 10M\(, Prix > 10\)
- Fine:
- P/E entre 5 et 15
- Dividend Yield > 2%
- Market Cap > 5B$
- Rebalancement trimestriel
- Top 30 actions par dividend yield
Ressources complementaires
Notebook complete. Pret pour QC-Py-06-Options-Trading.