research: timestamped Yahoo analyst snapshots and a forward-only EPS-revision overlay

collect archives Yahoo's earnings/revenue estimates, EPS trend, EPS
revisions and price targets as immutable snapshots; a snapshot is usable
only after its created_at, so the vendor's 30/90-day history never reaches
earlier bars. evaluate replays overnight_recovery_top10 with an EPS-revision
re-rank of the quant top 50 on the same ledger and costs and returns NEEDS
EVIDENCE until 252 active sessions from 20 collection dates exist. The
agent's candidate pack now carries each name's analyst context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:16:19 -07:00
parent 31155fdcfb
commit 0e1e0a202c
4 changed files with 505 additions and 2 deletions

View File

@@ -4,7 +4,7 @@ Daily loop of the agent-reviewed strategy ``overnight_recovery_agent_top10``
scores with a point-in-time stamp, and measure the scores against what
happened.
uv run python agent_review.py candidates [--refresh] # pack for the next session
uv run python agent_review.py candidates [--refresh] [--analyst-refresh]
uv run python agent_review.py submit data/agent/us/draft_<as_of>.json
uv run python agent_review.py evaluate # IC, vetoes, book vs control
@@ -75,6 +75,11 @@ def cmd_candidates(args) -> None:
bars = trader.strategy_bars("us", NAME, tickers, close, open_)
strat = trader.STRATEGY_REGISTRY[NAME]()
cand = strat.candidates(bars)
from research.lab.analyst_estimates import collect, candidate_context
if args.analyst_refresh:
collect(list(cand.index))
generated_at = datetime.now(timezone.utc).isoformat()
analyst = candidate_context(list(cand.index), generated_at)
c = close[cand.index].ffill()
r = lambda k: (c.iloc[-1] / c.iloc[-1 - k] - 1).round(4) # noqa: E731
@@ -109,6 +114,7 @@ def cmd_candidates(args) -> None:
"overnight_mom_12m": float(row["overnight_mom_12m"]), "recovery_126d": float(row["recovery_126d"]),
"held_weight": {k: v.get(t, 0.0) for k, v in held.items()},
**links.get(t, {}),
"analyst_estimates": analyst.get(t),
"previous": ({"as_of": prev["as_of"], "score": p.get("score"), "veto": p.get("veto", False),
"summary": p.get("summary", "")} if p else None),
})
@@ -120,7 +126,10 @@ def cmd_candidates(args) -> None:
"deadline_utc": str(ar.session_deadline(nxt)),
# 0: the next session rebalances on these scores; otherwise only vetoes act until then
"sessions_until_rebalance": int(until),
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"generated_at": generated_at,
"analyst_estimates": {"covered": len(analyst),
"eligible": sum(r["eligible"] for r in analyst.values()),
"note": "Known only after snapshot collection; stale fiscal metadata is excluded from the research overlay."},
"rule": {"agent_weight": ar.AGENT_WEIGHT, "score_range": list(ar.SCORE_RANGE),
"veto_reasons": list(ar.VETO_REASONS), "top_n": strat.top_n, "pool": strat.pool,
"rebal_freq": strat.rebal_freq},
@@ -290,6 +299,7 @@ def main() -> None:
sub = ap.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("candidates", help="write the candidate pack for the next session")
p.add_argument("--refresh", action="store_true", help="download prices first (the monitor normally has)")
p.add_argument("--analyst-refresh", action="store_true", help="archive current Yahoo analyst estimates before writing the pack")
p = sub.add_parser("submit", help="validate and store an agent scores file")
p.add_argument("file")
sub.add_parser("evaluate", help="score IC, veto outcomes, and the book against its control")