A practical guide to monitoring institutional investor portfolios using SEC 13F data.
- How to access 13F institutional holdings data
- Track specific fund managers (Berkshire Hathaway, Bridgewater, etc.)
- Find all institutional holders of a stock
- Detect position changes between quarters
- Build a portfolio tracking system
Institutional investment managers with $100M+ in assets must file Form 13F quarterly, disclosing their U.S. equity holdings. This reveals what hedge funds, mutual funds, pension funds, and other large investors own.
Why it matters: Following "smart money" is a time-tested strategy. When Berkshire Hathaway builds a position, the market pays attention.
The catch: 13F filings are due 45 days after quarter end, so the data is delayed. Use it for research and position building, not day trading.
Sign up at earningsfeed.com and generate an API key. The free tier works for this tutorial.
curl -s "https://earningsfeed.com/api/v1/institutional/holdings?limit=5" \
-H "Authorization: Bearer YOUR_API_KEY" | jq .Each holding includes:
| Field | Description |
|---|---|
managerCik |
SEC identifier for the fund manager |
managerName |
Name of the fund/manager |
cusip |
9-character security identifier |
issuerName |
Company name |
ticker |
Stock symbol (when available) |
value |
Market value in USD |
shares |
Number of shares held |
putCall |
"Put", "Call", or null (for common stock) |
reportPeriodDate |
Quarter end date (e.g., "2024-09-30") |
filedAt |
When the 13F was filed |
Find all of Warren Buffett's current holdings:
import requests
API_KEY = "your_api_key_here"
def get_berkshire_holdings():
"""Get Berkshire Hathaway's latest 13F holdings."""
holdings = []
cursor = None
while True:
params = {
"managerCik": 1067983, # Berkshire Hathaway
"limit": 100
}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://earningsfeed.com/api/v1/institutional/holdings",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params
)
data = response.json()
holdings.extend(data["items"])
if not data["hasMore"]:
break
cursor = data["nextCursor"]
return holdings
holdings = get_berkshire_holdings()
# Sort by value and display
holdings.sort(key=lambda x: x["value"], reverse=True)
print("Berkshire Hathaway Portfolio (Top 20):\n")
total_value = sum(h["value"] for h in holdings)
for h in holdings[:20]:
pct = (h["value"] / total_value) * 100
print(f"{h['ticker'] or h['cusip']:6} | {h['issuerName'][:30]:30} | "
f"${h['value']:>15,} | {pct:5.1f}%")
print(f"\nTotal portfolio value: ${total_value:,}")Sample output:
Berkshire Hathaway Portfolio (Top 20):
AAPL | APPLE INC | $91,234,567,000 | 45.2%
BAC | BANK OF AMERICA CORP | $29,876,543,000 | 14.8%
AXP | AMERICAN EXPRESS CO | $21,234,567,000 | 10.5%
KO | COCA-COLA CO | $18,765,432,000 | 9.3%
...
Who owns Apple?
def get_institutional_holders(ticker, min_value=1_000_000_000):
"""Find all institutional holders of a stock with position > min_value."""
holders = []
cursor = None
while True:
params = {
"ticker": ticker,
"minValue": min_value,
"limit": 100
}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://earningsfeed.com/api/v1/institutional/holdings",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params
)
data = response.json()
holders.extend(data["items"])
if not data["hasMore"]:
break
cursor = data["nextCursor"]
return holders
holders = get_institutional_holders("AAPL", min_value=5_000_000_000)
# Group by manager and get most recent filing per manager
from collections import defaultdict
latest_by_manager = {}
for h in holders:
key = h["managerCik"]
if key not in latest_by_manager or h["reportPeriodDate"] > latest_by_manager[key]["reportPeriodDate"]:
latest_by_manager[key] = h
# Sort by position size
sorted_holders = sorted(latest_by_manager.values(), key=lambda x: x["value"], reverse=True)
print("Largest Institutional Holders of AAPL:\n")
for h in sorted_holders[:15]:
print(f"{h['managerName'][:40]:40} | ${h['value']:>15,}")const API_KEY = 'your_api_key_here';
async function getBerkshirePortfolio() {
const params = new URLSearchParams({
managerCik: '1067983', // Berkshire Hathaway
limit: '100'
});
const response = await fetch(
`https://earningsfeed.com/api/v1/institutional/holdings?${params}`,
{
headers: { 'Authorization': `Bearer ${API_KEY}` }
}
);
const { items } = await response.json();
// Sort by value
items.sort((a, b) => b.value - a.value);
console.log('Berkshire Hathaway Top Holdings:\n');
for (const h of items.slice(0, 10)) {
console.log(`${h.ticker || h.cusip}: $${h.value.toLocaleString()}`);
}
}
getBerkshirePortfolio();The real alpha is in tracking what funds are buying and selling. Compare holdings across quarters:
def compare_quarters(manager_cik, current_quarter, previous_quarter):
"""Compare a fund's holdings between two quarters."""
def get_holdings_for_quarter(quarter):
holdings = []
cursor = None
while True:
params = {
"managerCik": manager_cik,
"reportPeriod": quarter,
"limit": 100
}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://earningsfeed.com/api/v1/institutional/holdings",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params
)
data = response.json()
holdings.extend(data["items"])
if not data["hasMore"]:
break
cursor = data["nextCursor"]
return {h["cusip"]: h for h in holdings}
current = get_holdings_for_quarter(current_quarter)
previous = get_holdings_for_quarter(previous_quarter)
# Find changes
new_positions = []
increased = []
decreased = []
closed = []
for cusip, holding in current.items():
if cusip not in previous:
new_positions.append(holding)
else:
prev_shares = previous[cusip]["shares"]
curr_shares = holding["shares"]
if curr_shares > prev_shares * 1.1: # >10% increase
increased.append({**holding, "prev_shares": prev_shares})
elif curr_shares < prev_shares * 0.9: # >10% decrease
decreased.append({**holding, "prev_shares": prev_shares})
for cusip, holding in previous.items():
if cusip not in current:
closed.append(holding)
return {
"new": new_positions,
"increased": increased,
"decreased": decreased,
"closed": closed
}
# Example: Track Bridgewater's changes
changes = compare_quarters(
manager_cik=1350694, # Bridgewater Associates
current_quarter="2024-09-30",
previous_quarter="2024-06-30"
)
print("NEW POSITIONS:")
for h in changes["new"][:5]:
print(f" {h['ticker'] or h['cusip']}: ${h['value']:,}")
print("\nINCREASED POSITIONS:")
for h in changes["increased"][:5]:
pct_change = ((h["shares"] - h["prev_shares"]) / h["prev_shares"]) * 100
print(f" {h['ticker'] or h['cusip']}: +{pct_change:.1f}% (${h['value']:,})")
print("\nCLOSED POSITIONS:")
for h in changes["closed"][:5]:
print(f" {h['ticker'] or h['cusip']}: ${h['value']:,}")13F filings also include put and call options. Track bearish bets:
def get_put_positions(min_value=10_000_000):
"""Find large put option positions (bearish bets)."""
response = requests.get(
"https://earningsfeed.com/api/v1/institutional/holdings",
headers={"Authorization": f"Bearer {API_KEY}"},
params={
"putCall": "put",
"minValue": min_value,
"limit": 100
}
)
holdings = response.json()["items"]
print(f"Large Put Positions (>${min_value/1_000_000:.0f}M):\n")
for h in holdings:
print(f"{h['managerName'][:30]:30} | PUT {h['ticker'] or h['cusip']:6} | ${h['value']:>12,}")
get_put_positions()Monitor multiple funds and get alerts on changes:
import json
from datetime import datetime
from pathlib import Path
WATCHLIST = {
1067983: "Berkshire Hathaway",
1350694: "Bridgewater Associates",
1336528: "Citadel Advisors",
1649339: "Tiger Global Management",
1037389: "Renaissance Technologies",
}
CACHE_FILE = Path("holdings_cache.json")
def load_cache():
if CACHE_FILE.exists():
return json.loads(CACHE_FILE.read_text())
return {}
def save_cache(cache):
CACHE_FILE.write_text(json.dumps(cache, indent=2))
def get_fund_holdings(cik):
"""Fetch all holdings for a fund."""
holdings = []
cursor = None
while True:
params = {"managerCik": cik, "limit": 100}
if cursor:
params["cursor"] = cursor
response = requests.get(
"https://earningsfeed.com/api/v1/institutional/holdings",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params
)
data = response.json()
holdings.extend(data["items"])
if not data["hasMore"]:
break
cursor = data["nextCursor"]
return holdings
def check_for_changes():
"""Check watchlist funds for portfolio changes."""
cache = load_cache()
for cik, name in WATCHLIST.items():
print(f"\nChecking {name}...")
current = get_fund_holdings(cik)
current_by_cusip = {h["cusip"]: h for h in current}
cached_key = str(cik)
if cached_key in cache:
previous = cache[cached_key]
# Find new positions
for cusip, holding in current_by_cusip.items():
if cusip not in previous:
print(f" NEW: {holding['ticker'] or cusip} - ${holding['value']:,}")
# Find closed positions
for cusip in previous:
if cusip not in current_by_cusip:
print(f" CLOSED: {cusip}")
# Update cache
cache[cached_key] = {h["cusip"]: h["shares"] for h in current}
save_cache(cache)
print(f"\nCache updated at {datetime.now()}")
if __name__ == "__main__":
check_for_changes()Run this weekly or after each 13F filing deadline (Feb 14, May 15, Aug 14, Nov 14).
Quick reference for popular managers:
| Manager | CIK |
|---|---|
| Berkshire Hathaway | 1067983 |
| Bridgewater Associates | 1350694 |
| Citadel Advisors | 1336528 |
| Renaissance Technologies | 1037389 |
| Two Sigma | 1179392 |
| Tiger Global | 1649339 |
| Pershing Square | 1336528 |
| Appaloosa Management | 929855 |
| Greenlight Capital | 1079114 |
| Third Point | 1040273 |
Find any manager by searching:
def find_manager(name_query):
"""Search for a fund manager by name."""
response = requests.get(
"https://earningsfeed.com/api/v1/companies/search",
headers={"Authorization": f"Bearer {API_KEY}"},
params={
"q": name_query,
"limit": 10
}
)
for company in response.json()["items"]:
print(f"{company['name']}: CIK {company['cik']}")
find_manager("Soros")13F filings are due 45 days after quarter end:
| Quarter | Period End | Filing Deadline | Data Available |
|---|---|---|---|
| Q1 | March 31 | May 15 | Mid-May |
| Q2 | June 30 | August 14 | Mid-August |
| Q3 | September 30 | November 14 | Mid-November |
| Q4 | December 31 | February 14 | Mid-February |
Pro tip: Most funds file close to the deadline. Set up monitoring to catch filings as they come in.
| Tier | Rate Limit | Monthly Cost |
|---|---|---|
| Free | 15 req/min | $0 |
| Pro | 60 req/min | $15 |
| Trader | 300 req/min | $75 |
For bulk historical analysis, use the maximum limit=100 and implement pagination to minimize requests.
Full documentation: earningsfeed.com/api/docs
Endpoints used in this tutorial:
GET /api/v1/institutional/holdings- List 13F holdingsGET /api/v1/companies/search- Find fund manager CIKGET /api/v1/filings?forms=13F-HR- Track when new 13Fs are filed
- Combine with insider data: See if insiders are buying when institutions are selling
- Build a dashboard: Visualize portfolio changes over time
- Backtest strategies: Test if following specific managers generates alpha
- Track sector concentration: Monitor how funds rotate between sectors
Built with the Earnings Feed API. Questions? support@earningsfeed.com