QC-Py-Cloud-02 : Classification de Texte et Sentiment NLP

< Retour au sommaire | Suivant : Risk Parity >

Niveau : Intermediaire | Duree estimee : 30 min | Kernel : Python 3

Objectifs

  • Comprendre le pipeline NLP : tokenisation, vocabulaire, bag-of-words
  • Implementer un classifieur Naive Bayes pour le sentiment financier
  • Combiner signaux textuels et indicateurs techniques (modèle hybride)
  • Deployer l’algorithme sur QuantConnect Cloud via les outils MCP

Partie 1 : Traitement du Langage Naturel pour la Finance

Le traitement du langage naturel (NLP) applique aux marches financiers permet d’extraire du signal a partir de textes : articles de presse, rapports d’analystes, fils d’actualite.

Le pipeline classique comporte quatre étapes :

  1. Collecte des données textuelles (news, filings, social media)
  2. Pretraitement : tokenisation, nettoyage, construction du vocabulaire
  3. Vectorisation : representation numérique (bag-of-words, TF-IDF)
  4. Classification : prediction du sentiment (positif, negatif, neutre)

Approche Bag-of-Words

La representation bag-of-words ignore l’ordre des mots et se concentre sur leur frequence d’apparition. Pour le domaine financier, un vocabulaire restreint (500-2000 mots) capture déjà l’essentiel du signal sentimental.

# Demonstration : construction d'un vocabulaire financier
from collections import Counter

sample_headlines = [
    "AAPL surges on heavy volume after earnings beat",
    "MSFT declines on weak guidance outlook",
    "GOOGL trades mixed amid market uncertainty",
    "AMZN rallies strongly on Prime Day results",
    "NVDA plummets on high volume after downgrade",
    "AAPL gains ground following product launch",
    "MSFT profit exceeds expectations significantly",
    "GOOGL drops sharply on antitrust concerns",
]

all_words = []
for h in sample_headlines:
    words = h.lower().split()
    all_words.extend(words)

word_counts = Counter(all_words)
top_20 = word_counts.most_common(20)

print("Top 20 mots du vocabulaire financier :")
for word, count in top_20:
    print(f"  {word:20s} {count}")

print(f"\nVocabulaire total : {len(word_counts)} mots uniques")
Top 20 mots du vocabulaire financier :  on                   5  aapl                 2  volume               2  after                2  msft                 2  googl                2  surges               1  heavy                1  earnings             1  beat                 1  declines             1  weak                 1  guidance             1  outlook              1  trades               1  mixed                1  amid                 1  market               1  uncertainty          1  amzn                 1Vocabulaire total : 42 mots uniques

Le vocabulaire issu des titres d’actualite financiere contient a la fois des mots neutres et des mots porteurs de sentiment (surges, plummets, beat, downgrade). Le classifieur Naive Bayes apprend a associer ces mots aux mouvements de prix.

Lecture de la sortie

Le top 20 affiche parle avant tout du corpus, pas du sentiment. Trois constats, dans l’ordre de la sortie :

  1. Le mot le plus frequent est on (5 occurrences) — une preposition, pas une opinion. Viennent ensuite les tickers (aapl, msft, googl a 2 ; amzn, nvda a 1) puis seulement les mots porteurs (surges, beat, declines, plummets, tous a 1). Sur 42 mots uniques, la frequence brute mesure d’abord la grammaire de l’anglais financier.
  2. Les tickers polluent le vocabulaire : aapl ne porte aucun sentiment — c’est une identite, pas un jugement. Un classifieur qui l’apprend memorise quel titre parle, pas ce qu’on en dit.
  3. Consequence directe pour Naive Bayes : sans filtrage, on (5x) pese cinq fois le poids de surges (1x) dans les comptages du modele MultinomialNB. La premiere etape d’un pipeline reel est donc l’ecart des stop-words et des tickers avant l’estimation des P(classe|mot).

Modele Naive Bayes Multinomial

Le classifieur MultinomialNB est adapte aux donnees textuelles representees en bag-of-words. Il estime P(classe|mot) via le theoreme de Bayes : chaque mot vote pour une classe au proportionnel de sa vraisemblance, et le document est classe par le produit des votes (en log-probabilites pour la stabilite numerique). C’est ce mecanisme que la cellule suivante utilise, en gardant volontairement le vocabulaire brut pour montrer l’effet du desequilibre on/surges sur le score final.

Exercice 1 : Extension du vocabulaire financier

Le vocabulaire utilise dans la demonstration couvre seulement 42 mots uniques. Pour ameliorer la precision du classifieur, il faut enrichir ce vocabulaire.

Objectif : Ajoutez au moins 10 nouveaux mots financiers aux listes positive_words et negative_words de la cellule de scoring Naive Bayes. Testez le classifieur sur de nouveaux titres que vous inventez.

Indices : - Indice : Pensez aux termes utilises dans les rapports financiers : “dividend”, “guidance”, “outlook”, “upgrade”, “downgrade”. - Indice : Testez avec des titres ambigus contenant des mots des deux listes.

# Exercice 1 : Extension du vocabulaire financier
# TODO etudiant : ajouter au moins 10 mots positifs et 10 negatifs
# Indice : pensez aux termes de rapports financiers, upgrades/downgrades

extended_positive = set()  # TODO etudiant : ajouter vos mots ici
extended_negative = set()  # TODO etudiant : ajouter vos mots ici

def extended_sentiment_score(headline):
    # TODO etudiant : implementer le scoring avec le vocabulaire etendu
    return None  # TODO etudiant : retourner le score

result_vocab = None  # TODO etudiant : tester sur de nouveaux titres
print("Exercice a completer : extension du vocabulaire financier")
# Simulation : scoring Naive Bayes sur des titres
import numpy as np

positive_words = {"surges", "rallies", "gains", "beat", "profit", "growth",
                  "strongly", "exceeds", "significantly", "strong"}
negative_words = {"declines", "plummets", "drops", "weak", "downgrade",
                  "concerns", "sharply", "uncertainty", "antitrust"}

def sentiment_score(headline):
    words = set(headline.lower().split())
    pos = len(words & positive_words)
    neg = len(words & negative_words)
    total = pos + neg
    if total == 0:
        return 0.0
    return (pos - neg) / total

for h in sample_headlines:
    score = sentiment_score(h)
    label = "BULL" if score > 0 else "BEAR" if score < 0 else "NEUTRAL"
    print(f"  [{label:6s}] {score:+.2f}  {h}")
  [BULL  ] +1.00  AAPL surges on heavy volume after earnings beat  [BEAR  ] -1.00  MSFT declines on weak guidance outlook  [BEAR  ] -1.00  GOOGL trades mixed amid market uncertainty  [BULL  ] +1.00  AMZN rallies strongly on Prime Day results  [BEAR  ] -1.00  NVDA plummets on high volume after downgrade  [BULL  ] +1.00  AAPL gains ground following product launch  [BULL  ] +1.00  MSFT profit exceeds expectations significantly  [BEAR  ] -1.00  GOOGL drops sharply on antitrust concerns

Interprétation du résultat : Les 8 titres d’actualité reçoivent tous un score extrême (+1.00 ou -1.00) — aucun ne tombe entre les deux. Ce n’est pas un hasard : chaque phrase ne contient des mots que d’un seul des deux lexiques (positive_words / negative_words), donc le ratio (pos - neg) / total vaut toujours ±1. Un score intermédiaire (ex. 0.33) n’apparaîtrait qu’avec un titre mélangeant les deux registres, comme « AAPL beats earnings but faces antitrust concerns ».

Les limites de l’approche lexicométrique : le dictionnaire ignore la négation (« not surges » resterait positif), la modalité (« could decline » n’est pas « declines ») et le contexte — « GOOGL trades mixed amid market uncertainty » est classé BEAR à -1.00 alors que « mixed » suggère l’indécision, parce que « uncertainty » est le seul mot du lexique présent. C’est le prix de la simplicité : interprétable, rapide, mais aveugle au langage.

Pourquoi c’est pourtant la bonne première étape : ce scorer naive-Bayes-like fournit une baseline mesurable (8/8 classifications plausibles ici) contre laquelle comparer un vrai modèle NLP. En production QuantConnect, on remplacerait le lexique par un classifieur entraîné (TF-IDF + Logistic Regression) ou un LLM — l’algorithme déployé plus bas dans ce notebook conserve exactement cette interface sentiment_score(), seul le moteur change.

Partie 2 : Algorithme QuantConnect - Classification de Texte

La demonstration locale s’arrete sur un constat : les 8 titres d’actualite simules (5 tickers) recoivent tous un score extreme (+1.00 ou -1.00). Un signal binaire ne sait pas dire faiblement haussier — il ne connait que haussier et baissier. En production, cette brutalite se paie : chaque bascule de signe est un trade complet, avec ses frais.

L’algorithme QuantConnect repond a cette limite par une ponderation :

  • Signal textuel : vocabulaire bag-of-words + Naive Bayes Multinomial — la probabilite continue du classifieur remplace le vote binaire (+0.7 est desormais distinguishable de +1.0) ;
  • Signal technique : RSI, ratio EMA, momentum — un contre-poids qui ne lit pas les memes donnees ;
  • Score hybride : 50% sentiment + 50% technique — quand les deux sources divergent, le score reste proche de zero, c’est-a-dire pas de position. La neutralite devient un etat atteignable, ce que le scoring lexical seul ne savait pas produire.

L’univers comprend 5 actions tech (AAPL, MSFT, GOOGL, AMZN, NVDA) avec un rebalancement hebdomadaire : le signal a le temps de se constituer plusieurs actualites avant chaque decision, et l’Exercice 2 fait implanter exactement cette fonction hybrid_score.

Exercice 2 : Score hybride sentiment + technique

L’algorithme QC combine un signal textuel (50%) et un signal technique (50%) pour produire un score hybride. Le signal technique utilise le RSI, le ratio EMA et le momentum.

Objectif : Implementez une fonction hybrid_score(sentiment, rsi, ema_ratio, momentum) qui combine les signaux avec des poids personnalisables. Testez différents poids (60/40, 40/60, 50/50) et observez comment le signal final change.

Indices : - Indice : Normalisez chaque signal entre 0 et 1 avant la combinaison. - Indice : Un RSI < 30 est un signal d’achat (contrarien), un RSI > 70 est un signal de vente.

# Exercice 2 : Score hybride sentiment + technique
# TODO etudiant : implementer hybrid_score(sentiment, rsi, ema_ratio, momentum, weights)
# Indice : normaliser chaque input entre 0 et 1 avant combinaison

def hybrid_score(sentiment, rsi, ema_ratio, momentum, weights=(0.5, 0.2, 0.15, 0.15)):
    # TODO etudiant : normaliser le sentiment (deja entre -1 et 1)
    # TODO etudiant : normaliser le RSI (0-100 -> 0-1, inverser car contrarien)
    # TODO etudiant : calculer le score pondere
    return None  # TODO etudiant : retourner le score hybride

result_hybrid = None  # TODO etudiant : tester avec differents poids
print("Exercice a completer : score hybride sentiment + technique")
# Code source de l'algorithme - pret pour deploiement QC Cloud
qc_code = '''# region imports
from AlgorithmImports import *
# endregion

class MLTextClassificationAlgorithm(QCAlgorithm):
    """
    NLP Text Classification Strategy.

    Strategy:
    - Use simulated news sentiment for trading decisions
    - Train text classifier (Naive Bayes) on sentiment labels
    - Features: Bag-of-words, TF-IDF from news headlines
    - Target: Next-day price direction (up/down)
    - Combine sentiment with technical indicators for hybrid model
    """

    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2025, 1, 1)
        self.SetCash(100000)

        # Assets
        self.tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "NVDA"]
        self.symbols = {}
        for ticker in self.tickers:
            self.symbols[ticker] = self.AddEquity(ticker, Resolution.DAILY).Symbol

        # Text classification parameters
        self.lookback = 60
        self.rebalance_freq = 5
        self.vocabulary_size = 1000
        self.sentiment_threshold = 0.6

        # Rebalance schedule
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday),
                         self.TimeRules.AfterMarketOpen("SPY", 30),
                         self.Rebalance)

        # Train model bi-weekly
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday),
                         self.TimeRules.AfterMarketOpen("SPY", 30),
                         self.TrainModel)

        self.model = None
        self.scaler = None

    def SimulateNewsHeadlines(self, ticker, prices, volumes):
        """Simulate news headlines based on price/volume action.

        In production, this would use real news APIs.
        """
        headlines = []

        # Calculate recent changes
        returns = prices.pct_change()
        vol_changes = volumes.pct_change()

        for i in range(len(prices)):
            if i == 0:
                headlines.append("Market opens steady")
                continue

            ret = returns.iloc[i]
            vol_change = vol_changes.iloc[i] if i < len(vol_changes) else 0

            # Generate headline based on market action
            if ret > 0.03:
                if vol_change > 0.2:
                    headlines.append(f"{ticker} surges on heavy volume")
                else:
                    headlines.append(f"{ticker} rallies strongly")
            elif ret > 0.01:
                headlines.append(f"{ticker} gains ground")
            elif ret > -0.01:
                headlines.append(f"{ticker} trades mixed")
            elif ret > -0.03:
                headlines.append(f"{ticker} declines")
            else:
                if vol_change > 0.2:
                    headlines.append(f"{ticker} plummets on high volume")
                else:
                    headlines.append(f"{ticker} drops sharply")

        return headlines

    def CreateVocabulary(self, headlines):
        """Create vocabulary from headlines."""
        # Simple word tokenization
        all_words = []
        for headline in headlines:
            words = headline.lower().split()
            all_words.extend(words)

        # Count word frequencies
        from collections import Counter
        word_counts = Counter(all_words)

        # Get top words
        vocabulary = [word for word, _ in word_counts.most_common(self.vocabulary_size)]
        return {word: idx for idx, word in enumerate(vocabulary)}

    def HeadlineToFeatures(self, headline, vocabulary):
        """Convert headline to feature vector (bag-of-words)."""
        words = headline.lower().split()
        features = np.zeros(self.vocabulary_size)

        for word in words:
            if word in vocabulary:
                features[vocabulary[word]] = 1

        return features

    def TrainModel(self):
        """Train text classification model."""
        self.Debug("Training text classification model...")

        all_features = []
        all_targets = []

        for ticker in self.tickers:
            history = self.History(self.symbols[ticker], self.lookback, Resolution.Daily)

            if history.empty or len(history) < self.lookback:
                continue

            closes = history['close']
            volumes = history['volume']

            # Simulate headlines
            headlines = self.SimulateNewsHeadlines(ticker, closes, volumes)

            # Create vocabulary
            self.vocabulary = self.CreateVocabulary(headlines)

            # Create features and targets
            for i in range(1, len(headlines) - 1):
                features = self.HeadlineToFeatures(headlines[i], self.vocabulary)

                # Target: next day direction
                next_return = (closes.iloc[i + 1] - closes.iloc[i]) / closes.iloc[i]
                target = 1 if next_return > 0 else 0

                all_features.append(features)
                all_targets.append(target)

        if len(all_features) < 20:
            return

        # Train Naive Bayes classifier
        from sklearn.naive_bayes import MultinomialNB
        from sklearn.preprocessing import StandardScaler

        X = np.array(all_features)
        y = np.array(all_targets)

        self.scaler = StandardScaler()
        X_scaled = self.scaler.fit_transform(X)

        self.model = MultinomialNB()
        self.model.fit(X_scaled + 1, y)  # NB requires non-negative

        self.Debug("Text classification model trained.")

    def GetSentiment(self, ticker, history):
        """Get sentiment score from recent headlines."""
        if self.model is None or self.vocabulary is None:
            return 0.5

        closes = history['close']
        volumes = history['volume']

        # Simulate recent headlines
        headlines = self.SimulateNewsHeadlines(ticker, closes, volumes)

        # Get sentiment from last few headlines
        recent_headlines = headlines[-5:]
        sentiments = []

        for headline in recent_headlines:
            features = self.HeadlineToFeatures(headline, self.vocabulary)
            features_scaled = self.scaler.transform([features + 1])[0]
            prob = self.model.predict_proba([features_scaled])[0][1]
            sentiments.append(prob)

        return np.mean(sentiments)

    def GetTechnicalFeatures(self, history):
        """Get technical features for hybrid model."""
        closes = history['close']

        # RSI
        delta = closes.diff()
        gain = (delta.where(delta > 0, 0)).rolling(14).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
        rs = gain / loss
        rsi = 100 - (100 / (1 + rs))

        # EMA ratio
        ema20 = closes.ewm(span=20).mean()
        ema50 = closes.ewm(span=50).mean()
        ema_ratio = ema20.iloc[-1] / ema50.iloc[-1]

        # Momentum
        momentum = (closes.iloc[-1] / closes.iloc[-5] - 1) if len(closes) >= 5 else 0

        return {
            'rsi': rsi.iloc[-1],
            'ema_ratio': ema_ratio,
            'momentum': momentum
        }

    def Rebalance(self):
        """Rebalance based on sentiment + technical analysis."""
        if self.model is None:
            return

        predictions = {}

        for ticker in self.tickers:
            try:
                history = self.History(self.symbols[ticker], 60, Resolution.Daily)

                if history.empty:
                    continue

                # Get sentiment
                sentiment = self.GetSentiment(ticker, history)

                # Get technical features
                tech = self.GetTechnicalFeatures(history)

                # Hybrid score: sentiment + technical
                score = (
                    sentiment * 0.5 +
                    (1 if tech['rsi'] < 30 else 0 if tech['rsi'] > 70 else 0.5) * 0.2 +
                    (1 if tech['ema_ratio'] > 1 else 0) * 0.15 +
                    (1 if tech['momentum'] > 0 else 0) * 0.15
                )

                predictions[ticker] = score

            except:
                continue

        if not predictions:
            return

        # Sort by score
        sorted_preds = sorted(predictions.items(), key=lambda x: x[1], reverse=True)

        # Liquidate
        self.Liquidate()

        # Enter long positions
        count = 0
        for ticker, score in sorted_preds:
            if score > self.sentiment_threshold and count < 3:
                self.SetHoldings(self.symbols[ticker], 0.3)
                count += 1

        self.Debug(f"Positions: {count}, Best score: {sorted_preds[0][1]:.2f}" if sorted_preds else "No positions")

'''

Architecture de l’algorithme

Composant Méthode Description
Vocabulaire CreateVocabulary() Top 1000 mots par frequence
Vectorisation HeadlineToFeatures() Bag-of-words binaire
Entrainement TrainModel() MultinomialNB bi-hebdomadaire
Sentiment GetSentiment() Moyenne des probabilites recentes
Technique GetTechnicalFeatures() RSI, EMA ratio, momentum
Score hybride Rebalance() 50% sentiment + 50% technique

NOTE — simplification pédagogique : la méthode SimulateNewsHeadlines() ne fait pas du NLP réel. Elle génère des titres synthétiques par remplissage de templates basé sur les mouvements prix/volume (ex. variation > +2% → “Strong gains for SPY as market rallies”). Le classifieur Naive Bayes apprend sur ces données règle-based : l’objectif est la démonstration du pipeline textuel end-to-end (vocabulaire → features → entraînement → scoring), pas la maîtrise d’un pipeline NLP réaliste.


Partie 3 : Deploiement via MCP QuantConnect

Le workflow MCP pour deployer cet algorithme sur QC Cloud suit les étapes standard : creation du projet, ecriture du code, compilation, backtest.

Partie 4 : Resultats attendus et interpretation

Metriques cles a observer

Metrique Description Benchmark
Sharpe Ratio Rendement ajuste au risque > 0.5
CAGR Rendement annuel compose > 8%
Max Drawdown Perte maximale depuis un pic < 30%
Win Rate % de trades positifs > 50%
Alpha Rendement excessif vs benchmark > 0%

Lecture critique, ancoree a la demonstration

  • Le lexique de 42 mots ne peut pas produire d’alpha reel. Le vocabulaire construit en Partie 1 decrit 8 titres simules ; sur des actualites reelles, il ne couvre qu’une fraction du langage. Un Sharpe > 0.5 avec ce seul vocabulaire serait le signe d’un artefact (période favorable, chance), pas d’une competence informationnelle — l’alpha lexical exige un vocabulaire entraine sur un historique etiquete.
  • Win Rate > 50% seul ne prouve rien. Avec des frais de transaction de 5-10 bps par aller-retour et un rebalancement hebdomadaire sur 5 titres, un win rate de 52% peut detruire du rendement si les gains moyens sont plus petits que les pertes moyennes. La metrique qui arbitre vraiment est le rapport gain/perte, pas le taux de reussite.
  • Max Drawdown < 30% : la diversification est le vrai amortisseur. Le score hybride etant volontairement proche de zero quand sentiment et technique divergent, une part des rebalancements ne prend pas de position — c’est mecaniquement le principal amortisseur du drawdown, devant toute finesse du classifieur.
  • La comparaison qui compte : sentiment seul vs hybride. L’Exercice 2 construit le score hybride ; rejouer le backtest avec la composante technique desactivee donne la mesure de ce que le texte apporte reellement au-dela du signal de prix.

Conclusion

Ce notebook a demontre le pipeline complet de classification de texte applique au trading quantitatif :

  1. Pretraitement NLP : tokenisation, vocabulaire, bag-of-words
  2. Modèle Naive Bayes : classification probabiliste du sentiment
  3. Approche hybride : combinaison sentiment + indicateurs techniques
  4. Deploiement Cloud : algorithme pret pour QC Cloud via MCP
Concept Outil QC Utilisation
Vocabulaire History() Construction du lexique
Bag-of-Words numpy Vectorisation des titres
Naive Bayes sklearn Classification sentiment
RSI / EMA Indicators API Signal technique
Rebalancement Schedule.On() Exécution hebdomadaire

< Retour au sommaire | Suivant : Risk Parity >

Exercice 3 : Evaluation des performances du classifieur

Un classifieur Naive Bayes produit des probabilites P(hausse) pour chaque headline. Pour evaluer sa qualite, on utilise la precision (fraction des predictions positives qui sont correctes) et le rappel (fraction des vrais positifs detectes).

Objectif : Simulez 100 paires (prediction, verite terrain) avec un biais vers les bonnes predictions (55-60% de precision). Calculez la precision, le rappel et la F1-score. Affichez la matrice de confusion.

Indices : - Indice : Utilisez np.random.choice([0, 1]) pour simuler les predictions. - Indice : Precision = TP / (TP + FP), Rappel = TP / (TP + FN), F1 = 2 * P * R / (P + R).

# Exercice 3 : Evaluation des performances du classifieur
# TODO etudiant : simuler 100 paires (prediction, verite) avec biais
# TODO etudiant : calculer precision, rappel et F1-score
# Indice : Precision = TP/(TP+FP), Rappel = TP/(TP+FN)

import numpy as np
np.random.seed(42)

def evaluate_classifier(n_samples=100, accuracy=0.57):
    # TODO etudiant : generer les predictions et labels
    # TODO etudiant : calculer TP, FP, TN, FN
    # TODO etudiant : calculer precision, rappel, F1
    return None  # TODO etudiant : retourner les metriques

result_eval = None  # TODO etudiant : appeler evaluate_classifier
print("Exercice a completer : evaluation du classifieur")
Retour au sommet