Skip to main content
EARNINGS AUTOMATION · PART 4 OF 5

Post-Earnings Price Reaction Logger

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.

Overnight Move
Expected vs Actual
CSV Dataset
Options Context
Historical data, research only. Price reaction data is historical and for research only. Past moves do not predict future moves. Not financial advice.

What Is the Post-Earnings Move?

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.

The Expected Move

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.

A Concrete Example

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)

Why This Matters

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:

What OpenClaw Captures

Without Automation

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.

With OpenClaw

A script runs automatically. It:

  1. Pulls the closing price the day before earnings
  2. Pulls the opening price the day after earnings
  3. Calculates the overnight percentage move
  4. Optionally compares to the expected move (from options implied volatility)
  5. Logs everything to a CSV file

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.

The OpenClaw Advantage

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.

YAML Configuration

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"

Field Explanations

The Python Script

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}")

What This Script Does (Line by Line)

Heavy Comments Included

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.

Sample Output

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.

Building Your Edge Over Time

The 2-Year Mark

After 2 years of data (8 reports per stock), you'll have a meaningful dataset. Here's what to do with it:

Honest Reality Check

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.

Combine with Part 3: Surprise

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.

Automate with Cron

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

Using the Calendar from Part 1

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.

Pro Tip: Keep Logs

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

Next Steps

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.