earnings-session
Earnings calendars tag each release BMO (before the market opens) or AMC (after it closes). When the tag is wrong, or missing and treated as BMO, a backtest that buys at the open buys before the news is out, and the return it books is look-ahead. earnings-session checks the tag against SEC EDGAR. It finds the company's earnings 8-K, reads the time EDGAR accepted it, and tells you which trading session that time falls in and whether your backtest entered before it.
I wrote it after running this check by hand on my own post-earnings-drift backtest. About half of the backtest's trades came from events with no timing label, and the backtest entered them at the open as if they were BMO. In a sample of 95 of those events with an earnings 8-K, 59% of the 8-Ks were accepted at or after the close of the entry day. Once every event was moved to the session its 8-K supports, net alpha per trade fell from 1.21% to about 0.25%.
Install
pip install earnings-session
Python 3.10 or later. SEC asks every automated client to say who it is, so set a User-Agent with your name and a contact address before the first call:
export EARNINGS_SESSION_USER_AGENT="Your Name you@your-domain.com"
or pass --user-agent on each command. Without one the tool exits and prints the format SEC expects.
Use
One event
earnings-session classify --ticker OSPN --date 2019-10-29 --label bmo
{
"ticker": "OSPN",
"event_date": "2019-10-29",
"vendor_label": "bmo",
"entry_used": "2019-10-29",
"rule": "item_2.02",
"confidence": "high",
"cik": 1044777,
"company": "OneSpan Inc.",
"form": "8-K",
"accession": "0001558370-19-009352",
"accepted_et": "2019-10-29T20:27:07-04:00",
"api_value": "2019-10-30T00:27:07.000Z",
"zone_convention": "utc",
"session": "post_close",
"verdict": "after_entry",
"edgar_label": "amc",
"label_agrees": false,
"notes": ["..."]
}
(Output shortened.) The release was labelled BMO, but its 8-K was accepted at 20:27 ET. A backtest that bought at the 2019-10-29 open bought eleven hours before the filing; the stock opened 21% higher the next morning.
--entry-date sets the backtest entry (the position is opened at that day's open). Without it, the entry follows the label: the event day's open for bmo and dmh, the next trading day's open for amc, and the event day's open when there is no label.
A file of events
earnings-session batch events.csv timed.csv
Input columns: ticker, event_date (YYYY-MM-DD), and optionally entry_date and vendor_label (bmo, amc, dmh; anything else counts as no label). The output keeps the input columns and adds:
| column | meaning |
|---|---|
cik, company, form, accession |
the filing used |
accepted_et |
EDGAR acceptance time in US/Eastern, from the filing's index page |
api_value |
acceptanceDateTime as the submissions API gave it |
zone_convention |
utc, et_as_utc (Eastern time labelled Z), mismatch, or unverified (no index page) |
session, session_date |
pre_open, intraday or post_close, and the trading day it refers to |
edgar_label |
what the filing supports relative to the event date: bmo, dmh, amc, prior_day, later_day |
label_agrees |
edgar_label equals the vendor label |
entry_used, verdict |
the entry open compared against, and before_entry, entry_session or after_entry |
rule, confidence |
how the filing was found (item_2.02, other_8k, periodic, 6k_proxy) and high / medium / low |
reason |
why an event stayed unresolved (no_cik, fts_ambiguous, fts_unconfirmed, no_filing_in_window, ...) |
notes |
anything a reviewer should look at |
Audit a vendor's labels
earnings-session audit timed.csv # text
earnings-session audit timed.csv --json # the same report as JSON
== 8-K Item 2.02: n = 5 ==
vendor label x EDGAR acceptance session (relative to the event date)
bmo dmh amc prior_day later_day
bmo 0 0 2 0 0
amc 0 0 2 0 0
(none) 0 0 1 0 0
verdict vs entry open (Wilson 95%)
before_entry 2/5 = 40.0% (11.8%-76.9%)
entry_session 0/5 = 0.0% (0.0%-43.4%)
after_entry 3/5 = 60.0% (23.1%-88.2%)
by vendor label
bmo agrees 0/2 = 0.0% (0.0%-65.8%); after_entry 2/2 = 100.0% (34.2%-100.0%)
amc agrees 2/2 = 100.0% (34.2%-100.0%); after_entry 0/2 = 0.0% (0.0%-65.8%)
Rows found through other 8-Ks, 10-Qs or 10-Ks, and 6-K proxies, are reported in their own blocks and never mixed into the Item 2.02 numbers. If the input has no verdict column, audit classifies it first.
From Python
from earnings_session import Classifier
with Classifier() as c: # reads EARNINGS_SESSION_USER_AGENT
r = c.classify("OSPN", "2019-10-29", label="bmo")
print(r.session, r.verdict, r.accepted_et) # post_close after_entry 2019-10-29 20:27:07-04:00
r.to_dict() # the JSON above
The CLI calls the same Classifier.classify. For a single call there is also earnings_session.classify(...).
How it decides
- Issuer. The ticker is looked up in SEC's current ticker list. Tickers that are no longer listed (delisted, renamed, acquired) go through EDGAR full-text search, and a candidate is accepted only with evidence that it really is that ticker's issuer. Otherwise the event stays unresolved, with the reason.
- Window. Filings accepted (by US/Eastern date) from the trading day before the event date to the trading day after the entry date, or after the event date when no entry date is given.
- Filing. The original 8-K with Item 2.02 nearest the event date; 8-K/A amendments are skipped. Without one: the nearest other 8-K, then 10-Q or 10-K. A foreign issuer that files only 6-Ks gets its nearest 6-K, marked low confidence.
- Time. The "Accepted" time on the filing's index page, which is US/Eastern. The API's
acceptanceDateTimeis kept next to it. - Session. Before 09:30 ET is
pre_open, 09:30 to the close isintraday, the close and later ispost_close. The close is 16:00, or 13:00 on early-close days. Weekend and holiday acceptances count aspost_closeof the previous trading day. The calendar is NYSE's (exchange_calendarsXNYS), so Columbus Day and Veterans Day are trading days. - Verdict.
before_entryif accepted before the entry-day open,entry_sessionif during the entry-day session,after_entryif at the close or later.
METHOD.md has the full rules and the reasons behind them.
Limits
- An 8-K acceptance is an upper bound on the release time. A company can put out its press release first and file the 8-K later, never the other way round. So
before_entryis conclusive: the news was public when you bought.after_entrymeans look-ahead unless the release came a full session before its 8-K. - A late 8-K makes a real pre-open release look like intraday or post-close. In my check of 39 events labelled BMO, 34 had their 8-K before the entry open, 2 were evening releases the label got wrong, and 3 had an 8-K the next morning (a late filing or a wrong date; EDGAR alone can't tell which).
- A 6-K has no item code. For foreign issuers the tool takes the 6-K nearest the event date, which may be a different announcement. These rows are
lowconfidence and audited separately. - Tickers get reused. SEC's ticker list gives today's owner of a ticker. When the window's filings name a different company under that ticker, that company is tried first and the row says so in
notes. A ticker the issuer used only in the past can be missed if its press releases never print it next to an exchange name. Checkcompanywhen it matters. - Other 8-Ks, 10-Qs and 10-Ks are looser bounds. Small issuers sometimes announce results without an Item 2.02. The fallback filing may come days after the release, so these rows are
mediumorlowconfidence. - The API's time zone is not stable. In September 2026, 43 of 228
acceptanceDateTimevalues I checked were US/Eastern wall-clock times labelled "Z", all from issuers no longer in SEC's ticker list. Read as UTC, they put a 16:18 filing at 12:18. On 2026-10-27 I rechecked seven of them, and all seven came back as real UTC. The tool reads the index page either way and records which convention the API used. - The window is narrow. A vendor date off by more than one trading day finds no filing, and the event comes back
no_filing_in_window. - Calendar. Early closes are the ones
exchange_calendarslists. It has no early close on 2026-07-02, and published holiday tables disagree on that day. Dates must fall between 1995 and about a year from today.
Roadmap
- Price-reaction timing for events EDGAR can't place: compare the event-day open gap and volume with the next day's, calibrated on EDGAR-timed events. Calling a release BMO when it was AMC brings back look-ahead, so the rule only says BMO when out-of-sample precision clears a Wilson lower bound of 0.95. On my data no threshold cleared it, which is why it isn't in v0.1.
SEC access
Default 5 requests per second across www.sec.gov, data.sec.gov and efts.sec.gov (SEC's limit is 10; --max-rps changes it). Rate-limit and server errors are retried with back-off; a 403 stops the run. Responses are cached in ~/.cache/earnings-session (or $XDG_CACHE_HOME/earnings-session, or --cache-dir). The ticker list, submissions and search results are refetched after 24 hours; filing index pages are kept. --no-cache turns the cache off. Expect about 3 requests per event; the ticker list is fetched once per run.
Development
pip install -e ".[dev]"
ruff check src tests scripts && ruff format --check src tests scripts
mypy --strict
pytest # offline, uses recorded SEC responses in tests/fixtures
pytest -m network # live checks against sec.gov; needs EARNINGS_SESSION_USER_AGENT
scripts/record_fixtures.py re-records the fixtures. SEC EDGAR data is in the public domain.
License
MIT
Metadata
Release files for earnings-session 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| earnings_session-0.1.0.tar.gz | 50.1 kB | With provenance | Details for earnings_session-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| earnings_session-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for earnings_session-0.1.0-py3-none-any.whl |
Total release size: 78.9 kB
Release files / earnings_session-0.1.0.tar.gz
| Download URL | earnings_session-0.1.0.tar.gz |
|---|---|
| Size | 50.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
80f1be3dc9a3a59e66e21dd9b49c9c2a88df2ae5a776bc65edeec0430b708a3b
|
|
BLAKE2b-256 checksum How to use checksums |
09fbe7a76f494285c21bf24ecb4c079af4cd1aad7b23cd233f0350446c9f4617
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logRelease files / earnings_session-0.1.0-py3-none-any.whl
| Download URL | earnings_session-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5289e0535ab9f3593f805f673c8c472b61e90214aeee20ca96235a0efec681af
|
|
BLAKE2b-256 checksum How to use checksums |
89d2fbe84a1dfb537ed1d0f1b9d03a78ca4c8adb7f2ace74505ba82d22dce02c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logSecurity for earnings-session 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for earnings_session-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 27, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.