N-PORT: Parse Mutual Fund Portfolio Holdings with Python
Every registered mutual fund and ETF files Form NPORT-P with the SEC each month, disclosing their complete portfolio -- every stock, bond, swap, and option they hold. EdgarTools parses these filings into structured Python objects so you can analyze fund portfolios in a few lines of code.
from edgar import get_filings
filings = get_filings(form="NPORT-P")
report = filings[0].obj()
report

Three lines to get a fully parsed fund report with total assets, all investment positions, derivative exposures, and risk metrics.
Investment Data
The investment_data() method returns a DataFrame with one row per position, sorted by absolute value:
report.investment_data()
| Column | What it is |
|---|---|
name |
Issuer name ("APPLE INC") |
title |
Security title ("COMMON STOCK") |
cusip |
9-character CUSIP |
ticker |
Resolved ticker symbol |
balance |
Share count or par amount |
units |
"NS" (shares), "PA" (principal), or "NC" (contracts) |
value_usd |
Market value in US dollars |
pct_value |
Percentage of net asset value |
asset_category |
SEC category ("EC" equity, "DBT" debt, etc.) |
issuer_category |
Issuer type ("CORP", "USG", "MUN", etc.) |
currency_code |
Currency of the position |
investment_country |
Country code |
is_derivative |
True for derivatives, False for securities |
Values are in full US dollars -- unlike 13F filings, NPORT values are not in thousands.
Securities vs Derivatives
Split the portfolio into traditional securities and derivative positions:
# Non-derivative positions only (stocks, bonds, etc.)
report.securities_data()
# Derivative positions only
report.derivatives_data()
The derivatives_data() DataFrame adds columns specific to derivatives:
| Column | What it is |
|---|---|
subtype |
Derivative subtype (e.g., "CDS", "IRS") |
reference |
Reference entity or index |
counterparty |
Counterparty name |
notional_amount |
Notional value |
unrealized_pnl |
Unrealized appreciation/depreciation |
termination_date |
Contract expiration date |
pct_value |
Percentage of NAV |
See it live on edgar.tools
The code above parses N-PORT portfolio holdings in Python. edgar.tools renders fund portfolios visually — browse holdings, derivative exposures, and risk metrics for any registered fund.
Free tier available. Also includes a REST API for programmatic access. Pricing →
Derivative-Specific DataFrames
For detailed analysis of specific derivative types, use the dedicated methods. Each returns a DataFrame with type-specific fields:
report.swaps_data() # Interest rate swaps, CDS, total return swaps
report.options_data() # Options with strike price, expiry, put/call
report.forwards_data() # FX forwards with currencies and settlement
report.futures_data() # Futures with payoff profile and expiry
report.swaptions_data() # Swaption contracts
All derivative DataFrames share common fields (name, title, counterparty, notional_amount, unrealized_pnl) plus type-specific columns. For example, swaps_data() includes directional receive/pay legs, and options_data() includes strike_price, written_or_purchased, and shares_per_contract.
Fund Financials
Access the fund's balance sheet data through fund_info:
print(f"Total assets: ${report.fund_info.total_assets:,.0f}")
print(f"Total liabilities: ${report.fund_info.total_liabilities:,.0f}")
print(f"Net assets: ${report.fund_info.net_assets:,.0f}")
Risk Metrics
Interest rate sensitivity
NPORT filings include DV01 (dollar value of a 1 basis point move) and DV100 (dollar value of a 100 basis point move) across multiple time horizons:
for currency, metric in report.fund_info.current_metrics.items():
print(f"{currency} DV01 (1yr): {metric.intrstRtRiskdv01.period1Yr}")
print(f"{currency} DV100 (1yr): {metric.intrstRtRiskdv100.period1Yr}")
Credit spread risk
Investment grade and non-investment grade spread sensitivity, when reported:
ig = report.fund_info.credit_spread_risk_investment_grade
if ig:
print(f"IG spread risk (1yr): {ig.period1Yr}")
nig = report.fund_info.credit_spread_risk_non_investment_grade
if nig:
print(f"Non-IG spread risk (1yr): {nig.period1Yr}")
Look Up a Specific Fund
Search for a fund by its CIK or by the management company:
from edgar import Company
# By management company CIK
vanguard = Company("0000102909")
filing = vanguard.get_filings(form="NPORT-P").latest(1)
report = filing.obj()
print(report.general_info.name) # Management company
print(report.general_info.series_name) # Specific fund/series
print(report.reporting_period) # Report date
Or use the Fund class for a simpler path:
from edgar import Fund
fund = Fund("VFINX")
report = fund.get_latest_report() # Latest NPORT-P report
df = fund.get_portfolio() # Portfolio as DataFrame
Common Analysis Patterns
Top holdings by value
df = report.investment_data(include_derivatives=False)
total = df['value_usd'].sum()
df['weight'] = (df['value_usd'] / total * 100).round(2)
df[['name', 'ticker', 'value_usd', 'weight']].head(10)
Sector allocation
df = report.securities_data()
df.groupby('asset_category')['value_usd'].sum().sort_values(ascending=False)
Derivative exposure as percentage of NAV
net_assets = report.fund_info.net_assets
deriv = report.derivatives_data()
if not deriv.empty:
total_notional = deriv['notional_amount'].abs().sum()
print(f"Derivative notional: ${total_notional:,.0f}")
print(f"As % of NAV: {total_notional / net_assets * 100:.1f}%")
Metadata Quick Reference
| Property | Returns | Example |
|---|---|---|
general_info.name |
Management company | "VANGUARD CHESTER FUNDS" |
general_info.series_name |
Fund/series name | "Vanguard Target Retirement 2035 Fund" |
general_info.cik |
SEC CIK | "0000102909" |
general_info.rep_period_date |
Report date | "2024-03-31" |
general_info.fiscal_year_end |
Fiscal year end | "0131" |
general_info.series_id |
SEC series ID | "S000004104" |
general_info.reg_lei |
LEI | "549300..." |
reporting_period |
Same as rep_period_date | "2024-03-31" |
name |
Company - Series | "VANGUARD CHESTER FUNDS - Vanguard Target..." |
has_investments |
Has any positions? | True |
header.submission_type |
Form type filed | "NPORT-P" |
filing |
Source Filing object | Filing or None |
cik |
CIK of the filer | "0000102909" |
series_id |
SEC series ID | "S000004104" or None |
Methods Quick Reference
| Call | Returns | What it does |
|---|---|---|
report.investment_data() |
DataFrame |
All positions (securities + derivatives) |
report.securities_data() |
DataFrame |
Non-derivative positions only |
report.derivatives_data() |
DataFrame |
Derivative positions with P&L |
report.swaps_data() |
DataFrame |
Swap details with receive/pay legs |
report.options_data() |
DataFrame |
Options with strike, expiry, put/call |
report.forwards_data() |
DataFrame |
FX forwards with settlement info |
report.futures_data() |
DataFrame |
Futures contracts |
report.swaptions_data() |
DataFrame |
Swaption contracts |
report.derivatives |
list |
Raw derivative investment objects |
report.non_derivatives |
list |
Raw non-derivative investment objects |
report.get_fund_series() |
FundSeries |
Fund series object |
report.get_ticker_for_series() |
str |
Ticker for this series |
report.get_tickers_for_series() |
list[str] |
All tickers for this series |
report.matches_ticker(ticker) |
bool |
Whether report matches a ticker |
Things to Know
Values are in full dollars. Unlike 13F filings (which report in thousands), NPORT values are in actual USD. A value_usd of 135,364,000 means exactly $135.4 million.
NPORT-P vs NPORT-EX. NPORT-P is the full monthly filing with all positions and risk metrics. NPORT-EX is a quarterly exhibit containing only a subset of holdings. EdgarTools handles both, but NPORT-P has the most complete data.
Monthly reporting. Funds file NPORT-P every month, but only the quarter-end filings are made public. The other two months in each quarter are confidential (released after 60 days).
Derivative coverage. EdgarTools parses all five derivative types: swaps, options, forwards, futures, and swaptions. Each type has a dedicated *_data() method for detailed analysis.
Ticker resolution. Tickers are resolved from CUSIPs and fund metadata. Most common securities resolve correctly, but obscure or private placements may have blank tickers.
Empty DataFrames. Methods like derivatives_data() and swaps_data() return an empty DataFrame when no positions of that type exist. Always check with df.empty before analysis.
Related
- Fund Entities -- look up funds by ticker, navigate hierarchies
- Working with Filings -- general filing access patterns