Does Portfolio Construction Rescue a Weak Signal?
Twenty-four ways to build the same book
50 signals 1,200 backtested cells 4 rankings × 2 winsorizations × 3 sizings 2000–2024, monthly
The question
The shared harness answers does this signal predict returns? — and it answers it with one construction wired in. Every Sharpe on the main farming page is really a statement about a signal and a way of turning that signal into positions.
So: of the number you read off the leaderboard, how much is the signal and how much is the construction? Take the 50 highest raw-Sharpe cross-sectional signals in the library, and rebuild each one 24 ways.
| Axis | Variants | What it changes |
|---|---|---|
| Ranking | raw / cross-sectional rank / z-score / sector-neutral z | the score that orders the cross-section |
| Winsorization | off / 1% tails | whether extreme scores are clipped first |
| Sizing | flat / proportional / Kelly | how the score becomes a position weight |
Two invariants make the cells comparable. Every cell normalises gross exposure to 1 per leg, so sizing schemes redistribute weight and never lever the book — otherwise Kelly would win or lose on leverage rather than on construction. And nothing looks forward: ranking uses only the current cross-section, the Kelly variance is trailing realised volatility, and the strategy-level overlay further down estimates its parameters from an expanding window strictly before the month it sizes.
Finding 1: three of the four rankings cannot matter under flat weights
Raw scores, cross-sectional ranks and z-scores are monotone transforms of one another. They order the cross-section identically, so they select the identical decile — and if every selected name gets the same weight, they must produce the identical return, to the last decimal. The transform only starts to matter once the weights depend on the shape of the score.
That is a claim worth testing rather than asserting. Across all 50 signals, 44 produce bit-identical streams for the raw / rank / z-score cells under flat sizing. The remaining 6 differ by at most 0.0013 per month — floating-point noise reordering names that sit exactly tied on the decile boundary. Across winsorization levels the difference is a slightly larger 0.0027, because clipping creates the ties that do the reordering.
Six of the twenty-four cells, then, are the same experiment wearing different labels. The grid reports them rather than quietly dropping them, because a reader comparing “four ranking methods” deserves to know that three of them coincide wherever weights are flat.
Finding 2: sizing is the only axis that moves anything — downward

| Sizing | Median ΔSharpe | Mean ΔSharpe | Beats flat |
|---|---|---|---|
| Flat | +0.000 | +0.000 | 0/50 |
| Proportional | +0.002 | -0.000 | 27/50 |
| Kelly | -0.113 | -0.119 | 6/50 |
Proportional weighting is a coin flip: median +0.002 Sharpe against the flat baseline, better on 27 of 50 signals. Kelly is not a coin flip. It costs -0.113 Sharpe at the median and beats flat weights on only 6 of 50 signals.
The mechanism is visible in the components. Going from flat to Kelly weights cuts median annualised volatility from 15.2% to 12.4% — it does de-risk, as advertised. But it takes the return with it, from 1.34% to 0.01% a year. Dividing conviction by trailing variance piles weight onto the lowest-volatility names in each decile, and on this library those names carry proportionally less of the edge than of the risk.


Finding 3: winsorization is a coin flip, not a lever

| Ranking | Flat | Proportional | Kelly |
|---|---|---|---|
| Raw score | +0.000 | -0.001 | -0.000 |
| Cross-sectional rank | +0.000 | -0.000 | +0.000 |
| Z-score | +0.000 | -0.001 | -0.000 |
| Sector-neutral z | +0.006 | -0.006 | -0.000 |
The largest median effect anywhere in the table is 0.0058 Sharpe. Under flat weights that is exactly what theory predicts — clipping the tails of a score barely disturbs a ranking, and a ranking is all flat weights consume. The surprise is that the median stays negligible under proportional and Kelly weights too, where clipping genuinely changes the numbers that become weights. Deciles are already a blunt instrument: by the time a score has been bucketed into the top and bottom tenth, the outliers have been absorbed into their buckets and there is little left for winsorization to do.
The box plot adds the part the medians hide. Under flat weights the effect really is nil — those boxes are flat lines on zero. Under proportional and Kelly weights the median is still zero but the distribution is wide: the middle half of those cells lands between -0.010 and 0.009 Sharpe, 27% move by more than 0.05, and the largest single swing among them is 0.36. Winsorization improves 46% of them — a coin flip.
So it is not a free lever and not a safe default either; it is a source of variance with no expected return attached. Flip it on any individual backtest and you have a one-in-four chance of moving the Sharpe by more than 0.05 in a direction you cannot predict beforehand. That is worth knowing precisely because it is the kind of knob that gets tuned quietly, and then reported as though it were free.
Finding 4: the ranking transform barely registers
| Ranking | Median ΔSharpe vs baseline | Beats baseline |
|---|---|---|
| Raw score | +0.000 | 26/50 |
| Cross-sectional rank | +0.002 | 27/50 |
| Z-score | +0.000 | 26/50 |
| Sector-neutral z | -0.009 | 23/50 |
Measured under proportional sizing — the setting where the transform actually reaches the weights — every ranking method lands within a hundredth of a Sharpe point of the baseline. Sector-neutral scoring, the one transform that genuinely changes decile membership rather than just rescaling the score, is the weakest of the four at the median.
Which sits oddly against the heatmap, where the best cell in the whole grid is sector-neutral z scoring, unwinsorized, proportional weights at a median Sharpe of 0.212 — comfortably above the 0.187 baseline. Both numbers are correct, and the gap between them is the point. The heatmap compares a difference of medians: the median signal under one construction against the median signal under another, with no requirement that they be the same signal. The table above compares a median of differences: what the change does to each signal, paired. That same cell has a median paired effect of -0.017 and improves only 24 of 50 signals. It looks best in aggregate because it flatters a handful of signals enormously while quietly hurting slightly more than half of them. Aggregate league tables of construction settings are built from the first number; the decision you actually face is the second.
Finding 5: Kelly asks for leverage nobody would take
The sizing axis above applies Kelly within the cross-section. The other reading of Kelly is at the strategy level: size the whole book by f* = mu / sigma^2, re-estimated each month on an expanding window. Applied to gross profitability, the strongest signal in the library:
| Variant | Sharpe | Ann. return | Ann. vol | Max drawdown | Median leverage |
|---|---|---|---|---|---|
| Unlevered (1x) | 0.921 | 11.1% | 12.3% | -21.4% | — |
| Half Kelly | 0.810 | 24.2% | 34.6% | -54.2% | 3.0x |
| Full Kelly | 0.810 | 24.2% | 34.6% | -54.2% | 3.0x |

Full and half Kelly produce identical results, which looks like a bug and is not. Uncapped, the formula asks for a median of 13x leverage: Kelly divides by variance, and the variance of a monthly market-neutral book is tiny, so f* explodes. Half of 13x is still 7x. Both sit far above any tolerable limit, so both bind at the 3x cap in almost every month and the “full versus half Kelly” distinction dissolves. The cap, not the formula, is what sizes the book — which is worth knowing before quoting Kelly as though it were a parameter-free answer.
What the cap does deliver is the classic Kelly trade: annual return roughly doubles, volatility roughly doubles with it, and maximum drawdown deepens from -21.4% to -54.2%. Sharpe falls, from 0.921 to 0.810, because leverage forced through a cap is no longer proportional to the edge. Kelly maximises long-run growth, not Sharpe — but you have to actually want that drawdown, and you have to fund 3x on a strategy whose edge was estimated on the same history you are levering against.
The best cell is a mirage
Pick the best of 24 cells for each signal and the improvements look substantial — the median signal spans 0.339 Sharpe between its worst and best construction, wider than the median signal’s entire Sharpe of 0.187. It is tempting to read that as 0.339 of available upside.
It is not. No cell wins reliably:
| Ranking | Winsorization | Sizing | Times best |
|---|---|---|---|
| Raw score | none | Proportional | 10/50 |
| Sector-neutral z | none | Proportional | 9/50 |
| Sector-neutral z | 1% winsorized | Proportional | 5/50 |
| Sector-neutral z | 1% winsorized | Flat | 4/50 |
| Z-score | none | Proportional | 4/50 |
| Raw score | none | Flat | 4/50 |
The most frequent winner takes the crown on 10 of 50 signals — barely better than the 4% you would expect from drawing cells at random. The spread is real; the choosability is not. Picking the best construction per signal after the fact is the same mining that inflates a raw-Sharpe leaderboard, one level down. The honest summary of this grid is the marginal column: flat weights, any monotone ranking, winsorization optional.
What this does not show. The 50 signals here were chosen by raw Sharpe, so the study is conditional on a signal already looking good — it describes what construction does to apparent winners, not to signals in general, and those winners carry their own selection bias from the farming search. Everything is monthly rebalancing on one US equity panel with flat 10bp costs; the Kelly variance input is trailing total volatility rather than an idiosyncratic estimate; and the gross-exposure normalisation deliberately removes leverage as a channel. A practitioner who sizes by conviction and levers accordingly is running a different experiment from this one.
Reproduce
python scripts/run_construction_grid.py --top 50 # ~12 min, writes runs/construction/
python -c "from llm_alpha.reporting.construction_page import build; build()"The grid evaluator is src/llm_alpha/backtest/construction.py; it computes all 24 cells in a single pass over the dates, so a full sweep costs about one backtest per signal rather than twenty-four.