In short I built a dashboard that backtests and paper-trades a dividend-yield mean-reversion strategy across the S&P 500, self-hosted on my own Raspberry Pi cluster. Early backtests showed up to 251% return — the simulation simply wasn't stringent enough. Once I made it honest (survivorship-free universe, no look-ahead, real costs, slippage and foreign exchange, Danish tax, delisting exits), the strategy roughly matched the market with worse risk-adjusted returns. A systematic edge-hunt found no robust improvement, and a five-phase machine-learning study found genuine, out-of-sample-validated stock-selection skill — but skill that still doesn't beat simply holding the index once you account for risk. The deliverable is the rigour, not the alpha.
The idea, in general terms
The strategy is a dividend-yield mean-reversion screen. A stock's dividend yield rises when its price falls, so an unusually high yield relative to that stock's own history can flag a value entry, and a reverting yield flags an exit. Each stock is judged against its own multi-year normal range, not a single global threshold — a 6% utility and a 1.5% tech name are not comparable on raw yield. Concretely, I standardise each stock's trailing dividend yield against five years of its own history and trade the z-score:
# Standardise each stock's TTM dividend yield against its OWN multi-year history
mu, sigma = y.rolling(WINDOW).mean(), y.rolling(WINDOW).std()
z = (y - mu) / sigma
buy = y >= mu + N * sigma # statistically cheap vs its own norm
sell = y <= mu - M * sigma # yield reverting — take the exit
# In the backtest mu/sigma are recomputed in a trailing-only window at EVERY
# date, and fills land at the NEXT session's open — never the signal's own bar.That last comment is the whole game. The signal is trivial; making the measurement honest is the work.
Auditing the backtest
A backtest that looks too good usually is. Here is where mine was overstating returns, and the fix for each.
1. Survivorship bias. My first universe was today's 503 S&P 500 members. But "the companies still in the index in 2026" is a set hand-picked by history to contain winners — the failures were already removed. Backtesting on survivors is one of the most common ways to manufacture fake returns. The fix was a point-in-time, survivorship-free universe: every ticker that ever had price data, including delisted and removed names, each clipped to its actual index-membership window. A 2018 backtest can only buy what was in the index in 2018.
2. Look-ahead. The live signal anchors its mean/σ on the most recent data — correct for "today," but a leak if you use it in history. So the backtest runs a separate walk-forward path that recomputes thresholds in a trailing-only window at every date, and trades execute at the next session's open, never the same bar's close. I also ran a dedicated look-ahead audit; one change that raised the number turned out to be a legitimate date-alignment fix (matching a dividend to its value one calendar year earlier, by timestamp, instead of a fixed row offset that drifts across holidays), not a leak.
3. Transaction costs. Every fill pays commission (per-leg, with a realistic minimum) plus a combined slippage + FX charge — buys fill higher, sells fill lower. For a strategy that trades, this is the difference between a paper edge and a real loss.
4. Danish tax. As a Danish investor, returns are taxed progressively (a lower bracket up to a yearly threshold, a higher one above it), with dividends taxed in the year received and losses untaxed. The simulation applies one shared annual bracket across the whole portfolio rather than pretending each position is tax-free.
5. Delisting exits. A holding whose price series simply ends (delisting, index removal) is marked-to-market and exited with costs and tax — not frozen at a stale last price, which would silently flatter the result.
6. A fair benchmark. The comparison is the S&P 500 total-return index, shown three ways: pre-tax (the most sceptical read), after-tax single-lot (one lump realised at the end, slamming the top bracket — a harsh lower bound), and after-tax fair (the gain realised in equal annual slices, as a real long-term holder could arrange). The fair figure is the apples-to-apples one — I report all three rather than just the flattering read.
The result
Decomposing the optimistic 251% (2020 start) showed it was almost entirely survivorship bias. Removing it took roughly 114 percentage points off; adding realistic costs and delisting exits took most of the rest.
Extending the test back to 2015 to cover a full cycle (the 2018 correction, the 2020 COVID crash, the 2022 bear) gives a cleaner read over ~11.4 years:
| Metric | Strategy | S&P 500 TR |
|---|---|---|
| Total return | +189% (after-tax) | +340% (pre-tax) |
| — after-tax, fair | — | +192% |
| — after-tax, single-lot | — | +172% |
| CAGR | 9.7% | ~14% |
| Sharpe | 0.25 | ~0.6–0.7 |
| Max drawdown | −43% | ~−34% |
| Avg. capital invested | ~98% | — |
Against the fair after-tax benchmark the strategy slightly trails (−2.3pp); on the most sceptical pre-tax reading it trails badly. It roughly matches the market with worse risk-adjusted returns.
Then I went looking for an edge
Matching the market isn't nothing, but I wanted to know whether any obvious lever could turn it into a real edge. I built an A/B harness that sweeps candidate improvements, each implemented as a default-off switch so production output stays byte-identical. Six levers, same survivorship-free universe:
| Lever | What happened | Verdict |
|---|---|---|
| Stop-loss | Sells at the bottom of the dip it just bought (105% / 0.10) | Reject |
| Sector cap | Non-monotonic noise across the grid (163–235%) | Reject |
| 200-day MA filter | Removes the very dip-buys the strategy is built on (145% / 0.20) | Reject |
| Cash yield on idle | +2pp; the book is already ~98% invested | Negligible |
| Deployment multiplier | No gain; capital is already deployed | Reject |
| Position count (N) | Trades drawdown for concentration; no Sharpe gain | Reject |
No lever beats the baseline risk-adjusted. The stop-loss is the clearest lesson: a stop on a mean-reversion dip-buyer is self-defeating — it sells exactly the position the strategy bought because it was cheap. Edge-hunting on this signal is exhausted; any further upside would need a different signal, not tuning this one. That motivated the machine-learning study.
Can machine learning pick the winners?
The question I actually cared about: can a model rank future winners over losers better than the simple yield z-score rule — and does that translate into an edge? This ran entirely offline as research; the production app takes no ML dependency. The discipline mirrors the backtest: survivorship-free, membership-clipped data; every feature point-in-time; a forward three-month return as the label (hand-verified never to leak into the features); strictly walk-forward training with an embargo equal to the label horizon — no random k-fold, which would shuffle the future into the past. A gradient-boosted tree ranker (LightGBM) over yield, momentum, volatility, distance-from-moving-averages, dividend-growth and regime features.
It ran in five phases, each tightening the test:
- Phases 1–2 — the skill is real. Out-of-sample rank information coefficient +0.057 (t ≈ 6; 67% of months positive) versus ≈ 0 for the incumbent yield z-score. The model ranks winners far better than the rule. But on the dividend universe it only made ~8% CAGR — the dividend/value names structurally lagged the cap-weighted, tech-heavy index. Broadening to all S&P names lifted it to ~11–12% CAGR (and surfaced a data-quality landmine: corrupt post-delisting bars with >150% single-day jumps that had to be sanitised before any simulation was meaningful).
- Phase 3 — turnover is the hidden tax. A three-month signal rebalanced monthly churns ~1,700+ trades and realises gains short-term constantly. Cutting turnover (rebalance every six months with a hold-band) transformed it: the broad model went from 144% to 454% after-tax (Sharpe 0.44), beating the index's 359% pre-tax. And it was skill, not beta — at the same low turnover, random selection returned 66% and the yield rule 107%, both badly trailing the index.
- Phase 4 — the hold-out, the only honest test. Those turnover settings were chosen looking at the whole period — a mild overfit. So I grid-searched every portfolio config on a validation window (2015–2022) only, locked the single best, and evaluated it once on an untouched hold-out (2023–2026).
Phase 5 — can overlays rescue it? I tried the obvious risk-reduction overlays (volatility-targeting, a market-regime filter, an index blend), each tuned on the validation window and tested once on the hold-out. Volatility-targeting cut drawdown hard but cut return proportionally, leaving Sharpe flat. The regime filter whipsawed. The blend helped most — but only by replacing 75% of the book with the index, and it still landed below the pure index: the ML sleeve diluted the index's risk-adjusted return rather than enhancing it. No overlay produced a risk-adjusted edge over buy-and-hold.
The model has genuine, hold-out-validated stock-selection skill — it ranks winners over losers far better than the incumbent rule. But as a long-only strategy that skill yields, at best, a return edge bought with more risk; it is risk-adjusted inferior to simply holding the index. The verdict: the skill is real, a deployable edge is not.
Green backtests are easy; sobering results are hard on the ego
It is easy to publish a positive backtest; it is more useful to report that an idea roughly matches an index fund — and to show real machine-learning skill while being equally clear that it doesn't survive contact with risk, costs and a clean hold-out. That is the judgement you want in someone working with money or models. The most expensive mistakes in quantitative work come from believing a number you wanted to be true; this project is a record of not doing that.
The engineering, briefly
- Dashboard: a Python Shiny app — tabs for signals, per-stock detail, the survivorship-free portfolio backtest, a Risk & Insights view (underwater drawdown, rolling Sharpe, GICS sector concentration, realised-YTD tax vs the bracket), and paper trading.
- Speed: a daily precompute writes cached signals, backtests, and portfolio equity; the dashboard reads them in single-digit milliseconds, and only recomputes live on a cache miss or non-default parameters. Heavy work runs off the event loop so the UI never blocks; failures surface as toasts, not blank panels.
- ML research pipeline: a separate offline track
(
ml_dataset → ml_select → ml_backtest → ml_holdout → ml_risk) running on system Python with scikit-learn / LightGBM. Walk-forward, embargoed, hold-out-validated — and deliberately not wired into the app, because the study showed no deployable edge. - Paper trading: a weekday paper trader runs the live strategy against a virtual book and grades itself forward against the index — an information ratio and tracking error (annualised active return per unit of active risk) on the Paper Trade tab — and pushes a daily alert (via Apprise — ntfy/Telegram/email) whenever the buy/sell set changes.
- Testing and review: a 108-test suite, a websocket auth gate, and a four-batch internal review (correctness / safety / performance / features) — each fix verified, including the survivorship and look-ahead corrections above.
- Deployment: self-hosted on a 3-node Raspberry Pi 5 k3s (Kubernetes) cluster, with daily data jobs, alerts, and backups. No cloud, nothing leaves my network.
What's next
I'm not storming into wealth — but I have real learnings, a working, well-validated backtesting engine, and an ML pipeline that genuinely ranks stocks even if it can't beat the index risk-adjusted. The definitive out-of-sample test — forward paper trading, graded by information ratio against the index — is already running; it now needs time to accumulate a meaningful record, not more building. Beyond that, the earlier reads agree: this signal is tapped out, so real upside would mean a different one — plus point-in-time fundamentals (SEC EDGAR filing dates) if the ML sleeve is ever pursued. That fits me well as a long-horizon investor and builder.