Analyze proxy voting records with Form N-PX
Parse Form N-PX filings to see how funds and institutional managers voted on proxy proposals, by category and against management recommendations.
Form N-PX reports how a fund voted every proxy it was entitled to vote in the year to June 30. Since the SEC's 2022 amendments took effect for votes from July 1, 2023, institutional managers that file 13F also report their say-on-pay votes on N-PX, and every report uses a structured XML format with standard vote categories.
The result is a dataset of millions of individual votes: issuer, meeting date, proposal, category, shares voted, how they were voted and what management recommended.
Fetch filings and parse the vote tables
GET /v1/n-px returns N-PX filings parsed to JSON. The cover page is in primary_doc. The vote table is a separate document in the same filing, and its name depends on the filing agent (ProxyVotingTable, proxyvotetable, or a file-specific name), so the script reads every document other than the cover page that contains a proxyTable.
import os
from collections import Counter
import requests
BASE = "https://secfeedapi.com"
HEADERS = {"Authorization": f"Bearer {os.environ['SECFEED_KEY']}"}
def as_list(x):
return x if isinstance(x, list) else [x] if x else []
r = requests.get(f"{BASE}/v1/n-px", params={"startDate": "2026-07-01", "size": 100}, headers=HEADERS, timeout=120)
r.raise_for_status()
filings = r.json()["filings"]
print("N-PX filings since 2026-07-01:", len(filings))
print(Counter(f["data"]["primary_doc"]["formData"]["coverPage"]["reportInfo"]["reportType"] for f in filings))
votes = []
for f in filings:
for key, doc in f["data"].items(): # the vote table's document name varies by filing agent
if key == "primary_doc" or not isinstance(doc, dict):
continue
for row in as_list(doc.get("proxyTable")):
for rec in as_list((row.get("vote") or {}).get("voteRecord")):
votes.append((f["companyName"], row, rec))
print(f"\nVote records parsed: {len(votes):,}")
how = Counter(rec.get("howVoted") for _, _, rec in votes)
print("How shares were voted:", dict(how.most_common()))
# Only issuer proposals: on shareholder proposals filers code managementRecommendation inconsistently
issuer = [v for v in votes if v[1].get("voteSource") == "ISSUER" and v[2].get("managementRecommendation") in ("FOR", "AGAINST")]
against_mgmt = [v for v in issuer if v[2].get("howVoted") in ("FOR", "AGAINST") and v[2]["howVoted"] != v[2]["managementRecommendation"]]
print(f"Issuer proposals with a FOR or AGAINST recommendation: {len(issuer):,}")
print(f"Voted against the management recommendation: {len(against_mgmt):,} ({len(against_mgmt) / max(1, len(issuer)):.2%})")
holder = sum(1 for v in votes if v[1].get("voteSource") == "SECURITY HOLDER")
print(f"Votes on shareholder proposals: {holder:,}")
cats = Counter()
for _, row, _ in votes:
for c in as_list((row.get("voteCategories") or {}).get("voteCategory")):
cats[c.get("categoryType")] += 1
print("\nTop proposal categories:")
for c, n in cats.most_common(6):
print(f" {c:<45}{n:>7,}")
print("\nExamples of votes against management:")
seen = Counter((row["issuerName"], row["voteDescription"][:60]) for _, row, _ in against_mgmt)
for (issuer_name, desc), n in seen.most_common(5):
print(f" {issuer_name[:26]:<27}| {n:>3} votes | {desc}")
Output:
N-PX filings since 2026-07-01: 34
Counter({'INSTITUTIONAL MANAGER NOTICE REPORT': 21, 'FUND VOTING REPORT': 11, 'INSTITUTIONAL MANAGER VOTING REPORT': 2})
Vote records parsed: 313,400
How shares were voted: {'FOR': 281672, 'AGAINST': 20780, 'WITHHOLD': 5419, 'ABSTAIN': 4070, 'ONE YEAR': 1424, 'NO VOTE': 21, '1 YEAR': 14}
Issuer proposals with a FOR or AGAINST recommendation: 298,737
Voted against the management recommendation: 80 (0.03%)
Votes on shareholder proposals: 13,132
Top proposal categories:
DIRECTOR ELECTIONS 185,275
CORPORATE GOVERNANCE 38,654
COMPENSATION 22,929
AUDIT-RELATED 22,733
CAPITAL STRUCTURE 20,644
SECTION 14A SAY-ON-PAY VOTES 17,250
Examples of votes against management:
Nestle SA | 16 votes | Transact Other Business (Voting)
Constellium SE | 10 votes | Advisory (non-binding) vote to hold an advisory (non-binding
ANZ Group Holdings Limited | 8 votes | Approve the Spill Resolution
Brookdale Senior Living In | 8 votes | Election of eight (8) nominees to serve as directors to the
Orange SA | 7 votes | Dismiss Pierre Chaussoneaux as Representative of Employees t
Reading the result
Of the 34 filings, 13 carry vote tables, and together they hold more than 313,000 vote records. Director elections make up well over half of the proposals.
Measuring votes against management takes care. On issuer proposals the recommendation is consistent, and in this sample the funds voted against it 80 times out of almost 300,000, mostly on foreign "other business" items, advisory votes and a board spill resolution. On shareholder proposals, filers code managementRecommendation inconsistently (some record the board's recommendation, some the direction of the proposal), so the script leaves those 13,132 votes out of the ratio. For shareholder proposal research, read howVoted directly and look up the board's recommendation in the proxy statement.
Report types
The cover page reportType tells you whether a filing has votes:
- FUND VOTING REPORT: a registered fund's votes
- INSTITUTIONAL MANAGER VOTING REPORT: a 13F manager's say-on-pay votes
- INSTITUTIONAL MANAGER NOTICE REPORT: the manager did not exercise voting power, or another filer reports the votes; there is no vote table
Fields in each vote record
| Field | Meaning |
|---|---|
issuerName, cusip, isin | The company holding the meeting |
meetingDate | Meeting date |
voteDescription | The proposal as described by the issuer |
voteCategories | Standard SEC categories, such as DIRECTOR ELECTIONS or SECTION 14A SAY-ON-PAY VOTES |
voteSource | ISSUER or SECURITY HOLDER (shareholder proposal) |
sharesVoted, sharesOnLoan | Shares voted, and shares on loan and not recalled |
vote.voteRecord | One or more records with howVoted, sharesVoted and managementRecommendation |
A proposal can have several vote records when a fund split its vote, which is why the script iterates voteRecord as a list.
Ideas to extend it
- Filter
voteSource == "SECURITY HOLDER"to study support for shareholder proposals. - Group by
voteSeriesto compare how different series of the same trust voted. - Join on
cusipwith 13F holdings to weight votes by position size.