Cross-sectional factor investing
Everything so far predicted one series through time: will this asset go up tomorrow? This chapter rotates the problem ninety degrees. Cross-sectional investing asks a different question at each date: of these hundred assets, which oneswill beat the others? The tools change with the question — portfolio sorts replace signal thresholds, and the Fama–MacBeth two-pass regression replaces the single time-series fit — but the statistical skepticism of the gauntlet transfers intact.
A time-series trader needs to know whether Bitcoin rises next week — a hard, low-signal call on one noisy series. A cross-sectional trader never makes that call. They rank 100 stocks by some characteristic — last year’s return, cheapness, profitability — buy the top slice, short the bottom slice, and let the market leg cancel out. Whether the whole market rises or crashes is (to first order) irrelevant: the bet is purely that the ranking carries information about relative returns. That is a statistically easier game: instead of one observation per period you get a whole cross-section of them, and instead of predicting a level you only need to predict an ordering. The price of admission is a new inference machinery — because now the noise is correlated across assets within each period, and naive t-stats lie.
Time-series vs cross-sectional prediction#
Formally, a time-series signal is a map from an asset’s own history to its expected return, . A cross-sectional signal is a characteristic — one number per asset per date — that predicts returns relative to the cross-sectional average:
The coefficient is the factor premium: the extra return earned per unit of the characteristic. A long-short portfolio built from the ranking earns times the spread in it holds, with the market component differenced away. This is the same alpha-vs-beta split as regression & factor models — but run across assets at a fixed daterather than across dates for a fixed asset. The CAPM’s security market line was already a cross-sectional prediction: expected return linear in beta, across assets.

Portfolio sorts: deciles and the long-short spread#
The workhorse test is embarrassingly simple. At each rebalance date, rank all assets by the characteristic, split them into deciles (D1 = lowest, D10 = highest), and hold each decile as an equal-weight portfolio until the next rebalance. If the characteristic prices the cross-section, average returns should rise monotonically across deciles, and the long-short spread
should have a positive mean. The spread portfolio is (roughly) market-neutral by construction — both legs hold the same market exposure with opposite signs — so its mean is a direct estimate of the premium, uncontaminated by whether the index happened to rally over the sample.

The factor zoo#
Four families of characteristics have survived decades of out-of-sample scrutiny — and hundreds more have been published and quietly died. The survivors:
- Momentum— the return from twelve months ago to one month ago (the “12-1” convention skips the last month, which reverses). Winners keep winning for 3–12 months. The premium is strong but crashes violently when the market snaps back (momentum is short the rebound).
- Value — cheap on fundamentals (book-to-market, earnings yield) beats expensive. Slow-moving, decade-scale cycles; the natural diversifier to momentum (their correlation is negative).
- Quality — profitable, low-accrual, conservatively financed firms beat junk. The premium is modest but stable, and pays most in downturns.
- Low volatility — boring low-beta assets earn more per unit of risk than the SML says they should (the flat-SML anomaly), plausibly because leverage-constrained investors overpay for lottery-like high-beta names.
Fama–MacBeth: the two-pass regression#
Sorts show a premium; Fama–MacBeth measures it with standard errors that respect the panel structure. The insight: run one cross-sectional regression per period, then treat the sequence of estimated slopes as a time series and test its mean.
Pass 1 (each period). At each date , regress the realised returns on the lagged characteristics across assets:
giving one slope estimate per period — the realised reward, that period, for holding a unit of the characteristic. Pass 2 (across periods). The premium is the time-average of the slopes, and — the elegant part — its standard error comes from the time-series dispersion of the estimates themselves:
Why this fixes the panel problem. A pooled regression on all observations would count each period’s stocks as independent draws — but stocks move together within a period, so the effective sample size is closer to than . Fama–MacBeth builds this in automatically: whatever cross-sectional correlation exists inside a period is swallowed into that period’s single , and the inference runs on honest observations. Each is also exactly the return of a tradable zero-cost portfolio with unit exposure to — the regression is a backtest in disguise.

Why Newey–West errors matter#
The formula assumes the period slopes are serially uncorrelated. They rarely are: premia trend and crash (momentum especially), characteristics overlap across formation windows, and volatility clusters. Positive autocorrelation makes the naive standard error too small — the same disease that afflicted plain OLS t-stats in regression & factor models. The repair is the Newey–West (HAC) estimator, which inflates the variance of the mean with a weighted sum of autocovariances:
where is the lag- autocovariance of the series and the Bartlett weights keep the estimate positive. edgekit’s ek.factors.fama_macbethreports Newey–West t-stats by default; ek.factors.newey_west exposes the HAC standard error of any mean directly.
import edgekit as ek
# returns: T×N DataFrame of asset returns
# mom: T×N DataFrame of the 12-1 momentum characteristic,
# ALREADY lagged — row t must be knowable before period t's return
fm = ek.factors.fama_macbeth(returns, {"momentum": mom})
fm["premia"] # time-average slope per characteristic (the premium)
fm["t_nw"] # Newey-West t-stat on each premium
fm["by_period"] # the full T-length series of period slopes
# the HAC machinery on its own — se of the mean of any series:
se = ek.factors.newey_west(fm["by_period"]["momentum"], lags=6)Take 100 stocks, 60 months. Each month, rank by the 12-1 return, form deciles, and record forward returns. Suppose the sort delivers D10 averaging /month and D1 averaging /month — a long-short spread of /month, roughly /year before costs. Now the inference. Fama–MacBeth on the standardised characteristic gives period slopes averaging with a standard deviation of across the 60 months:
Just significant. But the slope series has lag-1 autocorrelation of — momentum premia trend — and Newey–West with 6 lags inflates the standard error to , dragging the t-stat to . The premium is plausible but unproven on this sample: sixty months is simply not much evidence about a noisy, autocorrelated premium, no matter how wide the panel is. That is the recurring lesson of the two-pass logic — the cross-section buys you precision within a period, but the number of periods still governs what you can claim.
The tradable version is ek.strategy.CsMomentum: rank on the lagged 12-1 return, long the top fraction, short the bottom, rebalance monthly, with all weights shifted so no future information leaks in.
import edgekit as ek
strat = ek.strategy.CsMomentum(lookback=126, skip=21,
top_frac=0.2, rebalance="ME")
out = strat.backtest(prices) # prices: T×N DataFrame
out["weights"] # T×N weights, causal (shifted after formation)
out["returns"] # long-short portfolio return series
# then treat it like any strategy: gauntlet first, sizing second
sr = ek.metrics.sharpe(out["returns"], periods_per_year=252)Data-snooping in the factor zoo#
Hundreds of published factors, thousands of unpublished ones — and every one of them was found by researchers running sorts and Fama–MacBeth regressions on largely the same historical panel. That is a textbook multiple-testing problem: test 300 candidate characteristics at the bar and you expect a dozen “discoveries” from pure noise.
| Claim | The snooping failure mode | Defence |
|---|---|---|
| A new factor works | 300 candidates tested, winners published | Raise the bar: |t| > 3, not 2 (see multiple testing) |
| Premium is stable | Formation window tuned in-sample | Out-of-sample and post-publication data only |
| Spread is tradable | D1 leg lives in illiquid micro-caps | Cost model + capacity check before believing it |
| Factor is 'new' | Correlated 0.9 with value or momentum | Regress it on the survivors; test the residual |
Next:cross-sectional logic asks “which asset”; the event study asks “what happens after a defined trigger” — and how to test it without fooling yourself. Event studies & seasonality.


