I Connected Weather Forecasts to Earnings Calls and Found Signals Nobody Else Was Watching
Weather Creates Earnings Surprises the Market Is Slow to Price
Every hurricane season, the same thing happens: a storm hits the Gulf Coast, refineries shut down, and energy stocks move — but only after the quarterly earnings miss. Analysts rarely update models mid-quarter for weather events, even when crop damage, refinery shutdowns, or insurance claims are already visible in real-time NWS data.
I built a pipeline that connects live weather alerts to the stocks most exposed to them, scores the exposure, and cross-references with earnings calls, news, and SEC filings to identify tradeable signals before the market catches up.
The Thesis
Weather impacts at least four major equity sectors, and the market systematically underreacts to it:
| Sector | Mechanism | Example |
|---|---|---|
| Agriculture | Crop yields, equipment demand | Drought in Iowa crushes Deere guidance |
| Energy | Refinery shutdowns, demand spikes | Gulf hurricane takes 20% of refining offline |
| Retail | Foot traffic, seasonal demand | Blizzard kills Q4 same-store sales |
| Insurance | Catastrophe losses, reserve charges | Hurricane season drives Allstate earnings miss |
The information advantage is simple: NWS alerts are free, real-time, and public. Earnings revisions are slow. The gap between "storm hits" and "analyst downgrades" is where the alpha lives.
Step 1: Pull Severe Weather Alerts
Start with what's happening right now. NWS alerts include severity, affected areas, and event type.
curl "https://api.gettrawl.com/api/weather/alerts?severity=Severe"Step 2: Check the Forecast
For specific weather-sensitive locations, pull the 7-day forecast. This example uses coordinates for the Iowa farm belt.
curl "https://api.gettrawl.com/api/weather/forecast?lat=41.88&lon=-93.10"Step 3: Cross-Reference Earnings
Has Deere mentioned weather on recent earnings calls? Companies that acknowledge weather risk in transcripts are historically more exposed.
curl "https://api.gettrawl.com/api/earnings/search?ticker=DE"Step 4: Check News Signals
Is there already weather-related coverage for the affected stocks?
curl "https://api.gettrawl.com/api/news/search?q=Deere+weather+drought"The Sector-to-Ticker Mapping
The pipeline tracks 25+ weather-sensitive tickers across five sectors, each with HQ coordinates for localized weather lookups:
Agriculture: DE (Deere), ADM (Archer-Daniels-Midland), BG (Bunge), CORN (Teucrium Corn Fund), MOS (Mosaic), CF (CF Industries)
Energy: XOM (Exxon), CVX (Chevron), COP (ConocoPhillips), DVN (Devon Energy), HAL (Halliburton), SLB (Schlumberger)
Utilities: NEE (NextEra), DUK (Duke Energy), SO (Southern Company), AEP (American Electric Power)
Retail: WMT (Walmart), HD (Home Depot), LOW (Lowe's)
Insurance: ALL (Allstate), TRV (Travelers), PGR (Progressive), CB (Chubb)
Alert-to-sector matching uses geographic rules: corn belt states (Iowa, Illinois, Indiana, etc.) map to agriculture; Gulf Coast states (Texas, Louisiana, Florida) map to energy; major metros map to retail. Catastrophe-class events (hurricanes, tornadoes, wildfires) always map to insurance.
The Scoring Model
Each ticker gets a deterministic exposure score (0-100) from four equal-weight components:
Alert Severity (0-25): Based on NWS severity classification. Extreme = 25, Severe = 19, Moderate = 12, Minor = 6.
Historical Sensitivity (0-25): Count of weather-related keywords (drought, hurricane, freeze, etc.) found in earnings transcripts. More mentions = the company acknowledges weather risk more frequently = more historically exposed.
News Signal (0-25): Volume of recent weather-related news articles for the ticker. More coverage = the market is starting to pay attention.
Sector Base Risk (0-25): Inherent weather sensitivity by sector. Insurance (22) and agriculture (20) score highest. Retail (10) scores lowest.
Exposure labels: CRITICAL (75+), HIGH (50+), MODERATE (30+), LOW (under 30).
The Full Pipeline
from trawl import TrawlClient
client = TrawlClient()
WEATHER_KEYWORDS = [
"weather", "storm", "hurricane", "drought", "flood",
"freeze", "wildfire", "tornado", "blizzard", "heat wave",
]
SECTOR_TICKERS = {
"agriculture": ["DE", "ADM", "BG", "MOS", "CF"],
"energy": ["XOM", "CVX", "COP", "DVN", "HAL"],
"utilities": ["NEE", "DUK", "SO", "AEP"],
"retail": ["WMT", "HD", "LOW"],
"insurance": ["ALL", "TRV", "PGR", "CB"],
}
SECTOR_BASE_RISK = {
"agriculture": 20, "energy": 18, "insurance": 22,
"utilities": 15, "retail": 10,
}
def scan_weather_exposure():
"""Scan active weather alerts and map to exposed tickers."""
# 1. Fetch severe weather alerts
alerts = client.weather.alerts(severity="Severe")
for alert in alerts.results:
print(f"\nALERT: {alert.event} ({alert.severity})")
print(f" Area: {alert.area_desc}")
# 2. Map alert to affected sectors
affected_sectors = match_sectors(alert)
for sector in affected_sectors:
for ticker in SECTOR_TICKERS[sector]:
# 3. Score exposure
score = score_exposure(ticker, sector, alert)
label = classify(score)
print(f" {ticker:5s} [{sector:12s}] Score: {score}/100 ({label})")
def score_exposure(ticker, sector, alert):
"""Compute weather exposure score for a ticker."""
score = 0
# Alert severity (0-25)
severity_map = {"Extreme": 25, "Severe": 19, "Moderate": 12, "Minor": 6}
score += severity_map.get(alert.severity, 5)
# Earnings weather mentions (0-25)
earnings = client.earnings.search(ticker=ticker)
weather_mentions = 0
for call in (earnings.results or []):
text = (call.text or "").lower()
weather_mentions += sum(1 for kw in WEATHER_KEYWORDS if kw in text)
score += min(weather_mentions * 5, 25)
# News signal (0-25)
news = client.news.search(q=f"{ticker} weather")
score += min(len(news.results or []) * 5, 25)
# Sector base risk (0-25)
score += SECTOR_BASE_RISK.get(sector, 8)
return min(score, 100)
def match_sectors(alert):
"""Map an alert's affected area to weather-sensitive sectors."""
area = (alert.area_desc or "").lower()
event = (alert.event or "").lower()
sectors = []
ag_states = ["iowa", "illinois", "indiana", "ohio", "minnesota", "nebraska", "kansas"]
if any(state in area for state in ag_states):
sectors.append("agriculture")
gulf_states = ["texas", "louisiana", "florida", "mississippi", "alabama"]
if any(state in area for state in gulf_states):
sectors.append("energy")
if any(kw in event for kw in ["hurricane", "tornado", "wildfire", "flood"]):
sectors.append("insurance")
if any(kw in event for kw in ["hurricane", "tornado", "freeze", "heat"]):
sectors.append("utilities")
return sectors
def classify(score):
if score >= 75: return "CRITICAL"
if score >= 50: return "HIGH"
if score >= 30: return "MODERATE"
return "LOW"
# Run the scan
scan_weather_exposure()
Why This Matters
Weather data is one of the most underused alternative data sources in finance. NWS alerts are free, real-time, and high-quality — yet most quantitative models don't incorporate them. The lag between "severe weather event" and "analyst earnings revision" is consistently days to weeks. This pipeline closes that gap.
The approach is deliberately simple: no ML models, no proprietary weather data, no satellite imagery subscriptions. Just public NWS alerts cross-referenced with public earnings data through one API. The alpha comes from connecting data that already exists, not from better data.
Non-Code Options
Trawl's MCP server handles this naturally:
"Check current severe weather alerts and tell me which stocks in agriculture, energy, and insurance are most exposed. Cross-reference with their recent earnings calls — have any of them mentioned weather as a risk factor?"
Claude pulls the alerts, maps them to sectors, checks earnings transcripts for weather keywords, and ranks the exposure. No code required.
Source Code
The full implementation with alert-to-sector mapping, dual-mode CLI (alerts + ticker profile), and comprehensive scoring: