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:
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mode": "per_symbol",
|
||||
"fmp_requests": 25,
|
||||
"events_written_this_run": 2541,
|
||||
"total_events": 5612,
|
||||
"symbols_done": 48,
|
||||
"symbols_universe": 506,
|
||||
"announce_date_range": {
|
||||
"min": "1985-08-31",
|
||||
"max": "2026-07-16"
|
||||
},
|
||||
"events_with_actual_and_estimate": 5018,
|
||||
"budget": 25,
|
||||
"complete": false
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
{
|
||||
"generated_at": "2026-07-19T09:31:29.078611",
|
||||
"data_provenance": {
|
||||
"snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\prod.sqlite",
|
||||
"n_earnings_events": 5612,
|
||||
"backfill_meta": {
|
||||
"done": 48,
|
||||
"universe_tickers": 506
|
||||
},
|
||||
"announce_range": {
|
||||
"min": "1985-08-31",
|
||||
"max": "2026-07-16"
|
||||
},
|
||||
"with_actual_and_estimate": 5018
|
||||
},
|
||||
"experiment_2a": {
|
||||
"sim_summary": {
|
||||
"sharpe": 2.09,
|
||||
"sharpe_se": 0.497,
|
||||
"cagr_pct": 51.6,
|
||||
"max_drawdown_pct": 21.4,
|
||||
"trades": 322,
|
||||
"total_return_pct": 424.6
|
||||
},
|
||||
"n_trades_parsed": 322,
|
||||
"q1_losses_worse_than_minus_1r": {
|
||||
"n_losses_lt_minus_1r": 28,
|
||||
"n_with_earnings_in_hold": 1,
|
||||
"fraction_with_earnings": 0.0357,
|
||||
"all_trades_with_earnings_in_hold": 14,
|
||||
"fraction_all_trades_with_earnings": 0.0435
|
||||
},
|
||||
"q2_entry_within_3d_before_announce": {
|
||||
"pre_earn_entries": {
|
||||
"n": 4,
|
||||
"mean": 1.9379,
|
||||
"win_rate": 0.5,
|
||||
"p05": -1.2428,
|
||||
"p25": -0.8833,
|
||||
"p50": 1.1209,
|
||||
"p75": 3.942,
|
||||
"p95": 6.2623,
|
||||
"min": -1.3327,
|
||||
"max": 6.8424
|
||||
},
|
||||
"other_entries": {
|
||||
"n": 318,
|
||||
"mean": 0.6965,
|
||||
"win_rate": 0.3711,
|
||||
"p05": -1.1052,
|
||||
"p25": -1.0,
|
||||
"p50": -0.8259,
|
||||
"p75": 2.1053,
|
||||
"p95": 6.077,
|
||||
"min": -3.2587,
|
||||
"max": 12.8654
|
||||
},
|
||||
"all_entries": {
|
||||
"n": 322,
|
||||
"mean": 0.7119,
|
||||
"win_rate": 0.3727,
|
||||
"p05": -1.1209,
|
||||
"p25": -1.0,
|
||||
"p50": -0.8251,
|
||||
"p75": 2.1595,
|
||||
"p95": 6.2246,
|
||||
"min": -3.2587,
|
||||
"max": 12.8654
|
||||
},
|
||||
"tail_trim_note": "Compare p95/max and mean of pre_earn vs other. Rising win_rate with falling mean/p95 = right-tail trim red flag."
|
||||
},
|
||||
"note": "REPORT-ONLY \u2014 no filter shipped."
|
||||
},
|
||||
"experiment_2b": {
|
||||
"signal_eval_side_by_side": {
|
||||
"mom_12_1": {
|
||||
"signal": "mom_12_1",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0531,
|
||||
"ic_t_stat": 1.61,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0206,
|
||||
"reliable": true
|
||||
},
|
||||
"mom_12_1_resid": {
|
||||
"signal": "mom_12_1_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0552,
|
||||
"ic_t_stat": 1.98,
|
||||
"ic_positive_pct": 60.0,
|
||||
"mean_quintile_spread": 0.0207,
|
||||
"reliable": true
|
||||
},
|
||||
"mom_12_1_sector_resid": {
|
||||
"signal": "mom_12_1_sector_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0578,
|
||||
"ic_t_stat": 2.34,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0245,
|
||||
"reliable": true
|
||||
},
|
||||
"mom_12_1_sector_demeaned": {
|
||||
"signal": "mom_12_1_sector_demeaned",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 496.7,
|
||||
"mean_ic": 0.034,
|
||||
"ic_t_stat": 1.32,
|
||||
"ic_positive_pct": 62.9,
|
||||
"mean_quintile_spread": 0.0154,
|
||||
"reliable": true
|
||||
},
|
||||
"sue_latest": {
|
||||
"signal": "sue_latest",
|
||||
"weeks": 44,
|
||||
"avg_cross_section": 47.4,
|
||||
"mean_ic": 0.0172,
|
||||
"ic_t_stat": 0.6,
|
||||
"ic_positive_pct": 47.7,
|
||||
"mean_quintile_spread": 0.0064,
|
||||
"reliable": true
|
||||
},
|
||||
"fip_id": {
|
||||
"signal": "fip_id",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": -0.045,
|
||||
"ic_t_stat": -2.91,
|
||||
"ic_positive_pct": 25.7,
|
||||
"mean_quintile_spread": -0.0168,
|
||||
"reliable": true
|
||||
}
|
||||
},
|
||||
"signal_eval_identical_sue_subset": {
|
||||
"mom_12_1": {
|
||||
"signal": "mom_12_1",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 47.3,
|
||||
"mean_ic": -0.0174,
|
||||
"ic_t_stat": -0.42,
|
||||
"ic_positive_pct": 45.7,
|
||||
"mean_quintile_spread": 0.0077,
|
||||
"reliable": true
|
||||
},
|
||||
"mom_12_1_resid": {
|
||||
"signal": "mom_12_1_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 47.3,
|
||||
"mean_ic": -0.0104,
|
||||
"ic_t_stat": -0.27,
|
||||
"ic_positive_pct": 51.4,
|
||||
"mean_quintile_spread": 0.0075,
|
||||
"reliable": true
|
||||
},
|
||||
"sue_latest": {
|
||||
"signal": "sue_latest",
|
||||
"weeks": 44,
|
||||
"avg_cross_section": 47.4,
|
||||
"mean_ic": 0.0172,
|
||||
"ic_t_stat": 0.6,
|
||||
"ic_positive_pct": 47.7,
|
||||
"mean_quintile_spread": 0.0064,
|
||||
"reliable": true
|
||||
}
|
||||
},
|
||||
"identical_subset_note": "Mom baselines re-scored only on (week, symbol) cells where SUE exists. Use this table when backfill is incomplete \u2014 full-universe mom N is not comparable.",
|
||||
"full_signal_eval": [
|
||||
{
|
||||
"signal": "vol_6m",
|
||||
"weeks": 39,
|
||||
"avg_cross_section": 498.2,
|
||||
"mean_ic": 0.0609,
|
||||
"ic_t_stat": 1.48,
|
||||
"ic_positive_pct": 64.1,
|
||||
"mean_quintile_spread": 0.0337,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1_sector_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0578,
|
||||
"ic_t_stat": 2.34,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0245,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0552,
|
||||
"ic_t_stat": 1.98,
|
||||
"ic_positive_pct": 60.0,
|
||||
"mean_quintile_spread": 0.0207,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0531,
|
||||
"ic_t_stat": 1.61,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0206,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1_sector_demeaned",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 496.7,
|
||||
"mean_ic": 0.034,
|
||||
"ic_t_stat": 1.32,
|
||||
"ic_positive_pct": 62.9,
|
||||
"mean_quintile_spread": 0.0154,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "sue_latest",
|
||||
"weeks": 44,
|
||||
"avg_cross_section": 47.4,
|
||||
"mean_ic": 0.0172,
|
||||
"ic_t_stat": 0.6,
|
||||
"ic_positive_pct": 47.7,
|
||||
"mean_quintile_spread": 0.0064,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "trend_200",
|
||||
"weeks": 37,
|
||||
"avg_cross_section": 497.9,
|
||||
"mean_ic": 0.0161,
|
||||
"ic_t_stat": 0.44,
|
||||
"ic_positive_pct": 59.5,
|
||||
"mean_quintile_spread": 0.006,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "reversal_1m",
|
||||
"weeks": 43,
|
||||
"avg_cross_section": 498.7,
|
||||
"mean_ic": 0.0059,
|
||||
"ic_t_stat": 0.22,
|
||||
"ic_positive_pct": 53.5,
|
||||
"mean_quintile_spread": 0.0053,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_6_1",
|
||||
"weeks": 39,
|
||||
"avg_cross_section": 498.2,
|
||||
"mean_ic": 0.0051,
|
||||
"ic_t_stat": 0.21,
|
||||
"ic_positive_pct": 56.4,
|
||||
"mean_quintile_spread": 0.0087,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_3_1",
|
||||
"weeks": 42,
|
||||
"avg_cross_section": 498.5,
|
||||
"mean_ic": -0.0064,
|
||||
"ic_t_stat": -0.25,
|
||||
"ic_positive_pct": 50.0,
|
||||
"mean_quintile_spread": 0.0046,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "high_52w",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": -0.0086,
|
||||
"ic_t_stat": -0.26,
|
||||
"ic_positive_pct": 54.3,
|
||||
"mean_quintile_spread": -0.0088,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "fip_id",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": -0.045,
|
||||
"ic_t_stat": -2.91,
|
||||
"ic_positive_pct": 25.7,
|
||||
"mean_quintile_spread": -0.0168,
|
||||
"reliable": true
|
||||
}
|
||||
],
|
||||
"sue_grade": {
|
||||
"green": false,
|
||||
"checks": {
|
||||
"mean_ic": 0.0172,
|
||||
"sign_positive": true,
|
||||
"abs_ge_0_03": false,
|
||||
"reliable": true,
|
||||
"ic_t_stat": 0.6,
|
||||
"weeks": 44
|
||||
},
|
||||
"reason": "iron rule not met",
|
||||
"row": {
|
||||
"signal": "sue_latest",
|
||||
"weeks": 44,
|
||||
"avg_cross_section": 47.4,
|
||||
"mean_ic": 0.0172,
|
||||
"ic_t_stat": 0.6,
|
||||
"ic_positive_pct": 47.7,
|
||||
"mean_quintile_spread": 0.0064,
|
||||
"reliable": true
|
||||
}
|
||||
},
|
||||
"momentum_conditional_sue": {
|
||||
"mean_ic": -0.0065,
|
||||
"ic_t_stat": -0.1,
|
||||
"weeks": 35,
|
||||
"note": "IC of sue_latest within top mom_12_1 quintile (non-overlapping weeks)"
|
||||
},
|
||||
"sue_coverage": {
|
||||
"symbols_with_sue": 48,
|
||||
"avg_weeks_with_sue": 47.1,
|
||||
"weeks_with_min_cross_section": 256
|
||||
}
|
||||
},
|
||||
"verdict": "PARK",
|
||||
"verdict_detail": "SUE IC=0.0172 below iron bar or unreliable; keep data, no wire.",
|
||||
"human_next": "- No SUE book change.\n- Read 2a tails before considering any earnings-avoid filter.",
|
||||
"report_path": "reports/earnings-gap-sue-20260719-093129.json",
|
||||
"fmp_note": "Bulk earnings-calendar is paid (402 on free tier). Backfill used per-symbol /stable/earnings; see earnings-backfill-status.json."
|
||||
}
|
||||
@@ -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 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
|
||||
|
||||
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) |
|
||||
@@ -0,0 +1,412 @@
|
||||
{
|
||||
"generated_at": "2026-07-19T08:33:56.651229",
|
||||
"snapshot_guard": {
|
||||
"snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\prod.sqlite",
|
||||
"manifest": null,
|
||||
"manifest_ok": null,
|
||||
"note": "No completion manifest (prod.sqlite is expected without one). Bar-count sanity still applied.",
|
||||
"ticker_count": 506,
|
||||
"ohlcv_row_count": 629263,
|
||||
"bars_min_avg_max": {
|
||||
"min": 14,
|
||||
"avg": 1246.1,
|
||||
"max": 1261
|
||||
},
|
||||
"ohlcv_date_range": {
|
||||
"min": "2021-06-24",
|
||||
"max": "2026-07-02"
|
||||
},
|
||||
"benchmark_prices": [
|
||||
{
|
||||
"symbol": "SPY",
|
||||
"n": 1516,
|
||||
"min": "2020-07-06",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLB",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLC",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLE",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLF",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLI",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLK",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLP",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLRE",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLU",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLV",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
},
|
||||
{
|
||||
"symbol": "XLY",
|
||||
"n": 1512,
|
||||
"min": "2020-07-10",
|
||||
"max": "2026-07-17"
|
||||
}
|
||||
],
|
||||
"missing_sector_etfs": []
|
||||
},
|
||||
"sector_coverage": {
|
||||
"universe": 506,
|
||||
"mapped": 505,
|
||||
"mapped_pct": 99.8,
|
||||
"with_etf": 505,
|
||||
"missing": [
|
||||
"RHM"
|
||||
],
|
||||
"by_sector": {
|
||||
"Industrials": 81,
|
||||
"Financials": 75,
|
||||
"Information Technology": 72,
|
||||
"Health Care": 58,
|
||||
"Consumer Discretionary": 47,
|
||||
"Consumer Staples": 34,
|
||||
"Real Estate": 31,
|
||||
"Utilities": 31,
|
||||
"Materials": 26,
|
||||
"Communication Services": 23,
|
||||
"Energy": 22,
|
||||
"Consumer Defensive": 2,
|
||||
"Technology": 2,
|
||||
"Financial Services": 1
|
||||
}
|
||||
},
|
||||
"sector_map_path": "C:\\Workspace\\signal-platform\\data\\research\\ticker_sector_map.json",
|
||||
"signal_eval": [
|
||||
{
|
||||
"signal": "vol_6m",
|
||||
"weeks": 39,
|
||||
"avg_cross_section": 498.2,
|
||||
"mean_ic": 0.0609,
|
||||
"ic_t_stat": 1.48,
|
||||
"ic_positive_pct": 64.1,
|
||||
"mean_quintile_spread": 0.0337,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1_sector_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0578,
|
||||
"ic_t_stat": 2.34,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0245,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0552,
|
||||
"ic_t_stat": 1.98,
|
||||
"ic_positive_pct": 60.0,
|
||||
"mean_quintile_spread": 0.0207,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0531,
|
||||
"ic_t_stat": 1.61,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0206,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_12_1_sector_demeaned",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 496.7,
|
||||
"mean_ic": 0.034,
|
||||
"ic_t_stat": 1.32,
|
||||
"ic_positive_pct": 62.9,
|
||||
"mean_quintile_spread": 0.0154,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "trend_200",
|
||||
"weeks": 37,
|
||||
"avg_cross_section": 497.9,
|
||||
"mean_ic": 0.0161,
|
||||
"ic_t_stat": 0.44,
|
||||
"ic_positive_pct": 59.5,
|
||||
"mean_quintile_spread": 0.006,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "reversal_1m",
|
||||
"weeks": 43,
|
||||
"avg_cross_section": 498.7,
|
||||
"mean_ic": 0.0059,
|
||||
"ic_t_stat": 0.22,
|
||||
"ic_positive_pct": 53.5,
|
||||
"mean_quintile_spread": 0.0053,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_6_1",
|
||||
"weeks": 39,
|
||||
"avg_cross_section": 498.2,
|
||||
"mean_ic": 0.0051,
|
||||
"ic_t_stat": 0.21,
|
||||
"ic_positive_pct": 56.4,
|
||||
"mean_quintile_spread": 0.0087,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "mom_3_1",
|
||||
"weeks": 42,
|
||||
"avg_cross_section": 498.5,
|
||||
"mean_ic": -0.0064,
|
||||
"ic_t_stat": -0.25,
|
||||
"ic_positive_pct": 50.0,
|
||||
"mean_quintile_spread": 0.0046,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "high_52w",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": -0.0086,
|
||||
"ic_t_stat": -0.26,
|
||||
"ic_positive_pct": 54.3,
|
||||
"mean_quintile_spread": -0.0088,
|
||||
"reliable": true
|
||||
},
|
||||
{
|
||||
"signal": "fip_id",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": -0.045,
|
||||
"ic_t_stat": -2.91,
|
||||
"ic_positive_pct": 25.7,
|
||||
"mean_quintile_spread": -0.0168,
|
||||
"reliable": true
|
||||
}
|
||||
],
|
||||
"ic_grades": {
|
||||
"mom_12_1_sector_resid": {
|
||||
"promote_to_ab": true,
|
||||
"checks": {
|
||||
"sign_ok": true,
|
||||
"abs_mean_ic_ge_0_03": true,
|
||||
"reliable": true,
|
||||
"t_ge_resid": true,
|
||||
"mean_ic": 0.0578,
|
||||
"ic_t_stat": 2.34,
|
||||
"resid_ic_t_stat": 1.98,
|
||||
"weeks": 35
|
||||
},
|
||||
"reason": "clears iron rule and t \u2265 mom_12_1_resid \u2014 authorized for A/B only",
|
||||
"row": {
|
||||
"signal": "mom_12_1_sector_resid",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 497.7,
|
||||
"mean_ic": 0.0578,
|
||||
"ic_t_stat": 2.34,
|
||||
"ic_positive_pct": 65.7,
|
||||
"mean_quintile_spread": 0.0245,
|
||||
"reliable": true
|
||||
}
|
||||
},
|
||||
"mom_12_1_sector_demeaned": {
|
||||
"promote_to_ab": false,
|
||||
"checks": {
|
||||
"sign_ok": true,
|
||||
"abs_mean_ic_ge_0_03": true,
|
||||
"reliable": true,
|
||||
"t_ge_resid": false,
|
||||
"mean_ic": 0.034,
|
||||
"ic_t_stat": 1.32,
|
||||
"resid_ic_t_stat": 1.98,
|
||||
"weeks": 35
|
||||
},
|
||||
"reason": "does not clear pre-registered IC promotion bar",
|
||||
"row": {
|
||||
"signal": "mom_12_1_sector_demeaned",
|
||||
"weeks": 35,
|
||||
"avg_cross_section": 496.7,
|
||||
"mean_ic": 0.034,
|
||||
"ic_t_stat": 1.32,
|
||||
"ic_positive_pct": 62.9,
|
||||
"mean_quintile_spread": 0.0154,
|
||||
"reliable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"portfolio_ab": {
|
||||
"signal": "mom_12_1_sector_resid",
|
||||
"ranking_key": "residual_high_vol_blend_80_20_score",
|
||||
"fill_mode": "close",
|
||||
"validation_split": "2024-07-01",
|
||||
"control": {
|
||||
"label": "control_mom_12_1_resid",
|
||||
"n_qualified_longs": 1086,
|
||||
"windows": {
|
||||
"train": {
|
||||
"sharpe": 1.3,
|
||||
"sharpe_se": 0.685,
|
||||
"cagr_pct": 29.2,
|
||||
"max_drawdown_pct": 21.4,
|
||||
"total_return_pct": 70.9,
|
||||
"trades": 176,
|
||||
"win_rate_pct": null,
|
||||
"avg_r": null,
|
||||
"n_returns": 525,
|
||||
"return_skew": 0.3722,
|
||||
"return_kurtosis": 4.6208,
|
||||
"psr": 0.971
|
||||
},
|
||||
"validation": {
|
||||
"sharpe": 2.92,
|
||||
"sharpe_se": 0.709,
|
||||
"cagr_pct": 76.3,
|
||||
"max_drawdown_pct": 11.7,
|
||||
"total_return_pct": 210.7,
|
||||
"trades": 150,
|
||||
"win_rate_pct": null,
|
||||
"avg_r": null,
|
||||
"n_returns": 501,
|
||||
"return_skew": 0.1734,
|
||||
"return_kurtosis": 4.4625,
|
||||
"psr": 1.0
|
||||
},
|
||||
"full": {
|
||||
"sharpe": 2.09,
|
||||
"sharpe_se": 0.497,
|
||||
"cagr_pct": 51.6,
|
||||
"max_drawdown_pct": 21.4,
|
||||
"total_return_pct": 424.6,
|
||||
"trades": 322,
|
||||
"win_rate_pct": null,
|
||||
"avg_r": null,
|
||||
"n_returns": 1000,
|
||||
"return_skew": 0.2686,
|
||||
"return_kurtosis": 4.5653,
|
||||
"psr": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"treatment": {
|
||||
"label": "treatment_mom_12_1_sector_resid",
|
||||
"n_qualified_longs": 1210,
|
||||
"windows": {
|
||||
"train": {
|
||||
"sharpe": 1.57,
|
||||
"sharpe_se": 0.677,
|
||||
"cagr_pct": 35.5,
|
||||
"max_drawdown_pct": 19.8,
|
||||
"total_return_pct": 90.0,
|
||||
"trades": 176,
|
||||
"win_rate_pct": null,
|
||||
"avg_r": null,
|
||||
"n_returns": 530,
|
||||
"return_skew": 0.466,
|
||||
"return_kurtosis": 4.4413,
|
||||
"psr": 0.99
|
||||
},
|
||||
"validation": {
|
||||
"sharpe": 2.57,
|
||||
"sharpe_se": 0.701,
|
||||
"cagr_pct": 66.3,
|
||||
"max_drawdown_pct": 14.8,
|
||||
"total_return_pct": 176.4,
|
||||
"trades": 163,
|
||||
"win_rate_pct": null,
|
||||
"avg_r": null,
|
||||
"n_returns": 501,
|
||||
"return_skew": 0.3003,
|
||||
"return_kurtosis": 4.4305,
|
||||
"psr": 0.9999
|
||||
},
|
||||
"full": {
|
||||
"sharpe": 2.09,
|
||||
"sharpe_se": 0.491,
|
||||
"cagr_pct": 51.0,
|
||||
"max_drawdown_pct": 19.8,
|
||||
"total_return_pct": 421.3,
|
||||
"trades": 337,
|
||||
"win_rate_pct": null,
|
||||
"avg_r": null,
|
||||
"n_returns": 1005,
|
||||
"return_skew": 0.4066,
|
||||
"return_kurtosis": 4.3627,
|
||||
"psr": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"promotion": {
|
||||
"promote": true,
|
||||
"checks": {
|
||||
"validation_sharpe_ge_control_minus_half_se": true,
|
||||
"full_sharpe_not_worse": true,
|
||||
"full_maxdd_not_worse": true,
|
||||
"control_validation_sharpe": 2.92,
|
||||
"treatment_validation_sharpe": 2.57,
|
||||
"se_used": 0.701,
|
||||
"control_full_sharpe": 2.09,
|
||||
"treatment_full_sharpe": 2.09,
|
||||
"control_full_maxdd": 21.4,
|
||||
"treatment_full_maxdd": 19.8
|
||||
},
|
||||
"reason": "clears pre-registered A/B bar \u2014 human decides wire-in"
|
||||
}
|
||||
},
|
||||
"verdict": "PROMOTE",
|
||||
"verdict_detail": "mom_12_1_sector_resid cleared IC + A/B bars. Human must design wire-in; do not ship from this branch.",
|
||||
"human_next": "- Approve or reject production residual swap vs dual-signal design.\n- If sector-cap arm ran, review tail-trim diagnostics before any cap.",
|
||||
"report_path": "reports/sector-residual-20260719-083356.json",
|
||||
"pre_registration": {
|
||||
"iron_ic_bar": 0.03,
|
||||
"validation_split": "2024-07-01",
|
||||
"fill_mode": "close",
|
||||
"cost_per_side": 0.001,
|
||||
"ab_rule": "val Sharpe >= control - 0.5*SE; full Sharpe & maxDD not worse"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
# Sector-residual momentum (Tier-1 alpha research)
|
||||
|
||||
**Status:** **PROMOTE (to human design decision only)** — IC + A/B bars cleared; **do not ship**.
|
||||
**Branch:** `research/sector-residual-momentum`
|
||||
**Production impact:** none. Local research only. No scheduler / gate / prod-config changes.
|
||||
**Artifacts:** `reports/sector-residual-20260719-083356.json` (+ companion `.md`)
|
||||
|
||||
---
|
||||
|
||||
## Pre-registration (locked before first research run)
|
||||
|
||||
### Hypothesis
|
||||
|
||||
Residualizing 12–1 momentum against the sector, not only the market, reduces
|
||||
factor volatility at similar return (Blitz / Huij / Martens-style) → higher
|
||||
Sharpe on the production book when the residual replaces market-only residual
|
||||
as the momentum leg.
|
||||
|
||||
### Signals (candidates)
|
||||
|
||||
| signal | construction |
|
||||
|---|---|
|
||||
| `mom_12_1_sector_resid` | Two-factor residual vs SPY + ticker’s sector ETF. Same window as `mom_12_1_resid`: ≥100 daily obs, 252-bar lookback, 21-bar skip; two-factor OLS betas **without intercept**; cumulate residual returns over the formation window. |
|
||||
| `mom_12_1_sector_demeaned` | Plain `mom_12_1` minus the **cross-sectional** mean of `mom_12_1` within the same GICS sector that week (≥2 names in sector). No regression. |
|
||||
|
||||
### Baselines (same run, same cross-sections — iron rule)
|
||||
|
||||
Always report side-by-side with:
|
||||
|
||||
- `mom_12_1`
|
||||
- `mom_12_1_resid`
|
||||
|
||||
Computed on the **identical** weekly non-overlapping cross-sections in this run.
|
||||
Never compare against IC numbers from another report.
|
||||
|
||||
### Iron rule (IC harness)
|
||||
|
||||
Source of truth: `_signal_evaluation` in `app/services/backtest_service.py`.
|
||||
|
||||
- Mean weekly Spearman IC on **non-overlapping** weekly windows
|
||||
- Bar: \|mean IC\| ≥ ~0.03, **consistent positive sign**, `reliable: true` (≥ 12 windows)
|
||||
|
||||
### Promotion to portfolio A/B (candidate → book)
|
||||
|
||||
A candidate promotes to A/B **only if**:
|
||||
|
||||
1. It clears the iron-rule bar **and**
|
||||
2. Its IC **t-stat ≥** that of `mom_12_1_resid` on the same cross-sections.
|
||||
|
||||
### Portfolio A/B grading (if and only if IC promotion fires)
|
||||
|
||||
- Swap candidate in as the **momentum leg** of the production 80/20 momentum/vol
|
||||
rank **and** as the gate-percentile signal.
|
||||
- `fill_mode=close`, `COST_PER_SIDE = 0.001`, full config otherwise unchanged.
|
||||
- Validation window = entries ≥ **2024-07-01** (call it **validation**, not
|
||||
holdout — contaminated by prior experiments).
|
||||
- Pre-registered promotion bar:
|
||||
- validation Sharpe ≥ control − 0.5·SE
|
||||
- full-period Sharpe and max-DD **not worse** than control
|
||||
- Report Lo / Mertens-adjusted SEs.
|
||||
|
||||
### Optional sector-cap sub-experiment
|
||||
|
||||
Only if labels are in **and** A/B ran: max **3** positions per sector in the
|
||||
10-slot book. Same A/B grading. **Tail-trim presumption of guilt** (rule 4):
|
||||
report entry counts and both tails of the R distribution. Rising win rate with
|
||||
falling Sharpe/CAGR = red flag → do not promote.
|
||||
|
||||
**This run:** sector-cap arm **not executed** (optional; A/B unconstrained book
|
||||
only). Can be a human-approved follow-up.
|
||||
|
||||
### Verdict labels
|
||||
|
||||
| label | meaning |
|
||||
|---|---|
|
||||
| **PROMOTE** | Clears pre-registered bar; human decides next (wire design separate) |
|
||||
| **PARK** | Inconclusive / weak; keep machinery, no book change |
|
||||
| **DEAD** | Failed iron rule or worse than residual baseline with clear sign |
|
||||
|
||||
### Explicit non-goals
|
||||
|
||||
- No production deploy from this doc
|
||||
- Do not resurrect: take-profit exits, EV gate, regime entry-blocking,
|
||||
inverse-vol sizing, gap-caps, unconditional FIP filter
|
||||
|
||||
---
|
||||
|
||||
## Data provenance
|
||||
|
||||
### Snapshot race guard
|
||||
|
||||
| check | result |
|
||||
|---|---|
|
||||
| Snapshot path | `backtest_snapshots/prod.sqlite` |
|
||||
| Manifest | none (expected for prod snapshot); bar-count sanity applied |
|
||||
| Tickers / OHLCV | **506** / **629,263** |
|
||||
| Bars min / avg / max | 14 / 1246.1 / 1261 |
|
||||
| OHLCV range | 2021-06-24 → 2026-07-02 |
|
||||
| Partial-build red flags | none (avg bars healthy) |
|
||||
|
||||
Integrity fingerprint on same run: `fip_id` mean IC **−0.045** / t **−2.91**
|
||||
(35 weeks, N≈498) — matches the established prod fingerprint.
|
||||
|
||||
### Sector labels
|
||||
|
||||
| source | count |
|
||||
|---|---:|
|
||||
| Public S&P 500 GICS CSV | 496 newly filled |
|
||||
| FMP profile requests | 10 (all missing after CSV) |
|
||||
| Mapped / universe | **505 / 506 (99.8%)** |
|
||||
| With mappable ETF | 505 |
|
||||
| Still missing | **RHM** only |
|
||||
|
||||
Persist path: `data/research/ticker_sector_map.json`.
|
||||
|
||||
FMP aliases (`Technology`, `Consumer Defensive`, `Financial Services`) map to
|
||||
SPDRs via the alias table in `app/services/sector_map.py`.
|
||||
|
||||
### Sector ETFs in `benchmark_prices` (auxiliary only — not tradable)
|
||||
|
||||
| symbol | bars | min date | max date |
|
||||
|---|---:|---|---|
|
||||
| SPY | 1516 | 2020-07-06 | 2026-07-17 |
|
||||
| XLB…XLY (11) | 1512 each | 2020-07-10 | 2026-07-17 |
|
||||
|
||||
Fetched via Alpaca `Adjustment.SPLIT` into **`benchmark_prices`** (same table as
|
||||
SPY) so they never enter the ticker universe or candidate replay.
|
||||
|
||||
---
|
||||
|
||||
## Results
|
||||
|
||||
Generated: `2026-07-19T08:33:56`
|
||||
|
||||
### IC harness (identical cross-sections, production 506-name universe)
|
||||
|
||||
| signal | mean_ic | ic_t_stat | weeks | avg_N | reliable | ic+_pct | quintile spread |
|
||||
|---|---:|---:|---:|---:|---|---:|---:|
|
||||
| **mom_12_1_sector_resid** | **0.0578** | **2.34** | 35 | 497.7 | true | 65.7 | 0.0245 |
|
||||
| mom_12_1_resid | 0.0552 | 1.98 | 35 | 497.7 | true | 60.0 | 0.0207 |
|
||||
| mom_12_1 | 0.0531 | 1.61 | 35 | 497.7 | true | 65.7 | 0.0206 |
|
||||
| mom_12_1_sector_demeaned | 0.0340 | 1.32 | 35 | 496.7 | true | 62.9 | 0.0154 |
|
||||
|
||||
### IC promotion grades
|
||||
|
||||
| candidate | iron rule | t ≥ resid | promote_to_ab |
|
||||
|---|---|---|---|
|
||||
| `mom_12_1_sector_resid` | pass (IC 0.058, +sign, reliable) | **yes** (2.34 ≥ 1.98) | **yes** |
|
||||
| `mom_12_1_sector_demeaned` | pass (IC 0.034, +sign, reliable) | **no** (1.32 < 1.98) | **no** |
|
||||
|
||||
### Portfolio A/B — `mom_12_1_sector_resid` as residual leg
|
||||
|
||||
Config: production 80/20 residual/high-vol rank + gate percentile, `fill_mode=close`,
|
||||
cost 10 bps/side, ATR trail / gate-reset re-entry as live. Validation split
|
||||
2024-07-01.
|
||||
|
||||
| window | arm | Sharpe | Sharpe SE (Mertens) | CAGR % | max DD % | trades | n_days |
|
||||
|---|---|---:|---:|---:|---:|---:|---:|
|
||||
| train | control (resid) | 1.30 | 0.685 | 29.2 | 21.4 | 176 | 525 |
|
||||
| train | treatment (sector resid) | **1.57** | 0.677 | **35.5** | **19.8** | 176 | 530 |
|
||||
| validation | control | **2.92** | 0.709 | **76.3** | **11.7** | 150 | 501 |
|
||||
| validation | treatment | 2.57 | 0.701 | 66.3 | 14.8 | 163 | 501 |
|
||||
| full | control | 2.09 | 0.497 | 51.6 | 21.4 | 322 | 1000 |
|
||||
| full | treatment | 2.09 | 0.491 | 51.0 | **19.8** | 337 | 1005 |
|
||||
|
||||
**Pre-registered A/B checks**
|
||||
|
||||
| check | result |
|
||||
|---|---|
|
||||
| val Sharpe ≥ control − 0.5·SE | **pass** (2.57 ≥ 2.92 − 0.5×0.701 = 2.5695) — **knife-edge** |
|
||||
| full Sharpe not worse | **pass** (2.09 = 2.09) |
|
||||
| full max DD not worse | **pass** (19.8 < 21.4) |
|
||||
|
||||
Qualified long candidates: control 1086 vs treatment 1210 (sector residual
|
||||
gates a slightly larger set).
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
| signal | verdict | note |
|
||||
|---|---|---|
|
||||
| **`mom_12_1_sector_resid`** | **PROMOTE → human wire-in decision** | IC modestly beats market residual; A/B clears pre-reg bar narrowly. **Do not ship from this branch.** |
|
||||
| **`mom_12_1_sector_demeaned`** | **DEAD** (for promotion) | Iron-rule IC magnitude ok, but t-stat loses to `mom_12_1_resid`. Cheap variant not competitive. |
|
||||
|
||||
### Read carefully (for the human)
|
||||
|
||||
1. **IC edge is real but small.** Sector residual IC 0.0578 / t 2.34 vs market
|
||||
residual 0.0552 / t 1.98 on the **same** 35 windows — better consistency
|
||||
(ic+ 65.7% vs 60%) and slightly higher mean, not a different factor class.
|
||||
2. **A/B is not a clear Sharpe win.** Full-period Sharpe is flat (2.09).
|
||||
Validation Sharpe is **lower** than control (2.57 vs 2.92) and only clears
|
||||
the pre-registered “within 0.5 SE” cushion by ~0.001. Train improves;
|
||||
validation worsens — classic regime-split noise on ~2 years.
|
||||
3. **Risk side is friendly.** Full max DD improves (19.8% vs 21.4%); train DD
|
||||
also better. Matches the “lower factor vol” half of the hypothesis more than
|
||||
the “higher Sharpe” half on this window.
|
||||
4. **Survivorship / short history.** Same caveats as all current research:
|
||||
today’s constituents, ~35 independent weekly windows, one post-2021 regime
|
||||
dominant. Task 3 (history depth) should re-check IC stability before any
|
||||
wire-in.
|
||||
5. **Not shipped.** Machinery lives on the research branch; production residual
|
||||
path is untouched.
|
||||
|
||||
---
|
||||
|
||||
## What a human must decide next
|
||||
|
||||
1. **Accept or reject** replacing `mom_12_1_resid` with `mom_12_1_sector_resid`
|
||||
as the production residual (gate + 80/20 mom leg), **or** keep market residual
|
||||
and treat sector residual as research-only.
|
||||
2. If leaning accept: require **Task 3 history-depth** confirmation (IC era split
|
||||
pre/post-2021) before any production PR.
|
||||
3. Optional: run **sector-cap ≤3** A/B with full tail diagnostics (not run here).
|
||||
4. **Do not** merge this verdict into main strategy docs without review.
|
||||
5. Wire-in design (live sector map refresh, ETF series ops, fallback when sector
|
||||
missing) is a **separate** approved engineering step.
|
||||
|
||||
---
|
||||
|
||||
## Implementation notes (research machinery)
|
||||
|
||||
| piece | role |
|
||||
|---|---|
|
||||
| `app/services/sector_map.py` | GICS→ETF map, symbol normalise, JSON load/save |
|
||||
| `app/services/backtest_service.py` | multi-factor residual; `mom_12_1_sector_resid` in `_signal_values`; demean inject |
|
||||
| `scripts/build_ticker_sector_map.py` | SP500 CSV + FMP gap fill |
|
||||
| `scripts/fetch_sector_etfs_to_snapshot.py` | Alpaca → snapshot `benchmark_prices` |
|
||||
| `scripts/run_sector_residual_research.py` | race guard, IC, optional A/B, reports |
|
||||
| `data/research/ticker_sector_map.json` | persisted labels (research only) |
|
||||
|
||||
---
|
||||
|
||||
## Artifacts
|
||||
|
||||
- JSON: `reports/sector-residual-20260719-083356.json`
|
||||
- MD copy: `reports/sector-residual-20260719-083356.md`
|
||||
Reference in New Issue
Block a user