From 86f31da3ced8915aafeb8b33c1b6bba93e848537 Mon Sep 17 00:00:00 2001 From: jsboige Date: Thu, 22 Oct 2026 15:19:59 +0200 Subject: [PATCH] Fix(qc,#21851): ML-TextClassification -- split chronologique, comptes naturels NB, lecture vs baseline - split temporel 70/30 (coupure 2019-02-20) a la place du tirage aleatoire - cellule CV temporelle TimeSeriesSplit 5 decoupes + baseline majoritaire - Naive Bayes sur comptes naturels (suppression du pseudo-compte +1) - objectif du carnet reframe honnetement : titres simules depuis les prix, le carnet ne teste pas le chemin vraies news (header + section 10) - cellule d'interpretation apres les rapports : NB 50.41% / LR 53.46% vs baseline majoritaire 55.48% sur le test -- classifieurs domines par la classe dominante, coherents avec l'ecart -34.48% vs buy&hold - re-execution reelle : kernel LEAN Research local sur data materialisee (converter yfinance->LEAN, voie sanctionnee #8788/#9515), 15/15, 0 erreur See #21851 Co-Authored-By: Claude Sonnet 5.5 --- .../ML-TextClassification/quantbook.ipynb | 327 +++++++++++------- 1 file changed, 211 insertions(+), 116 deletions(-) diff --git a/MyIA.AI.Notebooks/QuantConnect/projects/ML-TextClassification/quantbook.ipynb b/MyIA.AI.Notebooks/QuantConnect/projects/ML-TextClassification/quantbook.ipynb index 2402d2846ca..ac4e4f09456 100644 --- a/MyIA.AI.Notebooks/QuantConnect/projects/ML-TextClassification/quantbook.ipynb +++ b/MyIA.AI.Notebooks/QuantConnect/projects/ML-TextClassification/quantbook.ipynb @@ -6,21 +6,24 @@ "source": [ "# ML Text Classification for Trading\n", "\n", - "**Objectif**: Utiliser le NLP (Natural Language Processing) pour analyser le sentiment des news financières et générer des signaux de trading.\n", + "**Objectif**: démontrer la chaîne complète de vectorisation de texte (prétraitement, bag-of-words, classifieurs) sur un panier d'actions, avec une évaluation temporelle honnête.\n", "\n", - "## Stratégie\n", + "**Ce que ce carnet démontre**: la mécanique NLP appliquée à des titres **simulés à partir des prix et volumes passés** (section 2), et le constat mesuré que ce signal ne prédit pas la direction du lendemain mieux que le hasard une fois l'évaluation faite hors échantillon temporel.\n", "\n", - "1. **Collection de données**: Récupérer les headlines de news financières\n", - "2. **Feature extraction**: Bag-of-words, TF-IDF pour transformer le texte en vecteurs\n", - "3. **Classification**: Entraîner un classifieur (Naive Bayes, SVM, ou Logistic Regression)\n", - "4. **Hybridation**: Combiner le sentiment avec des indicateurs techniques\n", - "5. **Trading**: Long sur les actions avec sentiment positif + confirmations techniques\n", + "**Ce que ce carnet ne démontre pas**: que le NLP sur de **vraies** news financières prédit les cours. Les titres sont fabriqués depuis les rendements passés : le classifieur ré-apprend, au mieux, la transformation prix → texte → prix, une circularité sans valeur prédictive démontrée. Le passage à de vraies news (API dédiée, modèles finetunés) est une extension réelle, non testée ici (section 10).\n", + "\n", + "## Pipeline démontré\n", + "\n", + "1. **Données**: 5 actions tech, 730 barres journalières (fenêtre effective divulguée en section 1)\n", + "2. **Feature extraction**: bag-of-words sur fenêtre de 5 titres\n", + "3. **Classification**: Naive Bayes et régression logistique, avec **split chronologique** (train = première période, test = dernière période) et validation croisée temporelle\n", + "4. **Hybridation**: combinaison du score texte avec RSI/EMA/momentum\n", + "5. **Trading**: backtest comparé au buy & hold équipondéré sur la même fenêtre\n", "\n", "## Prérequis\n", "\n", "- sklearn: `pip install scikit-learn`\n", - "- nltk: `pip install nltk` (optionnel pour preprocessing avancé)\n", - "- Compréhension basique du NLP (tokenization, stopwords, stemming)\n", + "- Compréhension basique du NLP (tokenization, stopwords)\n", "\n", "## Durée estimée: 45 minutes" ] @@ -30,10 +33,10 @@ "execution_count": 1, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:12.698058Z", - "iopub.status.busy": "2026-07-29T07:13:12.697888Z", - "iopub.status.idle": "2026-07-29T07:13:14.081935Z", - "shell.execute_reply": "2026-07-29T07:13:14.080343Z" + "iopub.execute_input": "2026-10-22T13:17:21.060809Z", + "iopub.status.busy": "2026-10-22T13:17:21.060554Z", + "iopub.status.idle": "2026-10-22T13:17:23.247097Z", + "shell.execute_reply": "2026-10-22T13:17:23.245577Z" } }, "outputs": [ @@ -73,10 +76,10 @@ "execution_count": 2, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:14.146396Z", - "iopub.status.busy": "2026-07-29T07:13:14.146040Z", - "iopub.status.idle": "2026-07-29T07:13:14.264157Z", - "shell.execute_reply": "2026-07-29T07:13:14.262352Z" + "iopub.execute_input": "2026-10-22T13:17:23.287006Z", + "iopub.status.busy": "2026-10-22T13:17:23.286708Z", + "iopub.status.idle": "2026-10-22T13:17:23.627537Z", + "shell.execute_reply": "2026-10-22T13:17:23.623427Z" } }, "outputs": [ @@ -111,10 +114,10 @@ "execution_count": 3, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:14.267695Z", - "iopub.status.busy": "2026-07-29T07:13:14.267425Z", - "iopub.status.idle": "2026-07-29T07:13:14.739365Z", - "shell.execute_reply": "2026-07-29T07:13:14.737872Z" + "iopub.execute_input": "2026-10-22T13:17:23.631912Z", + "iopub.status.busy": "2026-10-22T13:17:23.631578Z", + "iopub.status.idle": "2026-10-22T13:17:24.549406Z", + "shell.execute_reply": "2026-10-22T13:17:24.547933Z" } }, "outputs": [ @@ -266,10 +269,10 @@ "execution_count": 4, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:14.742159Z", - "iopub.status.busy": "2026-07-29T07:13:14.741765Z", - "iopub.status.idle": "2026-07-29T07:13:14.759128Z", - "shell.execute_reply": "2026-07-29T07:13:14.757525Z" + "iopub.execute_input": "2026-10-22T13:17:24.552907Z", + "iopub.status.busy": "2026-10-22T13:17:24.552474Z", + "iopub.status.idle": "2026-10-22T13:17:24.584673Z", + "shell.execute_reply": "2026-10-22T13:17:24.583560Z" } }, "outputs": [ @@ -360,10 +363,10 @@ "execution_count": 5, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:14.761527Z", - "iopub.status.busy": "2026-07-29T07:13:14.761241Z", - "iopub.status.idle": "2026-07-29T07:13:14.769692Z", - "shell.execute_reply": "2026-07-29T07:13:14.767898Z" + "iopub.execute_input": "2026-10-22T13:17:24.588722Z", + "iopub.status.busy": "2026-10-22T13:17:24.588454Z", + "iopub.status.idle": "2026-10-22T13:17:24.597880Z", + "shell.execute_reply": "2026-10-22T13:17:24.596186Z" } }, "outputs": [ @@ -424,10 +427,10 @@ "execution_count": 6, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:14.772483Z", - "iopub.status.busy": "2026-07-29T07:13:14.772184Z", - "iopub.status.idle": "2026-07-29T07:13:14.795472Z", - "shell.execute_reply": "2026-07-29T07:13:14.793813Z" + "iopub.execute_input": "2026-10-22T13:17:24.601591Z", + "iopub.status.busy": "2026-10-22T13:17:24.601274Z", + "iopub.status.idle": "2026-10-22T13:17:24.649139Z", + "shell.execute_reply": "2026-10-22T13:17:24.635384Z" } }, "outputs": [ @@ -498,10 +501,10 @@ "execution_count": 7, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:14.797735Z", - "iopub.status.busy": "2026-07-29T07:13:14.797515Z", - "iopub.status.idle": "2026-07-29T07:13:15.917521Z", - "shell.execute_reply": "2026-07-29T07:13:15.916084Z" + "iopub.execute_input": "2026-10-22T13:17:24.656229Z", + "iopub.status.busy": "2026-10-22T13:17:24.655979Z", + "iopub.status.idle": "2026-10-22T13:17:28.045972Z", + "shell.execute_reply": "2026-10-22T13:17:28.044232Z" } }, "outputs": [ @@ -511,6 +514,7 @@ "text": [ "Features shape: (3620, 54)\n", "Targets shape: (3620,)\n", + "Période couverte: 2017-02-14 à 2019-12-30\n", "Distribution des targets: [1646 1974]\n", "Ratio positifs/négatifs: 54.53%\n" ] @@ -518,9 +522,14 @@ ], "source": [ "def prepare_training_data(tickers, closes, vocabulary, window=5):\n", - " \"\"\"Prépare les données X (features) et y (targets).\"\"\"\n", + " \"\"\"Prépare les données X (features), y (targets) et les dates associées.\n", + " \n", + " Les dates sont retournées avec chaque échantillon pour permettre un split\n", + " chronologique en aval : un échantillon = une barre (ticker, date).\n", + " \"\"\"\n", " X = []\n", " y = []\n", + " dates = []\n", " \n", " for ticker in tickers:\n", " # Simuler headlines\n", @@ -542,14 +551,16 @@ " \n", " X.append(features)\n", " y.append(target)\n", + " dates.append(closes.index[i])\n", " \n", - " return np.array(X), np.array(y)\n", + " return np.array(X), np.array(y), np.array(dates)\n", "\n", "# Préparer les données\n", - "X, y = prepare_training_data(tickers, closes, vocabulary)\n", + "X, y, sample_dates = prepare_training_data(tickers, closes, vocabulary)\n", "\n", "print(f\"Features shape: {X.shape}\")\n", "print(f\"Targets shape: {y.shape}\")\n", + "print(f\"Période couverte: {sample_dates.min().date()} à {sample_dates.max().date()}\")\n", "print(f\"Distribution des targets: {np.bincount(y)}\")\n", "print(f\"Ratio positifs/négatifs: {y.sum() / len(y):.2%}\")" ] @@ -566,10 +577,10 @@ "execution_count": 8, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:15.920824Z", - "iopub.status.busy": "2026-07-29T07:13:15.920622Z", - "iopub.status.idle": "2026-07-29T07:13:16.953723Z", - "shell.execute_reply": "2026-07-29T07:13:16.952075Z" + "iopub.execute_input": "2026-10-22T13:17:28.049888Z", + "iopub.status.busy": "2026-10-22T13:17:28.049619Z", + "iopub.status.idle": "2026-10-22T13:17:30.168995Z", + "shell.execute_reply": "2026-10-22T13:17:30.167141Z" } }, "outputs": [ @@ -577,25 +588,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "Train set: 2534 échantillons\n", - "Test set: 1086 échantillons\n" + "Coupure temporelle au 2019-02-20 (quantile 70% des dates)\n", + "Train: 2535 échantillons, du 2017-02-14 au 2019-02-20 (ratio up: 54.12%)\n", + "Test : 1085 échantillons, du 2019-02-21 au 2019-12-30 (ratio up: 55.48%)\n" ] } ], "source": [ - "from sklearn.model_selection import train_test_split\n", + "from sklearn.model_selection import train_test_split, TimeSeriesSplit\n", "from sklearn.naive_bayes import MultinomialNB\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.ensemble import RandomForestClassifier\n", "from sklearn.metrics import classification_report, confusion_matrix\n", "\n", - "# Split train/test\n", - "X_train, X_test, y_train, y_test = train_test_split(\n", - " X, y, test_size=0.3, random_state=42, stratify=y\n", - ")\n", + "# Split CHRONOLOGIQUE : le panel est temporel -- un tirage aléatoire mettrait des\n", + "# jours adjacents (fortement autocorrélés) des deux côtés de la coupure et mesurerait\n", + "# une accuracy de voisinage, pas une prédiction hors échantillon temporel.\n", + "# Train = première période, test = dernière période, coupure sur le calendrier (70/30).\n", + "split_date = pd.Series(sample_dates).quantile(0.7)\n", + "train_mask = sample_dates <= split_date\n", + "test_mask = ~train_mask\n", + "\n", + "X_train, X_test = X[train_mask], X[test_mask]\n", + "y_train, y_test = y[train_mask], y[test_mask]\n", "\n", - "print(f\"Train set: {X_train.shape[0]} échantillons\")\n", - "print(f\"Test set: {X_test.shape[0]} échantillons\")" + "print(f\"Coupure temporelle au {pd.Timestamp(split_date).date()} (quantile 70% des dates)\")\n", + "print(f\"Train: {X_train.shape[0]} échantillons, du {sample_dates[train_mask].min().date()} au {sample_dates[train_mask].max().date()} (ratio up: {y_train.mean():.2%})\")\n", + "print(f\"Test : {X_test.shape[0]} échantillons, du {sample_dates[test_mask].min().date()} au {sample_dates[test_mask].max().date()} (ratio up: {y_test.mean():.2%})\")" ] }, { @@ -610,10 +629,10 @@ "execution_count": 9, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:16.956210Z", - "iopub.status.busy": "2026-07-29T07:13:16.955903Z", - "iopub.status.idle": "2026-07-29T07:13:16.977871Z", - "shell.execute_reply": "2026-07-29T07:13:16.976226Z" + "iopub.execute_input": "2026-10-22T13:17:30.179388Z", + "iopub.status.busy": "2026-10-22T13:17:30.178912Z", + "iopub.status.idle": "2026-10-22T13:17:30.216234Z", + "shell.execute_reply": "2026-10-22T13:17:30.207410Z" } }, "outputs": [ @@ -621,27 +640,31 @@ "name": "stdout", "output_type": "stream", "text": [ - "Naive Bayes Accuracy: 51.29%\n", + "Naive Bayes Accuracy: 50.41%\n", "\n", "Rapport de classification:\n", " precision recall f1-score support\n", "\n", - " Down 0.44 0.28 0.34 494\n", - " Up 0.54 0.71 0.61 592\n", + " Down 0.43 0.33 0.37 483\n", + " Up 0.54 0.65 0.59 602\n", "\n", - " accuracy 0.51 1086\n", - " macro avg 0.49 0.49 0.48 1086\n", - "weighted avg 0.50 0.51 0.49 1086\n", + " accuracy 0.50 1085\n", + " macro avg 0.49 0.49 0.48 1085\n", + "weighted avg 0.49 0.50 0.49 1085\n", "\n" ] } ], "source": [ - "# Modèle 1: Naive Bayes\n", + "# Modèle 1: Naive Bayes, sur comptes naturels.\n", + "# X est une somme de vecteurs binaires sur la fenêtre de 5 titres : valeurs >= 0,\n", + "# MultinomialNB les accepte tels quels. Le \"+1\" historique ajoutait un pseudo-compte\n", + "# uniforme à chaque feature -- un décalage identique pour tous les documents, qui ne\n", + "# pouvait qu'écraser les priors sans corriger quoi que ce soit.\n", "nb_model = MultinomialNB()\n", - "nb_model.fit(X_train + 1, y_train) # +1 pour éviter les valeurs négatives\n", + "nb_model.fit(X_train, y_train)\n", "\n", - "nb_pred = nb_model.predict(X_test + 1)\n", + "nb_pred = nb_model.predict(X_test)\n", "nb_accuracy = (nb_pred == y_test).mean()\n", "\n", "print(f\"Naive Bayes Accuracy: {nb_accuracy:.2%}\")\n", @@ -661,10 +684,10 @@ "execution_count": 10, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:16.980764Z", - "iopub.status.busy": "2026-07-29T07:13:16.980495Z", - "iopub.status.idle": "2026-07-29T07:13:21.004780Z", - "shell.execute_reply": "2026-07-29T07:13:20.991341Z" + "iopub.execute_input": "2026-10-22T13:17:30.219641Z", + "iopub.status.busy": "2026-10-22T13:17:30.219308Z", + "iopub.status.idle": "2026-10-22T13:17:34.907964Z", + "shell.execute_reply": "2026-10-22T13:17:34.906856Z" } }, "outputs": [ @@ -672,17 +695,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "Logistic Regression Accuracy: 49.54%\n", + "Logistic Regression Accuracy: 53.46%\n", "\n", "Rapport de classification:\n", " precision recall f1-score support\n", "\n", - " Down 0.39 0.20 0.27 494\n", - " Up 0.53 0.74 0.62 592\n", + " Down 0.43 0.14 0.21 483\n", + " Up 0.55 0.85 0.67 602\n", "\n", - " accuracy 0.50 1086\n", - " macro avg 0.46 0.47 0.44 1086\n", - "weighted avg 0.47 0.50 0.46 1086\n", + " accuracy 0.53 1085\n", + " macro avg 0.49 0.50 0.44 1085\n", + "weighted avg 0.50 0.53 0.47 1085\n", "\n" ] } @@ -712,10 +735,10 @@ "execution_count": 11, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:21.020426Z", - "iopub.status.busy": "2026-07-29T07:13:21.007232Z", - "iopub.status.idle": "2026-07-29T07:13:21.087819Z", - "shell.execute_reply": "2026-07-29T07:13:21.086237Z" + "iopub.execute_input": "2026-10-22T13:17:34.922090Z", + "iopub.status.busy": "2026-10-22T13:17:34.921668Z", + "iopub.status.idle": "2026-10-22T13:17:35.011262Z", + "shell.execute_reply": "2026-10-22T13:17:35.010467Z" } }, "outputs": [ @@ -724,21 +747,21 @@ "output_type": "stream", "text": [ " Model Accuracy Avg_Prob_Up\n", - "0 Naive Bayes 0.512891 0.543860\n", - "1 Logistic Regression 0.495396 0.544084\n" + "0 Naive Bayes 0.504147 0.547472\n", + "1 Logistic Regression 0.534562 0.542317\n" ] } ], "source": [ - "# Comparaison des modèles\n", + "# Comparaison des modèles (les deux consomment les mêmes comptes naturels >= 0)\n", "results = []\n", "\n", "for name, model in [('Naive Bayes', nb_model), ('Logistic Regression', lr_model)]:\n", - " pred_proba = model.predict_proba(X_test + 1 if name == 'Naive Bayes' else X_test)[:, 1]\n", + " pred_proba = model.predict_proba(X_test)[:, 1]\n", " \n", " results.append({\n", " 'Model': name,\n", - " 'Accuracy': (model.predict(X_test + 1 if name == 'Naive Bayes' else X_test) == y_test).mean(),\n", + " 'Accuracy': (model.predict(X_test) == y_test).mean(),\n", " 'Avg_Prob_Up': pred_proba.mean()\n", " })\n", "\n", @@ -746,6 +769,76 @@ "print(results_df)" ] }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "execution": { + "iopub.execute_input": "2026-10-22T13:17:35.068968Z", + "iopub.status.busy": "2026-10-22T13:17:35.068435Z", + "iopub.status.idle": "2026-10-22T13:17:58.591408Z", + "shell.execute_reply": "2026-10-22T13:17:58.577776Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Model CV temporelle moyenne CV temporelle min CV temporelle max\n", + " Naive Bayes 0.5078 0.4743 0.5473\n", + "Logistic Regression 0.5257 0.5008 0.5771\n", + "\n", + "Baseline majoritaire (toujours prédire la classe dominante du train): 54.12%\n" + ] + } + ], + "source": [ + "# Robustesse : validation croisée temporelle (TimeSeriesSplit, 5 découpes) sur les\n", + "# échantillons triés par date -- chaque découpe entraîne sur le passé et évalue sur\n", + "# le futur immédiat. Comparée à la baseline majoritaire, cette mesure dit si le\n", + "# signal textuel apporte quoi que ce soit au-delà de la classe dominante.\n", + "order = np.argsort(sample_dates)\n", + "Xs, ys = X[order], y[order]\n", + "\n", + "tscv = TimeSeriesSplit(n_splits=5)\n", + "rows = []\n", + "for name, make_model in [('Naive Bayes', MultinomialNB),\n", + " ('Logistic Regression', lambda: LogisticRegression(max_iter=1000, random_state=42))]:\n", + " accs = []\n", + " for tr_idx, te_idx in tscv.split(Xs):\n", + " m = make_model()\n", + " m.fit(Xs[tr_idx], ys[tr_idx])\n", + " accs.append((m.predict(Xs[te_idx]) == ys[te_idx]).mean())\n", + " rows.append({'Model': name,\n", + " 'CV temporelle moyenne': np.mean(accs),\n", + " 'CV temporelle min': np.min(accs),\n", + " 'CV temporelle max': np.max(accs)})\n", + "\n", + "cv_df = pd.DataFrame(rows)\n", + "print(cv_df.round(4).to_string(index=False))\n", + "majority_acc = max(y_train.mean(), 1 - y_train.mean())\n", + "print(f\"\\nBaseline majoritaire (toujours prédire la classe dominante du train): {majority_acc:.2%}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Lecture du résultat : le signal textuel ne bat pas la classe dominante\n", + "\n", + "Sur le **test chronologique** (2019-02-21 à 2019-12-30, jamais vu à l'entraînement), Naive Bayes atteint **50,41 %** et la régression logistique **53,46 %** de bonnes réponses sur la direction du lendemain. Le point de comparaison n'est pas 50 % : c'est la **baseline majoritaire**. Le test compte 55,48 % de jours « up » : toujours prédire « up » aurait rendu **55,48 %**. **Les deux classifieurs sont dominés par cette baseline triviale** — aucun n'extrait du texte plus que ce que contient déjà la fréquence de base des hausses.\n", + "\n", + "La validation croisée temporelle (5 découpes entraînées sur le passé, évaluées sur le futur immédiat) confirme la tendance : Naive Bayes 50,78 % en moyenne (47,43 % à 54,73 % selon la découpe), régression logistique 52,57 % (50,08 % à 57,71 %) — autour du hasard, jamais nettement au-dessus de la classe dominante.\n", + "\n", + "Cette lecture est **cohérente avec le backtest de la section 9** : la stratégie retire **77,19 %** sur la fenêtre, mais le buy & hold équipondéré du même univers en retire **111,67 %** (écart **-34,48 %**). Le rendement positif vient du panier tech 2017-2019 (le beta du panier), pas du signal — le signal, lui, reste sous la baseline. C'est l'attente d'un pipeline où les « news » sont fabriquées depuis les prix passés (section 2) : le classifieur ré-apprend la transformation prix → texte → prix, une circularité sans bord prédictif. Notons enfin que le backtest balaye toute la fenêtre, période d'entraînement comprise : il illustre le pipeline, il n'évalue pas le classifieur hors échantillon.\n", + "\n", + "Trois leçons à retenir :\n", + "1. **Sur données temporelles, le split aléatoire surestime l'accuracy** : il met des jours voisins (fortement autocorrélés) des deux côtés de la coupure et mesure un effet de voisinage, pas une prédiction.\n", + "2. **Une accuracy « au-dessus de 50 % » ne dit rien tant qu'elle n'est pas comparée à la classe dominante de son propre test** — ici, 53 % serait un bon score sur un test équilibré ; sur un test à 55 % de « up », c'est un score négatif.\n", + "3. **Un backtest rentable ne valide pas un classifieur** tant qu'il n'est pas comparé à la baseline passive du même univers sur la même fenêtre." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -755,13 +848,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:21.115410Z", - "iopub.status.busy": "2026-07-29T07:13:21.101758Z", - "iopub.status.idle": "2026-07-29T07:13:21.223796Z", - "shell.execute_reply": "2026-07-29T07:13:21.222211Z" + "iopub.execute_input": "2026-10-22T13:17:58.599277Z", + "iopub.status.busy": "2026-10-22T13:17:58.598930Z", + "iopub.status.idle": "2026-10-22T13:17:58.736341Z", + "shell.execute_reply": "2026-10-22T13:17:58.734878Z" } }, "outputs": [ @@ -770,11 +863,11 @@ "output_type": "stream", "text": [ "Scores de sentiment actuels (probabilité de hausse):\n", - "MSFT: 67.81%\n", - "AMZN: 59.82%\n", - "AAPL: 56.11%\n", - "NVDA: 53.94%\n", - "GOOGL: 49.09%\n" + "AMZN: 73.54%\n", + "MSFT: 57.09%\n", + "NVDA: 51.48%\n", + "GOOGL: 47.99%\n", + "AAPL: 47.34%\n" ] } ], @@ -796,9 +889,9 @@ " for h in recent_headlines:\n", " features += headline_to_bow(h, vocabulary)\n", " \n", - " # Prédire la probabilité de hausse\n", + " # Prédire la probabilité de hausse (comptes naturels, sans pseudo-compte)\n", " features = features.reshape(1, -1)\n", - " prob = model.predict_proba(features + 1)[0][1]\n", + " prob = model.predict_proba(features)[0][1]\n", " \n", " return prob\n", "\n", @@ -823,13 +916,13 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:21.226686Z", - "iopub.status.busy": "2026-07-29T07:13:21.226375Z", - "iopub.status.idle": "2026-07-29T07:13:21.255130Z", - "shell.execute_reply": "2026-07-29T07:13:21.253421Z" + "iopub.execute_input": "2026-10-22T13:17:58.739173Z", + "iopub.status.busy": "2026-10-22T13:17:58.738930Z", + "iopub.status.idle": "2026-10-22T13:17:58.778426Z", + "shell.execute_reply": "2026-10-22T13:17:58.776680Z" } }, "outputs": [ @@ -839,11 +932,11 @@ "text": [ "Scores hybrides (triés par score total):\n", " sentiment rsi ema_ratio momentum hybrid\n", - "MSFT 0.678 78.101 1.031 0.002 0.639\n", - "AMZN 0.598 79.069 1.009 0.033 0.599\n", - "AAPL 0.561 95.124 1.054 0.033 0.581\n", - "GOOGL 0.491 48.000 1.022 -0.004 0.495\n", - "NVDA 0.539 79.031 1.060 -0.014 0.420\n" + "AMZN 0.735 79.069 1.009 0.033 0.668\n", + "MSFT 0.571 78.101 1.031 0.002 0.585\n", + "AAPL 0.473 95.124 1.054 0.033 0.537\n", + "GOOGL 0.480 48.000 1.022 -0.004 0.490\n", + "NVDA 0.515 79.031 1.060 -0.014 0.407\n" ] } ], @@ -924,13 +1017,13 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "metadata": { "execution": { - "iopub.execute_input": "2026-07-29T07:13:21.257815Z", - "iopub.status.busy": "2026-07-29T07:13:21.257532Z", - "iopub.status.idle": "2026-07-29T07:13:23.238010Z", - "shell.execute_reply": "2026-07-29T07:13:23.236522Z" + "iopub.execute_input": "2026-10-22T13:17:58.781608Z", + "iopub.status.busy": "2026-10-22T13:17:58.781300Z", + "iopub.status.idle": "2026-10-22T13:18:02.290030Z", + "shell.execute_reply": "2026-10-22T13:18:02.288565Z" } }, "outputs": [ @@ -941,11 +1034,11 @@ "\n", "Résultats du Backtest:\n", "Valeur initiale: $100,000.00\n", - "Valeur finale: $178,368.67\n", - "Return total: 78.37%\n", - "Nombre de trades: 382\n", + "Valeur finale: $177,190.59\n", + "Return total: 77.19%\n", + "Nombre de trades: 404\n", "Buy & hold (équipondéré, même univers/fenêtre) : 111.67%\n", - "Écart stratégie - buy&hold : -33.30%\n" + "Écart stratégie - buy&hold : -34.48%\n" ] } ], @@ -1053,6 +1146,8 @@ "source": [ "## 10. Améliorations Possibles\n", "\n", + "**Rappel d'objectif** : ce carnet ne teste pas le chemin « vraies news ». Les extensions ci-dessous changent la source des textes autant que le modèle — aucune n'est validée par les mesures précédentes, qui portent exclusivement sur des titres simulés depuis les prix.\n", + "\n", "### NLP Avancé\n", "- **TF-IDF**: Pondération par importance des mots\n", "- **Word Embeddings**: Word2Vec, GloVe pour capturer le contexte\n",