Objectif: Utiliser le NLP (Natural Language Processing) pour analyser le sentiment des news financières et générer des signaux de trading.
Stratégie
Collection de données: Récupérer les headlines de news financières
Feature extraction: Bag-of-words, TF-IDF pour transformer le texte en vecteurs
Classification: Entraîner un classifieur (Naive Bayes, SVM, ou Logistic Regression)
Hybridation: Combiner le sentiment avec des indicateurs techniques
Trading: Long sur les actions avec sentiment positif + confirmations techniques
Prérequis
sklearn: pip install scikit-learn
nltk: pip install nltk (optionnel pour preprocessing avancé)
Compréhension basique du NLP (tokenization, stopwords, stemming)
Durée estimée: 45 minutes
# Initialisation de QuantBookfrom AlgorithmImports import*qb = QuantBook()# Période d'analyseqb.SetStartDate(2020, 1, 1)qb.SetEndDate(2024, 12, 31)# NB : SetStartDate/SetEndDate sont les bookends DEMANDÉS, pas la fenêtre calculée.# qb.History(span) s'ancre à qb.Time (=StartDate dans un QuantBook frais) et regarde# en arrière -- la fenêtre réellement calculée est divulguée dans la cellule History.print(f"Période demandée (bookends): {qb.StartDate.date()} à {qb.EndDate.date()} (fenêtre effective calculée ci-dessous)")
Période demandée (bookends): 2020-01-01 à 2024-12-31 (fenêtre effective calculée ci-dessous)
Pivot des séries de prix et de volumes en DataFrame large, avec remapping des colonnes Symbol → ticker pour ML-TextClassification.
# Récupération des données historiqueshistory = qb.History(list(symbols.values()), 365*2, Resolution.Daily)# Traitement des donnéescloses = history['close'].unstack(level=0)volumes = history['volume'].unstack(level=0)# Remap columns from Lean Symbol objects to simple ticker stringsticker_map = {symbols[t]: t for t in tickers}closes = closes.rename(columns=ticker_map)volumes = volumes.rename(columns=ticker_map)# Keep only tickers that have data in the DataFrameavailable = [t for t in tickers if t in closes.columns]closes = closes[available]volumes = volumes[available]tickers = availableprint(f"Shape des données: {closes.shape}")print(f"Tickers disponibles: {tickers}")# Fenêtre effective (règle #8772 window-honesty, recette #8776) : qb.History(span) est# ancrée à qb.Time, qui dans un QuantBook frais vaut StartDate (2020-01-01) -- donc le# lookback 365*2 (730 barres) finit VERS StartDate, pas vers EndDate. La période réellement# calculée est ~2 ans AVANT StartDate, non la plage 2020-2024 des bookends ci-dessus.print(f"Fenêtre effective: {closes.index[0].date()} à {closes.index[-1].date()} ({len(closes)} barres, calculée non annoncée)")closes.head()
Shape des données: (730, 5)
Tickers disponibles: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'NVDA']
Fenêtre effective: 2017-02-07 à 2019-12-31 (730 barres, calculée non annoncée)
symbol
AAPL
MSFT
GOOGL
AMZN
NVDA
time
2017-02-07 16:00:00
32.8825
63.43
41.4615
40.6250
2.9783
2017-02-08 16:00:00
33.0100
63.34
41.4940
40.9855
2.9653
2017-02-09 16:00:00
33.1050
64.06
41.5030
41.0680
2.9095
2017-02-10 16:00:00
33.0300
64.00
41.7425
41.3730
2.8405
2017-02-13 16:00:00
33.3225
64.72
41.9480
41.8265
2.7095
2. Simulation de News Headlines
Note: En production, vous utiliseriez une API de news réelles (Bloomberg, Reuters, NewsAPI). Ici, nous simulons des headlines basés sur l’action des prix.
def simulate_news_headlines(ticker, prices, volumes):"""Simule des headlines basés sur l'action des prix/volume. En production, remplacez par une API de news réelles. """ headlines = []# Calculer les changements récents returns = prices.pct_change() vol_changes = volumes.pct_change()# Mots-clés positifs et négatifs positive_words = ['surges', 'rallies', 'gains', 'jumps', 'soars', 'climbs','strengthens', 'advances', 'rises', 'buys', 'bullish'] negative_words = ['plummets', 'drops', 'declines', 'falls', 'slides','weakens', 'retreats', 'sells', 'bearish', 'losses'] neutral_words = ['trades', 'holds', 'steady', 'flat', 'mixed', 'unchanged']for i inrange(len(prices)):if i ==0: headlines.append(f"{ticker} market opens steady")continue ret = returns.iloc[i] vol_change = vol_changes.iloc[i] if i <len(vol_changes) else0# Générer un headline basé sur l'actionif ret >0.03:if vol_change >0.2: headlines.append(f"{ticker} surges on heavy volume")else: headlines.append(f"{ticker} rallies strongly, gains {ret*100:.1f}%")elif ret >0.01: headlines.append(f"{ticker} gains ground, up {ret*100:.1f}%")elif ret >-0.01: headlines.append(f"{ticker} trades mixed in volatile session")elif ret >-0.03: headlines.append(f"{ticker} declines {abs(ret)*100:.1f}% on profit taking")else:if vol_change >0.2: headlines.append(f"{ticker} plummets on high volume, down {abs(ret)*100:.1f}%")else: headlines.append(f"{ticker} drops sharply, loses {abs(ret)*100:.1f}%")return headlines# Exemple avec AAPLaapl_headlines = simulate_news_headlines('AAPL', closes['AAPL'].iloc[:60], volumes['AAPL'].iloc[:60])print("Exemples de headlines simulés:")for i, h inenumerate(aapl_headlines[-10:]):print(f"{i+1}. {h}")
Exemples de headlines simulés:
1. AAPL gains ground, up 1.3%
2. AAPL trades mixed in volatile session
3. AAPL trades mixed in volatile session
4. AAPL trades mixed in volatile session
5. AAPL trades mixed in volatile session
6. AAPL trades mixed in volatile session
7. AAPL trades mixed in volatile session
8. AAPL gains ground, up 2.0%
9. AAPL trades mixed in volatile session
10. AAPL trades mixed in volatile session
3. Prétraitement du Texte (NLP Preprocessing)
import refrom collections import Counterdef preprocess_text(text):"""Prétraite le texte: minuscules, suppression ponctuation."""# Minuscules text = text.lower()# Supprimer ponctuation text = re.sub(r'[^\w\s]', '', text)return textdef tokenize(text):"""Tokenise le texte en mots."""return text.split()def remove_stopwords(tokens, stopwords=None):"""Supprime les mots courants (stopwords)."""if stopwords isNone: stopwords = {'on', 'in', 'at', 'the', 'a', 'an', 'and', 'or', 'but', 'for'}return [t for t in tokens if t notin stopwords]# Exemple de prétraitementheadline ="AAPL surges on heavy volume, gains 3.2%"processed = preprocess_text(headline)tokens = tokenize(processed)filtered = remove_stopwords(tokens)print(f"Original: {headline}")print(f"Prétraité: {processed}")print(f"Tokens: {tokens}")print(f"Filtré: {filtered}")
Original: AAPL surges on heavy volume, gains 3.2%
Prétraité: aapl surges on heavy volume gains 32
Tokens: ['aapl', 'surges', 'on', 'heavy', 'volume', 'gains', '32']
Filtré: ['aapl', 'surges', 'heavy', 'volume', 'gains', '32']
4. Feature Extraction - Bag of Words
def create_vocabulary(headlines, max_features=1000):"""Crée un vocabulaire à partir des headlines.""" all_words = []for headline in headlines: processed = preprocess_text(headline) tokens = tokenize(processed) filtered = remove_stopwords(tokens) all_words.extend(filtered)# Compter les fréquences word_counts = Counter(all_words)# Retourner les mots les plus fréquents vocabulary = {word: idx for idx, (word, _) inenumerate(word_counts.most_common(max_features))}return vocabularydef headline_to_bow(headline, vocabulary):"""Convertit un headline en vecteur bag-of-words.""" processed = preprocess_text(headline) tokens = tokenize(processed) filtered = remove_stopwords(tokens)# Créer vecteur binaire vector = np.zeros(len(vocabulary))for word in filtered:if word in vocabulary: vector[vocabulary[word]] =1return vector# Créer vocabulaireall_headlines = []for ticker in tickers: ticker_headlines = simulate_news_headlines(ticker, closes[ticker].iloc[:60], volumes[ticker].iloc[:60]) all_headlines.extend(ticker_headlines)vocabulary = create_vocabulary(all_headlines, max_features=100)print(f"Taille du vocabulaire: {len(vocabulary)}")print(f"\nMots les plus fréquents: {list(vocabulary.keys())[:20]}")
Taille du vocabulaire: 54
Mots les plus fréquents: ['trades', 'mixed', 'volatile', 'session', 'aapl', 'msft', 'googl', 'amzn', 'nvda', 'gains', 'ground', 'up', 'declines', 'profit', 'taking', '11', '13', 'market', 'opens', 'steady']
5. Préparation des Données d’Entraînement
def prepare_training_data(tickers, closes, vocabulary, window=5):"""Prépare les données X (features) et y (targets).""" X = [] y = []for ticker in tickers:# Simuler headlines headlines = simulate_news_headlines(ticker, closes[ticker], volumes[ticker])# Pour chaque headline (sauf les derniers)for i inrange(window, len(headlines) -1):# Features: bag-of-words des N derniers headlines recent_headlines = headlines[i-window:i] features = np.zeros(len(vocabulary))for h in recent_headlines: features += headline_to_bow(h, vocabulary)# Target: direction du prix le jour suivant current_price = closes[ticker].iloc[i] next_price = closes[ticker].iloc[i +1] target =1if next_price > current_price else0 X.append(features) y.append(target)return np.array(X), np.array(y)# Préparer les donnéesX, y = prepare_training_data(tickers, closes, vocabulary)print(f"Features shape: {X.shape}")print(f"Targets shape: {y.shape}")print(f"Distribution des targets: {np.bincount(y)}")print(f"Ratio positifs/négatifs: {y.sum() /len(y):.2%}")
Features shape: (3620, 54)
Targets shape: (3620,)
Distribution des targets: [1646 1974]
Ratio positifs/négatifs: 54.53%
def get_sentiment_score(ticker, model, closes, volumes, vocabulary, window=5):"""Calcule un score de sentiment moyen pour une action."""# closes/volumes: DataFrame complet (cellule sentiment) OU Series single-ticker fenetree# (cellule backtest, window_closes = closes[ticker].iloc[i-60:i]). Normaliser pour eviter le# KeyError d'une indexation de Series par le label 'ticker'. Preserve la semantique de fenetre# (headlines[-window:] = 5 derniers headlines de la fenetre, pas de la serie globale). price_series = closes[ticker] ifhasattr(closes, "columns") and ticker in closes.columns else closes vol_series = volumes[ticker] ifhasattr(volumes, "columns") and ticker in volumes.columns else volumes# Simuler headlines récents headlines = simulate_news_headlines(ticker, price_series, vol_series) recent_headlines = headlines[-window:]# Agréger les features features = np.zeros(len(vocabulary))for h in recent_headlines: features += headline_to_bow(h, vocabulary)# Prédire la probabilité de hausse features = features.reshape(1, -1) prob = model.predict_proba(features +1)[0][1]return prob# Analyser le sentiment pour chaque actionsentiment_scores = {}for ticker in tickers: sentiment = get_sentiment_score(ticker, nb_model, closes, volumes, vocabulary) sentiment_scores[ticker] = sentimentprint("Scores de sentiment actuels (probabilité de hausse):")for ticker, score insorted(sentiment_scores.items(), key=lambda x: x[1], reverse=True):print(f"{ticker}: {score:.2%}")
Scores de sentiment actuels (probabilité de hausse):
MSFT: 67.81%
AMZN: 59.82%
AAPL: 56.11%
NVDA: 53.94%
GOOGL: 49.09%