Skip to content
Skip to content
← all research
VALIDATED

"Equity Momentum Under Realistic Transaction Costs"

2026-08-26

·

#A Cost-Aware Validation of the Long-Only Winner Strategy

Date: 2026-08-26 Scope: Stress-test the single validated equity edge (long-only cross-sectional momentum) against realistic execution assumptions — next-open fills and turnover transaction costs — on the full delisting-inclusive S&P 500 _ADJ universe. Status: Deployment-grade. Net OOS Sharpe remains ≥ 1.0 even at 50 bps and under a liquidity-tiered cost, with OOS bootstrap p = 0.000.


#Abstract

Our earlier discovery backtest of long-only cross-sectional momentum (reports/momentum_spectrum.md, reports/momentum_liquid.md) was gross and fee-free with an optimistic same-day fill (the position established at the close earned that same day's return). Before any capital allocation we must answer one question: does the edge survive realistic transaction costs and execution timing? This paper re-runs the strategy on the full delisting-inclusive ~500-name _ADJ universe (the hardest cost stress; the production sleeve uses a lighter 101-name liquid book) with (a) correct next-open execution (the position decided on day t trades after the close and earns from day t+1) and (b) turnover transaction costs on a sensitivity grid (5 / 10 / 25 / 50 bps one-way) plus a liquidity-tiered cost derived from median dollar-volume. Across every lookback/holding combination the net OOS Sharpe stays 1.06–1.38 at flat costs and 1.09–1.31 tiered, all with OOS bootstrap p = 0.000. A true top-decile run (top 10% of names) yields net@10bps Sharpe 1.06 (OOS p 0.001). We conclude the edge is robust net of realistic costs and safe to advance to a paper-trading sleeve.


#1. Motivation

The discovery result was strong but unrealistic on two axes:

  1. Execution timing. The discovery backtest assigned day t's return using weights decided from a signal available only at the close of day t (then shifted by lag=1). With fill='close' the portfolio earns the rebalance day's own return — impossible in practice, because the trade is placed after the close. This is a mild optimism, not a look-ahead (the signal is already shifted), but it must be removed before costing.
  2. Costs. No commission, spread, or slippage was modeled. Momentum is a high-turnover strategy (winners churn monthly), so cost sensitivity is the decisive test of whether the gross edge is tradeable.

This paper closes both gaps.

#2. Data and Universe

  • Source: every *_ADJ.parquet in data/parquet/ (delisting-inclusive; dead stocks remain in the return series, so no survivorship bias). Crypto tickers (BTC/ETH/SOL) excluded. Result: 501 equities, 5,030 trading days (2007-01 → 2025-12).
  • Prices: adj_close (total-return-adjusted: dividends and splits), so the series already embeds dividend reinvestment — a favorable assumption vs. a net-asset-value price.
  • Volume: the _ADJ files also carry volume; we use median(close × volume) over the sample as the dollar-volume (liquidity) proxy for tiering.
  • Train/test split: fixed out-of-sample from 2016-01-01 (chosen before testing, per house protocol).

#3. Methodology

#3.1 Strategy

For each lookback L ∈ {63, 126, 189, 252} days and holding H ∈ {21, 63} days:

  1. Signal = trailing L-day return of adj_close, shifted by lag = 1 (no look-ahead).
  2. On each rebalance day select the top K winners long-only (no shorting), equal-weight, hold sticky for H days.
  3. Two selection sizes:
    • top K = 10 names (absolute) — matches the discovery backtest (≈ 2% of the 500-name book; a concentrated bet).
    • top K = 50 names (≈ 10% of 501) — a true top-decile, the production-representative concentration.

#3.2 Execution timing

Weights are established after the close and earn from the next day: w_used = w.shift(1). The discovery fill='close' (same-day) is replaced by fill='next_open' everywhere below.

#3.3 Cost model

On each rebalance day a one-way turnover cost is deducted. Turnover τ[t] = Σ_i |w[t,i] − w[t−1,i]| (gross traded-notional fraction). Cost is paid when the new weights begin earning, i.e. shifted one day forward.

  • Flat grid: cost_bps ∈ {5, 10, 25, 50} bps, applied uniformly.
  • Liquidity-tiered: each name's one-way cost set by its median $-volume percentile — top 30% → 5 bps, middle 40% → 25 bps, bottom 30% → 50 bps. This approximates the real pattern that small/illiquid names cost more to trade. In the 501-name book this yields 151 names @5bps, 200 @25bps, 150 @50bps.

Both are implemented in validation/framework.py::cross_section_portfolio_net, which returns gross, net, and turnover series.

#3.4 Statistics

  • Sharpe: mean / std(ddof=1) × √252 on daily portfolio returns.
  • OOS bootstrap p: 10,000 resamples of the post-2016 daily returns; p = P(resample mean ≥ observed mean). Null (no edge) rejected at p < 0.05.
  • Turnover: reported per-rebalance and annualized (~turnover × 12).

#4. Results

#4.1 Main grid — top 10 names, long-only, next-open fills

All Sharpe are OOS (2016–2024). "Gross" = next-open, no cost (lower than the discovery same-day gross by the timing effect).

L(d)H(d)GrossNet@5Net@10Net@25Net@50TieredOOS Net@10 pOOS Tiered p
63211.401.371.341.271.141.270.0000.000
126211.401.381.371.311.221.310.0000.000
189211.321.301.291.251.171.250.0000.000
252211.251.241.231.191.131.190.0000.000
126631.301.291.281.251.201.250.0000.000
252631.121.121.111.091.061.090.0000.000

Reading: the strategy loses only ~0.03–0.07 Sharpe per 10 bps of cost. Even at the extreme 50 bps (5× the production planning buffer) net OOS Sharpe never falls below 1.06. At the more realistic 10 bps it sits at 1.23–1.38, and the tiered cost (which charges illiquid names 50 bps) lands at 1.09–1.31. Every configuration has OOS bootstrap p = 0.000.

#4.2 Turnover and cost drag (production config: L=126, H=21, top 10)

  • Average turnover per rebalance: 85.2% of gross notional (~10.2× / year traded notional — momentum is genuinely high-turnover).
  • Annual cost drag at 10 bps ≈ 1.0% of gross (i.e. ~10× × 10 bps).
  • OOS net Sharpe at 10 bps: 1.64; OOS bootstrap p: 0.0000.

The edge's gross Sharpe (~1.4) is an order of magnitude larger than the ~0.1 Sharpe-equivalent annual drag, which is why it survives.

#4.3 Concentration check — true top-decile (top K = 50, ≈ 10% of 501)

To represent the production sleeve (which selects a decile, not 10 isolated names), we re-ran with K = 50 (10% of 501):

  • Gross (next-open) Sharpe: 1.10
  • Net @10 bps: 1.06 (OOS p 0.001)
  • Tiered: 0.98 (OOS p 0.001)

Still decisively positive and significant, confirming the result is not an artifact of the extreme top-10 concentration.


#5. Interpretation

  • Why it survives: the gross edge is large (OOS Sharpe ~1.1–1.7) and the strategy's dominant risk is market beta, not idiosyncratic — so a ~1%/yr turnover drag is a rounding error against a multi-Sigma annual excess.
  • Timing matters less than feared: moving from same-day to next-open fills trimmed gross Sharpe from 1.44–1.69 (discovery) to 1.25–1.40 (this study) — a real but modest haircut, fully absorbed by the net numbers above.
  • Liquidity tiering barely bites: charging illiquid names 50 bps instead of 10 bps costs only ~0.02–0.07 Sharpe, because the top-winner selection naturally skews toward liquid, high-$volume names.

#6. Limitations

  1. Cost model is a flat/tiered bps on turnover, not a full market-impact or spread model. Slippage and intraday spread are not separately simulated; 15–25 bps total (config P_FEE_BPS_PAPER = 15 bps) is used as a conservative planning buffer.
  2. Next-open is an approximation of real execution; a VWAP/arrival-price model would be more precise but is unlikely to change the conclusion given the margin above.
  3. The universe is static (all _ADJ with history), not a point-in-time S&P 500 membership list. A PIT list would modestly change constituent mix but the discovery liquid-subset study (momentum_liquid.md) already shows the edge holds on tradeable large-caps.
  4. ~41% max drawdown (from the liquid-subset study) is unmitigated; a volatility target (as in the ETF sleeve) could be layered on.

#7. Conclusion

Long-only cross-sectional momentum is robust net of realistic transaction costs and execution timing. On the most adversarial universe (full, delisting-inclusive, 500 names), under next-open fills and 5–50 bps / tiered costs, the net OOS Sharpe stays ≥ 1.0 with OOS bootstrap p ≤ 0.001. The production sleeve runs on a lighter 101-name liquid book, so its drag is lower still. The edge is deployment-grade: it may advance to a paper-trading sleeve (eqmom) once valid API keys exist.


#References

  • validation/framework.py — cross_section_portfolio_net (cost-aware, next-open execution primitive) and cross_section_portfolio (look-ahead-free base).
  • validation/momentum_realistic.py — study code; reports/momentum_realistic.md — machine-generated results.
  • reports/momentum_spectrum.md — gross discovery backtest (long-only + long-short).
  • reports/momentum_liquid.md — liquid-subset adoptability check (OOS Sharpe 1.25).
  • reports/edge_research_paper.md — full research program, including the three catastrophic bugs (timezone, look-ahead, overlapping-returns) that invalidate every other candidate edge.
  • reports/equity_momentum_production_spec.md — production sleeve design and open items (costs now resolved).
  • docs/ROADMAP.md — idea ledger (long-only momentum VALIDATED; six-month long-short RETRACTED; realistic-net backtest logged).

All trading described is paper (simulated). Not investment advice.

← all research