Skip to content

~/teaching · machine learning · reference

Predicting an asset price: time series, XGBoost and news sentiment, measured honestly

A complete finance project, from the framing to the evening batch, where the measurement matters more than the model.

Abstract

This reference deck of the Machine Learning course at Centrale Méditerranée (option DDEFI, 2026-2027) walks through one full student project: forecasting the next-day return of a liquid stock by combining three tools of the course, time series, gradient boosting with XGBoost, and a language model that scores the tone of financial news. The running example is a large Paris-listed energy stock, chosen because it reacts both to company news and to the geopolitics of oil.

It is written for teams who want a finance subject and for anyone who has seen a stock-price chart that looks too good. The deck spends as much time on framing, data timing and evaluation as on models, because that is where market projects usually go wrong: a leak, a missing baseline, or a gain that disappears once trading costs are paid.

After reading it, you should be able to set up a target that is not trivially predictable, validate a model walk-forward, turn headlines into daily features without looking at tomorrow, measure what the news actually adds, and write a conclusion that matches the size of the result, including a null one.

The slides

Predicting an asset price: time series, XGBoost and news sentiment

Reference. Machine Learning, Centrale Méditerranée, option DDEFI, 2026-2027.

Open the slides (password from your teacher)

The deck itself is encrypted for the students of the course; this page summarises it for everyone.

The course page · Attention and transformers, step by step

Plan of the deck

  1. The problem, well posed. The decision served, why the target is a return, the horizon, three metrics, the guard-rails and what market efficiency lets you hope for.
  2. The data. Daily price bars, a few lagged macro series, open news sources, and the rule that a piece of data belongs to the session in which it became known.
  3. Time series. Log returns, stylised facts, naive baselines, ARIMA and GARCH, walk-forward validation, purging and the classic leaks.
  4. XGBoost on lagged features. Turning a series into a table, six families of features, tuning inside the past, and why trees cannot extend a trend.
  5. News sentiment. From word lists to VADER and FinBERT, then daily aggregation and geopolitical tone from GDELT.
  6. The combination. Sentiment as extra columns, a three-step ladder of models, stacking as an option, and an ablation plan.
  7. Evaluating honestly. A walk-forward results table, the Diebold-Mariano and binomial tests, a strategy net of costs and the deflated Sharpe ratio.
  8. Traps and ethics. Regime changes, leakage through headlines, data that vanish, EU market abuse rules and a visible disclaimer.
  9. Going live and the team plan. The evening batch, drift monitoring, a small Streamlit page, then the repository layout and three roles.

Frame the problem: predict a return, never a price

Yesterday's price is always close to today's, so any model that copies it looks brilliant on prices while knowing nothing; the honest target is the change.

The deck starts with the decision, as the credit scoring project does. Three uses are possible: adjusting a position, hedging an exposure, or briefing a committee with a daily note. The project picks the third: a report every evening, no real order, and a simulated strategy only to put a value on the signal.

Then comes the trap that ruins most stock-market projects. On a simulated random walk of 2,520 days, where nothing can be predicted, the forecast "tomorrow equals today" reaches an R2R^2 of 0.993 on prices. The same idea applied to returns gives R2=−1.02R^2 = -1.02, worse than predicting zero, because the difference of two independent returns has twice their variance. A predicted curve that hugs the real one with a one-day lag is therefore no evidence at all.

The target is the next-day log return of the adjusted close, and the direction is read from the sign of the forecast. Everything placed in the features must belong to the information set Ft\mathcal{F}_t available at the Paris close of day tt.

rt+1=ln⁡Ct+1Ct,xt∈Ft,yt=rt+1r_{t+1} = \ln\frac{C_{t+1}}{C_t}, \qquad x_t \in \mathcal{F}_t, \qquad y_t = r_{t+1}
(1)

Log returns are preferred because they add up over time, so a five-day target is just the sum of five one-day targets. Simple returns do not add up, as the deck shows on five invented prices.

Table 1: Five prices, four returns (example from the deck, recomputed). The sum of log returns equals ln⁡(104/100)\ln(104/100) exactly; the sum of simple returns does not equal the true 4% gain.
MoveLog returnSimple return
100 → 1020.01980.0200
102 → 99−0.0299−0.0294
99 → 1010.02000.0202
101 → 1040.02930.0297
Total of the four0.03920.0405

Remark (What to hope for). Fama (1970) describes markets in which public information is already in the price; Welch and Goyal (2008) found that most classic predictors of the US equity premium do no better out of sample than the historical mean. The realistic goal is a small edge, tested and net of costs. A clean demonstration that the naive model is not beaten is a valid project result.

Back to contents.

Walk-forward validation, baselines and leaks

On data ordered in time you learn on the past, test on the block that follows, step forward and repeat, exactly as if you had lived through the period day by day.

Shuffled cross-validation would let the model learn from the future to predict the past. The deck replaces it with walk-forward validation: train on everything up to a date (expanding window) or on the last few years only (rolling window), predict the next block untouched, move forward one block. The test blocks, laid end to end, make the out-of-sample record. In scikit-learn this is TimeSeriesSplit.

The arithmetic of the deck's split

  1. History. Ten years of trading make about 2,520 sessions.
  2. First training window. Three years, i.e. 756 sessions, before the first test.
  3. Test blocks. One quarter each, 63 sessions.
  4. Folds. (2520−756)/63=28(2520 - 756)/63 = 28 folds, covering 28×63=176428 \times 63 = 1764 test days, the same days for every model.

Every model is compared with baselines written before anything else. Predicting a zero return is the hardest benchmark in RMSE; repeating yesterday's return (persistence) roughly doubles the error variance. ARIMA (Box and Jenkins) and GARCH (Bollerslev, 1986) complete the time-series toolbox: ARIMA usually finds tiny coefficients on daily returns, which is efficiency rather than failure, while GARCH forecasts the size of tomorrow's move, not its sign.

σt+12=ω+α rt2+β σt2\sigma^2_{t+1} = \omega + \alpha\, r_t^2 + \beta\, \sigma^2_t
(2)

With a five-day target, neighbouring rows share four days of their targets. López de Prado (2018) recommends purging the training rows whose target spills into the test block and adding an embargo after it when training data follow the test. With a one-day target and a pure walk-forward, the purge shrinks to a single row.

Three families of leakage are listed: look-ahead bias (a feature dated tt but known later, such as a US series that closes after Paris), survivorship bias (choosing stocks from today's index), and statistics recomputed with the future (standardising over the whole sample, revised macro data). The most useful test in the repository truncates all raw data at a date, rebuilds that day's row, and checks that it is identical to the row in the full table.

Back to contents.

XGBoost on lagged features

A tree model does not know about time, so each day becomes a row whose columns describe only what happened before the close, and whose label is the next day's return.

Lagged features give the past to a model that only sees rows and columns: the day's return and the four before it via shift(k), a 20-day cumulative return for momentum, a 20-day rolling volatility and the GARCH forecast, a 14-day RSI (Wilder, 1978), the day of the week, and a few macro series from FRED lagged by one day. The target is moved up one row (shift(-1)), and a single sign error there means the model learns a return it already knows. The last row has no target: it is the one predicted tonight, so a global dropna() must not remove it.

import numpy as np, pandas as pd, xgboost as xgb

r = np.log(px["Close"]).diff()                # log return, known at the close
X = pd.concat({f"lag{k}": r.shift(k) for k in range(5)}, axis=1)
X["vol20"] = r.rolling(20).std()               # trailing window only
X["mom20"] = r.rolling(20).sum()
y = r.shift(-1)                                 # target: tomorrow's return
ok = X.notna().all(axis=1) & y.notna()          # tonight's row is kept aside
model = xgb.XGBRegressor(max_depth=3, learning_rate=0.02,
                         subsample=0.8, n_estimators=500)
model.fit(X[ok].iloc[:-250], y[ok].iloc[:-250])  # one fold, for reading
pred = model.predict(X[ok].iloc[-250:])

XGBoost (Chen and Guestrin, 2016) adds small trees one by one and charges a price for leaves and leaf values. On returns, where the signal is faint, that regularisation matters: shallow trees, a small learning rate, subsampling, and early stopping. The validation set for early stopping is the last stretch of the training window, never the test block, and hyperparameters are frozen in a config file once chosen over all folds. The details of boosting are in the note on random forests and gradient boosting.

L=∑iℓ(yi,y^i)+∑kΩ(fk),Ω(f)=γT+12λ∥w∥2\mathcal{L} = \sum_i \ell(y_i, \hat y_i) + \sum_k \Omega(f_k), \qquad \Omega(f) = \gamma T + \tfrac12 \lambda \lVert w \rVert^2
(3)

Two more points. Permutation importance and SHAP values say what the model used; volatility features often rank first because they help with magnitude, not direction. And a tree predicts leaf averages, so it can never go beyond the largest value it has seen: on a rising price it stays flat at its edge leaf. A stationary target, the return, removes the problem.

Back to contents.

News sentiment: lexicons, FinBERT and GDELT

Each headline gets a score between minus one and plus one, the scores of a trading day are averaged, and those averages become a few extra columns in the table.

The deck climbs the same ladder as for models. A finance word list (Loughran and McDonald, 2011) counts positive and negative words; it was built because general dictionaries treat words like liability or tax as negative when, in a financial text, they are just accounting items. VADER (Hutto and Gilbert, 2014) adds rules for negation and intensifiers but was designed for social media. FinBERT (Araci, 2019) is a BERT model further trained on financial text; thanks to attention it reads each word in context, so "costs fall" and "sales fall" are not scored alike.

s=p+−p−∈[−1,1]s = p_{+} - p_{-} \in [-1, 1]
(4)

FinBERT returns three probabilities (positive, negative, neutral); the score keeps the balance of the two extremes, and the largest probability serves as a confidence. A rerun by the teacher in October 2026 found that a terse headline and a full sentence with the same meaning can get opposite scores, so test the model on your real headlines first. It also says whether news is good, not whether it was a surprise, and the surprise is what moves prices.

The alignment rule: a headline belongs to the first session whose close comes after its publication time, all times converted to UTC. A story published at 18:45 Paris time on a Friday goes into Monday's row (merge_asof with a forward direction does it). Wrong time zones are the most common leak in sentiment projects.

St=∑isi e−λΔi∑ie−λΔi,λ=ln⁡2half-lifeS_t = \frac{\sum_i s_i\, e^{-\lambda \Delta_i}}{\sum_i e^{-\lambda \Delta_i}}, \qquad \lambda = \frac{\ln 2}{\text{half-life}}
(5)
Table 2: Daily aggregation with a 24-hour half-life (illustrative scores from the deck, recomputed). The weight is 2−Δ/242^{-\Delta/24}.
HeadlineScoreAge at the closeWeight
1+0.82 h0.944
2−0.620 h0.561
3+0.330 h0.420
Simple mean0.167
Weighted mean0.283

The weighted mean is higher because the positive headline is the most recent. The day also gets a headline count, so the model can tell "neutral" from "no news". GDELT, an open database that codes the world's press into events, themes and tone every fifteen minutes, adds geopolitical columns: average tone of events in oil-producing countries, material conflict counts compared with their 60-day level, tone of energy-themed articles, and attention volume. Lists of countries and themes are fixed before looking at results.

Back to contents.

Combining the signals and measuring what the news adds

The simplest combination is one table with prices, macro, sentiment and geopolitics side by side, then a careful check of whether removing the news changes anything.

Sentiment features join the lags and macro columns of the same row, each with a documented time of availability, and a single XGBoost decides whether to use them. The project compares three rungs: a naive model (zero return, sign persistence for direction), a more complex one (ARIMA, or ridge on the same lags, which isolates the effect of non-linearity), and the heavy XGBoost with every family. The rule deciding whether the heavy model wins is written in the decision log before any computation.

Table 3: The ablation plan: same hyperparameters, same seeds, same 28 folds; each row is compared with the previous one.
RunXGBoost columnsQuestion
Apricesis the price history enough?
BA + macrodoes context help?
CB + FinBERTdoes company news help?
DC + GDELTdoes geopolitics add more?
EC with VADERis the transformer worth its cost?

The team writes down its expected outcome before running the ablation study. If C does not beat B, that is a legitimate finding, consistent with the semi-strong form of efficiency. Stacking (Wolpert, 1992) is offered as an alternative: specialised base models whose forecasts are blended by a ridge, provided the weights are learnt only on out-of-sample forecasts produced walk-forward; otherwise the blend trusts whichever model overfits most.

Back to contents.

Honest metrics: tests, costs and the number of attempts

A model that is right 52 days out of 100 may just be lucky, and a strategy that looks profitable may vanish after fees or after counting how many variants were tried.

Three metrics run from model to business: RMSE against the zero forecast, the share of correct directions against 50%, and the annualised Sharpe ratio of a simple long-or-flat rule after costs. Each gap must pass a test. The Diebold-Mariano test (1995) asks whether the average daily difference in squared errors between two forecasts is larger than noise.

dt=e1,t2−e2,t2,DM=dˉsd/n1/2  ∼H0  N(0,1)d_t = e_{1,t}^2 - e_{2,t}^2, \qquad DM = \frac{\bar d}{s_d / n^{1/2}} \;\overset{H_0}{\sim}\; \mathcal{N}(0,1)
(6)

Worked example from the deck: naive model (1) against XGBoost (2), eight invented days

  1. Loss differences. Day 1 gives 1.22−1.02=0.441.2^2 - 1.0^2 = 0.44, and the eight values are 0.44, −0.17, 0.81, 0.12, −0.19, 0.57, 0.20 and 0.25 (in units of 10−410^{-4}).
  2. Mean and spread. dˉ=0.254\bar d = 0.254 and sd=0.347s_d = 0.347.
  3. Statistic. DM=0.254/(0.347/8)=2.07DM = 0.254 / (0.347/\sqrt8) = 2.07, above 1.96: tempting to declare XGBoost better.
  4. Small-sample correction. Harvey, Leybourne and Newbold (1997) multiply by 7/8\sqrt{7/8}, giving 1.94, to compare with 2.36 for a Student law with 7 degrees of freedom: no conclusion. Only a long test period can decide.

For directions, the binomial test treats the model as a coin. Over the 1,764 test days, 52.0% correct means 917 hits and a one-sided p-value of 0.050, borderline. Turning the formula around, a 52% rate needs about 1,691 trading days (6.7 years) to pass at 5%, and 51% needs 6,764 days. If the asset rose on more than half the days, "always up" is the right benchmark, not 50%.

πt+1=post rt+1−c ∣post−post−1∣,SR=πˉsπ252\pi_{t+1} = \text{pos}_t\, r_{t+1} - c\,\lvert \text{pos}_t - \text{pos}_{t-1} \rvert, \qquad SR = \frac{\bar\pi}{s_\pi}\sqrt{252}
(7)

Costs decide many projects. In the deck's illustration, a gross gain of 0.04% a day with a 1% daily standard deviation gives a Sharpe of 0.63; paying 5 basis points on 40% of days halves the mean gain and the Sharpe falls to 0.32. Finally, the deck simulates strategies with no skill at all: over one year, the best of 100 has an average annualised Sharpe near 2.5. Hence the deflated Sharpe ratio of Bailey and López de Prado (2014), and the minimum rule of declaring every configuration tried.

Back to contents.

Pitfalls, ethics and going live

Markets change regime, headlines can describe the very move you predict, sources move under your feet, and a public prediction about a real stock has legal limits.

  • Regimes. The 2020 crash, when the VIX closed above 80 in March, and the 2022 energy shock changed volatility and the oil link of an energy stock. Report results by period and compare expanding and rolling windows.
  • Leakage through headlines. Many headlines narrate the session ("shares slide 4%"). Attached to the wrong day they predict perfectly. The deck treats directional accuracy above 60% as an alarm, a personal rule of caution, and suggests shifting the news by one more day: a collapsing score is suspicious.
  • Moving data. Adjusted prices are recomputed after each dividend, delisted stocks vanish from free sources, macro series are revised (ALFRED keeps the vintages), RSS feeds forget. Raw files are dated, hashed and never overwritten.
  • Law and disclosure. The EU Market Abuse Regulation (No 596/2014) forbids insider dealing and spreading misleading information. The project uses public sources only, places no real order, and states at the top of the README and of the interface that it is a teaching project, not investment advice.

Going live means an evening batch, about an hour after the close, that calls the same functions as the backtest on a single day and writes a dated report; a test compares the batch's prediction for a past day with the backtest's. Drift, treated in the note on drift and conformal prediction, is watched at three levels: feature distributions (PSI), source volumes (a feed at zero is broken, not calm), and the cumulative hit rate, whose pure-chance band is about ±12.7 points over 60 days. A small Streamlit page only reads the reports, so it cannot introduce a leak.

Back to contents.

Key takeaways

  1. Predict a return or a direction, never a price level: on prices even a random walk gives an R2R^2 near 1.
  2. Every feature has a time of availability, and a headline belongs to the first session that closes after it was published.
  3. Validate walk-forward, with baselines (zero, persistence) computed on the same days and under the same cost assumption.
  4. Sentiment enters XGBoost as a few daily columns; an ablation decided in advance shows whether it adds anything.
  5. A gap is a result only after a test: Diebold-Mariano for errors, binomial for directions, and enough test days to detect a small edge.
  6. Count every configuration tried and report the Sharpe net of costs; the best of many useless strategies still looks good.
  7. A clean, well-tested conclusion that the naive model is not beaten is a valid outcome in finance.

Glossary

Log return
The logarithm of the ratio of two consecutive prices; log returns add up over time.
Walk-forward validation
Training on the past, testing on the block that follows, then moving forward; the time-series replacement for shuffled cross-validation.
Lagged features
Columns holding past values of a series (shifted by one or more days) so that a tabular model can see the history.
Data leakage
Information from after the decision time slipping into the features or the training set, producing results that cannot be reproduced live.
Purging and embargo
Removing training rows whose target overlaps the test period, and a margin after it, when targets span several days.
Baseline
A naive forecast, such as zero return or persistence, that every model must beat on the same days.
Sentiment analysis
Turning a text into a score of tone, here between minus one and plus one per headline.
Ablation study
Removing or adding one family of features at a time to measure its marginal contribution.
Stacking
Combining the forecasts of several base models with a meta-model trained on their out-of-sample forecasts.
Diebold-Mariano test
A test of equal predictive accuracy based on the daily differences of the losses of two forecasts.
Deflated Sharpe ratio
A Sharpe ratio corrected for the number of strategies tried, compared with the maximum expected by luck.
Drift
A change over time in the inputs, the sources or the performance of a model in production.

The other notes of the course

One public summary per deck, in the order of the course.

  1. IntroductionAI winters, jobs and method
  2. ToolsKaggle, GitHub, Python: the tools
  3. Session 1Project framing and least squares
  4. Session 2Maximum likelihood and logistic regression
  5. Session 3Gradient descent, ridge and lasso
  6. Session 4Decision threshold and reproducible ML
  7. HomeworkA defensible baseline model
  8. Session 5Kernels, SVM and decision trees
  9. Session 6Forests, boosting, honest comparison
  10. Session 7SHAP, fairness, PCA and clustering
  11. HomeworkA documented champion model
  12. Session 8Neural networks, drift, conformal prediction
  13. HomeworkA delivered machine learning project
  14. Session 9Defending a machine learning project
  15. ReferenceCredit default scoring project
  16. ReferenceAsset returns: XGBoost and sentiment
  17. ReferencePython code for every ML model
  18. ReferenceDeep learning essentials
  19. ReferenceOpen data ML projects by sector
  20. ReferenceData and AI engineer handbook
  21. ReferenceData engineering, SRE and cloud
  22. ReferencePanorama of machine learning models

All the notes, as a reading list

Attention and transformers, step by step

About these notes

Written by the lecturer from the slides of the deck, as a public summary: the key ideas, the formulas and one worked example. The exercises, their solutions, the tests and the grading stay in the deck, for the students of the course.

Revision: .