# Earnings date estimates (top-30 S&P 500)

Estimates the next earnings release day(s) and before/after-the-bell timing for the 30 largest S&P 500
companies, from free public data only. No vendor calendar.

Live copy: `/var/www/apps.inncommand.com/earnings-dates/` on the plant EC2 box, served at
https://apps.inncommand.com/earnings-dates/ (page), `earnings_dates.json`, `earnings_dates.csv`.
Refreshed by `run.sh` from the ec2-user crontab at 04:50 and 14:50 Pacific on weekdays (the afternoon run
picks up same-day after-close filings). Log: `run.log`. Dev copy: `/home/ec2-user/claude/earnings_dates/`
on the research box.

## Importing into a database

`FEED.md` is the contract: field meanings, enums, upsert key `(symbol, fiscal_quarter)`, a suggested
MariaDB table, and the post-refresh hook `/etc/earnings-dates/post_run.sh` that `run.sh` calls with the
JSON path after each successful run so an importer need not poll.

## Confirmation step (daily)

Estimates are the base. On top of them, once a day:

* Google Finance's quote page for every symbol yields the vendor's scheduled event with a timestamp (usually
  the call time). It is attached to the feed as `vendor_schedule`, and when it falls within 21 days
  (`--vendor-window`) the row becomes `status = reported` with that date and bell. Google does not say whether
  a date is company-confirmed (Berkshire shows a Friday for its Saturday releases), so it never yields
  `confirmed` on its own.
* Once a release is within 30 days of the earliest estimated day (`--confirm-window`), the company's own
  announcement is searched daily: Google News and Bing News RSS, filtered to titles that start with the
  company name and read like a date announcement ("Micron Technology to Report ... on September 30, 2026",
  "Oracle Sets the Date for its ... Earnings Announcement"). The date and bell come from the title or the
  article body (Bing supplies direct article links; Google's are opaque). The company's site, a press wire,
  or a wire syndicator carrying the release gives `confirmed`; two independent third-party reports agreeing
  also give `confirmed`; a single third-party report gives `reported`.
* State lives in `confirm_state.json` (per symbol and quarter, with check times so each source is hit once a
  day); `confirmed_auto.csv` lists the automatic results; `confirm.log` has the search trail.
  `confirmed_dates.csv` remains the manual override and always wins.

Standalone check of the press-release search: `python3 confirm_dates.py ORCL "Oracle" 2026-09-08 2026-09-14`.

## Data sources

* State Street SPY daily holdings (xlsx) -> which companies. Share classes of one issuer are merged by CIK
  (Alphabet). Only the ranking is used; weights are not republished. If the download fails the cached copy
  is used, then the built-in `FALLBACK_TOP` list.
* SEC `company_tickers.json` -> ticker to CIK; `company_tickers_exchange.json` -> listing exchange for
  Google Finance symbols.
* Google Finance quote pages (vendor schedule) and Google News / Bing News RSS (company announcements),
  see the confirmation step above. These are ordinary web pages read with a browser User-Agent; if they
  ever block, the tool logs it and the estimates still publish.
* SEC EDGAR submissions API -> 8-K filings with Item 2.02 (results of operations) and 10-Q/10-K periods.
  EDGAR data is a US government work in the public domain; the SEC only asks for a contact in the
  User-Agent and under 10 requests/second. `run.sh` reads `EDGAR_CONTACT` from
  `~/.config/earnings-dates.env` so the email is not in the web root.

## Method (per company)

1. History. Each 10-Q/10-K defines a fiscal quarter. Its earnings release is the Item-2.02 8-K assigned to
   that quarter (the quarter that ended most recently before the 8-K's report date); where a quarter has
   several such 8-Ks (Tesla delivery reports, recasts) the one filed nearest the 10-Q/10-K wins. The 8-K's
   report date is the release day; its acceptance time in New York says before-open / after-close /
   weekend.
2. Target quarter. The fiscal quarter after the latest released one. An 8-K filed after the last 10-Q is
   counted as a release if its offset from the projected period end matches the company's history
   (10-Q pending). The period end is projected from the same fiscal quarter a year earlier: calendar
   filers keep the calendar date, 52/53-week filers add 364 or 371 days according to their month-anchor.
3. Candidate days. Each release in the last 12 quarters is a template: target period end plus that
   template's offset, snapped to that template's weekday. Weights: last 4 quarters 1.0, 5-8 0.6, 9-12 0.3;
   same fiscal quarter x2; when the annual quarter runs materially later than the others (AMZN, AVGO,
   Berkshire) the other type is dropped. Templates landing on the same day merge, and the merged weight is
   the share. Top share >= 70% -> one day; otherwise every day at >= 10% share (max 4). Days on a market
   holiday move to the previous business day and are flagged `*`.
4. Timing. Weighted mode of BMO / AMC / weekend over the same templates.

Confidence: high >= 85% share, medium >= 70%, low = multiple days shown.

## Walk-forward accuracy (these 30 companies, 796 quarters, 2020-2026)

| measure | result |
|---|---|
| quarters given a single day | 41% |
| single-day calls exactly right | 72% |
| actual day among the listed days | 76% |
| nearest listed day within 7 days | 99% |
| before/after-bell class right | 98% |
| projected fiscal period end right | 97% |
| average days listed | 2.15 |

Structural residual: some companies alternate weeks (NVDA 24 vs 31 days after quarter end, AVGO 32 vs 39)
or weekdays (MSFT, GOOGL, BAC), and nothing in history resolves that. Only the company's own announcement,
usually two to four weeks ahead, does. Put those in `confirmed_dates.csv`.

Reproduce: `python3 earnings_dates.py --backtest`

## Confirmed dates

`confirmed_dates.csv` (`ticker,date,timing`; timing BMO or AMC, optional). A row within 45 days of the
estimate for the next quarter replaces it and is shown as `confirmed`.

## Usage

```
python3 earnings_dates.py                                   # top 30, next quarter, table
python3 earnings_dates.py --quarters 2 --top 50
python3 earnings_dates.py --tickers AAPL,NVDA,BRK-B
python3 earnings_dates.py --csv out.csv --json out.json --html index.html
python3 earnings_dates.py --backtest
```

Python 3.9+, standard library only. Cache in `cache/` (submissions 6 h, older filing indexes 7 days,
holdings 12 h, ticker map 7 days).

## Known limits

* A holding-company reorganisation gives a new CIK and strands the history under the old one (Exxon,
  2026). Add the old CIK to `PREDECESSOR_CIKS`; the tool warns `history short` when this is likely.
* Foreign private issuers file 6-K without item codes and are skipped with a warning.
* The pending-10-Q rule needs the release offset within 10 days of the company's median; a company that
  changes cadence sharply will show its release only after the 10-Q arrives.
