Skip to main content

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

  1. 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.
  2. 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.
  3. 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.
  4. Time. The "Accepted" time on the filing's index page, which is US/Eastern. The API's acceptanceDateTime is kept next to it.
  5. Session. Before 09:30 ET is pre_open, 09:30 to the close is intraday, the close and later is post_close. The close is 16:00, or 13:00 on early-close days. Weekend and holiday acceptances count as post_close of the previous trading day. The calendar is NYSE's (exchange_calendars XNYS), so Columbus Day and Veterans Day are trading days.
  6. Verdict. before_entry if accepted before the entry-day open, entry_session if during the entry-day session, after_entry if 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_entry is conclusive: the news was public when you bought. after_entry means 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 low confidence 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. Check company when 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 medium or low confidence.
  • The API's time zone is not stable. In September 2026, 43 of 228 acceptanceDateTime values 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_calendars lists. 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)

Source distribution for earnings-session 0.1.0
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)

Table of built distributions (wheels) for earnings-session 0.1.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page