Skip to main content
EARNINGS AUTOMATION · PART 3 OF 5

Earnings Surprise Tracker

The beat rate is one of the most underrated edges in systematic investing. OpenClaw logs every earnings beat and miss automatically — so over time you know exactly which companies consistently exceed expectations.

Beat/Miss Logger Historical Data CSV Export Free via yfinance
⚠️ Disclaimer: Historical earnings surprises are for research purposes only. Past surprise rates do not predict future results. Not financial advice.

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:

Surprise % = ((Actual EPS − Estimated EPS) / |Estimated EPS|) × 100 Example: Apple (AAPL) estimated EPS: $1.55 Apple reported EPS: $1.70 Surprise: +9.7% beat

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: true

Field Explanations

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}")
Heavy Comments: This script is designed to be beginner-friendly. Every section has a comment explaining what's happening. Read the comments line by line if you're new to Python. Ask Claude or ChatGPT if a line confuses you.

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% 8

Reading 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.py

How to Install Cron (macOS/Linux)

Open your terminal and type:

crontab -e

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

← Part 2: Estimates Part 4: Price Reaction →