Skip to content
Skip to content
← all research
REJECTED

"Edge proposal: PEAD as a sizing overlay on equity-momentum (EqMom)"

2026-09-05

·

Date: 2026-09-04 Status: Research proposal (pre-registered before any implementation). Predecessor: edge_hunt15 PEAD x Momentum double-sort (REJECT, reports/edge_hunt15.md). This proposal is a different mechanism — sizing overlay, not universe filter.

#Background and motivation

The validated equity-momentum edge (reports/momentum_spectrum.md, momentum_liquid.md): long-only top-decile 6-month winners, OOS Sharpe 1.25-1.69 on the liquid subset, bootstrap p<0.001. PEAD (post-earnings drift, edge_hunt7.md, edge_hunt10.md): long top-quintile of recent earnings surprises, OOS Sharpe 1.04, p=0.001.

Both edges are live in production (eqmom and pead bots). They run independently: each sleeve has its own deploy fraction, and the combined-book allocator (production/book_allocator.py) sets relative weights at the book level. There is no per-name meta-signal: a name can be in the EqMom top decile AND have an active PEAD signal, but the two sleeves don't know about each other.

The rejected edge_hunt15 PEAD x Momentum double-sort filtered the universe to names that were in BOTH top deciles. It was REJECTED because the mom filter diluted PEAD's earnings signal and the filtered universe had only 12-19 names, too sparse to be significant. This proposal is structurally different: it KEEPS the EqMom top decile (101 names) and modulates position size based on whether each name has an active PEAD signal.

#Mechanism (pre-registered, no re-grids)

Inputs (per rebalance date D):

  • eqmom_target[D] ∈ {0, 1}: the EqMom top-decile set (existing long-only momentum, validated 1.06-1.25 Sharpe)
  • pead_active[D, sym] ∈ {0, 1}: 1 if the symbol sym had a positive earnings surprise in the trailing 60 calendar days (matches the production PEAD sleeve's hold window)
  • pead_surprise[D, sym] ∈ ℝ: the magnitude of the most recent positive surprise (in percent; from data/earnings_dates.parquet)

Sizing multiplier s[D, sym] ∈ {0.75, 1.0, 1.25}:

  • If pead_active[D, sym] == 1 and pead_surprise[D, sym] >= 5.0: both signals agree strongly → s = 1.25 (boost 25%)
  • If pead_active[D, sym] == 1 and pead_surprise[D, sym] < 5.0: both signals agree weakly → s = 1.0 (passthrough, since PEAD-with-positive-surprise is already in EqMom and shouldn't change the conviction)
  • If pead_active[D, sym] == 0 AND the symbol had a NEGATIVE earnings surprise in the trailing 60 days → conflict between momentum (EqMom says long) and earnings (recent miss) → s = 0.75 (reduce 25%)
  • If pead_active[D, sym] == 0 AND no earnings event in trailing 60 days → no information → s = 1.0 (passthrough)

The bucket logic is conditional on EqMom's signal, not on PEAD's: PEAD only modulates the size of names that EqMom already wants to hold. This avoids contaminating the EqMom signal with PEAD's universe (which is a smaller, more earnings-active subset).

effective_position[D, sym] = eqmom_base_size * s[D, sym]

#Hypotheses

The expected mechanism is information-theoretic: PEAD and EqMom have correlation 0.39 (per edge_hunt15.md), which is moderate (not zero, not high). When the two signals agree, the joint information is stronger than either alone; when they disagree, the signal is mixed and the position should be smaller. The asymmetry (1.25 boost on strong agreement, 0.75 on conflict) is heuristic, not optimized.

#Validation protocol (pre-registered)

  • Universe: top-100 by listing persistence (the liquid subset from reports/momentum_liquid.md)
  • Window: 2016-01-01..2026-07-31 (the existing momentum_liquid OOS window, plus the 2.5 years since the start of the earnings cache)
  • IS/OOS split: pre-registered at 2021-01-01 (midpoint of the earnings cache; pre-registered BEFORE seeing results)
  • Hold: 21 calendar days (matches EqMom rebalance)
  • Lag: 1 day (signal at D-1 close decides for D+1 open; matches the existing EqMom convention)
  • Costs: 10bps round trip (matches validation/framework.py default)
  • Pre-registered primary metric: OOS Sharpe lift (overlay vs baseline) >= 0.05 AND bootstrap p < 0.10 AND maxDD not increase by > 5pp
  • Pre-registered buckets: {0.75x, 1.0x, 1.25x} (NOT to be re-gridded after seeing the IS verdict; the look-ahead bug class from R10-13)
  • Robustness grid (REPORTED, not selected): surprise threshold ∈ {2.5, 5.0, 7.5, 10.0}, hold window ∈ {30, 60, 90} days, sizing multipliers ∈ {0.5/1.0/1.5, 0.75/1.0/1.25, 1.0/1.0/1.0}
  • Pre-registered pass: at least 18 of 27 (3x3x3) robustness cells show positive Sharpe lift (>= 2/3 majority) AND the pre-registered configuration passes the primary metric

#Why this might fail (and what to do)

  1. No PEAD signal in the EqMom hold period: most EqMom names don't have an earnings event in the trailing 60 days. The bucket distribution will be ~80% passthrough, ~15% boost, ~5% reduce. If the boost/reduce days don't have a clear pattern, the test is INERT (no information). Acceptable: this is a realistic outcome and the overlay can still be useful as a "tilt" that pays off on the few days it fires.
  2. PEAD and EqMom are too correlated: if correlation is actually > 0.7, the signals are mostly redundant and the meta-signal doesn't add information. The validation will show no Sharpe lift.
  3. The 0.75/1.25 asymmetry is wrong: if the optimal is symmetric (e.g. 0.5/1.5) or fully one-sided (e.g. only boost, no reduce), the pre-registered configuration will look weak. The robustness grid covers the alternative configurations as REPORTED outcomes.
  4. Survivorship bias in the earnings cache: yfinance only serves recent quarters (~5 per name), so the PEAD signal for pre-2020 names is incomplete. The validation window starts at 2016-01-01, but most pre-2020 names will have no PEAD signal. This biases toward "no signal" rather than toward false positives. Acceptable.

#Code locations to touch (when implementing)

  • validation/eqmom_pead_overlay.py (new) — the validation study following the same protocol as momentum_liquid.py and edge_hunt7.py (PEAD).
  • production/equity_momentum_runner.py — add a sizing hook that consults data/earnings_dates.parquet and returns a multiplier in {0.75, 1.0, 1.25}, mirroring the structure of production/carry_basis_overlay.py and production/depth_filter.py. The existing EQMOM_NEWS_WEIGHT hook shows the wire-up pattern.
  • start_bots.py — add EQMOM_PEAD_OVERLAY=0 (default off, operator opts in).
  • tests/test_eqmom_pead_overlay.py (new) — unit tests for the sizing function and the per-name bucket logic.

#What this proposal does NOT do

  • Does not change the EqMom signal (still top-decile 6-month winners, no shorting, monthly rebalance)
  • Does not change the PEAD signal (still top-quintile positive surprises, 60-day hold)
  • Does not introduce leverage (the multiplier is bounded to [0.75, 1.25], not [0.0, 2.0])
  • Does not require new data (uses the existing data/earnings_dates.parquet cache)
  • Does not introduce a new sleeve (the overlay runs inside equity_momentum_runner.py, not as a separate bot)

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

← all research