research: Task 2 closed — SUE dead, earnings gap informational

Earnings backfill sourced from the public DoltHub earnings repo at a
pinned commit rather than the FMP API: reproducible for anyone re-running
the study, and it burns no request quota. 12,414 events, 98.6% of symbols
with >=8 announcements, 99.2% paired actual/estimate, no keyed duplicates.

2a earnings-gap diagnostic: INFORMATIONAL, no filter shipped. The
pre-earnings cohort's right tail was better, so the registered
avoid-earnings condition failed. Note the raw 23/266 vs 115/574 incidence
gap is largely a duration confound -- severe losses stop out fast and have
less time to span an announcement -- so it is not evidence that holding
through earnings is safe.

2b SUE: FAIL against the pre-registered +0.03 bar (unconditional IC
+0.0151 over 56 reliable windows, momentum-conditional +0.0213). Signs
stable across eras, so this is a clean null rather than an ambiguous one,
consistent with post-earnings drift having decayed in large caps.

Closes the Tier-1 arc: Task 1 dead on deep evidence, Task 2 dead here,
Task 3 complete as diagnostic. No in-sample research thread remains open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 21:10:46 +02:00
co-authored by Claude Fable 5
parent 1fa3d70dec
commit c7c60a64f2
14 changed files with 3705 additions and 1828 deletions
+60 -3
View File
@@ -97,6 +97,14 @@ def _parse_args() -> argparse.Namespace:
default=5,
help="Retries per symbol on RateLimitError.",
)
p.add_argument(
"--source-symbols-only",
action="store_true",
help=(
"Refresh only symbols present in --source. Useful for repairing "
"per-symbol depth without re-fetching the broad rank-only pool."
),
)
p.add_argument("--quiet", action="store_true")
return p.parse_args()
@@ -218,6 +226,15 @@ async def _main() -> None:
if not source.exists():
raise SystemExit(f"Source snapshot not found: {source}")
source_engine = create_engine(
f"sqlite:///{source.resolve().as_posix()}", future=True
)
with source_engine.connect() as conn:
source_symbols = {
str(row[0]) for row in conn.execute(text("SELECT symbol FROM tickers"))
}
source_engine.dispose()
# Any rebuild/update invalidates prior completion until we finish cleanly.
clear_manifest(output)
@@ -241,7 +258,12 @@ async def _main() -> None:
start = end - timedelta(days=int(args.history_days))
print("Resolving universe pool (nasdaq_all sp500)…")
pool, sources = await _resolve_pool()
if args.source_symbols_only:
pool = sorted(source_symbols)
sources = {"pool": "source_snapshot"}
print(" source snapshot: symbol pool selected")
else:
pool, sources = await _resolve_pool()
print(f"Pool size: {len(pool)} (sources={sources})")
# Sync sqlite via raw SQL — one short transaction per symbol so a failed
@@ -257,7 +279,7 @@ async def _main() -> None:
text("SELECT id, symbol FROM tickers")
).fetchall()
existing_ids = {str(sym): int(tid) for tid, sym in existing_rows}
prod_symbols = set(existing_ids)
prod_symbols = set(source_symbols)
bar_counts: dict[str, int] = {}
for sym, tid in existing_ids.items():
@@ -363,7 +385,7 @@ async def _main() -> None:
for b in bars
],
)
if is_new:
if is_new and sym not in prod_symbols:
write.execute(
text(
"INSERT OR REPLACE INTO research_rank_only "
@@ -385,6 +407,39 @@ async def _main() -> None:
f"elapsed={elapsed/60:.1f}m last={sym} bars={len(bars)}"
)
benchmark_rows = 0
try:
benchmark_bars = await _fetch_symbol_bars(
provider,
"SPY",
start,
end,
max_retries=args.max_retries,
sleep_s=args.sleep,
)
with engine.begin() as write:
write.execute(
text(
"DELETE FROM benchmark_prices WHERE symbol='SPY' "
"AND date >= :start AND date <= :end"
),
{"start": start.isoformat(), "end": end.isoformat()},
)
if benchmark_bars:
write.execute(
text(
"INSERT INTO benchmark_prices(symbol, date, close) "
"VALUES ('SPY', :date, :close)"
),
[
{"date": bar.date.isoformat(), "close": float(bar.close)}
for bar in benchmark_bars
],
)
benchmark_rows = len(benchmark_bars)
except Exception as exc:
print(f" benchmark SPY refresh FAIL {exc}")
rank_only_n = conn.execute(
text("SELECT COUNT(*) FROM research_rank_only")
).scalar_one()
@@ -409,6 +464,8 @@ async def _main() -> None:
"prod_symbols_at_start": len(prod_symbols),
"pool_size": len(pool),
"to_fetch": len(to_fetch),
"source_symbols_only": bool(args.source_symbols_only),
"benchmark_spy_rows": benchmark_rows,
},
)