<< Sommaire QC | Précédent : QC-Py-15-Parameter-Optimization << | Suivant : QC-Py-17-Sentiment-Analysis >>

QC-Py-16 - Alternative Data dans QuantConnect

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

Données non-traditionnelles pour generer de l’alpha Duree: 75 minutes | Niveau: Intermediaire-Avance | Python + QuantConnect


Objectifs d’Apprentissage

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

  1. Comprendre le concept d’Alternative Data et son rôle dans la generation d’alpha
  2. Acceder aux Fundamentals Data de Morningstar (P/E, ROE, revenus)
  3. Construire une stratégie Value basee sur les fondamentaux
  4. Comprendre les SEC Filings (8-K) et leur utilisation
  5. Créer des Custom Data Sources pour integrer des données externes
  6. Importer des données CSV locales personnalisees
  7. Utiliser les Economic Indicators (macro data)
  8. Construire une stratégie Event-Driven basee sur les earnings

Prerequis

  • Notebooks QC-Py-01 a 15 completes
  • Comprehension du Framework Algorithm et des Alpha Models
  • Notions de base en analyse fondamentale
  • (Optionnel) Cle API NewsAPI pour les exemples avances

Structure du Notebook

  1. Introduction aux Données Alternatives (15 min)
  2. Fundamentals Data Morningstar (25 min)
  3. SEC Filings (20 min)
  4. Custom Data Sources (30 min)
  5. Economic Indicators (15 min)
  6. Stratégie Event-Driven: Earnings (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 : Introduction aux Données Alternatives (15 min)

Qu’est-ce que l’Alternative Data?

L’Alternative Data (données alternatives) designe toute source de données non-traditionnelle utilisee pour generer un avantage informationnel (alpha) dans les stratégies d’investissement.

Contrairement aux données de marche standard (prix, volume), les données alternatives offrent des insights uniques sur l’economie, les entreprises ou le sentiment des investisseurs.

Types de Données Alternatives

Catégorie Exemples Source
Satellite Images de parkings, reservoirs, champs Planet Labs, Orbital Insight
Social Media Tweets, posts Reddit, avis Twitter API, StockTwits
Web Traffic Visites de sites, app downloads SimilarWeb, App Annie
Sentiment Analyse de news, NLP Brain, Tiingo, NewsAPI
Fundamentals Revenus, P/E, ROE Morningstar, Bloomberg
SEC Filings 8-K, 10-Q, 13-F SEC EDGAR
Geolocation Trafic en magasin, mobilite Foursquare, SafeGraph
Consumer Transactions cartes, e-commerce Second Measure, Earnest

Pourquoi l’Alternative Data?

                    Avantage Informationnel
                            |
+---------------------------+---------------------------+
|                           |                           |
v                           v                           v
Données de Marche      Données Alt.           Données Proprietaires
(Commoditise)          (Competitive Edge)     (Exclusif)
- Prix, Volume          - Sentiment            - Modèles internes
- Faible alpha          - Web scraping         - Research privee
- Tout le monde         - Satellite            - Avantage maximal
  y a acces             - Consumer data

Exemples d’Alpha Generation

Stratégie Donnee Alternative Signal
Retail Earnings Images satellite des parkings Plus de voitures = bonnes ventes
Oil Forecast Niveaux des reservoirs petrole Offre/demande avant publication officielle
Tech Sentiment Tweets sur AAPL Sentiment positif = acheter avant annonce
Value Investing Fundamentals Morningstar Low P/E + High ROE = sous-evalue
# Imports pour ce notebook
# Ces imports fonctionnent dans l'environnement QuantConnect

from AlgorithmImports import *
import json
from datetime import datetime, timedelta

print("Alternative Data dans QuantConnect")
print("="*50)
print("\nSources de donnees couvertes dans ce notebook:")
print("  1. Fundamentals Morningstar (P/E, ROE, Revenue)")
print("  2. SEC Filings (8-K forms)")
print("  3. Custom Data Sources (APIs externes)")
print("  4. Donnees CSV locales")
print("  5. Economic Indicators (macro)")
print("  6. Earnings Calendar")
Alternative Data dans QuantConnect==================================================Sources de donnees couvertes dans ce notebook:  1. Fundamentals Morningstar (P/E, ROE, Revenue)  2. SEC Filings (8-K forms)  3. Custom Data Sources (APIs externes)  4. Donnees CSV locales  5. Economic Indicators (macro)  6. Earnings Calendar

Données Alternatives dans QuantConnect

QuantConnect offre plusieurs sources de données alternatives via son Data Library :

Dataset Provider Tier Description
US Fundamentals Morningstar Free P/E, ROE, Revenue, Market Cap
SEC 8-K Reports SEC Free Corporate events filings
Tiingo News Tiingo Paid News articles avec sentiment
Brain Sentiment Brain Paid NLP sentiment sur 10K/10Q
Estimize Estimize Paid Consensus earnings crowd-sourced

Note Free Tier : Ce notebook utilise NewsAPI (gratuit, 100 req/jour) comme alternative a Tiingo News (payant).

Architecture d’Integration

                    QuantConnect Algorithm
                            |
+---------------------------+---------------------------+
|                           |                           |
v                           v                           v
Built-in Data           AddData()              Custom Data Sources
- Fundamentals          - SEC Filings          - PythonData class
- Fine Sélection        - News feeds           - GetSource()
                                               - Reader()

Partie 2 : Fundamentals Data Morningstar (25 min)

2.1 Acceder aux Fundamentals

Les données fondamentales Morningstar sont accessibles via le Fine Universe Sélection. Elles incluent :

  • Valuation Ratios : P/E, P/B, EV/EBITDA, Dividend Yield
  • Financial Statements : Revenue, Net Income, Operating Income
  • Opération Ratios : ROE, ROA, Operating Margin
  • Earning Ratios : EPS Growth, DPS Growth
  • Asset Classification : Sector, Industry, Market Cap

Structure des Fundamentals

# Dans FineSelectionFunction(self, fine):
for stock in fine:
    # Valuation Ratios
    pe_ratio = stock.ValuationRatios.PERatio
    pb_ratio = stock.ValuationRatios.PBRatio
    ev_ebitda = stock.ValuationRatios.EVToEBITDA
    div_yield = stock.ValuationRatios.TrailingDividendYield
    
    # Financial Statements
    revenue = stock.FinancialStatements.IncomeStatement.TotalRevenue.Value
    net_income = stock.FinancialStatements.IncomeStatement.NetIncome.Value
    ebitda = stock.FinancialStatements.IncomeStatement.EBITDA.Value
    
    # Opération Ratios
    roe = stock.OperationRatios.ROE.Value
    roa = stock.OperationRatios.ROA.Value
    margin = stock.OperationRatios.OperationMargin.Value
    
    # Earning Ratios
    eps_growth = stock.EarningRatios.EquityPerShareGrowth.ThreeMonths
    
    # Classification
    sector = stock.AssetClassification.MorningstarSectorCode
    market_cap = stock.MarketCap
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Demonstration d'acces aux Fundamentals
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class FundamentalsExplorerAlgorithm(QCAlgorithm):
    """
    Explore les donnees fondamentales disponibles dans QuantConnect.
    Log les metriques cles pour comprendre la structure.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe avec Coarse + Fine selection
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        self.num_stocks = 10
        self.last_month = -1
    
    def CoarseSelectionFunction(self, coarse):
        """Premier filtre: liquidite."""
        if self.Time.month == self.last_month:
            return Universe.Unchanged
        self.last_month = self.Time.month
        
        # Filtrer par prix et volume
        filtered = [x for x in coarse 
                    if x.HasFundamentalData
                    and x.Price > 10 
                    and x.DollarVolume > 10000000]
        
        # Trier par volume et prendre les top 100
        sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        return [x.Symbol for x in sorted_stocks[:100]]
    
    def FineSelectionFunction(self, fine):
        """Deuxieme filtre: analyse des fondamentaux."""
        selected = []
        
        for stock in fine:
            try:
                # === VALUATION RATIOS ===
                pe_ratio = stock.ValuationRatios.PERatio
                pb_ratio = stock.ValuationRatios.PBRatio
                ev_ebitda = stock.ValuationRatios.EVToEBITDA
                div_yield = stock.ValuationRatios.TrailingDividendYield
                
                # === FINANCIAL STATEMENTS ===
                revenue = stock.FinancialStatements.IncomeStatement.TotalRevenue.Value
                net_income = stock.FinancialStatements.IncomeStatement.NetIncome.Value
                
                # === OPERATION RATIOS ===
                roe = stock.OperationRatios.ROE.Value
                roa = stock.OperationRatios.ROA.Value
                op_margin = stock.OperationRatios.OperationMargin.Value
                
                # === EARNING RATIOS ===
                eps_growth = stock.EarningRatios.EquityPerShareGrowth.ThreeMonths if hasattr(stock.EarningRatios.EquityPerShareGrowth, 'ThreeMonths') else None
                
                # === CLASSIFICATION ===
                sector = stock.AssetClassification.MorningstarSectorCode
                market_cap = stock.MarketCap
                
                # Log les metriques pour les premiers stocks
                if len(selected) < 3:
                    self.Log(f"\n{stock.Symbol.Value}:")
                    self.Log(f"  P/E: {pe_ratio:.2f}" if pe_ratio else "  P/E: N/A")
                    self.Log(f"  P/B: {pb_ratio:.2f}" if pb_ratio else "  P/B: N/A")
                    self.Log(f"  ROE: {roe:.2%}" if roe else "  ROE: N/A")
                    self.Log(f"  ROA: {roa:.2%}" if roa else "  ROA: N/A")
                    self.Log(f"  Revenue: ${revenue/1e9:.2f}B" if revenue else "  Revenue: N/A")
                    self.Log(f"  Market Cap: ${market_cap/1e9:.2f}B" if market_cap else "  Market Cap: N/A")
                    self.Log(f"  Sector: {sector}")
                
                # Critere de selection: P/E positif et raisonnable
                if pe_ratio and 0 < pe_ratio < 25 and roe and roe > 0.10:
                    selected.append(stock)
            
            except Exception as e:
                continue
        
        # Trier par ROE decroissant
        sorted_by_roe = sorted(selected, key=lambda x: x.OperationRatios.ROE.Value or 0, reverse=True)
        
        self.Log(f"\nFine selection: {len(fine)} -> {len(selected)} filtered -> {self.num_stocks} selected")
        
        return [x.Symbol for x in sorted_by_roe[:self.num_stocks]]

print("FundamentalsExplorerAlgorithm defini")
print("\nMetriques disponibles:")
print("  - ValuationRatios: PERatio, PBRatio, EVToEBITDA")
print("  - FinancialStatements: TotalRevenue, NetIncome")
print("  - OperationRatios: ROE, ROA, OperationMargin")
print("  - AssetClassification: MorningstarSectorCode, MarketCap")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 1526e20e4bdbcb5bf3a0b9d8780261d7)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 1526e20e4bdbcb5bf3a0b9d8780261d7

FundamentalsExplorerAlgorithm — univers uniquement, aucun trading dans OnData (squelette pedagogique). Verdict #3801 : INTRINSIC (0 trade verifie, 0 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 1526e20e4bdbcb5bf3a0b9d8780261d7) - 0 jour(s) boursier(s), Capital initial: $100,000

2.2 Stratégie Value Complete

Construisons une stratégie Value Investing basee sur les fondamentaux Morningstar.

Critères de sélection : - P/E Ratio entre 5 et 15 (pas trop bas pour eviter les value traps) - ROE > 15% (entreprise profitable) - Market Cap > $5B (large cap pour liquidite) - Rebalancement trimestriel

Théorie Value Investing

                    Warren Buffett Approach
                            |
+---------------------------+---------------------------+
|                           |                           |
v                           v                           v
Margin of Safety       Quality Business          Long-term Holding
- Low P/E               - High ROE                - Years, not days
- Low P/B               - Moat (competitive)      - Compound growth
- Below intrinsic       - Consistent earnings
  value

Ancre savante – Graham, B. (1949), « The Intelligent Investor: A Book of Practical Counsel », Harper & Brothers, New York (1re edition 1949 ; editions revises 1954, 1959, 1965, 1973). Origine canonique de l’investissement value : le chapitre 20 formalise la marge de securite (margin of safety) comme concept central – acheter un actif sensiblement en dessous de sa valeur intrinseque pour que la marge absorbe l’erreur d’estimation et les aleas defavorables. Graham y distingue aussi l’investisseur defensif de l’investisseur entreprenant et introduit l’allegorie de Mr. Market. Warren Buffett, eleve de Graham a Columbia, en a fait l’application moderne illustree par le schema ci-dessus (marge de securite + qualite de l’entreprise + detention long terme) – les critères P/E bas, ROE eleve et market cap liquide de la stratégie Value Complete materialisent ce principe.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Strategie Value complete avec Fundamentals
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class ValueInvestingStrategy(QCAlgorithm):
    """
    Strategie Value Investing:
    - Selection: Low P/E (5-15) + High ROE (>15%)
    - Filtre: Large Cap (>5B), exclut financials
    - Rebalancement: Trimestriel
    - Allocation: Equal-weight
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe settings
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        # Parametres de la strategie
        self.num_stocks = 20
        self.min_pe = 5
        self.max_pe = 15
        self.min_roe = 0.15        # 15%
        self.min_market_cap = 5e9  # $5B
        
        # Tracking pour rebalancement trimestriel
        self.last_rebalance = None
        self.rebalance_days = 90  # ~trimestriel
        
        # Reference pour schedule
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        
        self.Log("Value Investing Strategy initialized")
        self.Log(f"  P/E range: {self.min_pe} - {self.max_pe}")
        self.Log(f"  Min ROE: {self.min_roe:.0%}")
        self.Log(f"  Min Market Cap: ${self.min_market_cap/1e9:.0f}B")
    
    def CoarseSelectionFunction(self, coarse):
        """Filtre initial: liquidite et prix."""
        # Verifier si c'est le moment de rebalancer
        if self.last_rebalance is not None:
            days_since = (self.Time - self.last_rebalance).days
            if days_since < self.rebalance_days:
                return Universe.Unchanged
        
        # Filtrer
        filtered = [x for x in coarse 
                    if x.HasFundamentalData
                    and x.Price > 10 
                    and x.DollarVolume > 5000000]
        
        sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        
        self.Log(f"Coarse: {len(coarse)} -> {len(filtered)} filtered -> 500 candidates")
        
        return [x.Symbol for x in sorted_stocks[:500]]
    
    def FineSelectionFunction(self, fine):
        """Filtre Value: P/E, ROE, Market Cap."""
        self.last_rebalance = self.Time
        
        value_stocks = []
        
        for stock in fine:
            try:
                # Exclure le secteur financier (P/E non comparable)
                if stock.AssetClassification.MorningstarSectorCode == MorningstarSectorCode.FinancialServices:
                    continue
                
                # Criteres de selection
                pe = stock.ValuationRatios.PERatio
                roe = stock.OperationRatios.ROE.Value
                market_cap = stock.MarketCap
                
                # Verifier que les donnees existent
                if pe is None or roe is None or market_cap is None:
                    continue
                
                # Appliquer les filtres Value
                if (self.min_pe < pe < self.max_pe and
                    roe > self.min_roe and
                    market_cap > self.min_market_cap):
                    
                    value_stocks.append({
                        'symbol': stock.Symbol,
                        'pe': pe,
                        'roe': roe,
                        'market_cap': market_cap,
                        # Composite score: low P/E + high ROE
                        'value_score': (1/pe) * roe * 100
                    })
            
            except Exception:
                continue
        
        # Trier par value score (plus eleve = meilleur)
        sorted_by_score = sorted(value_stocks, 
                                  key=lambda x: x['value_score'], 
                                  reverse=True)
        
        # Log les top picks
        self.Log(f"\nValue Selection {self.Time.strftime('%Y-%m-%d')}:")
        for i, stock in enumerate(sorted_by_score[:5]):
            self.Log(f"  {i+1}. {stock['symbol'].Value}: P/E={stock['pe']:.1f}, ROE={stock['roe']:.1%}, Score={stock['value_score']:.2f}")
        
        self.Log(f"Fine: {len(fine)} -> {len(value_stocks)} value -> {self.num_stocks} selected")
        
        return [x['symbol'] for x in sorted_by_score[:self.num_stocks]]
    
    def OnSecuritiesChanged(self, changes):
        """Rebalancer lors des changements d'univers."""
        # Vendre les sortants
        for security in changes.RemovedSecurities:
            if security.Invested:
                self.Liquidate(security.Symbol)
                self.Log(f"SELL: {security.Symbol.Value}")
        
        # Calculer le poids cible (exclure SPY)
        trading_symbols = [s for s in self.ActiveSecurities.Keys if s != self.spy]
        count = len(trading_symbols)
        if count == 0:
            return
        
        weight = 1.0 / count
        
        # Acheter les nouvelles positions
        for symbol in trading_symbols:
            self.SetHoldings(symbol, weight)
        
        self.Log(f"Rebalanced: {count} positions @ {weight:.1%}")
    
    def OnEndOfAlgorithm(self):
        self.Log("="*50)
        self.Log("VALUE INVESTING STRATEGY SUMMARY")
        self.Log("="*50)
        self.Log(f"Final Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
        total_return = (self.Portfolio.TotalPortfolioValue - 100000) / 100000
        self.Log(f"Total Return: {total_return:.1%}")

print("ValueInvestingStrategy defini")
print("\nFiltres Value:")
print("  - P/E entre 5 et 15")
print("  - ROE > 15%")
print("  - Market Cap > $5B")
print("  - Exclut secteur financier")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 10b06227dad01787dcace56d13f86534)

Metrique Valeur
Status Completed
Sharpe Ratio 0.298
CAGR 8.218%
Max Drawdown 43.800%
PSR 1.332%
Jours boursiers 2516
Backtest ID 10b06227dad01787dcace56d13f86534

ValueInvestingStrategy — rebalance via OnSecuritiesChanged sur fenêtre 90 j ; 10 ans executes (2015-2024). Verdict #3801 : REAL - performance reelle verifiee. Le champ totalOrders n’est pas expose de facon fiable par l’API backtest, mais la courbe d’equity confirme l’activite (CAGR +8.2%/an).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 10b06227dad01787dcace56d13f86534) - 2516 jour(s) boursier(s), Capital initial: $100,000

2.3 Secteurs Morningstar

Pour filtrer par secteur, utilisez MorningstarSectorCode :

Code Secteur Caractéristiques P/E
BasicMaterials Materiaux Cyclique
ConsumerCyclical Conso. cyclique Cyclique
FinancialServices Finance P/E non comparable
RealEstate Immobilier Utiliser FFO
ConsumerDefensive Conso. defensive Stable
Healthcare Sante Mix growth/value
Utilities Services publics Dividende
CommunicationServices Communication Mix
Energy Energie Très cyclique
Industrials Industrie Cyclique
Technology Technologie Growth, P/E eleve
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Exemple: Filtrage par secteur dans Fine Selection
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class SectorFilterExample(QCAlgorithm):
    """
    Exemple de filtrage par secteur Morningstar.
    Selectionne uniquement les actions du secteur Technology.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        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[:500]]
    
    def FineSelectionFunction(self, fine):
        """Filtrer par secteur Technology uniquement."""
        tech_stocks = []
        
        for stock in fine:
            # Verifier le secteur
            sector = stock.AssetClassification.MorningstarSectorCode
            
            if sector == MorningstarSectorCode.Technology:
                tech_stocks.append(stock)
        
        # Trier par Market Cap
        sorted_by_cap = sorted(tech_stocks, 
                               key=lambda x: x.MarketCap or 0, 
                               reverse=True)
        
        self.Log(f"Tech sector: {len(tech_stocks)} stocks found")
        return [x.Symbol for x in sorted_by_cap[:20]]

print("SectorFilterExample defini")
print("\nCodes secteur disponibles:")
print("  MorningstarSectorCode.Technology")
print("  MorningstarSectorCode.Healthcare")
print("  MorningstarSectorCode.FinancialServices")
print("  MorningstarSectorCode.ConsumerCyclical")
print("  etc.")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 97420eec8aee6c8f111e88062c73386e)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 97420eec8aee6c8f111e88062c73386e

SectorFilterExample — filtre d’univers par secteur uniquement, AddData custom commente. Verdict #3801 : INTRINSIC (0 trade verifie, 0 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 97420eec8aee6c8f111e88062c73386e) - 0 jour(s) boursier(s), Capital initial: $100,000


Exercice 1 : Stratégie de rotation sectorielle

Les fundamentals Morningstar permettent de classer les secteurs par attractivite. Une stratégie de rotation sectorielle alloue plus de capital aux secteurs les plus attractifs.

Objectif : Implementer une stratégie qui selectionne les 2 meilleurs secteurs selon les fundamentals et sous-pondere les autres.

Règles : - Classez les 11 secteurs Morningstar par ratio P/E (croissant) et ROE (decroissant) - Score combine = rank(PE) * 0.4 + rank(ROE) * 0.6 - Allouez 30% aux 2 meilleurs secteurs, 5% aux 3 suivants, 0% aux autres - Rebalance mensuellement

Indices : - Indice : Fundamentals[symbol].ValuationRatios.PERatio pour le P/E - Indice : Fundamentals[symbol].OperationRatios.ROE.Value pour le ROE - Indice : FineFundamental dans OnFrameworkData donne acces aux fundamentals

# [REFERENCE QC] Exercice 1 : Rotation sectorielle basee sur les fundamentals
# TODO etudiant : Implementer une allocation sectorielle dynamique
# Indice : Scorer les secteurs par PE + ROE, allouer proportionnellement
# Etape 1 : Selectionner les actifs dans chaque secteur Morningstar
# Etape 2 : Calculer le score combine (PE + ROE) par secteur
# Etape 3 : Classer et determiner les poids d'allocation
# Etape 4 : Generer les insights d'allocation dans OnFrameworkData

result = None  # TODO etudiant : remplacer par la strategie de rotation sectorielle
print("Exercice a completer")

Partie 3 : SEC Filings (20 min)

3.1 Comprendre les SEC Filings

Les SEC Filings sont des documents officiels deposes aupres de la Securities and Exchange Commission. Ils contiennent des informations importantes sur les entreprises cotees.

Types de Filings

Form Description Frequence
8-K Current Report - événements materiels Ad hoc
10-K Annual Report Annuel
10-Q Quarterly Report Trimestriel
13-F Institutional Holdings Trimestriel
4 Insider Transactions Ad hoc

8-K Events (Current Reports)

Les 8-K sont particulierement interessants car ils annoncent des événements importants :

Item Événement Impact Potentiel
1.01 Entry into Material Agreement Positif
1.02 Termination of Agreement Negatif
2.01 Completion of Acquisition Variable
2.02 Results of Opérations (Earnings) Majeur
5.02 Departure/Appointment of Directors Variable
5.07 Shareholder Vote Results Informatif
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# SEC 8-K Filings dans QuantConnect
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class SEC8KAlgorithm(QCAlgorithm):
    """
    Strategie basee sur les SEC 8-K filings.
    Detecte les evenements materiels et trade en consequence.
    
    Note: L'acces aux donnees SEC peut necessiter un abonnement.
    Cet exemple montre la structure d'integration.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter les actions a surveiller
        self.symbols = [
            self.AddEquity("AAPL", Resolution.Daily).Symbol,
            self.AddEquity("MSFT", Resolution.Daily).Symbol,
            self.AddEquity("GOOGL", Resolution.Daily).Symbol,
        ]
        
        # Dictionnaire pour tracker les filings
        self.recent_filings = {}
        
        self.Log("SEC 8-K Algorithm initialized")
        self.Log("Monitoring: " + ", ".join([s.Value for s in self.symbols]))
    
    def OnData(self, data):
        """
        Note: En production, vous utiliseriez AddData() avec SECReport8K
        pour recevoir les filings en temps reel.
        
        Exemple de structure:
        
        if data.ContainsKey(self.sec_symbol):
            report = data[self.sec_symbol]
            filing_date = report.Time
            form_type = report.FormType
            
            # Analyser le contenu
            if "acquisition" in report.Report.lower():
                self.SetHoldings(symbol, 0.1)
        """
        pass

print("SEC8KAlgorithm defini (structure d'exemple)")
print("\nNote: L'integration complete des SEC filings dans QuantConnect")
print("necessite l'acces au dataset SEC via le Data Library.")
print("\nTypes d'analyses possibles:")
print("  - Detection d'acquisitions (Item 2.01)")
print("  - Earnings surprises (Item 2.02)")
print("  - Changements de direction (Item 5.02)")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 8be64c6b3d7af6d57d4904f13991e2dc)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 8be64c6b3d7af6d57d4904f13991e2dc

SEC8KAlgorithm — OnData passe ; données SEC 8-K non connectees (souscription requise). Verdict #3801 : INTRINSIC (0 trade verifie, 2516 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 8be64c6b3d7af6d57d4904f13991e2dc) - 2516 jour(s) boursier(s), Capital initial: $100,000

On connecte ici une source de données alternatives (sentiment, actualités, données satellitaires) à l’environnement QuantConnect.

# Exemple: Analyse NLP simple des SEC Filings
# Demonstration de keyword detection

# Liste de mots-cles pour analyse de filings
BULLISH_KEYWORDS = [
    'growth', 'expansion', 'increase', 'profit', 'beat', 'exceed',
    'acquisition', 'partnership', 'innovation', 'dividend', 'buyback'
]

BEARISH_KEYWORDS = [
    'decline', 'loss', 'decrease', 'investigation', 'lawsuit', 'layoff',
    'restructuring', 'impairment', 'default', 'warning', 'miss'
]

def analyze_filing_sentiment(text):
    """
    Analyse simple du sentiment d'un filing SEC.
    
    Args:
        text: Contenu textuel du filing
    
    Returns:
        dict: score et classification
    """
    text_lower = text.lower()
    
    # Compter les keywords
    bullish_count = sum(1 for kw in BULLISH_KEYWORDS if kw in text_lower)
    bearish_count = sum(1 for kw in BEARISH_KEYWORDS if kw in text_lower)
    
    # Calculer le score
    total = bullish_count + bearish_count
    if total == 0:
        return {'score': 0, 'classification': 'NEUTRAL', 'bullish': 0, 'bearish': 0}
    
    score = (bullish_count - bearish_count) / total
    
    if score > 0.2:
        classification = 'BULLISH'
    elif score < -0.2:
        classification = 'BEARISH'
    else:
        classification = 'NEUTRAL'
    
    return {
        'score': score,
        'classification': classification,
        'bullish': bullish_count,
        'bearish': bearish_count
    }

# Test avec exemples
test_texts = [
    "Company announces record profit and expansion into new markets",
    "Investigation opened, significant loss expected, layoffs announced",
    "Quarterly results in line with expectations"
]

print("Analyse de sentiment de filings SEC:")
print("="*50)
for text in test_texts:
    result = analyze_filing_sentiment(text)
    print(f"\nTexte: {text[:50]}...")
    print(f"  Classification: {result['classification']}")
    print(f"  Score: {result['score']:.2f}")
    print(f"  Bullish keywords: {result['bullish']}, Bearish: {result['bearish']}")
Analyse de sentiment de filings SEC:==================================================Texte: Company announces record profit and expansion into...  Classification: BULLISH  Score: 1.00  Bullish keywords: 2, Bearish: 0Texte: Investigation opened, significant loss expected, l...  Classification: BEARISH  Score: -1.00  Bullish keywords: 0, Bearish: 3Texte: Quarterly results in line with expectations...  Classification: NEUTRAL  Score: 0.00  Bullish keywords: 0, Bearish: 0

3.2 Structure d’Integration SEC (Reference)

Voici comment integrer les SEC filings quand ils sont disponibles :

# Dans Initialize()
# Ajouter les données SEC pour un symbole
self.sec_symbol = self.AddData(SECReport8K, "AAPL").Symbol

# Dans OnData()
def OnData(self, data):
    if data.ContainsKey(self.sec_symbol):
        report = data[self.sec_symbol]
        
        # Informations disponibles
        filing_date = report.Time
        form_type = report.FormType  # "8-K"
        report_text = report.Report  # Contenu textuel
        
        # Analyser et trader
        self.ProcessFiling(report)

Analyse NLP des Filings :

Les traders quantitatifs utilisent souvent le NLP pour analyser le texte des filings : - Sentiment Analysis : Positif/negatif - Keyword Detection : “acquisition”, “termination”, “investigation” - Complexity Analysis : Readability scores

Ancre savante – Loughran, T. & McDonald, B. (2011), « When Is a Liability Not a Liability? Textual Analysis, Dictionaries, and 10-Ks », The Journal of Finance 66(1):35-65 (DOI 10.1111/j.1540-6261.2010.01625.x). Montre que les dictionnaires de sentiment generiques (notamment le Harvard IV-4) classent a tort comme negatifs pres de trois quarts des mots frequents en contexte financier (ex. « liability », « tax », « cost » n’y sont pas pejoratifs) ; les auteurs construisent donc un lexique finance-spécifique (Loughran-McDonald Master Dictionary, six listes : negative, positive, uncertainty, litigious, constraining, superfluous) calibre sur un large corpus de 10-Ks 1994-2008, qu’ils relient au rendement, au volume, a la volatilite et aux fraudes. C’est la reference methodologique du NLP sur filings enseigne dans cette section 3.2 (sentiment, keyword detection, readability) – la raison pour laquelle un lexique finance-dedie est preferable a un dictionnaire generaliste pour analyser le texte des SEC filings.


Partie 4 : Custom Data Sources (30 min)

4.1 Créer une Custom Data Source

QuantConnect permet d’integrer des sources de données externes via la classe PythonData. C’est essentiel pour :

  • APIs externes : NewsAPI, Twitter, Alpha Vantage
  • Fichiers CSV : Données historiques personnalisees
  • Web scraping : Données proprietaires

Structure d’une Custom Data Class

class MyCustomData(PythonData):
    
    def GetSource(self, config, date, isLiveMode):
        # Retourne l'URL ou le chemin de la source
        return SubscriptionDataSource(url, transport_medium)
    
    def Reader(self, config, line, date, isLiveMode):
        # Parse une ligne de données
        # Retourne un objet MyCustomData ou None
        return custom_data_object

Transport Mediums

Medium Description
RemoteFile Fichier distant (HTTP/HTTPS)
LocalFile Fichier local
Rest API REST
# Custom Data Source: NewsAPI
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
import json

class NewsAPIData(PythonData):
    """
    Custom Data Source pour NewsAPI.
    NewsAPI offre un tier gratuit avec 100 requetes/jour.
    
    Documentation: https://newsapi.org/docs
    
    Note: En production, vous devriez:
    1. Stocker la cle API de maniere securisee
    2. Gerer le rate limiting
    3. Cacher les resultats
    """
    
    # Cle API (remplacer par votre cle)
    API_KEY = "YOUR_NEWSAPI_KEY_HERE"
    
    def GetSource(self, config, date, isLiveMode):
        """
        Retourne l'URL de la source de donnees.
        
        Parameters:
            config: Configuration de la subscription
            date: Date demandee
            isLiveMode: True si en mode live
        
        Returns:
            SubscriptionDataSource avec l'URL
        """
        # Construire l'URL NewsAPI
        # En mode backtest, on utiliserait des donnees historiques stockees
        # En mode live, on appelle l'API en temps reel
        
        ticker = config.Symbol.Value.replace(" ", "%20")
        date_str = date.strftime("%Y-%m-%d")
        
        # URL pour les articles sur le ticker
        url = f"https://newsapi.org/v2/everything?q={ticker}&from={date_str}&sortBy=publishedAt&apiKey={self.API_KEY}"
        
        return SubscriptionDataSource(
            url, 
            SubscriptionTransportMedium.RemoteFile,
            FileFormat.UnfoldingCollection  # Pour JSON avec plusieurs items
        )
    
    def Reader(self, config, line, date, isLiveMode):
        """
        Parse les donnees JSON de NewsAPI.
        
        Parameters:
            config: Configuration
            line: Ligne de donnees (JSON string)
            date: Date demandee
            isLiveMode: True si live
        
        Returns:
            NewsAPIData object ou None
        """
        if not line.strip():
            return None
        
        try:
            data = json.loads(line)
            
            # Verifier si c'est une reponse valide
            if 'articles' not in data:
                return None
            
            # Creer un objet NewsAPIData pour chaque article
            articles = []
            for article in data['articles']:
                news = NewsAPIData()
                news.Symbol = config.Symbol
                
                # Parser la date de publication
                pub_date = article.get('publishedAt', '')
                if pub_date:
                    news.Time = datetime.strptime(pub_date[:19], '%Y-%m-%dT%H:%M:%S')
                else:
                    news.Time = date
                
                # Stocker les proprietes de l'article
                news['Title'] = article.get('title', '')
                news['Description'] = article.get('description', '')
                news['Source'] = article.get('source', {}).get('name', '')
                news['URL'] = article.get('url', '')
                
                # Value par defaut (peut etre utilise pour scoring)
                news.Value = 1.0
                
                articles.append(news)
            
            # Retourner le premier article (simplifie)
            return articles[0] if articles else None
        
        except Exception as e:
            return None

print("NewsAPIData Custom Data Source defini")
print("\nMethodes implementees:")
print("  1. GetSource(): Retourne l'URL de l'API")
print("  2. Reader(): Parse le JSON et cree des objets")
print("\nProprietes disponibles apres parsing:")
print("  - Title, Description, Source, URL")
NewsAPIData Custom Data Source definiMethodes implementees:  1. GetSource(): Retourne l'URL de l'API  2. Reader(): Parse le JSON et cree des objetsProprietes disponibles apres parsing:  - Title, Description, Source, URL

On prétraite et normalise les données alternatives pour les rendre exploitables dans un modèle de trading.

# Exemple avance: Custom Data avec caching
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
import json

class CachedNewsData(PythonData):
    """
    Version avec cache pour reduire les appels API.
    Utile en backtest pour ne pas depasser les quotas.
    """
    
    # Cache statique (partagee entre instances)
    _cache = {}
    
    def GetSource(self, config, date, isLiveMode):
        # En backtest, charger depuis fichier cache
        if not isLiveMode:
            cache_file = f"data/news_cache/{config.Symbol.Value}/{date.strftime('%Y-%m-%d')}.json"
            return SubscriptionDataSource(
                cache_file,
                SubscriptionTransportMedium.LocalFile
            )
        
        # En live, appeler l'API
        ticker = config.Symbol.Value
        return SubscriptionDataSource(
            f"https://newsapi.org/v2/everything?q={ticker}&apiKey=YOUR_KEY",
            SubscriptionTransportMedium.RemoteFile
        )
    
    def Reader(self, config, line, date, isLiveMode):
        if not line.strip():
            return None
        
        try:
            data = json.loads(line)
            
            news = CachedNewsData()
            news.Symbol = config.Symbol
            news.Time = date
            
            # Stocker les donnees
            if 'title' in data:
                news['Title'] = data['title']
                news['Sentiment'] = data.get('sentiment', 0)
                news.Value = data.get('sentiment', 0)
            
            return news
        
        except Exception:
            return None

print("CachedNewsData defini")
print("\nPattern de caching:")
print("  1. Backtest: charger depuis fichiers JSON locaux")
print("  2. Live: appeler l'API en temps reel")
print("  3. Pre-generer le cache avant le backtest")
CachedNewsData definiPattern de caching:  1. Backtest: charger depuis fichiers JSON locaux  2. Live: appeler l'API en temps reel  3. Pre-generer le cache avant le backtest

On intègre le signal issu des données alternatives dans la logique de génération d’ordres de l’algorithme.

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

from AlgorithmImports import *

class NewsAPIAlgorithm(QCAlgorithm):
    """
    Algorithme utilisant NewsAPI comme source de donnees.
    Detecte les news et trade en consequence.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Ajouter l'action principale
        self.aapl = self.AddEquity("AAPL", Resolution.Hour).Symbol
        
        # Ajouter la Custom Data Source
        # self.news_symbol = self.AddData(NewsAPIData, "AAPL", Resolution.Hour).Symbol
        
        # Compteur de news
        self.news_count = 0
        
        self.Log("NewsAPI Algorithm initialized")
    
    def OnData(self, data):
        """
        Traite les donnees de marche et les news.
        """
        # Verifier si des news sont disponibles
        # if data.ContainsKey(self.news_symbol):
        #     news = data[self.news_symbol]
        #     self.ProcessNews(news)
        pass
    
    def ProcessNews(self, news):
        """
        Analyse une news et genere des signaux.
        
        En production, vous utiliseriez:
        - Sentiment analysis (VADER, TextBlob)
        - Keyword detection
        - NLP models
        """
        title = news['Title'].lower()
        
        # Simple keyword detection
        bullish_keywords = ['beats', 'exceeds', 'growth', 'upgrade', 'positive']
        bearish_keywords = ['misses', 'decline', 'downgrade', 'negative', 'lawsuit']
        
        is_bullish = any(kw in title for kw in bullish_keywords)
        is_bearish = any(kw in title for kw in bearish_keywords)
        
        self.Log(f"News: {news['Title'][:50]}...")
        self.Log(f"  Source: {news['Source']}, Bullish: {is_bullish}, Bearish: {is_bearish}")
        
        # Trading basique sur keywords
        if is_bullish and not is_bearish:
            if not self.Portfolio[self.aapl].Invested:
                self.SetHoldings(self.aapl, 0.5)
                self.Log("BUY signal on positive news")
        
        elif is_bearish and not is_bullish:
            if self.Portfolio[self.aapl].Invested:
                self.Liquidate(self.aapl)
                self.Log("SELL signal on negative news")

print("NewsAPIAlgorithm defini")
print("\nNote: L'exemple complet necessite une cle API NewsAPI valide.")
print("Inscrivez-vous gratuitement sur https://newsapi.org/")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 408b70550a14c35cf4dbbab095b2e123)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 408b70550a14c35cf4dbbab095b2e123

NewsAPIAlgorithm — OnData passe ; cle API NewsAPI requise. Verdict #3801 : RECOVERABLE-USER-HAND (0 trade verifie, 2516 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 408b70550a14c35cf4dbbab095b2e123) - 2516 jour(s) boursier(s), Capital initial: $100,000

4.2 Données CSV Locales

Pour importer des données historiques personnalisees, vous pouvez utiliser des fichiers CSV.

Format attendu : Date, valeurs separees par virgules

date,gdp_growth,unemployment,inflation
2023-01-01,2.5,3.5,4.2
2023-02-01,2.3,3.6,4.1
2023-03-01,2.1,3.7,3.9
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Custom Data Source: CSV Local
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class EconomicCSVData(PythonData):
    """
    Custom Data Source pour donnees economiques en CSV.
    
    Format CSV attendu:
    date,gdp_growth,unemployment,inflation
    2023-01-01,2.5,3.5,4.2
    """
    
    def GetSource(self, config, date, isLiveMode):
        """
        Retourne le chemin du fichier CSV.
        
        Note: Le fichier doit etre uploade dans QuantConnect
        via le panneau 'Data' ou place dans le dossier /data/
        """
        # Chemin vers le fichier CSV
        # En production, utilisez le chemin dans votre projet QC
        path = "data/economic_indicators.csv"
        
        return SubscriptionDataSource(
            path,
            SubscriptionTransportMedium.LocalFile
        )
    
    def Reader(self, config, line, date, isLiveMode):
        """
        Parse une ligne du fichier CSV.
        """
        # Ignorer les lignes vides et l'en-tete
        if not line.strip() or line.startswith('date'):
            return None
        
        try:
            # Parser les colonnes
            cols = line.split(',')
            
            data = EconomicCSVData()
            data.Symbol = config.Symbol
            
            # Parser la date (colonne 0)
            data.Time = datetime.strptime(cols[0].strip(), '%Y-%m-%d')
            
            # Stocker les indicateurs economiques
            data['GDP_Growth'] = float(cols[1])
            data['Unemployment'] = float(cols[2])
            data['Inflation'] = float(cols[3])
            
            # Value = GDP Growth (peut servir pour filtering)
            data.Value = float(cols[1])
            
            return data
        
        except Exception as e:
            return None


class CSVDataAlgorithm(QCAlgorithm):
    """
    Algorithme utilisant des donnees CSV economiques.
    Tactical allocation basee sur les indicateurs macro.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # ETFs pour allocation tactique
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol  # Actions
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol  # Obligations
        self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol  # Or
        
        # Ajouter les donnees CSV
        # self.econ_data = self.AddData(EconomicCSVData, "ECON", Resolution.Daily).Symbol
        
        # Derniers indicateurs connus
        self.last_gdp = None
        self.last_inflation = None
        
        self.Log("CSV Data Algorithm initialized")
    
    def OnData(self, data):
        """
        Allocation tactique basee sur les indicateurs macro.
        """
        # En production avec les donnees CSV:
        # if data.ContainsKey(self.econ_data):
        #     econ = data[self.econ_data]
        #     self.last_gdp = econ['GDP_Growth']
        #     self.last_inflation = econ['Inflation']
        #     self.Rebalance()
        pass
    
    def Rebalance(self):
        """
        Allocation tactique basee sur l'environnement macro.
        
        | GDP High | GDP Low  |
        |----------|----------|
        | Inflation High: SPY 30%, TLT 20%, GLD 50% |
        | Inflation Low:  SPY 70%, TLT 30%, GLD 0%  |
        """
        if self.last_gdp is None or self.last_inflation is None:
            return
        
        # Seuils
        gdp_high = self.last_gdp > 2.0
        inflation_high = self.last_inflation > 3.0
        
        if gdp_high and not inflation_high:
            # Expansion sans inflation = risk-on
            self.SetHoldings(self.spy, 0.70)
            self.SetHoldings(self.tlt, 0.30)
            self.SetHoldings(self.gld, 0.00)
        
        elif gdp_high and inflation_high:
            # Expansion avec inflation = or
            self.SetHoldings(self.spy, 0.30)
            self.SetHoldings(self.tlt, 0.20)
            self.SetHoldings(self.gld, 0.50)
        
        elif not gdp_high and not inflation_high:
            # Recession sans inflation = bonds
            self.SetHoldings(self.spy, 0.20)
            self.SetHoldings(self.tlt, 0.70)
            self.SetHoldings(self.gld, 0.10)
        
        else:
            # Stagflation = defensive
            self.SetHoldings(self.spy, 0.10)
            self.SetHoldings(self.tlt, 0.40)
            self.SetHoldings(self.gld, 0.50)
        
        self.Log(f"Rebalanced: GDP={self.last_gdp:.1f}%, Inflation={self.last_inflation:.1f}%")

print("EconomicCSVData et CSVDataAlgorithm definis")
print("\nPour utiliser en production:")
print("  1. Creer le fichier CSV avec vos donnees")
print("  2. Uploader dans QuantConnect (Data panel)")
print("  3. Decommentez les lignes AddData()")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 6e9675d6cf50f9a3c088cb63aca5ddcf)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 6e9675d6cf50f9a3c088cb63aca5ddcf

CSVDataAlgorithm — OnData passe ; fichier CSV economique a uploader dans QC. Verdict #3801 : RECOVERABLE-USER-HAND (0 trade verifie, 2516 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 6e9675d6cf50f9a3c088cb63aca5ddcf) - 2516 jour(s) boursier(s), Capital initial: $100,000


Exercice 2 : Source de données personnalisee multi-format

QuantConnect accepte les CSV mais aussi les JSON et les données en temps reel via API. Cet exercice combine deux sources en une seule custom data source.

Objectif : Créer une custom data source qui fusionne des données CSV (historique) et JSON (temps reel simule).

Règles : - Definissez une classe MultiFormatData heritant de DynamicData - Méthode GetSource() : retourne différentes URL selon la date (avant 2020 = CSV, après = JSON) - Méthode Reader() : parsez selon le format et remplissez les proprietes - Proprietes : sentiment_score, volume_indicator, signal_strength

Indices : - Indice : SourceType peut etre Csv ou Rest selon le format - Indice : Utilisez self.SetField() pour les propriete dynamiques

# [REFERENCE QC] Exercice 2 : Custom data source multi-format (CSV + JSON)
# TODO etudiant : Creer une source fusionnant CSV historique et JSON temps reel
# Indice : Heriter de DynamicData, differencier par date dans GetSource()
# Etape 1 : Definir la classe MultiFormatData(PythonData)
# Etape 2 : Implementer GetSource() avec logique CSV/JSON selon la date
# Etape 3 : Implementer Reader() avec parsing multi-format
# Etape 4 : Utiliser les donnees dans OnData()

result = None  # TODO etudiant : remplacer par la source multi-format
print("Exercice a completer")

5.2 FRED Data Integration

Pour integrer des données FRED (Federal Reserve Economic Data), vous pouvez créer une Custom Data Source qui charge les series economiques.

Series FRED utiles pour le trading :

Code FRED Description Usage
FEDFUNDS Fed Funds Rate Politique monetaire
UNRATE Unemployment Rate Sante economique
CPIAUCSL CPI All Items Inflation
T10Y2Y 10Y-2Y Spread Yield curve, recession
VIXCLS VIX Close Volatilite, stress
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Custom Data Source: FRED Economic Data
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class FREDData(PythonData):
    """
    Custom Data Source pour FRED (Federal Reserve Economic Data).
    
    Note: Necessite un fichier CSV avec les donnees FRED pre-telechargees
    ou une API key pour Alpha Vantage / Quandl qui fournissent FRED.
    """
    
    def GetSource(self, config, date, isLiveMode):
        # Charger depuis fichier CSV local
        # Format: date,value
        series_code = config.Symbol.Value  # ex: "FEDFUNDS"
        path = f"data/fred/{series_code}.csv"
        
        return SubscriptionDataSource(
            path,
            SubscriptionTransportMedium.LocalFile
        )
    
    def Reader(self, config, line, date, isLiveMode):
        if not line.strip() or line.startswith('date'):
            return None
        
        try:
            cols = line.split(',')
            
            fred = FREDData()
            fred.Symbol = config.Symbol
            fred.Time = datetime.strptime(cols[0].strip(), '%Y-%m-%d')
            fred.Value = float(cols[1])
            fred['Rate'] = float(cols[1])
            
            return fred
        
        except Exception:
            return None


class YieldCurveAlgorithm(QCAlgorithm):
    """
    Strategie basee sur la yield curve (10Y-2Y spread).
    - Spread positif: economie saine -> risk-on
    - Spread negatif (inversion): recession proche -> risk-off
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # ETFs
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol
        
        # FRED data (si disponible)
        # self.yield_spread = self.AddData(FREDData, "T10Y2Y", Resolution.Daily).Symbol
        
        self.last_spread = None
        
        # Schedule mensuel
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.Rebalance
        )
    
    def Rebalance(self):
        # En production avec les donnees FRED:
        # if self.last_spread is not None:
        #     if self.last_spread > 0:
        #         # Yield curve normale -> risk-on
        #         self.SetHoldings(self.spy, 0.80)
        #         self.SetHoldings(self.tlt, 0.20)
        #     else:
        #         # Yield curve inversee -> risk-off
        #         self.SetHoldings(self.spy, 0.30)
        #         self.SetHoldings(self.tlt, 0.70)
        pass

print("FREDData et YieldCurveAlgorithm definis")
print("\nYield curve interpretation:")
print("  - Spread > 0: Normal curve -> Risk-on (more equities)")
print("  - Spread < 0: Inverted curve -> Risk-off (more bonds)")
print("  - Inversion predicts recessions 12-18 months ahead")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 2f3a6f14ba27e64adcd427e9da763544)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 2f3a6f14ba27e64adcd427e9da763544

YieldCurveAlgorithm — Rebalance() passe ; données FRED (CSV yield curve) a connecter. Verdict #3801 : RECOVERABLE-USER-HAND (0 trade verifie, 2516 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 2f3a6f14ba27e64adcd427e9da763544) - 2516 jour(s) boursier(s), Capital initial: $100,000


Partie 5 : Economic Indicators (15 min)

5.1 Macro Data pour Trading

Les indicateurs economiques peuvent guider l’allocation tactique :

Indicateur Source Impact Marche
Fed Funds Rate Federal Reserve Taux d’intérêt, valuations
GDP Growth Bureau of Economic Analysis Croissance economique
Unemployment Bureau of Labor Statistics Sante du travail
CPI Inflation BLS Politique monetaire
PMI ISM Activite manufacturiere
Retail Sales Census Bureau Consommation

Integration FRED

Le Federal Reserve Economic Data (FRED) offre des milliers de series economiques. Vous pouvez les integrer via Custom Data ou des APIs tierces.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Strategie Macro: Tactical Asset Allocation
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MacroTacticalAllocation(QCAlgorithm):
    """
    Allocation tactique basee sur le regime de marche.
    
    Utilise des indicateurs simples comme proxy:
    - VIX (volatilite) comme proxy de stress
    - Yield curve (TLT vs SHY) comme proxy de recession
    - SPY momentum comme proxy de trend
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # ETFs pour allocation
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol   # Large cap US
        self.tlt = self.AddEquity("TLT", Resolution.Daily).Symbol   # Long-term bonds
        self.shy = self.AddEquity("SHY", Resolution.Daily).Symbol   # Short-term bonds
        self.gld = self.AddEquity("GLD", Resolution.Daily).Symbol   # Gold
        self.vix = self.AddEquity("VXX", Resolution.Daily).Symbol   # VIX ETN (proxy)
        
        # Indicateurs
        self.spy_sma = self.SMA("SPY", 200, Resolution.Daily)
        self.spy_roc = self.ROC("SPY", 20, Resolution.Daily)  # Momentum 1 mois
        
        # Warmup
        self.SetWarmup(200)
        
        # Rebalancement mensuel
        self.Schedule.On(
            self.DateRules.MonthStart("SPY"),
            self.TimeRules.AfterMarketOpen("SPY", 30),
            self.Rebalance
        )
        
        self.Log("Macro Tactical Allocation initialized")
    
    def Rebalance(self):
        """Rebalancement mensuel basee sur le regime."""
        if self.IsWarmingUp:
            return
        
        # Determiner le regime
        spy_price = self.Securities[self.spy].Price
        spy_above_sma = spy_price > self.spy_sma.Current.Value
        spy_momentum = self.spy_roc.Current.Value
        
        # Yield curve: TLT/SHY ratio (simplifie)
        tlt_price = self.Securities[self.tlt].Price
        shy_price = self.Securities[self.shy].Price
        
        # Regimes
        is_bullish = spy_above_sma and spy_momentum > 0
        is_bearish = not spy_above_sma and spy_momentum < 0
        
        # Allocation selon regime
        if is_bullish:
            # Risk-on: actions
            allocations = {self.spy: 0.80, self.tlt: 0.15, self.gld: 0.05}
            regime = "BULLISH"
        
        elif is_bearish:
            # Risk-off: bonds et or
            allocations = {self.spy: 0.20, self.tlt: 0.50, self.gld: 0.30}
            regime = "BEARISH"
        
        else:
            # Neutral: balance
            allocations = {self.spy: 0.50, self.tlt: 0.35, self.gld: 0.15}
            regime = "NEUTRAL"
        
        # Appliquer l'allocation
        for symbol, weight in allocations.items():
            self.SetHoldings(symbol, weight)
        
        self.Log(f"Regime: {regime} | SPY vs SMA: {spy_above_sma}, Momentum: {spy_momentum:.2%}")
    
    def OnData(self, data):
        pass  # Le trading est gere par le schedule

print("MacroTacticalAllocation defini")
print("\nRegimes detectes:")
print("  - BULLISH: SPY > SMA200 et momentum positif")
print("  - BEARISH: SPY < SMA200 et momentum negatif")
print("  - NEUTRAL: conditions mixtes")

Résultats Backtest QC Cloud - MacroTacticalAllocation (SMA200+ROC20 regime, monthly rebalance, 2015-2024, $100,000 initial)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.38
CAGR 7.642%
Max Drawdown 28.900%
Net Profit +108.8%
Equity finale $208,842
Probabilistic Sharpe Ratio 6.848%

Backtest QC Cloud 2015-2024 (projet 33177683, bt 70b5e8a1e5a851a7e430bcf4161fbfbe). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).


Partie 6 : Stratégie Event-Driven: Earnings (20 min)

6.1 Earnings Calendar Strategy

Les earnings announcements (résultats trimestriels) sont des événements majeurs qui créent de la volatilite. Les stratégies event-driven exploitent ces mouvements.

Patterns d’Earnings

Pattern Description Stratégie
Pre-earnings drift Mouvement avant l’annonce Position avant
Post-earnings drift Mouvement après surprise Position après
Volatility crush IV baisse après l’annonce Vendre options
Earnings momentum Winners/losers persistants Momentum

Données d’Earnings dans QuantConnect

QuantConnect fournit les dates d’earnings via les Fundamentals :

# Acceder aux données d'earnings
earnings_date = stock.EarningReports.BasicEPS.ReportDate
last_eps = stock.EarningReports.BasicEPS.Value
# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Exemple combinant plusieurs sources de donnees alternatives
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class MultiSourceAlternativeDataStrategy(QCAlgorithm):
    """
    Strategie combinant plusieurs sources de donnees alternatives:
    - Fundamentals Morningstar (Value screening)
    - Momentum technique (Price momentum)
    - Macro regime (SPY vs SMA)
    
    Exemple d'integration multi-source.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe avec Fundamentals
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        
        # Reference pour regime detection
        self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.spy_sma = self.SMA("SPY", 200, Resolution.Daily)
        
        # Parametres
        self.num_stocks = 15
        self.momentum = {}
        self.last_month = -1
        
        # Warmup
        self.SetWarmup(200)
        
        self.Log("Multi-Source Alternative Data Strategy initialized")
    
    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[:300]]
    
    def FineSelectionFunction(self, fine):
        """Selection multi-criteres: Value + Quality."""
        candidates = []
        
        for stock in fine:
            try:
                pe = stock.ValuationRatios.PERatio
                roe = stock.OperationRatios.ROE.Value
                
                # Filtres Value + Quality
                if pe and 5 < pe < 20 and roe and roe > 0.12:
                    candidates.append({
                        'symbol': stock.Symbol,
                        'pe': pe,
                        'roe': roe,
                        'score': (1/pe) * roe * 100
                    })
            except:
                continue
        
        # Trier par score composite
        sorted_candidates = sorted(candidates, key=lambda x: x['score'], reverse=True)
        return [x['symbol'] for x in sorted_candidates[:self.num_stocks * 2]]
    
    def OnSecuritiesChanged(self, changes):
        """Initialiser momentum pour nouvelles securities."""
        for security in changes.AddedSecurities:
            if security.Symbol != self.spy:
                self.momentum[security.Symbol] = self.MOMP(security.Symbol, 20, Resolution.Daily)
        
        for security in changes.RemovedSecurities:
            if security.Symbol in self.momentum:
                del self.momentum[security.Symbol]
    
    def OnData(self, data):
        if self.IsWarmingUp:
            return
        
        # Executer une fois par mois
        if not self.Time.day == 1:
            return
        
        # Determiner le regime
        spy_price = self.Securities[self.spy].Price
        is_bullish = spy_price > self.spy_sma.Current.Value
        
        # Selectionner les meilleures actions par momentum
        momentum_scores = []
        for symbol, mom in self.momentum.items():
            if mom.IsReady:
                momentum_scores.append({
                    'symbol': symbol,
                    'momentum': mom.Current.Value
                })
        
        # Trier par momentum
        sorted_by_momentum = sorted(momentum_scores, key=lambda x: x['momentum'], reverse=True)
        top_momentum = sorted_by_momentum[:self.num_stocks]
        
        # Allocation selon regime
        if is_bullish:
            # Risk-on: full equity allocation
            weight = 0.90 / max(len(top_momentum), 1)
        else:
            # Risk-off: reduced allocation
            weight = 0.50 / max(len(top_momentum), 1)
        
        # Liquider positions non selectionnees
        for symbol in list(self.Portfolio.Keys):
            if symbol != self.spy:
                if symbol not in [x['symbol'] for x in top_momentum]:
                    self.Liquidate(symbol)
        
        # Acheter top momentum stocks
        for stock in top_momentum:
            self.SetHoldings(stock['symbol'], weight)
        
        self.Log(f"Regime: {'BULLISH' if is_bullish else 'BEARISH'}, Positions: {len(top_momentum)}, Weight: {weight:.1%}")

print("MultiSourceAlternativeDataStrategy defini")
print("\nCriteres combines:")
print("  1. Fundamentals: P/E 5-20, ROE > 12%")
print("  2. Momentum: Top 15 par momentum 20 jours")
print("  3. Regime: Allocation ajustee selon SPY vs SMA200")

Résultats Backtest QC Cloud - MultiSourceAlternativeDataStrategy (Coarse+Fine universe, Value+Momentum, 2015-2024, $100,000 initial)

Metrique Valeur
Dates negociables 2516
Sharpe Ratio 0.19
CAGR 5.526%
Max Drawdown 36.200%
Net Profit +71.2%
Equity finale $171,236
Probabilistic Sharpe Ratio 0.717%

Backtest QC Cloud 2015-2024 (projet 33177683, bt f782280c9fb5184328d1e7a182c7469a). Net Profit / Equity finale deduits du CAGR verifie (End = $100,000 x (1+CAGR)^10).

On évalue la contribution des données alternatives à la performance globale de la stratégie par une analyse d’attribution.

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Strategie Earnings: Pre-Earnings Momentum
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *
from datetime import timedelta

class EarningsMomentumStrategy(QCAlgorithm):
    """
    Strategie basee sur les earnings:
    - Acheter les actions avec momentum positif avant earnings
    - Vendre apres l'annonce (ou avant si momentum se retourne)
    
    Basee sur la recherche academique:
    - "Earnings Momentum and Earnings Management" (Chan et al., 2006)
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe dynamique
        self.AddUniverse(self.CoarseSelectionFunction)
        self.UniverseSettings.Resolution = Resolution.Daily
        
        # Tracking des positions earnings
        self.earnings_positions = {}  # symbol -> entry_date
        self.momentum = {}  # symbol -> MomentumPercent indicator
        
        # Parametres
        self.days_before_earnings = 10  # Entrer 10 jours avant
        self.days_after_earnings = 2    # Sortir 2 jours apres
        self.num_stocks = 30
        
        self.last_month = -1
        
        self.Log("Earnings Momentum Strategy initialized")
    
    def CoarseSelectionFunction(self, coarse):
        """Selection mensuelle des candidats."""
        if self.Time.month == self.last_month:
            return Universe.Unchanged
        self.last_month = self.Time.month
        
        # Filtrer
        filtered = [x for x in coarse 
                    if x.HasFundamentalData
                    and x.Price > 10 
                    and x.DollarVolume > 5000000]
        
        # Trier par volume
        sorted_stocks = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True)
        
        return [x.Symbol for x in sorted_stocks[:200]]
    
    def OnSecuritiesChanged(self, changes):
        """Initialiser les indicateurs de momentum."""
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.momentum:
                self.momentum[symbol] = self.MOMP(symbol, 20, Resolution.Daily)
        
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.momentum:
                del self.momentum[symbol]
    
    def OnData(self, data):
        """Logique de trading basee sur les earnings."""
        # Verifier les positions existantes
        self.CheckExistingPositions()
        
        # Chercher de nouvelles opportunites
        self.ScanForOpportunities(data)
    
    def CheckExistingPositions(self):
        """Gerer les positions existantes."""
        for symbol in list(self.earnings_positions.keys()):
            entry_date = self.earnings_positions[symbol]
            days_held = (self.Time - entry_date).days
            
            # Verifier si momentum se retourne
            if symbol in self.momentum and self.momentum[symbol].IsReady:
                momentum = self.momentum[symbol].Current.Value
                
                # Sortir si momentum devient negatif ou apres X jours
                if momentum < -5 or days_held > self.days_before_earnings + self.days_after_earnings:
                    self.Liquidate(symbol)
                    del self.earnings_positions[symbol]
                    self.Log(f"EXIT {symbol.Value}: Days={days_held}, Mom={momentum:.1f}%")
    
    def ScanForOpportunities(self, data):
        """Scanner pour de nouvelles opportunites pre-earnings."""
        # Limiter le nombre de positions
        if len(self.earnings_positions) >= self.num_stocks:
            return
        
        for symbol in self.ActiveSecurities.Keys:
            # Ne pas doubler une position
            if symbol in self.earnings_positions:
                continue
            
            if not data.ContainsKey(symbol):
                continue
            
            # Verifier le momentum
            if symbol not in self.momentum or not self.momentum[symbol].IsReady:
                continue
            
            momentum = self.momentum[symbol].Current.Value
            
            # Critere: momentum positif fort
            if momentum > 5:  # +5% sur 20 jours
                # Calculer la taille de position
                weight = 1.0 / self.num_stocks
                
                self.SetHoldings(symbol, weight)
                self.earnings_positions[symbol] = self.Time
                
                self.Log(f"ENTER {symbol.Value}: Momentum={momentum:.1f}%")
    
    def OnEndOfAlgorithm(self):
        self.Log("="*50)
        self.Log("EARNINGS MOMENTUM STRATEGY SUMMARY")
        self.Log("="*50)
        self.Log(f"Final Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")
        total_return = (self.Portfolio.TotalPortfolioValue - 100000) / 100000
        self.Log(f"Total Return: {total_return:.1%}")

print("EarningsMomentumStrategy defini")
print("\nLogique:")
print("  1. Scanner les actions avec momentum >5%")
print("  2. Entrer en position (equal-weight)")
print("  3. Sortir si momentum devient negatif ou apres earnings")

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

Résultat non verifiable (verdict #3801 : INTRINSIC / a diagnostiquer). Le backtest QC Cloud de cette stratégie de reference (universe Coarse, scanning de momentum quotidien sur jusqu’a 30 titres) sur 2015-2024 n’a produit AUCUNE journee negociable fiable : deux runs independants donnent tradeableDates = 0 avec des statistiques NON-DÉTERMINISTES (bt 6037134b : Sharpe 0.319 / CAGR 8.585% ; bt d6cb787b : Sharpe 0.298 / CAGR 7.829%). Un backtest déterministe doit donner des résultats identiques – ces statistiques sont donc inutilisables (probable timeout dû a l’intensite du trading quotidien sur 10 ans). La table fabriquee d’origine (“Q1 2023”, Sharpe -1.395, CAGR -32.6%) est supprimee. Aucune metrique de substitution degradee n’est commitee a la place.

Backtest QC Cloud 2015-2024 (projet 33177683). Aucune metrique fiable (2 runs non-déterministes, 0 tradeable dates) : bt 6037134b3942f8939906e1820ae1b067 et bt d6cb787bfac24fc3dd31d72af4022d29.

6.2 Post-Earnings Announcement Drift (PEAD)

Le PEAD est une anomalie de marche bien documentee : les actions qui surprennent positivement continuent de monter, et vice versa.

Implementation : 1. Detecter les earnings surprises (EPS actual vs consensus) 2. Acheter les surprises positives, vendre les negatives 3. Tenir pendant 60-90 jours (drift period)

# [REFERENCE QC] Code a copier dans main.py QC Lab (non executable ici)
# Structure pour PEAD Strategy (Reference)
# A copier dans l'IDE QuantConnect

from AlgorithmImports import *

class PEADStrategy(QCAlgorithm):
    """
    Post-Earnings Announcement Drift Strategy.
    
    Note: Necessite des donnees de consensus (Estimize, Zacks)
    qui peuvent etre payantes.
    
    Cet exemple montre la structure.
    """
    
    def Initialize(self):
        self.SetStartDate(2015, 1, 1)
        self.SetEndDate(2024, 12, 31)
        self.SetCash(100000)
        
        # Universe
        symbols = [Symbol.Create(t, SecurityType.Equity, Market.USA) 
                   for t in ["AAPL", "MSFT", "GOOGL", "AMZN", "META"]]
        self.SetUniverseSelection(ManualUniverseSelectionModel(symbols))
        
        # Tracking des positions PEAD
        self.pead_positions = {}  # symbol -> (entry_date, direction)
        self.holding_period = 60  # jours
        
        self.Log("PEAD Strategy initialized")
    
    def OnData(self, data):
        """
        En production:
        1. Detecter les nouvelles annonces d'earnings
        2. Comparer EPS actual vs consensus
        3. Calculer la surprise standardisee (SUE)
        4. Trader selon la direction de la surprise
        """
        # Gerer les positions existantes
        for symbol in list(self.pead_positions.keys()):
            entry_date, direction = self.pead_positions[symbol]
            days_held = (self.Time - entry_date).days
            
            if days_held >= self.holding_period:
                self.Liquidate(symbol)
                del self.pead_positions[symbol]
                self.Log(f"PEAD Exit: {symbol.Value} after {days_held} days")
    
    def ProcessEarningsSurprise(self, symbol, actual_eps, expected_eps, std_dev):
        """
        Traiter une surprise d'earnings.
        
        SUE = (Actual EPS - Expected EPS) / Std Dev
        
        SUE > 1: Positive surprise -> Buy
        SUE < -1: Negative surprise -> Short
        """
        if std_dev <= 0:
            return
        
        sue = (actual_eps - expected_eps) / std_dev
        
        if sue > 1:  # Positive surprise
            self.SetHoldings(symbol, 0.10)
            self.pead_positions[symbol] = (self.Time, "LONG")
            self.Log(f"PEAD Long: {symbol.Value}, SUE={sue:.2f}")
        
        elif sue < -1:  # Negative surprise
            self.SetHoldings(symbol, -0.10)  # Short
            self.pead_positions[symbol] = (self.Time, "SHORT")
            self.Log(f"PEAD Short: {symbol.Value}, SUE={sue:.2f}")

print("PEADStrategy defini (structure)")
print("\nStandardized Unexpected Earnings (SUE):")
print("  SUE = (Actual - Expected) / StdDev")
print("  SUE > 1: Positive surprise -> Buy")
print("  SUE < -1: Negative surprise -> Short")

Résultats Backtest QC Cloud - Backtest QC Cloud 2015-2024 (projet 33177683, bt 527043f5d70b2538fde3af5291fc5013)

Metrique Valeur
Status Completed
Sharpe Ratio 0.000
Sortino Ratio 0.000
CAGR 0.0%
Net Profit 0.00%
Total Orders 0
Trades 0
Win Rate 0%
Max Drawdown 0.0%
End Equity $100,000.00
Total Fees $0.00
Backtest ID 527043f5d70b2538fde3af5291fc5013

PEADStrategy — aucun declencheur d’entree ; consensus earnings requis. Verdict #3801 : INTRINSIC (0 trade verifie, 0 jour(s) boursier(s)).

Backtest QC Cloud 2015-01-01 -> 2024-12-31 (projet 33177683, bt 527043f5d70b2538fde3af5291fc5013) - 0 jour(s) boursier(s), Capital initial: $100,000


Exercice 3 : Stratégie PEAD multi-facteurs

Le PEAD classique achete après un earnings surprise positif. Une version multi-facteurs combine earnings surprise, volume anormal et volatilite implicite.

Objectif : Implementer un modèle PEAD qui filtre les signaux avec des conditions de volume et volatilite.

Règles : - Signal d’entree : earnings surprise > 5% ET volume du jour > 1.5x volume moyen 20j - Filtre de volatilite : ne pas entrer si volatilite 20j > 2x la volatilite 60j - Position sizing : min(surprise_pct / 20, 0.10) (max 10% du portfolio) - Sortie : après 10 jours ou si le prix revient au pre-earnings

Indices : - Indice : self.Securities[symbol].Volume pour le volume du jour - Indice : Utilisez une RollingWindow[TradeBar] pour calculer les volumes et volatilites glissantes

# [REFERENCE QC] Exercice 3 : Strategie PEAD multi-facteurs
# TODO etudiant : Implementer un modele PEAD avec filtres volume et volatilite
# Indice : Combinez earnings surprise + volume anormal + filtre volatilite
# Etape 1 : Detecter les earnings surprises dans OnData
# Etape 2 : Calculer le volume ratio (jour / moyenne 20j)
# Etape 3 : Calculer le ratio de volatilite (20j / 60j)
# Etape 4 : Appliquer les filtres et generer les ordres

result = None  # TODO etudiant : remplacer par la strategie PEAD multi-facteurs
print("Exercice a completer")

Conclusion et Prochaines Étapes

Recapitulatif

Dans ce notebook, nous avons couvert :

  1. Introduction aux Données Alternatives :
    • Types: Satellite, social media, sentiment, fundamentals
    • Avantage competitif vs données de marche standard
  2. Fundamentals Data Morningstar (Fama & French, 1992 - value/factor) :
    • Valuation Ratios (P/E, P/B, EV/EBITDA)
    • Financial Statements (Revenue, Net Income)
    • Opération Ratios (ROE, ROA)
    • Stratégie Value complete
  3. SEC Filings :
    • Types de filings (8-K, 10-K, 10-Q)
    • Structure d’integration
  4. Custom Data Sources :
    • PythonData class (GetSource, Reader)
    • NewsAPI integration
    • CSV local import
  5. Economic Indicators :
    • Tactical allocation basee sur macro
    • Regimes de marche
  6. Stratégie Event-Driven (Ball & Brown, 1968 ; Bernard & Thomas, 1989) :
    • Earnings momentum
    • Post-Earnings Announcement Drift (PEAD)

Points Cles a Retenir

Concept Point Cle
Alt Data Avantage informationnel vs données standard
Fundamentals Fine Sélection avec Morningstar (P/E, ROE)
Custom Data PythonData avec GetSource() et Reader()
Macro Allocation tactique basee sur regimes
Event-Driven Earnings momentum et PEAD

Ressources pour les Données Alternatives

Source Type Cout
NewsAPI News Gratuit (100 req/jour)
Alpha Vantage Fundamentals, Forex Gratuit (5 req/min)
Quandl Macro, Commodities Mix
FRED Economic Data Gratuit
QuantConnect Data Library Multiple Mix (Free tier inclut Fundamentals)

Prochaines Étapes

Pour approfondir l’Alternative Data :

  1. QC-Py-17 : Sentiment Analysis avec NLP
  2. QC-Py-18 : ML Features Engineering (combiner alt data + price data)
  3. QC-Py-19 : ML Supervised Classification avec features alternatives

References academiques

  • Ball, R. & Brown, P. (1968). An Empirical Evaluation of Accounting Income Numbers. Journal of Accounting Research. Post-Earnings Announcement Drift (PEAD) - anomalie la plus anciennement documentee.
  • Bernard, V. L. & Thomas, J. K. (1989). Post-Earnings-Announcement Drift: Delayed Price Response or Risk Premium? Journal of Accounting Research. Mécanisme du drift post-annonce (sous-reaction du marche).
  • Fama, E. F. & French, K. R. (1992). The Cross-Section of Expected Stock Returns. Journal of Finance. Facteurs de valeur (HML, P/B) - fondamentaux comme facteur de risque.
  • Chan, W. S. (2003). Stock Price Reaction to News and No-News: Drift and Reversal after Headlines. Journal of Financial Economics. Momentum des news / données alternatives textuelles.
  • Tetlock, P. C. (2007). Giving Content to Investor Sentiment: The Rôle of Media in the Stock Market. Journal of Finance. Données media alternatives comme predicteur des rendements.

Ressources Complementaires


Notebook complete. Vous maitrisez maintenant l’integration des Données Alternatives dans QuantConnect.

Retour au sommet