←Back to the notebook

Python Earnings Calendar Examples: Five Scripts for Earnings Season Data

Python earnings calendar and earnings data examples: five scripts that list the week's reports, rank them by importance, read the market-wide beat rate, measure one stock's reaction to past reports and pull last quarter's report. One pip install, one key, each runs as written.

SentiSense Team
SentiSense Team
October 8, 2026 · 10 min read

Earnings data in Python is pip install sentisense, one API key and the five scripts below, each of which runs as written. The season opens on Tuesday, 2026-10-13, when JPMorgan, Citigroup, Goldman Sachs and Wells Fargo report before the open; script one lists them. Each script is one method on the client, documented field by field in the Earnings Analysis API reference, and every output below was captured on 2026-10-08.

Setup

pip install sentisense
export SENTISENSE_API_KEY=ss_live_YOUR_KEY

The SDK needs Python 3.8 or later. Every script starts the same way:

import os
from sentisense import SentiSenseClient

client = SentiSenseClient(api_key=os.environ["SENTISENSE_API_KEY"])

1. Who reports next week?

get_earnings_calendar(week="next") returns the covered companies scheduled to report in the week after the current one, with the session, fiscal quarter, confirmation status and consensus EPS estimate. The script groups the week by day and marks an unconfirmed date with ?.

from collections import defaultdict

cal = client.get_earnings_calendar(week="next")
meta = cal.metadata
confirmed = sum(1 for e in cal.earnings if e.confirmed)
print(f"{meta.count} reports from {meta.windowStart} to {meta.windowEnd}, {confirmed} confirmed by the company")

by_day = defaultdict(list)
for e in cal.earnings:
    by_day[e.earningsDate].append(e)
for day in sorted(by_day):
    rows = sorted(by_day[day], key=lambda e: e.ticker)
    names = " ".join(e.ticker + ("" if e.confirmed else "?") for e in rows)
    print(f"{day}  {len(rows):2} reports  {names}")

season = client.get_earnings_calendar()      # no dates: this Monday through the end of the snapshot
m = season.metadata
print(f"\nseason view: {m.count} of {season.total_count or m.count} rows, "
      f"{m.windowStart} to {m.windowEnd}, preview={season.is_preview}")

Output on 2026-10-08, free key:

37 reports from 2026-10-12 to 2026-10-18, 22 confirmed by the company
2026-10-13   8 reports  AZZ C DPZ GS JNJ JPM UNH WFC
2026-10-14   7 reports  ASML? BAC BLK FAST HOMB MS STT
2026-10-15  17 reports  AA? BNY CMC? IBKR INDB? JBHT LBRT? MAN? MRSH PLD PNC? SCHW? SFNC SNA? TSM USB? WAFD
2026-10-16   5 reports  CFG? MTB? RF? TFC? TRV?

season view: 8 of 1110 rows, 2026-10-05 to 2026-10-11, preview=True

Run on 2026-10-08, the next week is 2026-10-12 to 2026-10-18, and a free key returns all 37 rows. The tiers part on the season view, the call with no dates: Free gets its first week and total_count reports the rest, PRO returns all 1,110 rows through 2026-12-06.

2. Which of those reports matter most?

Thirty-seven reports is a list, not a plan. get_ranked_earnings returns two sections ordered by importance, a 0 to 1 rank per row: the upcoming reports most worth watching and the recent ones that mattered most. This script asks for ten days ahead, covering the whole week of 2026-10-12, and fourteen behind.

board = client.get_ranked_earnings(upcoming_days=10, upcoming_limit=10, reported_days=14, reported_limit=6)

up = board.upcoming
print(f"upcoming {up.windowStart} to {up.windowEnd}: {len(up.rows)} of {up.totalInWindow} shown, preview={board.is_preview}")
for r in up.rows:
    est = f"consensus EPS {r.estimatedEps:.2f}" if r.estimatedEps is not None else "no consensus yet"
    status = "confirmed" if r.confirmed else "estimated"
    print(f"  {r.earningsDate}  {r.ticker:5} {r.earningsTime:12} {status:9}  {est:20}  importance {r.importance:.2f}")

rp = board.reported
print(f"\nreported {rp.windowStart} to {rp.windowEnd}: {len(rp.rows)} of {rp.totalInWindow} shown")
for r in rp.rows:
    if r.actualEps is None or r.estimateEps is None:
        eps = "awaiting consensus"
    else:                                   # surprisePct is None when the estimate is zero
        surprise = f" {r.surprisePct:+.1f}%" if r.surprisePct is not None else ""
        eps = f"EPS {r.actualEps:.2f} vs {r.estimateEps:.2f} est  {r.outcome}{surprise}"
    if r.movePct is not None:
        move = f"next close {r.movePct:+.2f}%"
    else:
        move = "reaction session pending" if r.reactionPending else "no measured reaction"
    print(f"  {r.reportDate}  {r.ticker:5} {eps:38}  {move}")

Output on 2026-10-08, PRO key:

upcoming 2026-10-08 to 2026-10-18: 10 of 38 shown, preview=False
  2026-10-15  TSM   before_open  confirmed  consensus EPS 4.42    importance 0.91
  2026-10-14  ASML  before_open  estimated  consensus EPS 12.07   importance 0.83
  2026-10-14  BAC   before_open  confirmed  consensus EPS 1.10    importance 0.79
  2026-10-13  JNJ   before_open  confirmed  consensus EPS 2.67    importance 0.78
  2026-10-14  MS    before_open  confirmed  consensus EPS 2.95    importance 0.77
  2026-10-13  GS    before_open  confirmed  consensus EPS 13.30   importance 0.76
  2026-10-13  C     before_open  confirmed  consensus EPS 2.65    importance 0.75
  2026-10-14  BLK   before_open  confirmed  consensus EPS 14.31   importance 0.73
  2026-10-15  BNY   before_open  confirmed  consensus EPS 2.26    importance 0.67
  2026-10-13  JPM   before_open  confirmed  consensus EPS 5.90    importance 0.66

reported 2026-09-24 to 2026-10-08: 6 of 24 shown
  2026-10-06  PENG  EPS 1.00 vs 0.77 est  BEAT +29.5%       next close +13.08%
  2026-10-08  PEP   EPS 2.34 vs 2.29 est  BEAT +2.2%        reaction session pending
  2026-10-07  APLD  EPS -0.01 vs -0.30 est  BEAT +96.6%     reaction session pending
  2026-10-06  NEOG  EPS 0.08 vs 0.05 est  BEAT +50.1%       next close -2.34%
  2026-10-06  STZ   EPS 3.74 vs 3.62 est  BEAT +3.3%        next close +2.35%
  2026-10-01  ACN   EPS 3.29 vs 3.18 est  BEAT +3.5%        next close +15.78%

All six reported rows beat the consensus; the next close is a separate measurement, higher after three, lower after NEOG, pending for PEP and APLD. Free returns the first three rows of each section with totalInWindow intact.

3. How often did companies beat, and what did the market do next?

get_earnings_statistics is the market-wide view: how many of the covered companies that reported in a window beat the consensus EPS, how many of those beats closed the reaction session lower, and the trailing 52-week baseline both are judged against. The default window is the last completed ISO week in New York time.

def pct(x):
    return f"{x:.0%}" if x is not None else "n/a"      # a rate with a zero denominator is None

def show(stats):
    d = stats.data
    w = d.window
    print(f"{w.key} ({w.startDate} to {w.endDate}): {d.eventsInWindow} reports, "
          f"{d.classifiedEvents} with both an estimate and a reported EPS")
    for name, o in (("beat", d.beat), ("miss", d.miss), ("inline", d.inline)):
        if o.withReaction:
            tail = f"{o.fell} of {o.withReaction} measured sessions closed lower, average move {o.averageMovePct:+.2f}%"
        else:
            tail = "no measured session"
        print(f"  {name:6} {o.count:5}  {pct(o.rate):>4}  {tail}")
    flag = "" if d.sufficientData else f"  ({d.insufficientDataReason}, floor {d.thresholds.minClassifiedEvents})"
    print(f"  fit to publish: {d.sufficientData}{flag}")
    b = d.baseline
    if b and b.sufficientData:
        print(f"  52-week baseline: beat rate {pct(b.beatRate)} over {b.classifiedEvents} reports, "
              f"{pct(b.beatsFellRate)} of beats closed lower")

show(client.get_earnings_statistics())                        # last completed ISO week, New York time
print()
show(client.get_earnings_statistics(window="trailing_52w"))

Output on 2026-10-08:

2026-W40 (2026-09-28 to 2026-10-04): 15 reports, 15 with both an estimate and a reported EPS
  beat      12   80%  5 of 12 measured sessions closed lower, average move +1.06%
  miss       3   20%  2 of 3 measured sessions closed lower, average move -1.07%
  inline     0    0%  no measured session
  fit to publish: False  (SAMPLE_BELOW_FLOOR, floor 30)
  52-week baseline: beat rate 74% over 4726 reports, 45% of beats closed lower

trailing_52w (2025-10-06 to 2026-10-04): 4770 reports, 4733 with both an estimate and a reported EPS
  beat    3498   74%  1348 of 2972 measured sessions closed lower, average move +1.24%
  miss    1126   24%  511 of 837 measured sessions closed lower, average move -1.70%
  inline   109    2%  52 of 90 measured sessions closed lower, average move -0.86%
  fit to publish: True

Read sufficientData before quoting anything. The week of 2026-09-28 was quiet ahead of the season, 15 reports against a floor of 30, so the endpoint flags its figures as not fit to publish. The long view is the number to use: 74% of 4,733 classified reports beat the consensus and 45% of the measured beats closed lower. Same output on every tier.

4. How has JPMorgan moved on its past reports?

get_earnings_reactions returns the signed close-to-close move of the session that traded on each of up to twelve past reports. The session is read from the earnings 8-K timestamp, so a before-open reporter like JPMorgan moves on the report date itself; timing is None where the session had to be inferred.

import statistics

history = client.get_earnings_reactions("JPM")
print(f"{history.ticker} as of {history.asOf}: {len(history.reactions)} measured reports")
for r in history.reactions:
    print(f"  {r.reportDate}  {r.timing or 'inferred':8} {r.priorClose:8.2f} -> {r.nextClose:8.2f}  {r.movePct:+6.2f}%")

observed = [r.movePct for r in history.reactions if r.timing]      # session read from the 8-K timestamp
if observed:
    higher = sum(1 for m in observed if m > 0)
    lower = sum(1 for m in observed if m < 0)
    print(f"\n{len(observed)} with an observed session: {higher} closed higher, {lower} lower, "
          f"median absolute move {statistics.median(abs(m) for m in observed):.2f}%")

Output on 2026-10-08:

JPM as of 2026-10-08: 12 measured reports
  2026-07-14  BMO        334.53 ->   342.89   +2.50%
  2026-04-14  BMO        313.68 ->   311.12   -0.82%
  2026-01-13  BMO        324.49 ->   310.90   -4.19%
  2025-10-14  BMO        307.97 ->   302.08   -1.91%
  2025-07-15  BMO        288.70 ->   286.55   -0.74%
  2025-04-11  BMO        227.11 ->   236.20   +4.00%
  2025-01-15  BMO        247.47 ->   252.35   +1.97%
  2024-10-11  BMO        212.84 ->   222.29   +4.44%
  2024-07-12  BMO        207.45 ->   204.94   -1.21%
  2024-04-12  inferred   195.43 ->   182.79   -6.47%
  2024-01-12  inferred   170.30 ->   169.05   -0.73%
  2023-10-13  inferred   145.81 ->   148.00   +1.50%

9 with an observed session: 4 closed higher, 5 lower, median absolute move 1.97%

The summary line leaves the three inferred rows out: four of nine observed sessions closed higher, five lower, median absolute move 1.97%. Pair the series with expectedMove1d from the options summary endpoint for the 2026-10-13 report. Full series on every tier.

5. What did the company say last quarter?

get_earnings_summaries returns one object per stored fiscal quarter, newest first: the headline, the KPI cards that matter for that company with year-over-year change, guidance as management phrased it, and a summary of the earnings call. JPMorgan's latest is Q2 2026; the Q3 quarter typically appears within 48 hours of the 2026-10-13 report. The script branches on is_preview: the tiers return different shapes, not a shorter list.

import textwrap

report = client.get_earnings_summaries("JPM", limit=4)
print(f"{report.total_count} stored quarter(s), preview={report.is_preview}")
for q in report.data:
    print(f"\n{q.fiscalPeriod}, reported {q.reportDate}, from the {(q.source or 'unknown').replace('_', ' ')}")
    print(textwrap.fill(q.headline, 100, initial_indent="  ", subsequent_indent="  "))
    for k in q.kpiHighlights:
        print(f"  {k.label:30} {k.value:<32} {k.yoy or ''}")
    if report.is_preview:
        print(f"  {q.kpiHighlightCount} KPI cards in the full quarter; call summary stored: {q.hasTranscript}")
        print(f"  guidance: {q.guidanceDirection or 'none'}; call topics: {', '.join(q.transcriptTopics[:4])}")
    else:
        print(f"  release guidance: {q.guidance or 'none in the release'}")
        for h in q.transcriptHighlights:
            print(f"  call: {h.label:30} {h.value}")

Output on 2026-10-08, PRO key:

1 stored quarter(s), preview=False

Q2 2026, reported 2026-07-14, from the press release
  JPMorgan reported Q2 2026 diluted EPS of $7.70, well above the roughly $5.55-5.59 analyst
  consensus, lifted by a $5.6 billion pretax gain from the Visa share exchange and equity investment
  gains; EPS excluding those significant items was $6.14.
  Diluted EPS                    $7.70                            +47%
  Managed Net Revenue            $58.0B                           +27%
  Net Interest Income            $25.6B                           +10%
  Provision for Credit Losses    $2.5B                            -12%
  ROTCE                          29% (23% ex. significant items)  
  release guidance: none in the release
  call: Net income / EPS               $16.9B / $6.14 (ROTCE 23%)
  call: Revenue growth ex-items        +15% YoY
  call: IB fees / Equities markets revenue +30% YoY / +86% YoY
  call: FY26 NII ex-Markets guide      ~$96.5B (raised from $95B)
  call: FY26 adjusted expense guide    ~$107.5B (+$2.5B)
  call: CET1 ratio                     14.1% (-20bps QoQ)
  call: Card net charge-off rate guide ~3.2%
  call: Quarterly dividend (effective Q3) $1.65/share

On a free key the same script prints the headline, two of the five KPI cards without their year-over-year change, and the shape of the rest:

  Diluted EPS                    $7.70                            
  Managed Net Revenue            $58.0B                           
  5 KPI cards in the full quarter; call summary stored: True
  guidance: RAISED; call topics: Net income / EPS, Revenue growth ex-items, IB fees / Equities markets revenue, FY26 NII ex-Markets guide

The release carried no updated full-year guidance, so the preview's RAISED classification reads the call summary. PRO returns the language itself, the full-year net interest income ex-Markets guide raised to about $96.5B from $95B, and links the SEC filings behind it.

What the free key returns

Every result but the reaction history is a PreviewResult: .is_preview says whether the tier shaped it, .total_count how much sits behind it, .upgrade the link to lift it. Free and PRO, by script:

  • Calendar: one week per call on Free; about 60 days forward on PRO.
  • Ranked board: three rows per section on Free; every row up to your limits on PRO.
  • Quarter report: the latest quarter in preview shape on Free; every stored quarter on PRO.
  • Statistics and reaction history: full on every tier.
  • Limits: 1,000 requests a month and 30 a minute on Free; unlimited monthly at 300 a minute on PRO. The five scripts make seven requests.

An AI agent gets the calendar without the script: npx -y sentisense earnings in the sentisense CLI or the earnings calendar tool on the MCP connector; the stock-earnings-analysis skill walks all five. Comparing providers first: the best earnings calendar APIs in 2026. Same shape, other verticals: Python stock sentiment examples and Python political trading examples.

Ready to run script one? Get a free API key → and paste it into the export line above.

Beat rates and past price moves are historical measurements, not forecasts. This article is educational and is not financial advice.