What Is an Earnings Surprise?
An earnings surprise happens when a company reports results that are different from what analysts expected. If the company beats the estimate, that's a positive surprise. If it misses, that's a negative surprise. The percentage difference between actual and estimated results is called the surprise factor.
Here's the formula, nice and simple:
Why Companies Often Beat
One of the dirty little secrets of corporate earnings is something called management sandbagging. Companies intentionally set low guidance so that beating the estimate becomes easier. It's not cheating — it's just smart investor relations. If you lower expectations and then beat them, the stock tends to pop. If you raise expectations and miss, it tanks.
This is a real phenomenon. Historically, approximately 70% of S&P 500 companies beat EPS estimates each quarter. That's not random. That's deliberate.
Here's what makes this valuable for you: If you can identify which companies always beat and which frequently miss, you know something about their guidance discipline. Companies that beat consistently tend to be conservative estimators. Companies that miss tend to either be wildly optimistic or have operational problems. Over time, that pattern tells you a lot about management quality.
How OpenClaw Builds Your Surprise Database
The Manual Way
Without OpenClaw, here's what you'd have to do: Each quarter, after a company reports, you manually record the actual EPS, find the estimate you saved from Part 2, calculate the surprise percentage by hand, and log it in a spreadsheet. After 2 years, if you were diligent about it, you might have useful data for 15–20 companies. But if you missed a quarter or forgot to record something, your dataset is now compromised.
The OpenClaw Way
With OpenClaw, here's what happens automatically: After every earnings date, your script runs in the background. It fetches the company's reported EPS from yfinance, compares it to the estimated EPS you saved in Part 2, calculates the surprise percentage, flags any misses that exceed your threshold, and logs everything to CSV. You don't have to touch it.
After one year, you have real data for your entire watchlist. After two years, you can identify patterns: which companies beat consistently, which miss, which have volatile surprises, and which are predictable. That's an edge.
YAML Configuration
Start with your OpenClaw config file. Add this section under your main watchlist:
watchlist:
tickers: [AAPL, MSFT, NVDA, AMZN, GOOGL]
surprise:
log_history: true
flag_threshold: 5.0 # % beat/miss worth flagging in alert
csv_path: "data/earnings_history.csv"
include_revenue_surprise: true
alerts:
email: "you@example.com"
daily_digest: trueField Explanations
log_history: true— Enable logging of all earnings surprises to CSVflag_threshold: 5.0— Only alert you if surprise exceeds ±5.0%. Beats/misses below 5% are quietcsv_path— Where to save your earnings history. OpenClaw will create the directory if it doesn't existinclude_revenue_surprise: true— Also track revenue vs. estimate, not just EPS. Optional but usefulalerts.email— Send flagged earnings to this address the morning after report datedaily_digest: true— Aggregate alerts into one daily email instead of individual notifications
The Python Script: earnings_surprise.py
This script does the heavy lifting. Run it the morning after each earnings date, or on a weekly schedule if you want to batch them. It fetches earnings history from yfinance, calculates surprises, logs everything to CSV, and prints a summary.
"""
earnings_surprise.py — OpenClaw Earnings Surprise Tracker
After a company reports, this script fetches actual vs. estimated EPS,
calculates the surprise percentage, and logs everything to CSV.
Run this the morning after each earnings report date.
"""
import yaml
import yfinance as yf
import pandas as pd
from datetime import datetime
import os
# ── Load config ────────────────────────────────────────────────────────────
with open("earnings-config.yaml") as f:
cfg = yaml.safe_load(f)
tickers = cfg["watchlist"]["tickers"]
flag_threshold = cfg["surprise"]["flag_threshold"]
csv_path = cfg["surprise"]["csv_path"]
os.makedirs("data", exist_ok=True)
# ── Fetch earnings history ─────────────────────────────────────────────────
print("📈 EARNINGS SURPRISE TRACKER")
print("=" * 60)
all_results = []
for ticker in tickers:
stock = yf.Ticker(ticker)
try:
# earnings_history contains actual vs. estimate for past quarters
history = stock.earnings_history
if history is None or history.empty:
print(f" {ticker}: No earnings history available")
continue
# Keep the last 8 quarters (2 years of data)
recent = history.head(8).copy()
beats = 0
misses = 0
print(f"\n {ticker} — Last {len(recent)} quarters:")
print(f" {'Quarter':<12} {'EPS Est':>9} {'EPS Act':>9} {'Surprise':>10} {'Result':>8}")
print(f" {'-'*12} {'-'*9} {'-'*9} {'-'*10} {'-'*8}")
for date, row in recent.iterrows():
eps_est = row.get("epsEstimate", None)
eps_act = row.get("epsActual", None)
if eps_est is None or eps_act is None:
continue
try:
surprise_pct = ((eps_act - eps_est) / abs(eps_est)) * 100
result = "✅ BEAT" if surprise_pct > 0 else "❌ MISS"
flag = " ⚠️" if abs(surprise_pct) >= flag_threshold else ""
if surprise_pct > 0:
beats += 1
else:
misses += 1
quarter = date.strftime("%Y-Q%m") if hasattr(date, 'strftime') else str(date)[:7]
print(f" {quarter:<12} {eps_est:>9.2f} {eps_act:>9.2f} {surprise_pct:>9.1f}% {result}{flag}")
all_results.append({
"Ticker": ticker,
"Quarter": str(date)[:10],
"EPS Estimate": eps_est,
"EPS Actual": eps_act,
"Surprise %": round(surprise_pct, 2),
"Result": "BEAT" if surprise_pct > 0 else "MISS",
"Logged": datetime.today().strftime("%Y-%m-%d"),
})
except Exception as e:
continue
total = beats + misses
beat_rate = (beats / total * 100) if total > 0 else 0
print(f"\n → Beat Rate: {beats}/{total} quarters = {beat_rate:.0f}%")
except Exception as e:
print(f" {ticker}: Error — {e}")
# ── Beat rate summary ──────────────────────────────────────────────────────
print("\n\n📊 BEAT RATE SUMMARY")
print("=" * 40)
if all_results:
df = pd.DataFrame(all_results)
summary = df.groupby("Ticker").apply(
lambda x: pd.Series({
"Beat Rate %": round((x["Result"] == "BEAT").mean() * 100, 0),
"Avg Surprise %": round(x["Surprise %"].mean(), 1),
"Quarters": len(x),
})
).reset_index()
print(summary.to_string(index=False))
# Save to CSV (append mode so history accumulates over time)
df.to_csv(csv_path, mode="a",
header=not os.path.exists(csv_path),
index=False)
print(f"\n💾 Logged to {csv_path}")Sample Output
Run the script and you'll see output like this:
📈 EARNINGS SURPRISE TRACKER
============================================================
AAPL — Last 8 quarters:
Quarter EPS Est EPS Act Surprise Result
------------ --------- --------- ---------- --------
2025-Q10 1.43 1.64 +14.7% ✅ BEAT ⚠️
2025-Q07 1.34 1.40 +4.5% ✅ BEAT
2025-Q04 2.09 2.18 +4.3% ✅ BEAT
2024-Q10 1.55 1.64 +5.8% ✅ BEAT ⚠️
2024-Q07 1.35 1.40 +3.7% ✅ BEAT
2024-Q04 2.10 2.18 +3.8% ✅ BEAT
2023-Q10 1.39 1.46 +5.0% ✅ BEAT ⚠️
2023-Q07 1.19 1.26 +5.9% ✅ BEAT ⚠️
→ Beat Rate: 8/8 quarters = 100%
📊 BEAT RATE SUMMARY
========================================
Ticker Beat Rate % Avg Surprise % Quarters
AAPL 100% 6.0% 8
MSFT 88% 4.2% 8
NVDA 88% 22.1% 8
AMZN 75% 3.8% 8
GOOGL 88% 5.5% 8Reading the Data
Now you have the numbers. Here's what they mean:
Beat Rate > 80%
A beat rate above 80% means the company consistently exceeds analyst expectations. This is often because management is conservative with guidance (sandbagging). Keep in mind: the market often prices this consistency in. If AAPL has beaten for 8 straight quarters, traders already know to expect it. This can actually make surprises less powerful over time.
NVDA's 22% Average Surprise
NVDA's average surprise of 22% is wild. That means analysts underestimated NVDA's growth by more than 20% on average. This tells you that the analyst consensus has been consistently wrong about NVDA's trajectory. The growth was faster and more sustained than expected. This kind of consistent surprise is rare and valuable to track.
Wide Surprise Range
If one company beats by 15% one quarter and misses by 8% the next, that's unpredictable. Wide surprise ranges indicate either volatile business conditions or inconsistent guidance. Higher volatility = higher risk around earnings dates.
Use This Data Strategically
Don't just look back. Look ahead. Before the next earnings date, review your historical surprise data. If MSFT has beaten 7 out of 8 quarters by 4% on average, go into earnings day with that expectation baked in. This gives you confidence about what "normal" looks like, so you can spot when something changes.
Cron Setup: Run It Automatically
You don't want to remember to run this script manually. Set it up on a schedule with cron. This command runs the script at 8 AM every Tuesday (adjust the day based on your earnings calendar from Part 1):
# Run the day after each earnings date, or once weekly
# This example: Tuesday at 8 AM local time
0 8 * * 2 cd /path/to/openclaw && python earnings_surprise.py
# To run daily instead (safer, script will just skip if no new data):
0 8 * * * cd /path/to/openclaw && python earnings_surprise.pyHow to Install Cron (macOS/Linux)
Open your terminal and type:
crontab -eThis opens an editor. Paste the cron line above, save, and exit. Your script will now run automatically. Check your email the next morning if you set up alerts in the YAML config.