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 :
Collecte des données textuelles (news, filings, social media)
Pretraitement : tokenisation, nettoyage, construction du vocabulaire
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 financierfrom collections import Countersample_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 :
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.
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.
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/downgradesextended_positive =set() # TODO etudiant : ajouter vos mots iciextended_negative =set() # TODO etudiant : ajouter vos mots icidef extended_sentiment_score(headline):# TODO etudiant : implementer le scoring avec le vocabulaire etendureturnNone# TODO etudiant : retourner le scoreresult_vocab =None# TODO etudiant : tester sur de nouveaux titresprint("Exercice a completer : extension du vocabulaire financier")
# Simulation : scoring Naive Bayes sur des titresimport numpy as nppositive_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 + negif total ==0:return0.0return (pos - neg) / totalfor h in sample_headlines: score = sentiment_score(h) label ="BULL"if score >0else"BEAR"if score <0else"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 combinaisondef 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 ponderereturnNone# TODO etudiant : retourner le score hybrideresult_hybrid =None# TODO etudiant : tester avec differents poidsprint("Exercice a completer : score hybride sentiment + technique")
# Code source de l'algorithme - pret pour deploiement QC Cloudqc_code ='''# region importsfrom AlgorithmImports import *# endregionclass 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 :
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 npnp.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, F1returnNone# TODO etudiant : retourner les metriquesresult_eval =None# TODO etudiant : appeler evaluate_classifierprint("Exercice a completer : evaluation du classifieur")