WASDE Ending Stocks: Tracking Revision Momentum
Every month, the USDA revises its ending stocks estimates in the WASDE report. Most traders focus on the headline number. The better signal is the direction of consecutive revisions — three months of tightening rarely reverses without a reason the market hasn't priced yet.
The Setup
The USDA's World Agricultural Supply and Demand Estimates (WASDE) report is released on or around the 12th of every month. It covers supply, demand, and ending stocks estimates for major crops — corn, soybeans, wheat, cotton, rice, and sorghum — for both the U.S. and global markets.
Ending stocks (also called carryout) is the estimated inventory remaining at the end of the marketing year. It's the single most important fundamental number in grain markets because it captures the net balance of supply and demand in one figure. Lower ending stocks mean a tighter market; higher ending stocks mean a looser market.
But here's what most analysis misses: the USDA doesn't get it right on the first estimate. The ending stocks number for a given crop year gets revised every month from May (first projection) through January of the following year (final). That's nine revisions, and the direction of those revisions — consecutive tightenings or loosenings — carries more information than any single month's number.
The Data
The Commodity Fundamentals API provides WASDE data as a time series of monthly estimates. Here's how to pull the ending stocks history for corn:
import requests
API_KEY = "your_api_key_here"
BASE = "https://commodityfundamentals.com/api/v1"
# Fetch WASDE corn ending stocks estimates
response = requests.get(
f"{BASE}/commodities/corn/series",
headers={"Authorization": f"Bearer {API_KEY}"},
params={
"source": "usda_wasde",
"start": "2020-01-01",
"end": "2026-01-01"
}
)
data = response.json()
print(f"Retrieved {data['meta']['total']} monthly estimates")
Each observation is a monthly WASDE estimate with the ending stocks value in millions of bushels:
{
"data": [
{
"date": "2025-10-10",
"value": 1738.0,
"commodity": "corn",
"source": "usda_wasde",
"unit": "million_bushels",
"marketing_year": "2025/26"
},
{
"date": "2025-11-11",
"value": 1702.0,
"commodity": "corn",
"source": "usda_wasde",
"unit": "million_bushels",
"marketing_year": "2025/26"
}
],
"meta": {
"total": 2,
"request_id": "req_ghi789"
}
}
The marketing_year field is critical — it tells you which crop year this
estimate applies to. The October and November estimates above are both for the 2025/26 marketing year,
and the drop from 1,738 to 1,702 million bushels is a tightening revision of 36 million bushels.
The Analysis
The revision momentum indicator tracks the direction of month-over-month changes in ending stocks for a given marketing year. The logic is simple: count consecutive revisions in the same direction.
import pandas as pd
df = pd.DataFrame(data["data"])
df["date"] = pd.to_datetime(df["date"])
def revision_momentum(group):
"""Calculate revision momentum for a single marketing year."""
group = group.sort_values("date")
group["revision"] = group["value"].diff()
group["direction"] = group["revision"].apply(
lambda x: "tighter" if x < 0 else ("looser" if x > 0 else "unchanged")
)
# Count consecutive same-direction revisions
momentum = 0
results = []
prev_direction = None
for _, row in group.iterrows():
if pd.isna(row["revision"]):
results.append(0)
continue
if row["direction"] == prev_direction:
momentum += (1 if row["direction"] == "tighter" else -1)
else:
momentum = 1 if row["direction"] == "tighter" else -1
prev_direction = row["direction"]
results.append(momentum)
group["momentum"] = results
return group
# Apply per marketing year
df = df.groupby("marketing_year", group_keys=False).apply(revision_momentum)
# Display the 2024/25 corn marketing year
my = df[df["marketing_year"] == "2024/25"].sort_values("date")
print(f"\nCorn Ending Stocks — 2024/25 Marketing Year")
print(f"{'Date':<12} {'Stocks':>8} {'Revision':>10} {'Momentum':>10}")
print("-" * 44)
for _, row in my.iterrows():
rev = f"{row['revision']:>+10.0f}" if pd.notna(row["revision"]) else " ---"
print(f"{row['date'].strftime('%Y-%m'):>12} {row['value']:>8.0f} {rev} {row['momentum']:>+10d}")
A momentum reading of +3 or higher (three consecutive tightening revisions) is a historically reliable signal that the USDA is chasing a fundamental shift — typically a yield disappointment, an export surge, or an ethanol demand surprise. Once the USDA starts revising in one direction, the bureaucratic inertia of their estimation process means the next revision is more likely to continue the trend than to reverse it.
Conversely, a momentum reading of -3 or lower (three consecutive loosenings) signals that supply is outrunning demand, and prices tend to grind lower as the market digests the growing surplus.
# Multi-commodity momentum comparison
commodities = ["corn", "soybeans", "wheat"]
for commodity in commodities:
resp = requests.get(
f"{BASE}/commodities/{commodity}/series",
headers={"Authorization": f"Bearer {API_KEY}"},
params={"source": "usda_wasde", "start": "2024-01-01"}
)
c_df = pd.DataFrame(resp.json()["data"])
c_df["date"] = pd.to_datetime(c_df["date"])
c_df = c_df.groupby("marketing_year", group_keys=False).apply(
revision_momentum
)
latest = c_df.sort_values("date").iloc[-1]
print(f"{commodity:<10} momentum: {latest['momentum']:>+3d} "
f"({latest['direction']}, {latest['date'].strftime('%b %Y')})")
When multiple grain markets show tightening momentum simultaneously, the signal is stronger. The 2020/21 marketing year was a textbook example: corn, soybeans, and wheat all showed 3+ consecutive tightenings starting in the September 2020 WASDE — foreshadowing the commodity rally that ran through May 2021.
Watch for the first reversal. The most actionable moment is when a string of 3+ consecutive tightenings gets its first loosening revision. If the loosening is small (under 20 million bushels for corn), the trend often resumes. If it's large (50+ million bushels), it frequently marks a turning point. The magnitude of the reversal matters as much as the fact of it.
Try It Yourself
Here's a complete script that pulls WASDE ending stocks data, calculates revision momentum across all major crops, and outputs a dashboard of the current signal state:
"""
WASDE Revision Momentum Dashboard
Tracks the direction of consecutive USDA ending stocks revisions
to identify sustained tightening or loosening trends.
Requires: pip install requests pandas
API key: https://commodityfundamentals.com/registration/new
"""
import requests
import pandas as pd
API_KEY = "your_api_key_here"
BASE = "https://commodityfundamentals.com/api/v1"
COMMODITIES = ["corn", "soybeans", "wheat", "cotton"]
SIGNAL_THRESHOLD = 3 # consecutive same-direction revisions
def fetch_wasde(commodity, start="2020-01-01"):
response = requests.get(
f"{BASE}/commodities/{commodity}/series",
headers={"Authorization": f"Bearer {API_KEY}"},
params={"source": "usda_wasde", "start": start}
)
response.raise_for_status()
df = pd.DataFrame(response.json()["data"])
df["commodity"] = commodity
return df
def calculate_momentum(df):
df["date"] = pd.to_datetime(df["date"])
df = df.sort_values("date")
df["revision"] = df.groupby("marketing_year")["value"].diff()
momentum = 0
prev_direction = None
results = []
for _, row in df.iterrows():
if pd.isna(row["revision"]):
results.append(0)
prev_direction = None
continue
direction = "tighter" if row["revision"] < 0 else "looser"
if direction == prev_direction:
momentum += (1 if direction == "tighter" else -1)
else:
momentum = 1 if direction == "tighter" else -1
prev_direction = direction
results.append(momentum)
df["momentum"] = results
return df
def main():
print("WASDE Revision Momentum Dashboard")
print("=" * 55)
all_data = []
for commodity in COMMODITIES:
df = fetch_wasde(commodity)
df = calculate_momentum(df)
all_data.append(df)
# Latest marketing year summary
latest_my = df["marketing_year"].iloc[-1]
my_data = df[df["marketing_year"] == latest_my].sort_values("date")
latest = my_data.iloc[-1]
signal = ""
if latest["momentum"] >= SIGNAL_THRESHOLD:
signal = "TIGHTENING"
elif latest["momentum"] <= -SIGNAL_THRESHOLD:
signal = "LOOSENING"
print(f"\n{commodity.upper()} ({latest_my})")
print(f" Current stocks: {latest['value']:,.0f} {latest.get('unit', '')}")
print(f" Last revision: {latest['revision']:>+,.0f}")
print(f" Momentum: {latest['momentum']:>+d} {signal}")
# Show revision history for current marketing year
print(f" {'Month':<8} {'Stocks':>10} {'Change':>10}")
for _, row in my_data.iterrows():
rev = f"{row['revision']:>+10,.0f}" if pd.notna(row["revision"]) else " ---"
print(f" {row['date'].strftime('%b %y'):<8} {row['value']:>10,.0f} {rev}")
# Cross-commodity signal
print("\n" + "=" * 55)
tightening = sum(
1 for df in all_data
if df.iloc[-1]["momentum"] >= SIGNAL_THRESHOLD
)
print(f"Crops with tightening momentum: {tightening}/{len(COMMODITIES)}")
if tightening >= 3:
print(">> BROAD TIGHTENING — historically bullish for grain complex")
if __name__ == "__main__":
main()
Get your free API key to run this analysis — sign up here. The free tier includes 1,000 API calls per day — plenty for monthly WASDE tracking across all crops.
For detailed field documentation, see the USDA WASDE API reference.