Business Development Companies
Access and analyze Business Development Companies (BDCs) - specialized investment companies that invest in small and mid-sized private companies.
What Are BDCs?
Business Development Companies are closed-end investment funds that:
- Invest in small and mid-sized private U.S. companies
- Provide financing (loans) and equity investments
- Must distribute 90%+ of taxable income as dividends
- File with the SEC under file numbers starting with "814-"
BDCs provide a unique window into private credit markets through their quarterly Schedule of Investments disclosures.
Finding BDCs
List All BDCs
Get the complete list of ~196 BDCs from the SEC BDC Report:
from edgar.bdc import get_bdc_list
bdcs = get_bdc_list()
print(f"Found {len(bdcs)} BDCs")
Output:
Found 196 BDCs
Get a Specific BDC
Find a BDC by ticker or CIK:
from edgar.bdc import get_bdc_list
bdcs = get_bdc_list()
# By ticker
arcc = bdcs.get_by_ticker("ARCC")
print(arcc.name) # ARES CAPITAL CORP
# By CIK
main = bdcs.get_by_cik(1396440)
print(main.name) # MAIN STREET CAPITAL CORP
Search for BDCs
Use fuzzy search to find BDCs by name or ticker:
from edgar.bdc import find_bdc
# Search by name
results = find_bdc("Ares")
print(results[0].name) # ARES CAPITAL CORP
# Search by ticker
results = find_bdc("MAIN")
print(results[0].name) # MAIN STREET CAPITAL CORP
Key points: - Search is case-insensitive - Supports partial name matching - Returns ranked results by relevance
Filter BDCs
Filter by state or activity status:
bdcs = get_bdc_list()
# Active BDCs only (filed within last 18 months)
active_bdcs = bdcs.filter(active=True)
print(f"Active BDCs: {len(active_bdcs)}")
# BDCs in New York
ny_bdcs = bdcs.filter(state='NY')
print(f"NY-based BDCs: {len(ny_bdcs)}")
# Combine filters
ny_active = bdcs.filter(state='NY', active=True)
Check if a Company is a BDC
Verify whether a CIK belongs to a BDC:
from edgar.bdc import is_bdc_cik
is_bdc_cik(1287750) # True - ARCC is a BDC
is_bdc_cik(320193) # False - Apple is not a BDC
BDC Properties
Each BDC entity provides useful information:
arcc = bdcs.get_by_ticker("ARCC")
# Basic info
print(f"Name: {arcc.name}")
print(f"CIK: {arcc.cik}")
print(f"File Number: {arcc.file_number}") # 814-00663
print(f"State: {arcc.state}")
print(f"Active: {arcc.is_active}")
# Filing info
print(f"Last Filing: {arcc.last_filing_date}")
print(f"Last Form: {arcc.last_filing_type}")
Output:
Name: ARES CAPITAL CORP
CIK: 1287750
File Number: 814-00663
State: MD
Active: True
Last Filing: 2025-05-29
Last Form: 10-Q
last_filing_date is as of the SEC BDC report the row came from, so it can be a year or more old. The SEC's newest report does not list every BDC (Ares Capital is in the 2025 report but not the 2026 one), so for a BDC missing from it whose report date is outside the 18-month window, is_active checks the company's own filings. That costs one request per such BDC per session. in_latest_report tells you which rows these are.
Portfolio Investments
BDCs disclose their portfolio holdings in the Schedule of Investments within their 10-K and 10-Q filings.
Get Individual Investments
Extract detailed investment positions from a BDC's latest filing:
arcc = bdcs.get_by_ticker("ARCC")
investments = arcc.portfolio_investments()
print(f"Total positions: {len(investments)}")
print(f"Total fair value: ${investments.total_fair_value:,.0f}")
print(f"Total cost: ${investments.total_cost:,.0f}")
Output:
Total positions: 1256
Total fair value: $26,800,000,000
Total cost: $25,100,000,000
Explore Investment Details
Each investment includes detailed information:
# Get the largest investment
inv = investments[0]
print(f"Company: {inv.company_name}")
print(f"Type: {inv.investment_type}")
print(f"Fair Value: ${inv.fair_value:,.0f}")
print(f"Cost: ${inv.cost:,.0f}")
# For debt investments
if inv.interest_rate:
print(f"Interest Rate: {inv.interest_rate:.2%}")
if inv.spread:
print(f"Spread: {inv.spread:.2%}")
Output:
Company: Ivy Hill Asset Management, L.P.
Type: First lien senior secured loan
Fair Value: $1,915,300,000
Cost: $1,890,000,000
Interest Rate: 11.75%
Spread: 5.75%
Filter Investments
Find specific types of investments:
# First lien loans only
first_lien = investments.filter(investment_type="First lien")
print(f"First lien positions: {len(first_lien)}")
# Search by company name
software = investments.filter(company_name="software")
print(f"Software companies: {len(software)}")
# Large positions only
from decimal import Decimal
large = investments.filter(min_fair_value=Decimal('100000000'))
print(f"Positions over $100M: {len(large)}")
# By industry or normalized sector
software = investments.filter(industry="software")
print(f"Software positions: {len(software)}")
Industry and Sector
Each investment carries the industry as the filer wrote it, where it came from, and a normalized sector:
inv = investments[0]
inv.industry # 'Software Sector' (as filed) or None
inv.industry_source # 'axis', 'enumeration', 'identifier', 'peer' or None
inv.sector # 'Software' (normalized) or None
investments.industry_coverage # share of positions with an industry
investments.by_industry() # sector | num_investments | total_fair_value | pct_of_portfolio
investments.by_industry(by='industry') # the labels as filed
Most BDCs tag an industry on the IndustrySectorAxis or the industry
extensible enumeration. The ones that do not (Sixth Street, PennantPark,
BlackRock TCP, Fidus and others) write it into the investment identifier
instead, as a schedule grouping ("Debt Investments Automotive Truck-Lite Co.,
LLC ...") or a labelled field ("... Industry Financial Services Current
Coupon ..."); industry_source is 'identifier' for those. A position whose
issuer another position in the same portfolio has tagged takes that industry
with source 'peer'. When nothing can be recovered, industry,
industry_source and sector are all None, never an empty string.
sector folds the filers' spellings into one vocabulary
(edgar.bdc.industry.SECTOR_ALIASES): "Software Sector", "Software And
Services" and "Software & Services" are all 'Software'. A label the table
does not know passes through as its own sector with a trailing "Sector"
removed.
Convert to DataFrame
For analysis in pandas:
df = investments.to_dataframe()
# Analyze by investment type
print(df.groupby('investment_type')['fair_value'].sum().sort_values(ascending=False).head())
# Find largest positions
print(df.nlargest(10, 'fair_value')[['company_name', 'investment_type', 'fair_value']])
Data Quality
Check data completeness:
quality = investments.data_quality
print(f"Fair value coverage: {quality.fair_value_coverage:.0%}")
print(f"Cost coverage: {quality.cost_coverage:.0%}")
print(f"Interest rate coverage: {quality.interest_rate_coverage:.0%}")
print(f"Debt investments: {quality.debt_count}")
print(f"Equity investments: {quality.equity_count}")
Note: Not all BDCs provide detailed XBRL data for individual investments. Use has_detailed_investments() to check:
if arcc.has_detailed_investments():
investments = arcc.portfolio_investments()
else:
print("This BDC only provides aggregate data")
Reconcile Against the Balance Sheet
The investment rows come from the Schedule of Investments, which carries subtotals, restated copies and other schedules alongside the holdings. Rows that only restate others are left out (see investments.excluded and each entry's reason). Before relying on a sum, compare it with the filer's own total:
investments.reported_total_fair_value # the balance-sheet figure
investments.reconciliation_gap # 0.003 = rows sum 0.3% above it
investments.unfunded_commitments_fair_value # e.g. -1,448.1M for FSK, or None
Some filers report each holding's funded fair value but net unfunded commitments out of the balance-sheet total, tagged only as totals by investment type. FSK's 2025 rows sum to $14.5 billion against a reported $13.0 billion for that reason. reconciliation_gap adds the netting back, which puts FSK at +0.3%. Use reported_total_fair_value for the portfolio's size.
Cross-BDC Analysis
Use SEC DERA bulk datasets for analysis across all BDCs.
Fetch Quarterly Data
from edgar.bdc import fetch_bdc_dataset
dataset = fetch_bdc_dataset(2024, 3)
print(f"Period: {dataset.period}")
print(f"BDCs: {dataset.num_companies}")
print(f"SOI entries: {dataset.num_soi_entries:,}")
Output:
Period: 2024Q3
BDCs: 148
SOI entries: 106,715
Search for Portfolio Companies
Find which BDCs hold a specific private company:
soi = dataset.schedule_of_investments
results = soi.search("Ivy Hill")
print(results)
Output:
company bdc_name bdc_cik fair_value
0 Ivy Hill Asset Management, L.P. ARES CAPITAL CORP 1287750 1915300000.0
Find Most Common Holdings
Identify the most widely-held private companies:
top = soi.top_companies(10)
print(top)
Output:
company num_bdcs total_fair_value bdc_names
0 OA Buyer, Inc. 6 322406000.0 BARINGS BDC, BARINGS CAPITAL...
1 MRI Software LLC 5 287500000.0 ARES CAPITAL, GOLUB CAPITAL...
Subset by BDC
Get data for a specific BDC from the bulk dataset:
# By CIK
arcc_soi = soi[1287750]
print(f"ARCC entries: {len(arcc_soi)}")
# By BDCEntity
arcc = bdcs.get_by_ticker("ARCC")
arcc_soi = soi[arcc]
Industry Analysis
Analyze industry concentration:
summary = dataset.summary_by_industry()
print(summary.head(10))
industry total_fair_value num_bdcs num_investments
0 Software And Services 6.572000e+09 1 1
1 Healthcare Sector 3.432558e+09 2 2
The summary counts only rows dated at each filing's own period end, so a 10-K's prior-year comparatives are left out, and within each filing it takes the industry subtotal where the filer tags one. Fair value and industry are resolved from whichever XBRL label the SEC used as the column header in that data set release, so the method keeps working when the header changes; if no fair value column can be found, the result carries counts only and a warning names the headers that were looked for.
Filers that tag no industry dimension at all are read from their investment
identifiers ("Debt Investments Automotive Truck-Lite Co., LLC ..."), and an
issuer still untagged takes the industry another BDC in the same data set
tagged for it. In the 2025Q2 file that lifts the filings with a resolvable
industry from 123 to 158 of 160. Pass by='sector' to fold the filers'
spellings into one row each:
dataset.summary_by_industry(by='sector')
sector total_fair_value num_bdcs num_investments
0 Software 7.002469e+10 88 1041
1 Healthcare 5.902270e+10 122 1138
To work with the raw extract yourself, dataset.schedule_of_investments.to_dataframe(clean=True)
applies the same resolution and gives you fair_value, cost, shares,
principal, industry, industry_source (axis, enumeration,
identifier or peer) and sector columns.
Per-BDC Totals
dataset.summary_by_company()
# cik | name | form | filed | num_investments | total_fair_value | total_source
num_investments counts each filing's line items at its period end (the
rows carrying an investment identifier, at the dimension depth where the
filing has most of them). total_fair_value is the filer's own undimensioned
InvestmentOwnedAtFairValue from dataset.numbers when the filing reports
one, and the fair value summed over those line items otherwise; total_source
says which ('filed' or 'summed'). Treat a summed total with care: DERA's
line items include subtotals and restated copies of holdings, so in the 2025Q4
data the sums ran 8.5% (MAIN) to 46.7% (FSK) above the filed totals. For one
filing, PortfolioInvestments reconciles its rows against the balance sheet.
Non-Accrual Analysis
BDCs must disclose which portfolio investments are on non-accrual status -- loans where the borrower has stopped paying interest. This is one of the most important credit quality metrics for BDC analysis.
edgartools extracts non-accrual data from XBRL filings using three layered methods:
- XBRL footnotes -- richest source; identifies specific investments with fair values
- Custom XBRL concepts -- proprietary taxonomy extensions (rate only, no investment-level detail)
- Standard us-gaap aggregate -- single concept fallback (dollar amount only)
Extract Non-Accrual Data
from edgar import Company
from edgar.bdc.nonaccrual import extract_nonaccrual
filing = Company("ARCC").get_filings(form="10-K", amendments=False).latest(1)
result = extract_nonaccrual(filing)
print(f"Non-accrual rate: {result.nonaccrual_rate:.2%}")
print(f"Non-accrual count: {result.num_nonaccrual}")
print(f"Non-accrual FV: ${result.nonaccrual_fair_value:,.0f}")
print(f"Total portfolio FV: ${result.total_portfolio_fair_value:,.0f}")
print(f"Extraction method: {result.extraction_method}")
print(f"Evidence level: {result.evidence_level}")
Output:
Non-accrual rate: 1.24%
Non-accrual count: 20
Non-accrual FV: $332,500,000
Total portfolio FV: $26,800,000,000
Extraction method: footnote
Evidence level: investment
Inspect Individual Investments
When footnote-based extraction succeeds, you get investment-level detail:
for inv in result.investments:
fv = f"${inv.fair_value:,.0f}" if inv.fair_value else "unknown"
print(f"{inv.company_name:40s} {inv.investment_type:30s} {fv}")
Each NonAccrualInvestment includes:
| Property | Description |
|---|---|
company_name |
Borrower name parsed from the XBRL identifier |
investment_type |
e.g., "First lien senior secured loan" |
fair_value |
Fair value at period end (Decimal or None) |
cost |
Investment cost basis (Decimal or None) |
footnote_text |
The raw footnote text that flagged non-accrual status |
Cross-Validation
When a BDC files both footnote data and a custom XBRL concept for the non-accrual rate, NonAccrualResult captures both for cross-validation:
if result.custom_concept_rate is not None:
print(f"Stated rate (XBRL concept): {result.custom_concept_rate:.2%}")
print(f"Computed rate (footnotes): {result.nonaccrual_rate:.2%}")
Batch Extraction
Extract non-accrual data for all active BDCs at once:
from edgar.bdc.nonaccrual import extract_nonaccrual_batch
results = extract_nonaccrual_batch(form="10-K", active_only=True)
for r in sorted(results, key=lambda x: x.nonaccrual_rate or 0, reverse=True)[:10]:
rate = f"{r.nonaccrual_rate:.2%}" if r.nonaccrual_rate is not None else "n/a"
print(f"{r.entity_name:45s} {rate:8s} ({r.extraction_method})")
Note: extract_nonaccrual_batch makes one network request per BDC. It is intended for periodic research, not real-time use.
Pass to an LLM
NonAccrualResult.to_context() produces token-efficient text for LLM pipelines:
# Standard context (~300 tokens): metrics + footnote text + investment list
print(result.to_context('standard'))
# Minimal (~100 tokens): metrics only
print(result.to_context('minimal'))
# Full (~600+ tokens): per-investment cost, unrealized gain/loss, footnote text
print(result.to_context('full'))
When Extraction Fails
extract_nonaccrual() returns None if the filing has no XBRL data. When it succeeds but finds no non-accrual data, it returns a NonAccrualResult with extraction_method='none' and nonaccrual_rate=None. A zero non-accrual rate is expressed as 0.0, not None.
result = extract_nonaccrual(filing)
if result is None:
print("No XBRL data available")
elif result.extraction_method == 'none':
print("XBRL available but no non-accrual data found")
elif result.nonaccrual_rate == 0.0:
print("BDC reported zero non-accrual investments")
Counting Non-Accrual Investments
num_nonaccrual is only a count when the filing itemizes its non-accrual investments, and evidence_level says whether it does:
evidence_level |
What the filing gave | num_nonaccrual |
|---|---|---|
'investment' |
Footnotes flagging individual investments | Count of flagged rows |
'aggregate' |
Only a portfolio-level rate or amount | None |
'none' |
No non-accrual data in the XBRL | None |
An aggregate figure proves non-accruals exist without saying how many. WhiteHorse Finance's 10-K for 2025 reports $10.6 million of non-accrual investments at fair value and no per-investment footnotes, so its count is None, not 0. The count is of investment rows, so a company with two non-accrual loans counts twice.
NonAccrualResult Reference
| Property | Type | Description |
|---|---|---|
nonaccrual_rate |
float \| None |
Non-accrual fair value / total portfolio FV |
num_nonaccrual |
int \| None |
Count of non-accrual investment rows; None unless evidence_level is 'investment' |
evidence_level |
str |
'investment', 'aggregate', or 'none' — see Counting Non-Accrual Investments |
nonaccrual_fair_value |
Decimal \| None |
Sum of non-accrual investment fair values |
total_portfolio_fair_value |
Decimal \| None |
Sum of all portfolio investment fair values |
investments |
list[NonAccrualInvestment] |
Individual non-accrual positions (footnote method only) |
extraction_method |
str |
'footnote', 'custom_concept', 'aggregate_concept', or 'none' |
custom_concept_rate |
float \| None |
Rate from BDC's custom XBRL concept (for cross-validation) |
has_investment_detail |
bool |
Whether individual investments were identified |
unique_footnote_texts |
list[str] |
Distinct footnote texts that flagged non-accrual status |
Integration with Company Objects
BDC entities connect to standard Company functionality:
arcc = bdcs.get_by_ticker("ARCC")
# Get the Company object
company = arcc.get_company()
print(company)
# Access filings
filings = arcc.get_filings(form="10-K")
latest_10k = filings[0]
print(f"Latest 10-K: {latest_10k.filing_date}")
# Get the Schedule of Investments statement
soi_statement = arcc.schedule_of_investments()
print(soi_statement)
Common Use Cases
Private Company Research
Find all BDC exposure to a private company:
from edgar.bdc import fetch_bdc_dataset
dataset = fetch_bdc_dataset(2024, 3)
soi = dataset.schedule_of_investments
# Search for the company
results = soi.search("MRI Software")
print(f"Found in {len(results)} BDC positions")
print(f"Total exposure: ${results['fair_value'].sum():,.0f}")
print(f"BDCs holding: {results['bdc_name'].unique().tolist()}")
BDC Portfolio Comparison
Compare portfolio composition across BDCs:
from edgar.bdc import get_bdc_list
bdcs = get_bdc_list()
tickers = ["ARCC", "MAIN", "GBDC"]
for ticker in tickers:
bdc = bdcs.get_by_ticker(ticker)
if bdc and bdc.has_detailed_investments():
inv = bdc.portfolio_investments()
quality = inv.data_quality
print(f"{ticker}: {len(inv)} positions, "
f"{quality.debt_count} debt, {quality.equity_count} equity")
Yield Analysis
Analyze interest rates across a BDC's debt portfolio:
arcc = bdcs.get_by_ticker("ARCC")
investments = arcc.portfolio_investments()
# Filter to debt with interest rates
df = investments.to_dataframe()
debt = df[df['interest_rate'].notna()]
print(f"Average interest rate: {debt['interest_rate'].mean():.2%}")
print(f"Rate range: {debt['interest_rate'].min():.2%} - {debt['interest_rate'].max():.2%}")
Data Sources
The BDC module uses two SEC data sources:
| Source | Content | Best For |
|---|---|---|
| SEC BDC Report | List of all BDCs with file numbers | Finding and identifying BDCs |
| DERA Quarterly Datasets | Pre-extracted Schedule of Investments | Cross-BDC analysis |
| Individual 10-K/10-Q | Detailed XBRL investment data | Deep dive into single BDC |
Performance Tips
- Cache the BDC list:
get_bdc_list()is cached after first call - Use bulk datasets for cross-BDC analysis: Much faster than parsing individual filings
- Check data availability: Use
has_detailed_investments()before parsing - Filter early: Use the
filter()method to reduce data before analysis
Next Steps
Now that you can access BDC data, learn how to:
- Extract Financial Statements - Get balance sheets and income statements
- Query XBRL Facts - Deep dive into XBRL data
- Company Facts API - Historical financial metrics
Related Documentation
- Company Subsets - Create groups of companies for analysis
- Finding Companies - General company lookup