Selected work
Live

RankAlpha

Built the ML, then beat it. A cross-sectional equity ranker that learns to order the S&P 500 — then a pre-registered duel showed plain 12-1 momentum wins the long-only product book (after-cost Sharpe 1.79 vs 1.41), so momentum ships and the ML stays research. The model's ordering skill is real, but it only looks monetisable long/short. Built on a SEC point-in-time fundamentals gate, a 380+ test suite, and published negative results.

  • Python
  • LightGBM · LambdaMART
  • Learning-to-rank
  • Walk-forward + embargo
  • Pre-registered duel
  • SEC EDGAR point-in-time
  • SHAP
After-cost Sharpe — momentum vs ML
1.79 vs 1.41
ML mean Rank IC (vs momentum)
0.0505 vs 0.0414 (t=1.64)
Audit machinery
380+ tests · SEC point-in-time

What it is

RankAlpha ranks the S&P 500 every month, trades the top and bottom of that ranking as a market-neutral book, and turns the result into a risk-managed, self-explaining portfolio. The key word is ranking: it doesn't predict prices, it predicts relative order — which stocks will outperform their peers — because that's the only thing the strategy actually uses.

The trap I escaped first

The first version did the obvious thing: predict a single stock's direction from its own technical features. It posted a Sharpe of 0.72 — until I audited it. Most of that edge was look-ahead leakage: training labels that quietly peeked into the test window. After purging the leakage and charging realistic costs, accuracy fell to ~0.50 — a coin flip.

That dead end was the most useful result in the project. Single-stock direction is dominated by market-wide noise nobody can predict. The honest failure is what forced the real design.

The cross-sectional pivot

Instead of "will this stock go up?" the question became "will it outperform the other 499?". You rank all the stocks each month, go long the top decile and short the bottom, and size the two legs equally so the market's move cancels out. What's left is the part that's actually about the individual stock — relative strength — which is where systematic equity edge tends to live.

The model

The ranker is LightGBM with a learning-to-rank objective (LambdaMART) — it optimises the ordering of stocks directly, not each stock's return. Seven price-based factors (momentum, volatility, size, reversal, liquidity), normalised cross-sectionally within each day. It's validated with walk-forward retraining plus a 21-day embargo, so no training label can overlap the test window — the same leakage discipline the first version failed.

The bar it had to clear was a dumb baseline: plain 12-month momentum, no learning. It cleared it — modestly and honestly.

After-cost Sharpe — same out-of-sample window
Learned ranker
1.14
Momentum baseline
0.82

A modest, honestly-measured edge of +0.32 Sharpe over a plain-momentum baseline, after 10 bps/side costs. Not yet statistically significant (Rank IC t = 1.64) and untested through a momentum crash — the believability is the point, not the headline.

What it actually learned — and a mistake I caught

Feature importance said volatility dominated, and I first wrote that up as "the model discovered the low-volatility anomaly." That was wrong, and checking the SHAP direction caught it: the long picks have high volatility with positive contribution — it tilts toward higher-vol names paired with momentum, the opposite of low-vol.

The lesson is worth more than the result: importance tells you which feature matters, never which direction. You confirm the sign before you name the effect.

What the model leans on — LightGBM gain %
6-month volatility
59.7%
Size (log price)
14%
12–1 momentum
10.3%
6-month momentum
6.7%
3-month momentum
5.8%
1-month reversal
2.9%
Liquidity
0.4%

Volatility dominates by importance — but SHAP direction shows the long book tilts toward higher-volatility names paired with momentum, not the low-volatility anomaly. Importance tells you which feature matters, never which direction.

Does the ranking hold?

The real test isn't the headline Sharpe — it's whether stocks the model ranks high actually outperform. Sort by the model's score and look at what happened next month: the returns climb cleanly through the top deciles. The top decile is the strongest; the bottom (short) decile is the weak spot, so the edge leans long.

Mean next-month return by model-score decile
1.12%
0
0.67%
1
1.19%
2
0.97%
3
1.07%
4
1.30%
5
1.48%
6
1.55%
7
2.39%
8
4.04%
9

Sorted by the model's score (known in advance), then measured against realised forward returns. The clean rise from decile 5 → 9 is the real signal; the short leg (decile 0) still sits slightly high, so the edge is concentrated on the long side.

The duel — I built the ML, then beat it

Ranking skill is not the same thing as a tradeable edge, so I pre-registered a decision rule before running the test: the ML earns its keep only if it beats plain 12-1 momentum on after-cost Sharpe over the full window AND wins Rank IC in more than half the calendar years. Ties or momentum wins ⇒ trade momentum, keep the ML as research. The rule is applied by code as arithmetic, so no judgement is exercised after seeing the numbers.

Both books run through the identical harness — same universe, same dates, same long-only top-N, same inverse-vol sizing, same weight cap, same vol target, same 10 bps/side cost. Only the score differs.

Over 47 monthly rebalances, momentum won:

  • —Momentum (12-1) — +23.93% annualised, Sharpe 1.792, Sortino 3.32, max drawdown −12.03%, mean Rank IC 0.0414 (t = 1.58).
  • —Frozen ML — +20.62% annualised, Sharpe 1.407, Sortino 2.44, max drawdown −15.20%, mean Rank IC 0.0505 (t = 1.64).

The split is the interesting part. The ML has the better Rank IC — its ordering skill is real, and slightly better than momentum's. It still loses the book, because the long-only top-decile product only harvests one tail of that ordering, and momentum's tail happens to be fatter and cheaper to trade. The ML's advantage lives across the whole cross-section, which is a long/short shape, not a long-only one.

So the product ships momentum, and the ML stays research with a specific open question rather than a vague hope.

The duel harness is lab/signal_duel.py, its generated report is figures/lab/signal_duel.md, and the whole system is documented in ARCHITECTURE.md.

From signal to portfolio

The score becomes an actual portfolio: long-only top picks, weighted inverse to volatility, capped per name, and scaled to a target volatility with the remainder held as cash. Every holding carries the factor reasons that put it there, and a local LLM turns those reasons into plain English — fed only the model's real numbers, so it explains rather than invents. It's framed throughout as an educational simulation, never investment advice, and it quotes no return probabilities.

Forward paper-trading

To test it honestly, I froze the model and traded it forward — no refitting with hindsight — for 23 months. Against an equal-weight benchmark it landed at Sharpe 1.25 vs 1.21, with realised volatility right on the 14% target, so the risk control is well-calibrated even where the edge is thin.

Honest limitations