Case study 02
Market Sentiment Pipeline
A containerized ELT pipeline that turns unstructured financial news into a quantified signal for S&P 500 equities — and then grades its own predictions on a dashboard so the model can't quietly get worse.
The problem I was actually solving
Price data is structured and easy. News is neither, and it's where the information actually lives. I wanted a system that reads the day's news for a few hundred tickers, turns it into something a model can consume, and produces a signal on a fixed schedule without me touching it.
The hard part in a daily pipeline is never the model. It's what happens the third time a task reruns.
A pipeline that runs every day will fail, get retried, and get backfilled. If a rerun can double-count a row, every downstream number is quietly wrong and nothing alerts you. So loads had to be idempotent before anything else was worth building.
Architecture
Why the merge matters — try it
This is the least glamorous decision in the project and the one everything else depends on. Both tables below receive the same day's data. One appends, one merges on (ticker, date). Press Run the same day again — which is what a retry, a backfill or a scheduler hiccup does — and watch them stop agreeing.
Live from the running pipeline
- Last run
- —
- Rows loaded
- —
- Tickers
- —
- Status
- —
Decisions and what they cost
| Decision | Why | What it costs |
|---|---|---|
| MERGE on a natural key, not append | Makes every task rerunnable and every backfill safe. A retry writes the same result as the first run. | More expensive per load than a bare append, and it requires an honest natural key — which forces you to actually understand the grain of your data. |
| ELT, not ETL | Land raw text first, transform in the warehouse. When the sentiment prompt changes, I can reprocess history instead of re-fetching it from a rate-limited source. | Storage cost for raw data I may never query, and transformation logic lives in SQL where it's harder to unit test. |
| LLM for sentiment instead of a classical classifier | Financial news is full of negation, hedging and sarcasm. A bag-of-words model reads "beats lowered expectations" as good news. | Non-deterministic scoring, a per-row API cost, and an external dependency in the critical path. Scores are cached so a rerun doesn't re-bill. |
| Random Forest over a neural model | Small tabular dataset, and I wanted feature importances I could actually read and argue with. | Almost certainly leaves accuracy on the table versus a gradient-boosted or sequence model. |
| Airflow for something one cron could trigger | The value isn't scheduling — it's per-task retries, dependency ordering, and being able to see which step failed at 4am without reading logs by hand. | Real operational weight for a personal project. A cron plus a shell script would have shipped in a day. |
- The pipeline runs daily end to end and the dashboard is live.
- This is not a trading strategy and I don't present it as one. It is a data engineering system that happens to produce a signal. I have not run it against transaction costs, slippage or a proper walk-forward backtest, so I make no claim about profitability.
- The model is retrained on a fixed cadence rather than triggered by measured drift.
- There is no alerting on task failure yet beyond Airflow's own UI — the next thing I'd add.
What I'd tell you in an interview
Ask me about the idempotency work. It's the least glamorous part and the part that makes the difference between a pipeline you can trust and a demo. I'd also rather talk about why the predictions table sits next to an outcomes table than about the model itself — a system that can't tell you when it's wrong isn't finished.
Stack
- Python
- Apache Airflow
- Docker
- Google BigQuery
- Gemini
- scikit-learn
- Looker Studio
- GCP