Options traders price in an 'expected move' before every report. OpenClaw captures the actual overnight move after each earnings release — so you can see which stocks move more than expected and which disappoint volatility buyers.
When a company reports earnings after market close, the stock reacts before the next open. This reaction — from the prior day's close to the next day's open — is called the overnight earnings move. It's one of the most dramatic regular events in the market.
Options market makers price options before earnings to imply an expected percentage move. For example, if the implied move is ±8% but the stock only moves ±3%, options sellers made money. If the stock moved ±15%, options buyers won.
This is the key insight: every earnings day, the options market has already priced in a consensus "fair" move. Your job is to capture what actually happens and compare the two.
Tuesday Close Report (AMC) Wednesday Open
$180.00 → EPS Beat! → $198.00
= +10% overnight move
Options implied: ±7%
Actual move: +10%
→ Options BUYERS profited (stock exceeded expected move)
Over time, some stocks consistently exceed the expected move (good for option buyers). Others consistently disappoint (good for option sellers). Building this dataset gives you an edge:
You manually note each stock's close price before the report, then check the open price after. You jot down the percentage move. Easy to miss. No historical record. After 100 earnings, you have 100 sticky notes.
A script runs automatically. It:
After 2 years of data, you have 50+ data points per stock. You can now analyze patterns, spot anomalies, and make smarter decisions on the next earnings day.
You're not manually hunting through historical prices. You're not copying data into a spreadsheet. OpenClaw does the heavy lifting. You build the dataset. The dataset builds your edge.
Here's how you tell OpenClaw what to capture:
watchlist: tickers: [AAPL, MSFT, NVDA, AMZN, GOOGL] reaction: capture_window: "open_next_day" # Compare prior close to next open log_to_csv: true csv_path: "data/price_reactions.csv" compare_implied_move: true # Flag when actual > implied alerts: email: "you@example.com"
tickers: Which stocks to track. Add your holdings or watchlist.capture_window: Always "open_next_day" for earnings moves. Measures the gap from yesterday's close to today's open.log_to_csv: Save all data to a spreadsheet. Set to true unless you only want console output.csv_path: Where to save the data. Creates the data/ folder if it doesn't exist.compare_implied_move: If enabled, the script flags whether the actual move beat or missed the options-implied expected move. Useful for spotting under-priced volatility.email: (Optional) Send yourself a summary after each run.Here's the full script that captures post-earnings price reactions. Save it as earnings_reaction.py:
"""
earnings_reaction.py — OpenClaw Post-Earnings Price Reaction Logger
For each ticker that recently reported, this script:
1. Finds the earnings date from yfinance history
2. Gets the closing price the day before the report
3. Gets the opening price the day after the report
4. Calculates the overnight move percentage
5. Compares to the implied expected move (from options data)
6. Logs everything to CSV for building your dataset
Run the morning after earnings day for each ticker.
"""
import yaml
import yfinance as yf
import pandas as pd
from datetime import datetime, timedelta
import os
# ── Load config ──────────────────────────────────────────────────────────
with open("earnings-config.yaml") as f:
cfg = yaml.safe_load(f)
tickers = cfg["watchlist"]["tickers"]
csv_path = cfg["reaction"]["csv_path"]
os.makedirs("data", exist_ok=True)
# ── Helper: get earnings dates with actual EPS ──────────────────────────
def get_recent_earnings_dates(ticker_obj):
"""Returns list of dates where earnings were reported.
Note: yfinance.earnings_history is a beta feature and may not be available
for all tickers. If unavailable, we return an empty list and skip that ticker.
"""
try:
hist = ticker_obj.earnings_history
if hist is None or hist.empty:
return []
# Return last 4 earnings dates (most recent first)
return list(hist.index[:4])
except:
return []
# ── Helper: get price on a specific date ─────────────────────────────────
def get_price_on_date(ticker_obj, target_date, price_type="Close"):
"""Fetch closing or opening price on a given date.
Args:
ticker_obj: yfinance Ticker object
target_date: datetime.date to look for
price_type: "Close" or "Open"
Returns:
float price, or None if data not available
"""
start = target_date - timedelta(days=3)
end = target_date + timedelta(days=3)
try:
hist = ticker_obj.history(start=start, end=end)
if hist.empty:
return None
# Convert target_date to string and find matching row
target_str = target_date.strftime("%Y-%m-%d")
matching = hist[hist.index.strftime("%Y-%m-%d") == target_str]
if matching.empty:
return None
return matching[price_type].iloc[0]
except:
return None
# ── Main loop ────────────────────────────────────────────────────────────
print("📊 POST-EARNINGS PRICE REACTION LOG")
print("=" * 65)
results = []
for ticker in tickers:
stock = yf.Ticker(ticker)
print(f"\n {ticker}")
# Get all recent earnings dates for this ticker
earnings_dates = get_recent_earnings_dates(stock)
if not earnings_dates:
print(f" No earnings history available")
continue
for earnings_date in earnings_dates:
# Date before the report (where we measure close)
day_before = earnings_date - timedelta(days=1)
# Day after (where we measure open)
day_after = earnings_date + timedelta(days=1)
close_before = get_price_on_date(stock, day_before, "Close")
open_after = get_price_on_date(stock, day_after, "Open")
if close_before is None or open_after is None:
print(f" {str(earnings_date)[:10]}: Price data not available")
continue
# Calculate the overnight move as a percentage
move_pct = ((open_after - close_before) / close_before) * 100
direction = "⬆️ UP" if move_pct > 0 else "⬇️ DOWN"
# Get implied expected move (from options data if available)
# Note: yfinance doesn't directly expose pre-earnings implied moves.
# For now, we just log the actual move. You can integrate options data
# from your broker's API or a service like Alpaca.
implied_move = None
try:
# Placeholder for future integration with options APIs
pass
except:
pass
# Print summary to console
print(f" {str(earnings_date)[:10]}: {direction} {abs(move_pct):.1f}% "
f"(${close_before:.2f} → ${open_after:.2f})")
# Append to results for CSV export
results.append({
"Ticker": ticker,
"Earnings Date": str(earnings_date)[:10],
"Close Before": round(close_before, 2),
"Open After": round(open_after, 2),
"Move %": round(move_pct, 2),
"Direction": "UP" if move_pct > 0 else "DOWN",
"Logged": datetime.today().strftime("%Y-%m-%d"),
})
# ── Summary statistics ───────────────────────────────────────────────────
if results:
df = pd.DataFrame(results)
print("\n\n📊 AVERAGE MOVE BY TICKER")
print("=" * 50)
summary = df.groupby("Ticker").agg(
Avg_Move=("Move %", lambda x: round(x.abs().mean(), 1)),
Up_Count=("Direction", lambda x: (x == "UP").sum()),
Down_Count=("Direction", lambda x: (x == "DOWN").sum()),
Reports=("Move %", "count"),
).reset_index()
print(summary.to_string(index=False))
# Save to CSV
# mode="a" appends to existing file; header=not os.path.exists avoids duplicate headers
df.to_csv(csv_path, mode="a",
header=not os.path.exists(csv_path),
index=False)
print(f"\n💾 Logged to {csv_path}")
earnings-config.yaml to get the watchlist and output paths.get_recent_earnings_dates(): Fetches the last 4 earnings dates for a stock from yfinance.get_price_on_date(): Gets the open or close price on any specific date. Handles missing data gracefully.data/price_reactions.csv. File grows over time; never overwrites.Notice the triple-quoted docstrings and inline comments. They explain *why* we're doing each step, not just *what* we're doing. Beginners can follow along even if they've never written Python before.
When you run the script, here's what you'll see in the console:
📊 POST-EARNINGS PRICE REACTION LOG
=================================================================
AAPL
2025-10-31: ⬆️ UP 5.3% ($222.01 → $233.79)
2025-07-31: ⬆️ UP 1.9% ($218.27 → $222.43)
2025-05-01: ⬇️ DOWN 1.4% ($169.89 → $167.51)
2025-01-30: ⬆️ UP 3.2% ($229.87 → $237.21)
NVDA
2025-11-20: ⬆️ UP 4.9% ($141.02 → $147.93)
2025-08-28: ⬇️ DOWN 6.4% ($116.14 → $108.71)
2025-05-28: ⬆️ UP 9.3% ($93.65 → $102.36)
2025-02-26: ⬇️ DOWN 8.5% ($131.38 → $120.20)
📊 AVERAGE MOVE BY TICKER
==================================================
Ticker Avg_Move Up_Count Down_Count Reports
AAPL 3.0 3 1 4
NVDA 7.3 2 2 4
MSFT 4.1 3 1 4
AMZN 7.8 3 1 4
GOOGL 5.2 3 1 4
Your CSV file (data/price_reactions.csv) looks like this:
Ticker,Earnings Date,Close Before,Open After,Move %,Direction,Logged AAPL,2025-10-31,222.01,233.79,5.3,UP,2025-10-31 AAPL,2025-07-31,218.27,222.43,1.9,UP,2025-07-31 AAPL,2025-05-01,169.89,167.51,-1.4,DOWN,2025-05-01 NVDA,2025-11-20,141.02,147.93,4.9,UP,2025-11-20 ...
Over time, this CSV becomes your personal earnings reaction dataset. You can load it into Excel, pandas, or Tableau to spot trends.
After 2 years of data (8 reports per stock), you'll have a meaningful dataset. Here's what to do with it:
This is historical pattern data, not a crystal ball. Past moves don't predict future moves exactly. But they reduce your uncertainty. If a stock has moved 2–5% on the last 8 earnings, and the options market is pricing a 10% move, something is off. That's your edge.
The best insight comes from combining reaction data with surprise data. A stock that consistently beats *and* moves big = market respects the upside. A stock that misses *but* still moves small = market is skeptical. These patterns repeat.
You don't want to manually run this script every morning. Use cron to schedule it:
# Run every weekday at 9:15 AM (after market open) # Adjust the time based on your timezone 15 9 * * 1-5 cd /path/to/openclaw && python earnings_reaction.py
Better yet, run it right after earnings are typically reported (evening EST) to capture the overnight move immediately:
# Run every day at 9 PM EST (checks for new earnings) 0 21 * * * cd /path/to/openclaw && python earnings_reaction.py
Combine this with the earnings calendar from Part 1. On earnings days, the script runs and captures reactions. On non-earnings days, it finds nothing and exits cleanly. No errors. No noise.
Redirect cron output to a log file so you can review what ran:
0 21 * * * cd /path/to/openclaw && python earnings_reaction.py >> logs/earnings.log 2>&1
You now have historical price reaction data. In Part 5 (Daily Brief), we'll build a summary that ties together:
All in one morning email. That's where automation becomes truly valuable.