Files
signal-platform/docs/research/earnings-gap-and-sue.md
T
dennisthiessen fa25b6ee68 research: sector residual, earnings gap/SUE, history-depth scaffolding
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.
2026-07-19 09:33:34 +02:00

7.2 KiB
Raw Blame History

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/earnings with 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:

  1. What fraction of losses worse than 1R occur with an earnings announcement between entry and exit (inclusive of the holding window)?
  2. 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 books 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

  1. Resume multi-day earnings backfill to 506/506, then re-run run_earnings_research.py (heavy — MacBook OK).
  2. Do not ship an earnings-avoid entry filter from 2a.
  3. Do not wire SUE until a full-coverage IC clears the iron rule (and preferably mom-conditional > 0).
  4. 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)