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:
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user