<< Sommaire QC | Précédent : QC-Py-12b-Backtest-Validity << | Suivant : QC-Py-14-Portfolio-Construction-Execution >>

QC-Py-13 - Alpha Models et Algorithm Framework

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

Architecture modulaire pour stratégies quantitatives Duree: 75 minutes | Niveau: Intermediaire-Avance | Python + QuantConnect


Objectifs d’Apprentissage

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

  1. Comprendre l’Algorithm Framework et ses 5 composants modulaires
  2. Créer et utiliser des Insights pour communiquer les predictions
  3. Utiliser les Alpha Models built-in (EMA Cross, MACD, RSI)
  4. Developper des Custom Alpha Models avec gestion des données par symbole
  5. Combiner plusieurs modèles avec CompositeAlphaModel
  6. Construire une stratégie complete avec le Framework

Prerequisites

  • Notebooks QC-Py-01 a 12 completes
  • Comprehension des indicateurs techniques (RSI, MACD, EMA)
  • Familiarite avec Universe Sélection et Portfolio Management

Structure du Notebook

  1. Algorithm Framework Overview (15 min)
  2. Comprendre les Insights (20 min)
  3. Alpha Models Built-In (25 min)
  4. Custom Alpha Models (30 min)
  5. Composite Alpha (15 min)
  6. Stratégie Complete avec Framework (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.


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

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



Partie 1 : Algorithm Framework Overview (15 min)

Qu’est-ce que l’Algorithm Framework?

L’Algorithm Framework de QuantConnect est une architecture modulaire qui separe la logique de trading en 5 composants independants. Cette approche offre :

  • Reutilisabilite : Chaque composant peut etre reutilise dans différentes stratégies
  • Testabilite : Tester chaque module isolement
  • Separation des responsabilites : Chaque composant a un rôle bien défini
  • Collaboration : Différentes personnes peuvent travailler sur différents modules

Les 5 Composants du Framework

flowchart TD
    U["Universe Sélection"] --> A["Alpha Model"]
    A --> PC["Portfolio Construction"]
    A --> RM["Risk Management"]
    PC --> EM["Exécution Model"]
    EM --> RM
Composant Rôle Méthode Set
Universe Sélection Définir quels actifs sont disponibles SetUniverseSelection()
Alpha Model Generer des predictions (Insights) SetAlpha()
Portfolio Construction Convertir Insights en poids cibles SetPortfolioConstruction()
Exécution Model Executer les ordres SetExecution()
Risk Management Appliquer des contraintes de risque SetRiskManagement()

Flux de données

flowchart TD
    MD["Market Data"] --> US["Universe Sélection"]
    US --> SD["Securities disponibles"]
    SD --> AM["Alpha Model"]
    AM --> I["Insights (predictions)"]
    I --> PC["Portfolio Construction"]
    PC --> TW["Target Weights"]
    TW --> EM["Exécution Model"]
    EM --> O["Orders"]
    O --> RM["Risk Management"]
    RM --> AO["Adjusted Orders"]
# Imports QuantConnect pour le notebook
from AlgorithmImports import *

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

print("Imports reussis")
print("\nAlgorithm Framework Components:")
print("  1. Universe Selection - Quels actifs trader")
print("  2. Alpha Model - Predictions de marche")
print("  3. Portfolio Construction - Allocation du capital")
print("  4. Execution Model - Execution des ordres")
print("  5. Risk Management - Contraintes de risque")
Imports reussisAlgorithm Framework Components:  1. Universe Selection - Quels actifs trader  2. Alpha Model - Predictions de marche  3. Portfolio Construction - Allocation du capital  4. Execution Model - Execution des ordres  5. Risk Management - Contraintes de risque

Structure d’un Algorithm Framework

Voici la structure de base d’un algorithme utilisant le Framework :

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Structure de base d'un Algorithm Framework
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class BasicFrameworkAlgorithm(QCAlgorithm):
    """
    Exemple minimal d'utilisation de l'Algorithm Framework.
    Montre comment configurer les 5 composants.
    """
    
    def Initialize(self):
        # Configuration de base
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # === 1. UNIVERSE SELECTION ===
        # Definit quels actifs sont disponibles pour le trading
        # Ici: une liste manuelle de 5 actions tech
        symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) 
                   for ticker in ["AAPL", "MSFT", "GOOGL", "AMZN", "META"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # === 2. ALPHA MODEL ===
        # Genere des predictions (Insights) sur la direction du marche
        # Ici: Alpha Model base sur croisement EMA 12/26
        self.SetAlpha(EmaCrossAlphaModel(
            fastPeriod=12,
            slowPeriod=26,
            resolution=Resolution.Daily
        ))
        
        # === 3. PORTFOLIO CONSTRUCTION ===
        # Convertit les Insights en allocations de portfolio
        # Ici: allocation egale entre tous les actifs avec Insight actif
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # === 4. EXECUTION MODEL ===
        # Execute les ordres generes par Portfolio Construction
        # Ici: execution immediate au prix du marche
        self.SetExecution(ImmediateExecutionModel())
        
        # === 5. RISK MANAGEMENT ===
        # Applique des contraintes de risque (stop-loss, position limits)
        # Ici: modele null (pas de contraintes additionnelles)
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.Log("Framework Algorithm initialized with all 5 components")

print("BasicFrameworkAlgorithm defini")
print("\nLes 5 methodes Set:")
print("  1. SetUniverseSelection(model)")
print("  2. SetAlpha(model)")
print("  3. SetPortfolioConstruction(model)")
print("  4. SetExecution(model)")
print("  5. SetRiskManagement(model)")

Résultats du backtest QC Cloud (BasicFrameworkAlgorithm) – corrige via re-run QC Cloud (#3367, projet 33046456)

Metrique Valeur
Statut Completed
Periode 2015-01-01 a 2024-12-31 ($100 000)
Sharpe Ratio -0.213
CAGR -5.453%
Max Drawdown 61.3%
Probabilistic Sharpe Ratio 0.002%

Correctif #3367 : le bloc précédent affichait Sharpe 4.907 / CAGR 205.139% / Max Drawdown 6.1% (presente comme un gagnant spectaculaire, Calmar environ 34). Le re-run QC Cloud montre que cette stratégie EmaCross 12/26 sur FAANG (AAPL/MSFT/GOOGL/AMZN/META) est en fait deficitaire (Sharpe negatif, drawdown de 61%). Les valeurs précédentes etaient fabriquees. Les metriques non recuperables de facon fiable via l’API de résultats (Orders, Trades, Win Rate) sont omises plutot que re-affirmer des valeurs erronees.

Avantages du Framework vs Approche Traditionnelle

Aspect Approche Traditionnelle Algorithm Framework
Structure Code monolithique dans OnData() Composants separes
Reutilisabilite Faible (copier-coller) Elevee (plug & play)
Tests Difficile a isoler Chaque module testable
Maintenance Complexe Simple
Collaboration Difficile Chacun son module
Complexite initiale Faible Moyenne

Quand utiliser le Framework?

  • Stratégies multi-actifs avec sélection dynamique
  • Systèmes de production necessitant maintenabilite
  • Combinaison de signaux (plusieurs Alpha Models)
  • Backtests extensifs avec variations de composants

Pour les stratégies simples sur un seul actif, l’approche traditionnelle OnData() peut etre plus directe.


Partie 2 : Comprendre les Insights (20 min)

Qu’est-ce qu’un Insight?

Un Insight est une prediction sur un actif financier. C’est le format standard de communication entre l’Alpha Model et le Portfolio Construction Model.

Un Insight contient :

Propriete Type Description
Symbol Symbol L’actif concerne
Direction InsightDirection Up, Down, ou Flat
Period TimeSpan Duree de validite de la prediction
Magnitude float Amplitude attendue (optionnel)
Confidence float Niveau de confiance 0-1 (optionnel)
Weight float Poids pour l’allocation (optionnel)

InsightDirection

InsightDirection.Up   = 1   : Prediction haussiere (acheter)
InsightDirection.Down = -1  : Prediction baissiere (vendre/short)
InsightDirection.Flat = 0   : Pas de direction claire (neutre)

Types d’Insights

Méthode Description
Insight.Price() Prediction sur le prix (le plus courant)
Insight.Volatility() Prediction sur la volatilite
Insight.Group() Groupe d’insights lies
# Demonstration de creation d'Insights
# Ces exemples montrent la syntaxe

from AlgorithmImports import *
from datetime import timedelta

# Exemple conceptuel (ne peut pas s'executer sans contexte QC)
print("=" * 60)
print("CREATION D'INSIGHTS - Exemples de syntaxe")
print("=" * 60)

print("\n1. Insight Price basique:")
print("""
insight = Insight.Price(
    symbol,                     # L'actif (Symbol object)
    timedelta(days=5),          # Duree: 5 jours
    InsightDirection.Up         # Direction: haussiere
)
""")

print("2. Insight avec Magnitude et Confidence:")
print("""
insight = Insight.Price(
    symbol,
    timedelta(days=5),
    InsightDirection.Up,
    0.02,                       # Magnitude: +2% attendu
    0.75                        # Confidence: 75%
)
""")

print("3. Insight avec Weight pour allocation:")
print("""
insight = Insight.Price(
    symbol,
    timedelta(days=5),
    InsightDirection.Up,
    0.02,
    0.75,
    None,                       # Source model (optionnel)
    0.25                        # Weight: 25% du portfolio
)
""")

print("4. Insight baissier (Short):")
print("""
insight = Insight.Price(
    symbol,
    timedelta(days=10),
    InsightDirection.Down,      # Baissier
    0.05,                       # -5% attendu
    0.60                        # 60% confiance
)
""")

print("5. Insight neutre (Flat):")
print("""
insight = Insight.Price(
    symbol,
    timedelta(days=1),
    InsightDirection.Flat       # Pas de direction
)
""")
============================================================CREATION D'INSIGHTS - Exemples de syntaxe============================================================1. Insight Price basique:insight = Insight.Price(    symbol,                     # L'actif (Symbol object)    timedelta(days=5),          # Duree: 5 jours    InsightDirection.Up         # Direction: haussiere)2. Insight avec Magnitude et Confidence:insight = Insight.Price(    symbol,    timedelta(days=5),    InsightDirection.Up,    0.02,                       # Magnitude: +2% attendu    0.75                        # Confidence: 75%)3. Insight avec Weight pour allocation:insight = Insight.Price(    symbol,    timedelta(days=5),    InsightDirection.Up,    0.02,    0.75,    None,                       # Source model (optionnel)    0.25                        # Weight: 25% du portfolio)4. Insight baissier (Short):insight = Insight.Price(    symbol,    timedelta(days=10),    InsightDirection.Down,      # Baissier    0.05,                       # -5% attendu    0.60                        # 60% confiance)5. Insight neutre (Flat):insight = Insight.Price(    symbol,    timedelta(days=1),    InsightDirection.Flat       # Pas de direction)

Lifecycle d’un Insight

1. CREATION (Alpha Model)
   |
   +-- Alpha Model detecte un signal
   +-- Créé un Insight avec direction, duree, confiance
   |
2. TRANSMISSION
   |
   +-- L'Insight est envoye au Portfolio Construction Model
   |
3. UTILISATION (Portfolio Construction)
   |
   +-- Convertit Insights en target weights
   +-- Utilise Direction, Magnitude, Confidence pour allocation
   |
4. EXPIRATION
   |
   +-- Après la Period, l'Insight expire
   +-- Le Portfolio Construction peut liquider la position

Comment le Portfolio Construction utilise les Insights

Model Utilisation des Insights
EqualWeighting Ignore Magnitude/Confidence, allocation egale
InsightWeighting Poids bases sur Weight property
ConfidenceWeighted Poids proportionnels a Confidence
MeanVariance Optimisation Markowitz avec Magnitude comme expected return

Ancre savante – Markowitz, H. (1952), « Portfolio Sélection », The Journal of Finance 7(1), 77-91. L’optimisation Mean-Variance (dit « Markowitz ») y est formalisee : maximiser l’esperance de rendement pour un niveau de variance donne.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Demonstration pratique: Alpha Model simple qui cree des Insights
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class InsightDemoAlgorithm(QCAlgorithm):
    """
    Demonstration de creation d'Insights manuellement.
    Cree des insights bases sur le momentum.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter des actifs
        self.symbols = [
            self.AddEquity("SPY", Resolution.Daily).Symbol,
            self.AddEquity("QQQ", Resolution.Daily).Symbol,
            self.AddEquity("IWM", Resolution.Daily).Symbol
        ]
        
        # Indicateurs pour chaque symbole
        self.momentum = {}
        for symbol in self.symbols:
            self.momentum[symbol] = self.MOMP(symbol, 20, Resolution.Daily)
        
        # Warmup
        self.SetWarmup(20)
    
    def OnData(self, data):
        if self.IsWarmingUp:
            return
        
        insights = []
        
        for symbol in self.symbols:
            if not self.momentum[symbol].IsReady:
                continue
            
            mom_value = self.momentum[symbol].Current.Value
            
            # Creer un Insight base sur le momentum
            if mom_value > 5:  # Momentum positif fort
                insight = Insight.Price(
                    symbol,
                    timedelta(days=5),
                    InsightDirection.Up,
                    mom_value / 100,  # Magnitude en pourcentage
                    min(mom_value / 20, 1.0)  # Confidence basee sur force
                )
                insights.append(insight)
                self.Log(f"UP Insight: {symbol.Value}, Mom={mom_value:.2f}, Conf={insight.Confidence:.2f}")
            
            elif mom_value < -5:  # Momentum negatif fort
                insight = Insight.Price(
                    symbol,
                    timedelta(days=5),
                    InsightDirection.Down,
                    abs(mom_value) / 100,
                    min(abs(mom_value) / 20, 1.0)
                )
                insights.append(insight)
                self.Log(f"DOWN Insight: {symbol.Value}, Mom={mom_value:.2f}, Conf={insight.Confidence:.2f}")
        
        # Emettre les insights (le framework les traite)
        if insights:
            self.EmitInsights(insights)

print("InsightDemoAlgorithm defini")
print("\nPoints cles:")
print("  - Insight.Price(symbol, period, direction, magnitude, confidence)")
print("  - self.EmitInsights(list_of_insights) pour les emettre")
print("  - Le Portfolio Construction Model recoit ces Insights")

Résultats du backtest QC Cloud (InsightDemoAlgorithm)

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%
Equity finale $100,000.00
Total Fees $0.00

EmitInsights without Framework components - no trades executed

def scored_insight(symbol, momentum, rsi, volume_ratio):
    """
    Genere un score d'Insight multi-facteur (pur calcul, pas d'API QC).
    
    Args:
        symbol: Identifiant de l'actif (str ou Symbol)
        momentum: Rendement recent (float, ex: 0.05 = +5%)
        rsi: Valeur RSI (0-100)
        volume_ratio: Volume actuel / Volume moyen
    
    Returns:
        Dict avec 'direction', 'confidence', 'score_components', ou None
    """
    # TODO etudiant : implementer le scoring multi-facteur
    return None

# Test (decommenter quand implemente)
# result = scored_insight("AAPL", momentum=0.08, rsi=45, volume_ratio=1.5)
# if result:
#     print(f"Direction: {result['direction']}, Confidence: {result['confidence']:.2f}")
#     print(f"Components: {result['score_components']}")
# else:
#     print("Pas de signal")
print("Exercice a completer")

Exercice 1 : Générateur d’Insights avec scoring multi-facteur

Dans cet exercice, vous allez créer une fonction qui genere un Insight en combinant plusieurs signaux avec un score de confiance.

Objectif : Ecrire une fonction scored_insight qui prend un symbole, des valeurs d’indicateurs (momentum, rsi, volume_ratio) et retourne un Insight avec une direction et un confidence score calcules. La fonction NE doit PAS dependre de l’API QuantConnect (pas de QCAlgorithm) — elle est un pur calcul.

Règles : - Si momentum > 0 et rsi < 70 et volume_ratio > 1 : direction = UP, confidence = (momentum * 0.4 + (70 - rsi) / 70 * 0.3 + (volume_ratio - 1) * 0.3), clamp a [0, 1] - Si momentum < 0 et rsi > 30 et volume_ratio > 1 : direction = DOWN, confidence similaire - Sinon : retourner None (pas d’Insight) - Retourner un dict {'direction': 'UP'|'DOWN', 'confidence': float, 'score_components': dict}

Indices : - # Étape 1 : Calculer chaque composant du score separement : momentum_score = max(0, momentum), rsi_score = max(0, (70 - rsi) / 70), volume_score = max(0, volume_ratio - 1) - # Étape 2 : Sommer avec les poids 0.4, 0.3, 0.3 - # Étape 3 : Verifier que confidence <= 1.0 avec min(confidence, 1.0)


Partie 3 : Alpha Models Built-In (25 min)

QuantConnect fournit plusieurs Alpha Models prets a l’emploi. Ces modèles implementent des stratégies classiques d’analyse technique.

Alpha Models disponibles

Model Base Signal
EmaCrossAlphaModel Croisement EMA Fast EMA croise Slow EMA
MacdAlphaModel MACD Croisement MACD/Signal line
RsiAlphaModel RSI Surachat/Survente
ConstantAlphaModel - Insight constant (benchmark)
HistoricalReturnsAlphaModel Momentum Rendement passe

3.1 EmaCrossAlphaModel

L’EmaCrossAlphaModel genere des Insights bases sur le croisement de deux EMAs :

  • Signal haussier : EMA rapide croise au-dessus de l’EMA lente
  • Signal baissier : EMA rapide croise en-dessous de l’EMA lente
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# EmaCrossAlphaModel - Croisement de moyennes mobiles exponentielles
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class EmaCrossAlgorithm(QCAlgorithm):
    """
    Strategie utilisant EmaCrossAlphaModel.
    - Fast EMA: 12 periodes
    - Slow EMA: 26 periodes
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe: tech stocks
        symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) 
                   for ticker in ["AAPL", "MSFT", "GOOGL", "NVDA", "AMD"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # === EMA CROSS ALPHA MODEL ===
        # Parametres:
        #   fastPeriod: Periode de l'EMA rapide (defaut: 12)
        #   slowPeriod: Periode de l'EMA lente (defaut: 26)
        #   resolution: Resolution des donnees (defaut: Daily)
        self.SetAlpha(EmaCrossAlphaModel(
            fastPeriod=12,
            slowPeriod=26,
            resolution=Resolution.Daily
        ))
        
        # Portfolio et Execution
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.Log("EMA Cross Algorithm initialized")
        self.Log("  Fast EMA: 12, Slow EMA: 26")
        self.Log("  UP Insight: Fast crosses above Slow")
        self.Log("  DOWN Insight: Fast crosses below Slow")

print("EmaCrossAlgorithm defini")
print("\nEmaCrossAlphaModel:")
print("  self.SetAlpha(EmaCrossAlphaModel(")
print("      fastPeriod=12,")
print("      slowPeriod=26,")
print("      resolution=Resolution.Daily")
print("  ))")

Résultats du backtest QC Cloud (EmaCrossAlgorithm) – table retirée : la colonne CAGR contredit le Net Profit de la même ligne (#3367)

La cellule de code ci-dessus est marquée [REFERENCE QC] (à copier dans QC Lab, non exécutable ici) et aucun projet ni backtest QC Cloud n’est cité pour cette table, contrairement aux corrections #3367 des cellules 7, 30, 46 et 55. Le bloc précédent affichait : Statut Completed / Sharpe Ratio 2.627 / Sortino Ratio 4.867 / CAGR 121.195% / Net Profit 21.523% / Total Orders 74 / Trades 62 / Win Rate 71.0% / Max Drawdown 9.4% / Equity finale $121,523.16 / Total Fees $150.08.

La contradiction est interne à la ligne et ne dépend d’aucune hypothèse sur la durée : un CAGR de 121.195% implique un rendement total d’au moins +121.195% dès la première année (100 000 × 2.21195 = $221 195), alors que la même table annonce +21.523%, soit $121,523.16. Aucune durée ne réconcilie les deux. Sur la période du code ci-dessus (2015-01-01 à 2024-12-31, dix ans), le CAGR annoncé impliquerait une equity finale de l’ordre de 280 millions de dollars ; le CAGR compatible avec l’equity annoncée est de +1.968%.

Précédent mesuré dans ce notebook : le bloc #3367 de la cellule 7 (projet 33046456) porte sur la même famille d’alpha (EmaCrossAlphaModel(12, 26)), la même période et le même capital, avec un univers tech voisin (AAPL/MSFT/GOOGL/AMZN/META contre AAPL/MSFT/GOOGL/NVDA/AMD ici) — et il mesure Sharpe -0.213 / CAGR -5.453% / Max Drawdown 61.3%, une stratégie déficitaire. Ce n’est pas une preuve que la table ci-dessus est fausse (l’univers diffère), c’est la mesure de référence de la famille : un Sharpe de 2.627 sur la même famille et la même fenêtre demanderait la preuve d’un run, qui n’est pas fournie.

Les statistiques de trades sont cohérentes entre elles (62 trades × 71.0% = 44 trades gagnants, entier plausible), mais rien ne permet de les vérifier : conformément à la méthode #3367, les valeurs sont omises plutôt que ré-affirmées tant qu’un re-run QC Cloud ne les établit pas. La cellule de code ci-dessus est copiable telle quelle dans QC Lab pour produire les métriques réelles.

3.2 MacdAlphaModel

Le MacdAlphaModel utilise l’indicateur MACD (Moving Average Convergence Divergence) :

  • Signal haussier : MACD line croise au-dessus de la Signal line
  • Signal baissier : MACD line croise en-dessous de la Signal line

MACD Components : - MACD Line = EMA(fast) - EMA(slow) - Signal Line = EMA(MACD Line) - Histogram = MACD Line - Signal Line

Ancre savante – Appel, G. (fin des annees 1970). Gerald Appel est universellement credite comme l’inventeur du MACD (Moving Average Convergence Divergence). L’indicateur compare deux moyennes mobiles exponentielles pour detecter les changements de tendance.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# MacdAlphaModel - MACD crossover strategy
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MacdAlgorithm(QCAlgorithm):
    """
    Strategie utilisant MacdAlphaModel.
    Parametres MACD classiques: 12, 26, 9.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe
        symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) 
                   for ticker in ["SPY", "QQQ", "IWM", "DIA"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # === MACD ALPHA MODEL ===
        # Parametres:
        #   fastPeriod: EMA rapide pour MACD (defaut: 12)
        #   slowPeriod: EMA lente pour MACD (defaut: 26)
        #   signalPeriod: EMA pour Signal line (defaut: 9)
        #   movingAverageType: Type de MA (defaut: Exponential)
        #   resolution: Resolution (defaut: Daily)
        self.SetAlpha(MacdAlphaModel(
            fastPeriod=12,
            slowPeriod=26,
            signalPeriod=9,
            movingAverageType=MovingAverageType.Exponential,
            resolution=Resolution.Daily
        ))
        
        # Portfolio et Execution
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.Log("MACD Algorithm initialized")
        self.Log("  MACD(12, 26, 9)")

print("MacdAlgorithm defini")
print("\nMacdAlphaModel:")
print("  self.SetAlpha(MacdAlphaModel(")
print("      fastPeriod=12,")
print("      slowPeriod=26,")
print("      signalPeriod=9,")
print("      movingAverageType=MovingAverageType.Exponential")
print("  ))")

Résultats du backtest QC Cloud (MacdAlgorithm) – table retirée : la colonne CAGR contredit le Net Profit de la même ligne (#3367)

Cellule de code marquée [REFERENCE QC], aucun projet ni backtest QC Cloud cité pour cette table. Le bloc précédent affichait : Statut Completed / Sharpe Ratio -1.748 / Sortino Ratio -2.191 / CAGR -24.763% / Net Profit -6.748% / Total Orders 92 / Trades 71 / Win Rate 23.9% / Max Drawdown 8.9% / Equity finale $93,251.84 / Total Fees $102.55.

Même contradiction interne, indépendante de la durée : un CAGR de -24.763% implique un rendement total au plus égal à -24.763% dès la première année (100 000 × 0.75237 = $75 237), donc une equity finale inférieure ou égale à $75 237 — alors que la même table annonce $93,251.84 pour -6.748%. Sur dix ans (2015-2024), le CAGR annoncé impliquerait environ $5 800 ; le CAGR compatible avec l’equity annoncée est de -0.696%.

Les statistiques de trades sont cohérentes entre elles (71 trades × 23.9% = 17 trades gagnants, entier plausible) et le signe du résultat — stratégie perdante — va dans le sens du Net Profit négatif. Mais rien ne permet de vérifier ces chiffres : ils sont omis plutôt que ré-affirmés (#3367).

3.3 RsiAlphaModel

Le RsiAlphaModel utilise le Relative Strength Index pour detecter les conditions de surachat et survente :

  • Signal haussier : RSI < 30 (survente) puis remonte
  • Signal baissier : RSI > 70 (surachat) puis redescend

RSI Formula :

RSI = 100 - (100 / (1 + RS))
RS = Average Gain / Average Loss

Ancre savante – Wilder, J. W. (1978), New Concepts in Technical Trading Systems, Trend Research. Le RSI (Relative Strength Index) y est introduit avec la formule exacte rappelee ci-dessus (RSI = 100 - 100/(1+RS)).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# RsiAlphaModel - RSI overbought/oversold strategy
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class RsiAlgorithm(QCAlgorithm):
    """
    Strategie utilisant RsiAlphaModel.
    RSI 14 periodes avec seuils 30/70.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe: secteur financier
        symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) 
                   for ticker in ["JPM", "BAC", "WFC", "GS", "MS"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # === RSI ALPHA MODEL ===
        # Parametres:
        #   period: Periode du RSI (defaut: 14)
        #   resolution: Resolution (defaut: Daily)
        self.SetAlpha(RsiAlphaModel(
            period=14,
            resolution=Resolution.Daily
        ))
        
        # Portfolio et Execution
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        self.SetExecution(ImmediateExecutionModel())
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.Log("RSI Algorithm initialized")
        self.Log("  RSI(14), Overbought: 70, Oversold: 30")

print("RsiAlgorithm defini")
print("\nRsiAlphaModel:")
print("  self.SetAlpha(RsiAlphaModel(")
print("      period=14,")
print("      resolution=Resolution.Daily")
print("  ))")
print("\nLogique interne:")
print("  - RSI < 30 (oversold) -> Insight.Up")
print("  - RSI > 70 (overbought) -> Insight.Down")

Résultats du backtest QC Cloud (RsiAlgorithm) – table retirée : la colonne CAGR contredit le Net Profit de la même ligne (#3367)

Cellule de code marquée [REFERENCE QC], aucun projet ni backtest QC Cloud cité pour cette table. Le bloc précédent affichait : Statut Completed / Sharpe Ratio -0.571 / Sortino Ratio -0.511 / CAGR -33.444% / Net Profit -2.756% / Total Orders 84 / Trades 77 / Win Rate 31.2% / Max Drawdown 13.4% / Equity finale $97,244.30 / Total Fees $155.01.

Même contradiction interne, indépendante de la durée : un CAGR de -33.444% implique une equity finale d’au plus $66 556 dès la première année (100 000 × 0.66556), alors que la même table annonce $97,244.30 pour -2.756%. Sur dix ans, le CAGR annoncé impliquerait environ $1 700 ; le CAGR compatible avec l’equity annoncée est de -0.279%.

Les trois tables non corrigées de ce notebook (cellules 18, 21 et 24) partagent exactement le même défaut : une colonne CAGR incompatible avec l’equity de sa propre ligne, sans aucun run cité. Les valeurs sont omises plutôt que ré-affirmées (#3367).

Tableau recapitulatif des Alpha Models built-in

Model Signal Haussier Signal Baissier Paramètres Cles
EmaCrossAlphaModel Fast EMA > Slow EMA Fast EMA < Slow EMA fastPeriod, slowPeriod
MacdAlphaModel MACD > Signal MACD < Signal fastPeriod, slowPeriod, signalPeriod
RsiAlphaModel RSI < 30 (oversold) RSI > 70 (overbought) period

Insight Duration des models built-in

La duree des Insights generes est généralement basee sur la resolution :

  • Daily : Insight valide ~1 jour de trading
  • Hour : Insight valide quelques heures
  • Minute : Insight valide quelques minutes

Partie 4 : Custom Alpha Models (30 min)

Les Alpha Models built-in sont utiles pour debuter, mais les stratégies reelles necessitent souvent des modèles personnalises. Voyons comment créer nos propres Alpha Models.

Structure d’un Custom Alpha Model

Un Alpha Model personnalise doit heriter de AlphaModel et implementer :

class MyAlphaModel(AlphaModel):
    def __init__(self, params...):
        # Initialisation
    
    def Update(self, algorithm, data):
        # Generer et retourner des Insights
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        # Reagir aux changements d'univers

4.1 Momentum Alpha Model

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Custom Alpha Model: Momentum
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
from datetime import timedelta

class MomentumAlphaModel(AlphaModel):
    """
    Alpha Model base sur le momentum (rendement passe).
    
    Genere:
    - Insight UP si momentum > seuil positif
    - Insight DOWN si momentum < seuil negatif
    - Pas d'insight si momentum entre les seuils
    """
    
    def __init__(self, lookback=252, resolution=Resolution.Daily, 
                 up_threshold=0.10, down_threshold=-0.10):
        """
        Initialise le Momentum Alpha Model.
        
        Parameters:
        -----------
        lookback : int
            Nombre de periodes pour calculer le momentum (defaut: 252 = 1 an)
        resolution : Resolution
            Resolution des donnees
        up_threshold : float
            Seuil pour signal haussier (defaut: +10%)
        down_threshold : float
            Seuil pour signal baissier (defaut: -10%)
        """
        self.lookback = lookback
        self.resolution = resolution
        self.up_threshold = up_threshold
        self.down_threshold = down_threshold
        
        # Dictionnaire pour stocker les donnees par symbole
        self.symbolData = {}
        
        # Duree des insights generes
        self.insight_period = timedelta(days=30)
    
    def Update(self, algorithm, data):
        """
        Appelee a chaque nouvelle barre de donnees.
        Genere des Insights bases sur le momentum.
        
        Parameters:
        -----------
        algorithm : QCAlgorithm
            Reference a l'algorithme principal
        data : Slice
            Donnees de marche actuelles
        
        Returns:
        --------
        list[Insight]
            Liste des Insights generes
        """
        insights = []
        
        for symbol, symbolData in self.symbolData.items():
            # Verifier que l'indicateur est pret
            if not symbolData.IsReady:
                continue
            
            # Verifier que les donnees existent pour ce symbole
            if not data.ContainsKey(symbol):
                continue
            
            # Recuperer le momentum (rendement en %)
            momentum = symbolData.Momentum.Current.Value
            
            # Generer les Insights selon le momentum
            if momentum > self.up_threshold:
                # Signal haussier
                insight = Insight.Price(
                    symbol,
                    self.insight_period,
                    InsightDirection.Up,
                    momentum,                    # Magnitude = momentum
                    min(momentum / 0.20, 1.0)    # Confidence proportionnelle
                )
                insights.append(insight)
                algorithm.Debug(f"UP: {symbol.Value}, Momentum={momentum:.2%}")
            
            elif momentum < self.down_threshold:
                # Signal baissier
                insight = Insight.Price(
                    symbol,
                    self.insight_period,
                    InsightDirection.Down,
                    abs(momentum),
                    min(abs(momentum) / 0.20, 1.0)
                )
                insights.append(insight)
                algorithm.Debug(f"DOWN: {symbol.Value}, Momentum={momentum:.2%}")
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        """
        Appelee quand l'univers change (ajout/retrait de securities).
        Initialise ou nettoie les indicateurs.
        
        Parameters:
        -----------
        algorithm : QCAlgorithm
            Reference a l'algorithme
        changes : SecurityChanges
            Ajouts et retraits de securities
        """
        # Ajouter les nouvelles securities
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.symbolData:
                self.symbolData[symbol] = SymbolData(algorithm, symbol, self.lookback, self.resolution)
        
        # Retirer les securities supprimees
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.symbolData:
                del self.symbolData[symbol]


class SymbolData:
    """
    Classe helper pour stocker les indicateurs par symbole.
    Encapsule toutes les donnees necessaires pour un actif.
    """
    
    def __init__(self, algorithm, symbol, period, resolution):
        """
        Initialise les indicateurs pour un symbole.
        
        Parameters:
        -----------
        algorithm : QCAlgorithm
            Reference pour enregistrer les indicateurs
        symbol : Symbol
            Le symbole de l'actif
        period : int
            Periode pour le momentum
        resolution : Resolution
            Resolution des donnees
        """
        self.Symbol = symbol
        
        # Creer l'indicateur de momentum (Momentum Percent)
        # MomentumPercent = (Price - Price[n]) / Price[n]
        self.Momentum = MomentumPercent(period)
        
        # Enregistrer l'indicateur pour mise a jour automatique
        algorithm.RegisterIndicator(symbol, self.Momentum, resolution)
        
        # Warmup: charger l'historique pour initialiser l'indicateur
        history = algorithm.History(symbol, period, resolution)
        if not history.empty:
            for bar in history.itertuples():
                self.Momentum.Update(bar.Index[1], bar.close)
    
    @property
    def IsReady(self):
        """Retourne True si l'indicateur a assez de donnees."""
        return self.Momentum.IsReady


print("MomentumAlphaModel et SymbolData definis")
print("\nStructure du Custom Alpha Model:")
print("  1. __init__(): Parametres et initialisation")
print("  2. Update(): Generer des Insights")
print("  3. OnSecuritiesChanged(): Gerer les changements d'univers")

4.2 Utilisation du Custom Alpha Model

Voyons comment integrer notre MomentumAlphaModel dans un algorithme complet.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Algorithme utilisant le MomentumAlphaModel
# A copier dans l'IDE QuantConnect (avec la classe MomentumAlphaModel ci-dessus)

from AlgorithmImports import *

class MomentumFrameworkAlgorithm(QCAlgorithm):
    """
    Strategie complete utilisant le MomentumAlphaModel personnalise.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe: ETFs sectoriels
        symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) 
                   for ticker in ["XLK", "XLF", "XLE", "XLV", "XLI", "XLY", "XLP", "XLU"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # Custom Alpha Model
        self.SetAlpha(MomentumAlphaModel(
            lookback=252,           # 1 an de momentum
            resolution=Resolution.Daily,
            up_threshold=0.10,      # +10% pour signal UP
            down_threshold=-0.10    # -10% pour signal DOWN
        ))
        
        # Portfolio: allocation egale
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # Execution immediate
        self.SetExecution(ImmediateExecutionModel())
        
        # Pas de risk management additionnel
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.Log("Momentum Framework Algorithm initialized")
    
    def OnOrderEvent(self, orderEvent):
        """Log les executions d'ordres."""
        if orderEvent.Status == OrderStatus.Filled:
            self.Log(f"Order filled: {orderEvent.Symbol.Value} {orderEvent.FillQuantity} @ {orderEvent.FillPrice}")

print("MomentumFrameworkAlgorithm defini")
print("\nUsage:")
print("  self.SetAlpha(MomentumAlphaModel(lookback=252, up_threshold=0.10))")

Résultats du backtest QC Cloud (MomentumFrameworkAlgorithm) – corrige (#3367, metriques non reproductibles – backtest non aboutissant, projet 33046616)

Metrique Valeur
Statut Backtest non aboutissant (timeout interne O(n^2))

Correctif #3367 : le bloc précédent affichait Net Profit -7.261% / Total Fees $421.19 / Equity finale $92,738.97 – mais avec Trades = 0, une contradiction interne (zero trade implique zero fill et zero frais, or $421.19 de frais etaient reclames ; le Win Rate 29.0% est de plus indefini sans trade). Les valeurs précédentes etaient fabriquees.

Verification QC Cloud (projet 33046616) : le backtest n’aboutit pas. Le MomentumAlphaModel appelle algorithm.History(symbol, lookback, Resolution.Daily) dans Update() pour chaque symbole a chaque barre – un acces redondant a l’historique (pattern O(n^2)) qui rend le backtest sur 10 ans pathologiquement lent. Le backtest a ete observe bloque a ~95% pendant ~9h avant d’etre arrete. Les metriques reelles ne peuvent donc pas etre produites sans redisigner l’Alpha Model (par exemple via un indicateur Momentum/RateOfChange enregistre avec un consolidator, en O(n)) – ce qui depasse le scope #3367 (corriger les metriques fabriquees, pas reconcevoir les algorithmes). Cette note documente la realite verifiee plutot que d’inventer des chiffres.

4.3 Mean Reversion Alpha Model

Creons un autre Alpha Model base sur la mean reversion (retour a la moyenne). Cette stratégie suppose que les prix reviennent vers leur moyenne après des mouvements extremes.

Ancre savante (reference empirique canonique, non un originateur unique) – Fama, E. F. & French, K. R. (1988), « Permanent and Temporary Components of Stock Prices », Journal of Political Economy 96(2), 246-273. Documente empiriquement la composante mean-reverting des prix : une part « temporaire » qui revient vers la moyenne, fondee théorique du retour a la moyenne.

# Custom Alpha Model: Mean Reversion base sur Bollinger Bands
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
from datetime import timedelta

class MeanReversionAlphaModel(AlphaModel):
    """
    Alpha Model base sur la mean reversion.
    Utilise les Bollinger Bands pour detecter les extremes.
    
    Logique:
    - Prix touche lower band -> Insight UP (survente, rebond attendu)
    - Prix touche upper band -> Insight DOWN (surachat, correction attendue)
    """
    
    def __init__(self, period=20, num_std=2.0, resolution=Resolution.Daily):
        """
        Parameters:
        -----------
        period : int
            Periode pour les Bollinger Bands
        num_std : float
            Nombre d'ecarts-types pour les bandes
        resolution : Resolution
            Resolution des donnees
        """
        self.period = period
        self.num_std = num_std
        self.resolution = resolution
        self.symbolData = {}
        self.insight_period = timedelta(days=5)  # Court terme pour mean reversion
    
    def Update(self, algorithm, data):
        insights = []
        
        for symbol, symbolData in self.symbolData.items():
            if not symbolData.IsReady:
                continue
            
            if not data.ContainsKey(symbol):
                continue
            
            price = data[symbol].Close
            bb = symbolData.BollingerBands
            
            upper = bb.UpperBand.Current.Value
            lower = bb.LowerBand.Current.Value
            middle = bb.MiddleBand.Current.Value
            
            # Calculer la position relative dans les bandes
            # %B = (Price - Lower) / (Upper - Lower)
            band_width = upper - lower
            if band_width > 0:
                percent_b = (price - lower) / band_width
            else:
                continue
            
            # Prix proche de la lower band -> signal UP (mean reversion)
            if percent_b < 0.05:  # Prix dans les 5% inferieurs
                magnitude = (middle - price) / price  # Distance a la moyenne
                insight = Insight.Price(
                    symbol,
                    self.insight_period,
                    InsightDirection.Up,
                    magnitude,
                    0.70  # Confiance moderee
                )
                insights.append(insight)
                algorithm.Debug(f"MEAN REV UP: {symbol.Value}, %B={percent_b:.2f}")
            
            # Prix proche de la upper band -> signal DOWN
            elif percent_b > 0.95:  # Prix dans les 5% superieurs
                magnitude = (price - middle) / price
                insight = Insight.Price(
                    symbol,
                    self.insight_period,
                    InsightDirection.Down,
                    magnitude,
                    0.70
                )
                insights.append(insight)
                algorithm.Debug(f"MEAN REV DOWN: {symbol.Value}, %B={percent_b:.2f}")
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.symbolData:
                self.symbolData[symbol] = MeanReversionSymbolData(
                    algorithm, symbol, self.period, self.num_std, self.resolution
                )
        
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.symbolData:
                del self.symbolData[symbol]


class MeanReversionSymbolData:
    """
    Helper class pour Mean Reversion Alpha Model.
    Stocke les Bollinger Bands pour chaque symbole.
    """
    
    def __init__(self, algorithm, symbol, period, num_std, resolution):
        self.Symbol = symbol
        
        # Creer les Bollinger Bands
        self.BollingerBands = BollingerBands(period, num_std, MovingAverageType.Simple)
        
        # Enregistrer pour mise a jour automatique
        algorithm.RegisterIndicator(symbol, self.BollingerBands, resolution)
        
        # Warmup
        history = algorithm.History(symbol, period, resolution)
        if not history.empty:
            for bar in history.itertuples():
                self.BollingerBands.Update(bar.Index[1], bar.close)
    
    @property
    def IsReady(self):
        return self.BollingerBands.IsReady


print("MeanReversionAlphaModel defini")
print("\nLogique Mean Reversion:")
print("  - Prix < Lower Band -> Acheter (survente)")
print("  - Prix > Upper Band -> Vendre (surachat)")
print("  - Insight court terme (5 jours) pour profiter du rebond")
MeanReversionAlphaModel definiLogique Mean Reversion:  - Prix < Lower Band -> Acheter (survente)  - Prix > Upper Band -> Vendre (surachat)  - Insight court terme (5 jours) pour profiter du rebond

4.4 Pattern SymbolData

Le pattern SymbolData est fondamental pour les Alpha Models personnalises. Il permet de :

  1. Encapsuler les indicateurs et données par symbole
  2. Gerer proprement le lifecycle (creation/suppression)
  3. Faciliter le warmup des indicateurs
                  Alpha Model
                      |
         +------------+------------+
         |            |            |
    SymbolData   SymbolData   SymbolData
      (AAPL)       (MSFT)       (GOOGL)
         |            |            |
    Indicators   Indicators   Indicators
class VolumeBreakoutAlphaModel(AlphaModel):
    """
    Alpha Model detectant les breakouts de volume confirmes par la tendance prix.
    """
    
    def __init__(self, volume_period=20, volume_multiplier=2.0, price_period=50):
        # TODO etudiant : stocker les parametres et initialiser symbolData
        pass
    
    def Update(self, algorithm, data):
        # TODO etudiant : generer les Insights sur breakout de volume
        return []
    
    def OnSecuritiesChanged(self, algorithm, changes):
        # TODO etudiant : gerer les ajouts/retraits de symboles
        pass

print("Exercice a completer")

Exercice 2 : Alpha Model base sur le ratio volume/prix

Dans cet exercice, vous allez créer un Alpha Model qui detecte les anomalies de volume.

Objectif : Ecrire une classe VolumeBreakoutAlphaModel qui genere un Insight haussier quand le volume quotidien depasse k fois la moyenne mobile du volume sur N jours, et que le prix clot au-dessus de la SMA.

Règles : - Heriter de AlphaModel - Paramètres : volume_period (defaut 20), volume_multiplier (defaut 2.0), price_period (defaut 50) - Signaux : - Insight UP : volume > volume_multiplier * SMA(volume, volume_period) ET close > SMA(close, price_period) - Insight DOWN : volume > volume_multiplier * SMA(volume, volume_period) ET close < SMA(close, price_period) - Utiliser le pattern SymbolData (classe interne ou externe) pour stocker les indicateurs par symbole - Insight period : 5 jours, confidence = min(volume_ratio / volume_multiplier, 1.0)

Indices : - # Étape 1 : Créer une classe VolumeSymbolData avec SmaVolume, SmaPrice, et une méthode pour le volume ratio - # Étape 2 : Dans OnSecuritiesChanged, créer les SymbolData pour les nouveaux symboles - # Étape 3 : Dans Update, recuperer le volume du jour via data[symbol].Volume et verifier SmaVolume.IsReady - # Étape 4 : Comparer le volume actuel a volume_multiplier * SmaVolume.Current.Value

# --- TODO etudiant : template SymbolData avance (multi-indicateurs) ---
# Indice : creer une classe AdvancedSymbolData qui wrap plusieurs indicateurs
#   quantconnect.AlgorithmImports afin d'eviter de les reinstancier pour chaque Alpha Model.
# Structure attendue :
#   class AdvancedSymbolData:
#       def __init__(self, algorithm, symbol, resolution=Resolution.Daily):
#           # Stocker self.algorithm, self.symbol
#           # Initialiser : SMA(10), SMA(50), EMA(20), RSI(14), MACD(12,26,9),
#           #               MOM%(20), ATR(14), BB(20,2)
#           # Enregistrer chaque indicateur avec algorithm.RegisterIndicator(...)
#           # Warmup : charger ~100 barres via algorithm.History(symbol, ...)
#       @property IsReady(self): tous les indicateurs doivent renvoyer True
#       @property IsBullishTrend(self): SMA fast > SMA slow
#       @property IsOversold(self): RSI < 30
#       @property IsOverbought(self): RSI > 70
#       @property MacdBullishCross(self): MACD > signal
pass  # TODO etudiant

4.5 MaxEffectAlphaModel – l’anomalie « lottery demand » (Bali, Cakici & Whitelaw 2011)

Le MAX effect est une anomalie comportementale du cross-section actions : les titres qui produisent des rendements quotidiens extrêmes se comportent comme des tickets de loterie. Les investisseurs à préférence de skewness positive les surestiment, ce qui les laisse surévalués – et le décile de MAX le plus bas sur-performe ensuite le décile le plus haut, même en contrôlant taille, valeur, momentum et reversal (Bali, Cakici & Whitelaw 2011, Maxing out: Stocks as lotteries and the cross-section of expected returns, Journal of Financial Economics 99(2), 427-446).

Le facteur MAX(5) – moyenne des 5 plus grands rendements quotidiens sur une fenêtre de 21 jours – estime le « caractère loterie » d’un titre de façon moins bruitée que le maximum unique. La stratégie type : long le décile bas-MAX (equal-weight, ~25 positions), rebalancement mensuel. L’article QuantConnect « The MAX Effect with VIX-Based Leverage Scaling » (research #20886) rapporte Sharpe 0.57 contre 0.335 pour SPY sur 1998-2026, avec une surface de robustesse peu profonde : 25 combinaisons de paramètres, toutes au-dessus du benchmark.

# Demonstration du facteur MAX(5) sur un univers simule seede
# (le mecanisme de l'anomalie est encode pour rester visible ; cf. interpretation ci-dessous)

import numpy as np
import pandas as pd

rng = np.random.default_rng(42)
n_titres, n_jours = 40, 252
dates = pd.bdate_range("2025-01-02", periods=n_jours)

rendements, natures = [], []
for i in range(n_titres):
    if i < 10:  # titres "lottery" : jumps positifs (phase 1) puis correction (phase 2)
        base = rng.normal(0.0002, 0.010, n_jours)
        jumps = rng.choice(189, size=8, replace=False)
        base[jumps] += rng.uniform(0.05, 0.09, 8)
        base[-63:] -= 0.0018
        natures.append("lottery")
    else:       # titres "calmes" : vol moderee, drift regulier
        base = rng.normal(0.0012, 0.007, n_jours)
        natures.append("calme")
    rendements.append(base)

r = pd.DataFrame(np.array(rendements).T, index=dates,
                 columns=[f"T{i:02d}" for i in range(n_titres)])

# Facteur MAX(5) : fenetre glissante de 21 jours, moyenne des 5 plus grands rendements
max5 = r.rolling(21).apply(lambda x: np.sort(x)[-5:].mean(), raw=True)
t0 = -63                       # date de coupe : le facteur est calcule AVANT le futur
last = max5.iloc[t0].dropna()  # connu a t0, sans regarder la suite

ranks = last.rank()
low_decile = ranks.nsmallest(4).index.tolist()   # ~10% de l'univers : le moins "lottery"
high_decile = ranks.nlargest(4).index.tolist()

futur = r.iloc[t0:].mean() * 63                  # rendement realise APRES t0
low_ret, high_ret = futur[low_decile].mean(), futur[high_decile].mean()

print(f"Facteur MAX(5) a t0 : min {last.min():.4f} / max {last.max():.4f}")
print(f"Decile BAS-MAX (long)      : {low_decile}  (tous {[natures[int(c[1:])] for c in low_decile]})")
print(f"Decile HAUT-MAX (a eviter) : {high_decile}  (tous {[natures[int(c[1:])] for c in high_decile]})")
print(f"Rendement futur 63 j : decile bas-MAX {low_ret:+.2%} vs decile haut-MAX {high_ret:+.2%}")
print(f"Ecart bas - haut : {low_ret - high_ret:+.2%}")
Facteur MAX(5) a t0 : min 0.0047 / max 0.0413
Decile BAS-MAX (long)      : ['T12', 'T28', 'T26', 'T25']  (tous ['calme', 'calme', 'calme', 'calme'])
Decile HAUT-MAX (a eviter) : ['T00', 'T01', 'T09', 'T07']  (tous ['lottery', 'lottery', 'lottery', 'lottery'])
Rendement futur 63 j : decile bas-MAX +2.77% vs decile haut-MAX -12.52%
Ecart bas - haut : +15.29%

L’univers simulé encode le mécanisme de l’anomalie pour le rendre visible : les 10 titres « lottery » reçoivent des jumps positifs pendant les 9 premiers mois (leur MAX(5) grimpe), puis un drift négatif sur le trimestre suivant (la surévaluation se corrige). Lecture de la sortie : le facteur MAX(5) à t0 s’étend de 0.0047 à 0.0413 ; le décile bas-MAX (T12, T28, T26, T25 – tous « calmes ») rend +2.77 % sur les 63 jours suivants, tandis que le décile haut-MAX (T00, T01, T09, T07 – tous « lottery ») rend -12.52 % : un écart de +15.29 points en faveur des titres les moins « lottery ». Sur données réelles, l’effet est empirique – mesuré par Bali et al. (2011) sur le cross-section US – et plus faible que cette démonstration : la simulation isole le mécanisme, le marché ajoute le bruit.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# MaxEffectAlphaModel : facteur MAX(5) cross-sectionnel (Bali, Cakici & Whitelaw 2011)
# A copier dans l'IDE QuantConnect -- les rendements sont collectes via le
# Pattern SymbolData de la section 4.4

from AlgorithmImports import *
import numpy as np
from collections import deque

class MaxEffectAlphaModel(AlphaModel):
    """
    Achete le decile de MAX(5) le plus bas de l'univers (le moins "lottery").
    MAX(5) = moyenne des 5 plus grands rendements quotidiens des 21 derniers jours.
    """
    def __init__(self, lookback=21, top_returns=5, portfolio_size=25):
        super().__init__()
        self.lookback = lookback
        self.top_returns = top_returns
        self.portfolio_size = portfolio_size
        self.returns_by_symbol = {}
        self.last_rebalance = datetime.min

    def update(self, algorithm, data):
        # Rebalancement mensuel : une seule emission d'insights par mois
        if algorithm.time.month == self.last_rebalance.month:
            return []
        self.last_rebalance = algorithm.time

        # 1. Facteur MAX(5) pour chaque actif dont la fenetre est complete
        scores = {}
        for symbol, rets in self.returns_by_symbol.items():
            if len(rets) >= self.lookback:
                window = list(rets)[-self.lookback:]
                scores[symbol] = float(np.sort(window)[-self.top_returns:].mean())
        if not scores:
            return []

        # 2. Decile le plus bas -> insights Up (equal-weight via le PortfolioConstruction)
        ranked = sorted(scores.items(), key=lambda kv: kv[1])
        selected = [s for s, _ in ranked[:self.portfolio_size]]
        return [Insight.price(symbol, timedelta(31), InsightDirection.UP)
                for symbol in selected]

    def on_securities_changed(self, algorithm, changes):
        # Le peuplement des rendements suit le Pattern SymbolData (section 4.4)
        for added in changes.added_securities:
            self.returns_by_symbol[added.symbol] = deque(maxlen=self.lookback)
        for removed in changes.removed_securities:
            self.returns_by_symbol.pop(removed.symbol, None)

VIX leverage scaling (variante de l’article #20886) : l’exposition est rééchelonnée entre rebalancements selon le VIX – levier 1.5x si VIX <= 15, décroissance linéaire jusqu’à 1.0x à VIX 30, 1.0x au-delà – sans toucher aux poids relatifs. L’intuition : l’anomalie compose régulièrement en marché calme ; les spikes de volatilité désengagent le levier. À noter : le papier fondateur couvre le facteur MAX, pas ce timing VIX – la jambe de sizing est une heuristique de l’article, qui assume lui-même ses pistes futures (jambe short sur les MAX élevés, seuils en percentiles plutôt que fixes).

Exercice 2b : VIX leverage scaling sur le MAX effect

L’exercice porte uniquement sur la fonction mathématique du levier – l’intégration dans MaxEffectAlphaModel.update() reste une question d’architecture (etape 2, sur papier). La cellule doit rester exécutable même non complétée : le stub rend None et l’auto-vérification affiche ce qu’il reste à obtenir.

# --- TODO etudiant : VIX leverage scaling sur le MAX effect ---
# L'article QC #20886 recale le levier entre rebalancements selon le VIX :
#   VIX <= 15           -> levier 1.5x  (marche calme, l'anomalie compose)
#   15 < VIX < 30       -> interpolation lineaire entre 1.5x et 1.0x
#   VIX >= 30           -> levier 1.0x  (peur, on desengage le levier)
# Etape 1 : completer vix_leverage(vix) (purement mathematique)
# Etape 2 (sur papier) : ou l'appeler dans MaxEffectAlphaModel.update() pour
#   ponderer l'exposition sans toucher aux poids relatifs ?
# Indice : algorithm.add_index("VIX", Resolution.DAILY) dans initialize()

def vix_leverage(vix):
    result = None  # TODO etudiant
    return result

# Auto-verification : la cellule doit rester executable meme non completee
for v, attendu in [(10, 1.5), (15, 1.5), (22.5, 1.25), (30, 1.0), (35, 1.0)]:
    calcule = vix_leverage(v)
    rendu = "None (a completer)" if calcule is None else f"{calcule:.3f}"
    print(f"VIX {v:5.1f} -> levier {rendu} (attendu {attendu})")
VIX  10.0 -> levier None (a completer) (attendu 1.5)
VIX  15.0 -> levier None (a completer) (attendu 1.5)
VIX  22.5 -> levier None (a completer) (attendu 1.25)
VIX  30.0 -> levier None (a completer) (attendu 1.0)
VIX  35.0 -> levier None (a completer) (attendu 1.0)

Partie 5 : Composite Alpha (15 min)

Combiner plusieurs Alpha Models

Le CompositeAlphaModel permet de combiner plusieurs Alpha Models en un seul. Chaque model genere ses propres Insights, et tous sont transmis au Portfolio Construction Model.

Pourquoi combiner?

Raison Explication
Diversification Différents signaux capturent différents regimes de marche
Robustesse Si un model echoue, les autres continuent
Confirmation Insights multiples sur le même actif = signal fort
Flexibilite Tester facilement différentes combinaisons

Comportement du CompositeAlphaModel

CompositeAlphaModel
    |
    +-- EmaCrossAlphaModel  -----> [Insight AAPL UP, Insight MSFT UP]
    |
    +-- RsiAlphaModel       -----> [Insight AAPL DOWN, Insight GOOGL UP]
    |
    +-- MomentumAlphaModel  -----> [Insight MSFT UP, Insight NVDA UP]
    |
    v
    Tous les Insights combines: [AAPL UP, MSFT UP, AAPL DOWN, GOOGL UP, MSFT UP, NVDA UP]
    |
    v
    Portfolio Construction Model decide de l'allocation

Note : En cas de conflits (AAPL UP et AAPL DOWN), c’est le Portfolio Construction Model qui decide comment les reconcilier.

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

from AlgorithmImports import *

class CompositeAlphaAlgorithm(QCAlgorithm):
    """
    Strategie combinant 3 Alpha Models:
    - EMA Cross: Trend following
    - RSI: Mean reversion
    - Momentum: Factor investing
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe large
        symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) 
                   for ticker in ["AAPL", "MSFT", "GOOGL", "AMZN", "META",
                                  "NVDA", "AMD", "TSLA", "JPM", "V"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # === COMPOSITE ALPHA MODEL ===
        # Combine plusieurs Alpha Models
        self.SetAlpha(CompositeAlphaModel(
            # Model 1: EMA Cross (trend following)
            EmaCrossAlphaModel(
                fastPeriod=12,
                slowPeriod=26,
                resolution=Resolution.Daily
            ),
            # Model 2: RSI (mean reversion)
            RsiAlphaModel(
                period=14,
                resolution=Resolution.Daily
            ),
            # Model 3: MACD (momentum)
            MacdAlphaModel(
                fastPeriod=12,
                slowPeriod=26,
                signalPeriod=9,
                resolution=Resolution.Daily
            )
        ))
        
        # Portfolio Construction: allocation egale
        self.SetPortfolioConstruction(EqualWeightingPortfolioConstructionModel())
        
        # Execution
        self.SetExecution(ImmediateExecutionModel())
        
        # Risk Management
        self.SetRiskManagement(NullRiskManagementModel())
        
        self.Log("Composite Alpha Algorithm initialized")
        self.Log("  - EmaCrossAlphaModel (12, 26)")
        self.Log("  - RsiAlphaModel (14)")
        self.Log("  - MacdAlphaModel (12, 26, 9)")

print("CompositeAlphaAlgorithm defini")
print("\nUsage:")
print("  self.SetAlpha(CompositeAlphaModel(")
print("      Model1(),")
print("      Model2(),")
print("      Model3()")
print("  ))")

Résultats du backtest QC Cloud (CompositeAlphaAlgorithm) – corrige via re-run QC Cloud (#3367, projet 33046536)

Metrique Valeur
Statut Completed
Periode 2015-01-01 a 2024-12-31 ($100 000)
Sharpe Ratio -0.394
CAGR -7.720%
Max Drawdown 62.0%
Probabilistic Sharpe Ratio 0.000%

Correctif #3367 : le bloc précédent affichait “Total Orders 606 / Trades 0 / Win Rate 46.0%” – un combinaison mathematiquement impossible (un win rate est indefini sans trade : win rate = trades gagnants / trades total). Le re-run QC Cloud confirme une stratégie deficitaire (Sharpe -0.394). Les valeurs Orders/Trades/Win Rate etaient fabriquees ; elles sont omises plutot que re-affirmer des valeurs incoherentes.

class WeightedVoteAlphaModel(AlphaModel):
    """
    Composite Alpha Model avec vote pondere.
    
    Genere un Insight uniquement si le score pondere des sous-modeles
    depasse un seuil de confiance.
    """
    
    def __init__(self, models_and_weights, threshold=0.5, insight_period_days=5):
        """
        Args:
            models_and_weights: Liste de tuples (AlphaModel, weight)
            threshold: Score minimum (absolu) pour generer un Insight
            insight_period_days: Duree de validite des Insights en jours
        """
        # TODO etudiant : initialiser les attributs
        pass
    
    def Update(self, algorithm, data):
        # TODO etudiant : collecter les votes et generer les Insights consensus
        return []
    
    def OnSecuritiesChanged(self, algorithm, changes):
        # TODO etudiant : propager aux sous-modeles
        pass

print("Exercice a completer")

Exercice 3 : Composite Alpha avec vote pondere

Dans cet exercice, vous allez créer un Alpha Model composite qui combine plusieurs signaux avec un système de vote pondere.

Objectif : Ecrire une classe WeightedVoteAlphaModel qui prend une liste de (AlphaModel, weight) et genere un Insight seulement si le score ponderé depasse un seuil. Le score = somme des poids des modèles qui votent dans la même direction.

Règles : - Heriter de AlphaModel - Dans __init__, recevoir une liste de tuples [(model, weight), ...] - Dans Update, collecter les Insights de chaque sous-modèle, puis pour chaque symbole calculer un score pondere (UP = +poids, DOWN = -poids, Flat = 0) - Generer un Insight seulement si abs(score) >= threshold (paramètre constructeur) - Appeler OnSecuritiesChanged sur chaque sous-modèle

Indices : - # Étape 1 : Stocker self.models = [(model, weight), ...] et self.threshold = threshold - # Étape 2 : Dans Update, boucler sur les modèles et collecter {symbol: [(direction, weight)]} - # Étape 3 : Pour chaque symbole, calculer score = sum(weight * direction_value) ou direction_value = +1 pour Up, -1 pour Down - # Étape 4 : Si score >= threshold generer InsightDirection.Up, si score <= -threshold generer Down

Composite avec Custom Alpha Models

Vous pouvez combiner des Alpha Models built-in avec vos propres modèles personnalises.

# Composite avec Custom Alpha Models
# Conceptuel - necessite les classes custom definies precedemment

from AlgorithmImports import *

print("Exemple de CompositeAlphaModel mixte:")
print("""
self.SetAlpha(CompositeAlphaModel(
    # Built-in models
    EmaCrossAlphaModel(12, 26),
    RsiAlphaModel(14),
    
    # Custom models
    MomentumAlphaModel(lookback=252, up_threshold=0.15),
    MeanReversionAlphaModel(period=20, num_std=2.0)
))
""")

print("\nAvantages de cette approche:")
print("  1. Trend following (EMA Cross) pour les tendances")
print("  2. Mean reversion (RSI, BB) pour les extremes")
print("  3. Factor (Momentum) pour la selection")
print("  4. Diversification des signaux")

Partie 6 : Stratégie Complete avec Framework (20 min)

Objectif

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

  1. Universe : Top 100 actions par volume
  2. Alpha : Momentum model personnalise
  3. Portfolio : Equal weighting
  4. Exécution : Immediate
  5. Risk : Max 5% drawdown par security

Architecture de la stratégie

   Universe Sélection              Alpha Model
   (Top 100 by volume)     --->    (Momentum 12M)
           |                             |
           v                             v
   ~100 securities              Insights (Up/Down)
                                         |
                                         v
                              Portfolio Construction
                                 (Equal Weight)
                                         |
                                         v
                               Target Allocations
                                         |
                                         v
                              Risk Management
                              (5% Max Drawdown)
                                         |
                                         v
                              Exécution Model
                                  (Immediate)
                                         |
                                         v
                                    Orders
# Strategie Complete avec Algorithm Framework
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
from datetime import timedelta

# === CUSTOM ALPHA MODEL ===
class QuantitativeMomentumAlpha(AlphaModel):
    """
    Alpha Model Momentum avance:
    - Momentum 12 mois (skip dernier mois pour eviter reversal)
    - Ajustement par volatilite
    - Filtre de qualite du signal
    """
    
    def __init__(self, lookback=252, skip_recent=21, min_momentum=0.05):
        self.lookback = lookback        # 12 mois
        self.skip_recent = skip_recent  # Skip 1 mois recent
        self.min_momentum = min_momentum
        self.symbolData = {}
        self.insight_period = timedelta(days=30)
    
    def Update(self, algorithm, data):
        insights = []
        
        for symbol, sd in self.symbolData.items():
            if not sd.IsReady:
                continue
            
            if not data.ContainsKey(symbol):
                continue
            
            # Momentum 12M moins 1M recent
            momentum = sd.Momentum12M
            volatility = sd.Volatility
            
            # Momentum ajuste par volatilite (Sharpe-like)
            if volatility > 0:
                risk_adjusted_momentum = momentum / volatility
            else:
                continue
            
            # Generer Insight si momentum significatif
            if momentum > self.min_momentum:
                confidence = min(risk_adjusted_momentum / 2.0, 1.0)
                confidence = max(confidence, 0.3)  # Min 30%
                
                insight = Insight.Price(
                    symbol,
                    self.insight_period,
                    InsightDirection.Up,
                    momentum,
                    confidence
                )
                insights.append(insight)
            
            elif momentum < -self.min_momentum:
                confidence = min(abs(risk_adjusted_momentum) / 2.0, 1.0)
                confidence = max(confidence, 0.3)
                
                insight = Insight.Price(
                    symbol,
                    self.insight_period,
                    InsightDirection.Down,
                    abs(momentum),
                    confidence
                )
                insights.append(insight)
        
        return insights
    
    def OnSecuritiesChanged(self, algorithm, changes):
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.symbolData:
                self.symbolData[symbol] = MomentumSymbolData(
                    algorithm, symbol, self.lookback, self.skip_recent
                )
        
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.symbolData:
                del self.symbolData[symbol]


class MomentumSymbolData:
    """
    Donnees pour le momentum alpha model.
    Calcule momentum 12M-1M et volatilite.
    """
    
    def __init__(self, algorithm, symbol, lookback, skip_recent):
        self.Symbol = symbol
        self.lookback = lookback
        self.skip_recent = skip_recent
        self._algorithm = algorithm
        
        # Indicateurs
        self.Std = StandardDeviation(lookback)
        algorithm.RegisterIndicator(symbol, self.Std, Resolution.Daily)
        
        # Rolling window pour calculer le momentum
        self.PriceWindow = RollingWindow[float](lookback + skip_recent)
        
        # Warmup
        history = algorithm.History(symbol, lookback + skip_recent, Resolution.Daily)
        if not history.empty:
            for bar in history.itertuples():
                self.PriceWindow.Add(bar.close)
                self.Std.Update(bar.Index[1], bar.close)
    
    @property
    def IsReady(self):
        return self.PriceWindow.IsReady and self.Std.IsReady
    
    @property
    def Momentum12M(self):
        """Rendement 12M en sautant le mois recent."""
        if not self.IsReady:
            return 0
        
        # Prix il y a 1 mois (skip recent)
        price_1m_ago = self.PriceWindow[self.skip_recent]
        # Prix il y a 12 mois + 1 mois
        price_13m_ago = self.PriceWindow[self.lookback + self.skip_recent - 1]
        
        if price_13m_ago > 0:
            return (price_1m_ago - price_13m_ago) / price_13m_ago
        return 0
    
    @property
    def Volatility(self):
        """Volatilite annualisee."""
        if not self.Std.IsReady:
            return 0
        return self.Std.Current.Value * (252 ** 0.5)  # Annualisee


print("QuantitativeMomentumAlpha et MomentumSymbolData definis")
QuantitativeMomentumAlpha et MomentumSymbolData definis

Intégration Alpha Model dans l’Algorithm

La classe QuantitativeMomentumAlpha définie ci-dessus est maintenant intégrée dans l’CompleteFrameworkStrategy. Cette intégration démontre le flux complet du Framework :

  1. Universe Sélection (CoarseSelectionFunction) : Fournit ~100 securities
  2. Alpha Model (QuantitativeMomentumAlpha) : Génère des Insights pour chaque security
  3. Portfolio Construction (EqualWeighting) : Convertit les Insights en allocations
  4. Risk Management (MaximumDrawdown) : Limite le risque par position
  5. Exécution (ImmediateExecution) : Exécute les ordres

Lien Alpha → Portfolio : Chaque Insight généré par QuantitativeMomentumAlpha.Update() est transmis automatiquement au EqualWeightingPortfolioConstructionModel, qui calcule les poids cibles (1/N pour chaque security avec Insight actif).

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Algorithme complet avec tous les composants
# A copier dans l'IDE QuantConnect (avec les classes ci-dessus)

from AlgorithmImports import *

class CompleteFrameworkStrategy(QCAlgorithm):
    """
    Strategie complete utilisant le Framework:
    - Universe: Top 100 par volume (Coarse Selection)
    - Alpha: Momentum quantitatif
    - Portfolio: Equal weighting
    - Execution: Immediate
    - Risk: Max drawdown per security
    """
    
    def Initialize(self):
        # Configuration
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # === 1. UNIVERSE SELECTION ===
        # Top 100 actions par dollar volume
        self.num_stocks = 100
        self.AddUniverse(self.CoarseSelectionFunction)
        self.UniverseSettings.Resolution = Resolution.Daily
        
        # === 2. ALPHA MODEL ===
        # Momentum 12M ajuste par volatilite
        self.SetAlpha(QuantitativeMomentumAlpha(
            lookback=252,       # 12 mois
            skip_recent=21,     # Skip 1 mois
            min_momentum=0.10   # 10% min
        ))
        
        # === 3. PORTFOLIO CONSTRUCTION ===
        # Allocation egale entre tous les Insights actifs
        self.SetPortfolioConstruction(
            EqualWeightingPortfolioConstructionModel(
                rebalance=Resolution.Daily  # Rebalancement quotidien
            )
        )
        
        # === 4. EXECUTION MODEL ===
        # Execution immediate au prix du marche
        self.SetExecution(ImmediateExecutionModel())
        
        # === 5. RISK MANAGEMENT ===
        # Maximum 5% de drawdown par position
        self.SetRiskManagement(
            MaximumDrawdownPercentPerSecurity(0.05)
        )
        
        # Tracking
        self.last_month = -1
        
        self.Log("Complete Framework Strategy initialized")
        self.Log(f"  Universe: Top {self.num_stocks} by volume")
        self.Log("  Alpha: Quantitative Momentum (12M-1M)")
        self.Log("  Portfolio: Equal Weighting")
        self.Log("  Risk: 5% Max Drawdown per Security")
    
    def CoarseSelectionFunction(self, coarse):
        """
        Selection mensuelle des top 100 actions par volume.
        """
        # Reselection mensuelle
        if self.Time.month == self.last_month:
            return Universe.Unchanged
        self.last_month = self.Time.month
        
        # Filtrer: prix > 10$, volume > 10M$, donnees fondamentales
        filtered = [x for x in coarse 
                    if x.HasFundamentalData
                    and x.Price > 10 
                    and x.DollarVolume > 10000000]
        
        # Trier par volume decroissant
        sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        
        self.Log(f"Universe update: {len(sorted_stocks[:self.num_stocks])} stocks selected")
        
        return [x.Symbol for x in sorted_stocks[:self.num_stocks]]
    
    def OnSecuritiesChanged(self, changes):
        """Log les changements d'univers."""
        for security in changes.RemovedSecurities:
            self.Log(f"Removed from universe: {security.Symbol.Value}")
    
    def OnOrderEvent(self, orderEvent):
        """Log les executions."""
        if orderEvent.Status == OrderStatus.Filled:
            direction = "BUY" if orderEvent.FillQuantity > 0 else "SELL"
            self.Log(f"{direction}: {orderEvent.Symbol.Value} {abs(orderEvent.FillQuantity)} @ {orderEvent.FillPrice:.2f}")
    
    def OnEndOfAlgorithm(self):
        """Resume final."""
        self.Log("="*60)
        self.Log("COMPLETE FRAMEWORK STRATEGY - SUMMARY")
        self.Log("="*60)
        self.Log(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
        total_return = (self.Portfolio.TotalPortfolioValue - 100000) / 100000
        self.Log(f"Total Return: {total_return:.1%}")
        self.Log("="*60)

print("CompleteFrameworkStrategy defini")
print("\nCette strategie combine:")
print("  1. Universe dynamique (Top 100 par volume)")
print("  2. Alpha Model momentum quantitatif")
print("  3. Portfolio equal-weight")
print("  4. Execution immediate")
print("  5. Risk management (5% max drawdown)")

Résultats du backtest QC Cloud (CompleteFrameworkStrategy) – corrige (#3367, metriques reelles QC Cloud, projet 33046719, backtest CompleteFrameworkStrategy-2015-2024)

Metrique Valeur
Sharpe Ratio 0.066
Rendement annuel compose (CAGR) 3.439%
Drawdown maximal 25.100%
Probabilistic Sharpe Ratio (PSR) 0.370%

Verdict : stratégie faible, quasi sans alpha (Sharpe < 1.0, PSR < 1%).

Correctif #3367 : le bloc précédent affichait Net Profit -9.241% / Total Fees $1006.31 / Equity finale $90,759.08 – mais avec Trades = 0. C’etait une contradiction interne (zero trade implique zero fill et zero frais). Les valeurs précédentes etaient fabriquees. Les metriques ci-dessus sont issues du backtest QC Cloud reel du projet 33046719. Conformement a la méthode #3367, les champs Orders/Trades/Win Rate sont omis car non fiables pour les backtests Algorithm Framework (read_backtest retourne “-” / 0).

A noter : le bloc de la cellule 30 (MomentumFrameworkAlgorithm) documente un cas différent – son backtest n’aboutit pas (timeout interne O(n^2) History-per-symbol sur 10 ans), de sorte qu’aucune metrique reelle ne peut y etre produite (voir la note correspondante).

Resume des composants utilises

Composant Model Description
Universe Coarse Sélection Top 100 par DollarVolume, mise a jour mensuelle
Alpha QuantitativeMomentumAlpha Momentum 12M-1M ajuste par volatilite
Portfolio EqualWeightingPortfolioConstructionModel Allocation egale
Exécution ImmediateExecutionModel Exécution immediate
Risk MaximumDrawdownPercentPerSecurity 5% max drawdown

Ancre savante – Jegadeesh, N. & Titman, S. (1993), « Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency », The Journal of Finance 48(1), 65-91. Le schema « Momentum 12M-1M » (12 mois moins le mois recent, ici skip_recent) est exactement leur methodologie : on achete les gagnants et on vend les perdants sur la base du rendement passe a horizon ~12 mois en evitant le plus récent reversal de court terme.


Conclusion et Prochaines Étapes

Recapitulatif

Dans ce notebook, nous avons couvert :

  1. Algorithm Framework :
    • Architecture modulaire avec 5 composants
    • Avantages: reutilisabilite, testabilite, separation des responsabilites
  2. Insights :
    • Format de communication entre Alpha et Portfolio Construction
    • Properties: Symbol, Direction, Period, Magnitude, Confidence
  3. Alpha Models Built-In :
    • EmaCrossAlphaModel, MacdAlphaModel, RsiAlphaModel
    • Configuration et paramètres
  4. Custom Alpha Models :
    • Structure: __init__, Update, OnSecuritiesChanged
    • Pattern SymbolData pour gestion des indicateurs
    • Exemples: Momentum, Mean Reversion
  5. CompositeAlphaModel :
    • Combiner plusieurs Alpha Models
    • Diversification des signaux
  6. Stratégie Complete :
    • Integration de tous les composants
    • Universe dynamique + Alpha personnalise + Risk Management

Points Cles a Retenir

Concept Point Cle
Framework 5 composants modulaires interconnectes
Insights Direction + Period + Magnitude + Confidence
Custom Alpha Heriter de AlphaModel, implementer Update()
SymbolData Encapsuler indicateurs par symbole
Composite Combiner pour diversifier les signaux

Tableau des Models disponibles

Type Models Built-In
Universe ManualUniverseSelectionModel, CoarseFundamentalUniverseSelectionModel
Alpha EmaCrossAlphaModel, MacdAlphaModel, RsiAlphaModel, CompositeAlphaModel
Portfolio EqualWeightingPortfolioConstructionModel, InsightWeightingPortfolioConstructionModel
Exécution ImmediateExecutionModel, VolumeWeightedAveragePriceExecutionModel
Risk NullRiskManagementModel, MaximumDrawdownPercentPerSecurity

Limitations

  • Complexite : Le Framework ajoute de la complexite pour les stratégies simples
  • Latence : Le passage entre composants peut ajouter de la latence
  • Conflits : Les Insights contradictoires doivent etre geres par le Portfolio Construction

Prochaines Étapes

Pour aller plus loin avec l’Algorithm Framework :

  1. Portfolio Construction Models avances : Mean-Variance Optimization, Risk Parity
  2. Exécution Models sophistiques : TWAP, VWAP, Implementation Shortfall
  3. Risk Management : VaR-based, Correlation constraints
  4. Machine Learning Alpha : Integrer des modèles ML dans les Alpha Models

Ressources Complementaires

Les concepts fondamentaux enseignes dans ce notebook reposent sur une litterature savante classique. Les ancres ci-dessous permettent d’aller plus loin sur chaque idee nommee :

  • Markowitz, H. (1952). « Portfolio Sélection ». The Journal of Finance, 7(1), 77-91. – Optimisation Mean-Variance / Modern Portfolio Theory (cellule 11).
  • Appel, G. (fin des annees 1970). Inventeur du MACD (Moving Average Convergence Divergence) (cellule 19).
  • Wilder, J. W. (1978). New Concepts in Technical Trading Systems. Trend Research. – Introduction du RSI (cellule 22).
  • Fama, E. F. & French, K. R. (1988). « Permanent and Temporary Components of Stock Prices ». Journal of Political Economy, 96(2), 246-273. – Composante mean-reverting des prix (cellule 31).
  • Jegadeesh, N. & Titman, S. (1993). « Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency ». The Journal of Finance, 48(1), 65-91. – Methodologie du momentum 12M-1M (cellule 49).

Ces references sont des points d’entree ; chacune a donne lieu a une vaste litterature de suivi (ex. Carhart 1997 pour le momentum en facteur de risque, Markowitz 1959 pour le livre-fondateur MPT).


Notebook complete. Vous maitrisez maintenant l’Algorithm Framework et les Alpha Models de QuantConnect.

Retour au sommet