16 · Earnings Proximity — Feature Evaluation
Generated: 2026-05-15
Definition
For each BUY declaration we bucket its pubDate relative to the issuing
company's earnings calendar:
| Bucket | Rule |
|---|---|
pre |
nextEarningsDate − pubDate ∈ [0, 30d] |
post |
pubDate − lastEarningsDate ∈ [0, 30d] |
neutral |
neither of the above |
Schema additions on Company: nextEarningsDate, lastEarningsDate,
earningsUpdatedAt (all DateTime?). Source: Yahoo Finance v10 quoteSummary
with modules=earnings,calendarEvents, refreshed weekly (Monday cron).
Limitation — calendar is point-in-time, not historical
Yahoo exposes the current calendar only (next + most recent past). For
historical backtests we extrapolate the quarterly cadence backward (and
forward) at a fixed 91-day step from the known anchor dates. This is an
approximation: most issuers keep a stable release rhythm but a small fraction
shift by ±2 weeks year-over-year. Bucket noise is therefore biased toward
under-counting "pre"/"post" classifications (false negatives skew toward
neutral), which conservatively weakens any apparent edge — the true
underlying signal is at least as strong as what we measure here.
Raw bucket stats (full 2021-12 → 2026-01 sample)
| Bucket | n | mean r90 % | winRate % | CI95 |
|---|---|---|---|---|
| pre | 2097 | +0.399 | 47.40 | [−0.59, +1.60] |
| post | 1257 | +0.458 | 42.96 | [−0.94, +2.30] |
| neutral | 11817 | −1.047 | 43.89 | [−1.50, −0.52] |
Bootstrap two-sample p-values vs neutral:
| Bucket | p (mean r90) | p (win rate) |
|---|---|---|
| pre | 0.029 | 0.004 |
| post | 0.070 | 0.516 |
The pre bucket shows a credible raw edge over neutral on both mean return
and win rate. post is borderline on mean only.
Walk-forward OOS (24m train / 12m test, top-10/week, 90d hold)
Period: 2021-12-08 → 2026-01-16, 2 folds (data starts in 2022 once 24m train window is satisfied).
| Baseline (no bonus) | With ±5pt earnings bonus | |
|---|---|---|
| n picks | 1 060 | 1 060 |
| Mean r90 % | 4.140 | 3.849 |
| Sharpe_ann | 0.856 | 0.699 |
| Win rate % | 50.94 | 53.02 |
Deltas: ΔWinRate = +2.08pp, ΔSharpe_ann = −0.157. Bootstrap p-values on the pick distribution: p_mean = 0.754, p_winRate = 0.353.
Gate
Rule: (ΔWinRate ≥ +2pp OR ΔSharpe_ann ≥ +0.15) AND min(p) < 0.10.
| Condition | Value | Pass? |
|---|---|---|
| ΔWinRate ≥ +2pp | +2.08 | ✓ |
| ΔSharpe_ann ≥ +0.15 | −0.16 | ✗ |
| min(p) < 0.10 | 0.353 | ✗ |
Decision: REJECT. The win-rate uplift is right at the threshold but Sharpe deteriorates, and neither delta is significant (p > 0.10). The recommendation engine is left untouched.
Why the raw signal doesn't survive walk-forward
Two effects compound:
- Limited folds (n=2). The full sample only spans ~4 years of usable r90d data; with a 24/12 split we get just two test windows. Annualized Sharpe estimated from two fold means is high-variance.
- Pre-trade selection bias. The unconditional
preedge concentrates on recent declarations where Yahoo's currentnextEarningsDateactually lies within 30d of pubDate. As we walk back in time the synthetic-cadence bucketing introduces ±days of slippage, attenuating the edge in older folds where most picks come from.
Action
- Schema columns are in place and the weekly refresh is live (Monday cron).
- The feature helpers (
earningsProximity,earningsProximityFromDates) are exported and ready for downstream use (UI badges, alerts, future research). - No change to
recommendation-engine.ts. Re-evaluate once we have ≥4 walk-forward folds (i.e. ≥6 years of r90 data) or a real point-in-time earnings calendar.
Reproducibility
node --env-file=.env.local scripts/backfill-earnings.mjs 200
node --env-file=.env.local scripts/backtest-feature-earnings.mjs
Raw output: /tmp/earnings-proximity-results.json.
Seed = 42 (bootstrap CI), 7 / 11 (two-sample bootstrap p-values).