XGBoost, Python, Streamlit, GitHub Actions
The first version was a Le Wagon bootcamp project: data collection, feature engineering, and two models trained in notebooks. It produced predictions. It was also incomplete, clunky, and impossible to keep running without manual work.
v2 is a rebuild from scratch: real Python modules instead of notebooks, a daily pipeline that fetches data and retrains itself via GitHub Actions, and an evaluation method I thought was honest enough to trust. That last part turned out to be wrong, and correcting it is most of what this page is about.
Every day at 22:00 UTC, after the US market close, a GitHub Actions workflow runs the whole chain end to end.
It pulls fresh data from free public sources: BTC and cross-asset prices (S&P 500, Nasdaq, Gold, Dollar Index) from Yahoo Finance, macro series from FRED, on-chain metrics from CoinMetrics, and the Fear and Greed index. A validation script then runs before anything downstream is allowed to touch the data. It checks that today's data is actually there, that no price has silently frozen (a dead source plus forward-fill looks exactly like a flat market), that values sit in sane ranges, that all 52 model features are present and non-null, and that there are no gaps or duplicate dates. If a check fails, the run stops, opens a GitHub issue, and yesterday's model stays in place.
If it passes, the model retrains on the full history, predicts the next 7-day return, logs it, and commits the result. No manual step anywhere, which also means no prediction on days its own validation blocks the run.
A four-page Streamlit app sits on top: market dashboard, live forecast, model performance, and documentation. A separate weekly job checks data freshness, prediction cadence, and how long since the model was last retuned, and reports live accuracy alongside them.
I originally published 76.7% direction accuracy. It was wrong, and finding out why was worth more than the number.
Two mistakes, both in the evaluation rather than the model.
The first was counting. I described 2,467 predictions as non-overlapping when they were daily, so consecutive windows shared six of their seven days and one good multi-week call was counted up to seven times. Re-running the evaluation on genuinely independent windows gives 73.3%, across 367 windows.
The second was worse. The 7-day target is built from the closing price seven days ahead. The walk-forward loop trained on every row up to the prediction date, which meant the last six training rows carried targets computed from prices the model was about to be tested on. It was being trained on the answer. The same re-run with those six rows dropped gives 48.5%, and the correlation between prediction and outcome falls from 0.77 to −0.03. Not a weaker edge. No edge.
The live log agrees, which is how I know the correction is right. Since late March 2026 the pipeline has logged 106 predictions, each recorded before the outcome was known. Across the 100 that have resolved, it has been right 49.0% of the time. Two independent measurements, one offline across seven years and one accumulated in the open, landing within a point of each other, against a 52.6% always-up baseline.
The same leak explains two other results I had believed. The model looked far more accurate when predicting a large move, 90% against 44% once the leak was removed. And it made the model comparison unfair: LSTM and GRU networks tested on the same target both scored a negative R², worse than predicting the average return, at 50% and 54% direction accuracy. But the deep-learning evaluation correctly dropped those seven training rows and the XGBoost one did not. The trees won a race the networks were not allowed to run.
None of this was caught by the monitoring built for exactly this purpose. A weekly job was supposed to compare live accuracy against the naive baseline and open an issue when it slipped. It had been silently failing since March: a one-off manual repair left one date in a different format from the rest, the read threw an error, a broad exception handler swallowed it, and the job reported "all clear" every week with a green checkmark. A check that could not run was being reported as the thing it checked passing. It now alerts when it cannot run, and treats a sudden jump in accuracy as a bug signal rather than good news.
Three fixes, in the order they matter.
Purge the last seven training rows at every walk-forward step. That is the fix for the leak above, and the one thing to do before trusting any future number.
Select features inside the walk-forward instead of before it. The 52 features were picked from 269 by importance measured across the full eight years, then scored on those same eight years.
Tune against a genuinely held-out period. The hyperparameters came from 100 Optuna trials, each scored on the entire history, keeping whichever scored highest. That is a maximum over 100 attempts on the data being reported, not an out-of-sample result.
None of this is done yet, and none of it is expected to rescue the model. Seven-day Bitcoin direction from free daily data may simply not be predictable, and a null result is the honest outcome rather than a failure. What it would buy is a number worth standing behind.
The pipeline keeps running in the meantime. Every day it adds one more independently logged prediction to a live test of exactly that question.