Jakobsen Analytics
← all writing Get in touch

Flagship · Quantitative data product

Beating the market is hard — notes from building a dividend-strategy backtester

A methodology write-up — this time with the numbers, the charts, and a little of the code. It is less about the strategy than about how I made the simulation report results honestly, and what happened when I went looking hard for an edge.

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.

Raw backtest today's 503 survivors 251% Survivorship-free point-in-time universe 137% + costs · FX · tax · exits the honest number 85%
Where the headline went. 2020-start backtest. Survivorship bias alone accounted for ≈114 percentage points; slippage, FX, Danish tax and delisting exits took the rest. The headline didn't just shrink — the conclusion changed.

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:

Single-pool portfolio backtest · survivorship-free · 2015–2026 · net of all costs and Danish tax
MetricStrategyS&P 500 TR
Total return+189% (after-tax)+340% (pre-tax)
— after-tax, fair+192%
— after-tax, single-lot+172%
CAGR9.7%~14%
Sharpe0.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:

Edge-hunt · baseline 189% / Sharpe 0.25 / maxDD −43%
LeverWhat happenedVerdict
Stop-lossSells at the bottom of the dip it just bought (105% / 0.10)Reject
Sector capNon-monotonic noise across the grid (163–235%)Reject
200-day MA filterRemoves the very dip-buys the strategy is built on (145% / 0.20)Reject
Cash yield on idle+2pp; the book is already ~98% investedNegligible
Deployment multiplierNo gain; capital is already deployedReject
Position count (N)Trades drawdown for concentration; no Sharpe gainReject

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:

RETURN SHARPE ML ranker the model +125.5% 0.72 S&P 500 TR buy & hold +106.2% 1.52 Yield rule incumbent +38.1% 0.36 Random control +27.5% 0.19
Hold-out 2023–2026, on data never used for tuning. The skill survives: model ≫ yield rule ≫ random. On return the model beats the index (+19pp). But on risk-adjusted return the index wins decisively — Sharpe 1.52 vs 0.72, at half the drawdown. The model buys more return with more risk.

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

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.

← all writing emil@jakobsenanalytics.dk · LinkedIn