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.
This commit is contained in:
2026-07-19 09:33:34 +02:00
parent 8f285acb00
commit fa25b6ee68
18 changed files with 6093 additions and 25 deletions
+202
View File
@@ -0,0 +1,202 @@
# 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:
```bash
# 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) |