Tier-1 alpha research (local only, no production deploy): Sector residual momentum: two-factor SPY+sector residual and sector demean signals, IC harness + A/B. Sector resid clears pre-registered bars narrowly (PROMOTE for human wire design only). Sector demean fails t vs market resid. Earnings: earnings_events backfill (FMP bulk paid; FMP/AV per-symbol), 2a gap diagnostic report-only, 2b SUE IC (PARK; incomplete 48/506 coverage). History-depth: pre-registered doc + runner for MacBook deep rebuild/harness. Do not ship production residual or filters from this branch.
7.2 KiB
Earnings gap diagnostic + SUE / PEAD (Tier-1 alpha research)
Status: PARK (incomplete earnings coverage; SUE fails iron rule on available sample).
Branch: research/earnings-gap-and-sue
Production impact: none. Local research only. No filters shipped from 2a.
Artifacts: reports/earnings-gap-sue-20260719-093129.json (+ companion .md)
Pre-registration (locked before first research run)
Data
- Historical earnings calendar for the production universe over the full snapshot window (and deeper if the feed provides it).
- Preferred source: FMP date-range earnings-calendar (bulk). If unavailable on
free tier, fall back to per-symbol
/stable/earningswith request accounting. - Store in a real local table
earnings_events(symbol + announce_date key). - Point-in-time: a surprise is usable only from announce date + 1 trading day onward.
Experiment 2a — earnings-gap risk (defense, report-only)
Join simulated production-config trades (fill_mode=close) with earnings dates.
Pre-registered questions:
- What fraction of losses worse than −1R occur with an earnings announcement between entry and exit (inclusive of the holding window)?
- What is the mean R of entries taken within 3 trading days BEFORE an announcement vs all other entries — report both tails of the R distribution (rule 4: any earnings-avoid entry filter is presumed guilty of right-tail trimming until the win distribution shows otherwise)?
Output: distributions and counts only.
No filter is shipped. If numbers argue for a filter → report and stop.
Experiment 2b — SUE / PEAD (offense)
Signal sue_latest:
[ \text{SUE} = \frac{\text{actual} - \text{estimate}}{\sigma(\text{trailing 8 surprises})} ]
Fallback if estimate history is thin: scale surprise by price.
Carry forward from announce+1 for 63 trading days, else NaN (name drops out
of that cross-section).
Iron rule (IC harness): mean weekly Spearman IC on non-overlapping weeks;
|mean IC| ≥ ~0.03, positive sign (drift), reliable: true (≥12 windows).
Always side-by-side with mom_12_1 and mom_12_1_resid on identical
cross-sections.
Also report momentum-conditional IC (within top momentum quintile).
If it passes iron rule: STOP and report. Book-integration design is a separate human-approved step — do not wire.
Verdict labels
| label | meaning |
|---|---|
| PROMOTE | (2b only) iron rule cleared → human designs tilt/gate |
| PARK | Interesting but incomplete / weak |
| DEAD | No edge / diagnostic argues against action |
| REPORT-ONLY | (2a) always — never auto-filter |
Data provenance
| item | result |
|---|---|
| Snapshot | backtest_snapshots/prod.sqlite (506 names) |
FMP bulk earnings-calendar |
402 Premium — not available on free tier |
FMP per-symbol /stable/earnings |
used; hit daily rate limit ~225 reqs |
Alpha Vantage EARNINGS |
used for +24 symbols (announce = reportedDate) |
| Symbols with events | 48 / 506 (9.5%) |
| Total events | 5,612 (5,018 with actual+estimate) |
| Announce range | 1985-08-31 → 2026-07-16 |
| FMP requests (first day) | 260 FMP + 25 AV (see reports/earnings-backfill-status.json) |
Incomplete backfill is first-class. 2a under-detects earnings overlaps; 2b SUE cross-section averages ~47 names, not ~500. Resume:
# Day N (FMP free ~250/day; AV free ~25/day — prefer FMP after reset)
python scripts/backfill_earnings_events.py \
--snapshot backtest_snapshots/prod.sqlite \
--provider fmp --force-symbol --limit 250 --sleep 0.4
# When done==506:
python scripts/run_earnings_research.py \
--snapshot backtest_snapshots/prod.sqlite \
--workers 6 --allow-spawn
Results
Generated: 2026-07-19T09:31:29
2a — Earnings-gap risk (report-only)
Production book sim: Sharpe 2.09 (SE 0.497), CAGR 51.6%, max DD 21.4%, 322 trades,
fill_mode=close.
Q1 — Losses worse than −1R with earnings in hold
| metric | value |
|---|---|
| n losses < −1R | 28 |
| of which earnings in hold | 1 |
| fraction | 3.6% |
| all trades with earnings in hold | 14 / 322 (4.4%) |
Read: On incomplete earnings labels this is a lower bound on earnings overlap, not a clean “earnings rarely hurt.” Do not conclude earnings risk is immaterial until coverage ≥ ~95% of the book’s names.
Q2 — Entry within 3 trading days before announce (both tails)
| cohort | n | mean R | win rate | p05 | p50 | p95 | max |
|---|---|---|---|---|---|---|---|
| pre-earn (≤3d before) | 4 | 1.94 | 50% | −1.24 | 1.12 | 6.26 | 6.84 |
| other | 318 | 0.70 | 37% | −1.11 | −0.83 | 6.08 | 12.87 |
| all | 322 | 0.71 | 37% | −1.12 | −0.83 | 6.22 | 12.87 |
Tail-trim presumption: n=4 is not a sample. Point estimate does not show right-tail destruction of pre-earn entries (p95 similar; max actually higher in “other”). No earnings-avoid filter is supported. Re-run after full backfill.
2b — SUE / PEAD IC
Full-universe harness (mom on ~500; SUE only where labeled)
| signal | mean_ic | ic_t_stat | weeks | avg_N | reliable |
|---|---|---|---|---|---|
| mom_12_1_sector_resid | 0.0578 | 2.34 | 35 | 497.7 | true |
| mom_12_1_resid | 0.0552 | 1.98 | 35 | 497.7 | true |
| mom_12_1 | 0.0531 | 1.61 | 35 | 497.7 | true |
| sue_latest | 0.0172 | 0.6 | 44 | 47.4 | true |
| fip_id | −0.045 | −2.91 | 35 | 497.7 | true |
Identical SUE subset (fair side-by-side — use this while coverage is thin)
| signal | mean_ic | ic_t_stat | weeks | avg_N |
|---|---|---|---|---|
| sue_latest | 0.0172 | 0.6 | 44 | 47.4 |
| mom_12_1 | −0.0174 | −0.42 | 35 | 47.3 |
| mom_12_1_resid | −0.0104 | −0.27 | 35 | 47.3 |
On the thin labeled subset, momentum itself is noise — so the subset is not yet a meaningful PEAD test.
Momentum-conditional SUE (top mom quintile)
| metric | value |
|---|---|
| mean IC | −0.0065 |
| t | −0.1 |
| weeks | 35 |
Wrong sign vs “ride positive surprises inside the momentum gate.”
Iron rule: fail (|IC| 0.017 < 0.03; t 0.6). No promote.
Verdict
| piece | verdict |
|---|---|
| 2a earnings-gap | REPORT-ONLY — no filter. Coverage too thin for risk claims; tails do not argue for an avoid-filter on n=4. |
| 2b SUE | PARK (effectively not green). Mild positive IC on ~48 names; fails iron bar; mom-conditional flat/negative. Re-score after full backfill before DEAD. |
| Production | no change |
What a human must decide next
- Resume multi-day earnings backfill to 506/506, then re-run
run_earnings_research.py(heavy — MacBook OK). - Do not ship an earnings-avoid entry filter from 2a.
- Do not wire SUE until a full-coverage IC clears the iron rule (and preferably mom-conditional > 0).
- Do not merge into main strategy docs without review.
Implementation notes
| piece | role |
|---|---|
scripts/backfill_earnings_events.py |
bulk attempt → FMP/AV per-symbol; earnings_events + meta on snapshot |
scripts/run_earnings_research.py |
2a trade join + 2b SUE IC / mom-conditional |
Snapshot table earnings_events |
real table (not SystemSetting JSON) |