Skip to content

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:

  1. XBRL footnotes -- richest source; identifies specific investments with fair values
  2. Custom XBRL concepts -- proprietary taxonomy extensions (rate only, no investment-level detail)
  3. 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

  1. Cache the BDC list: get_bdc_list() is cached after first call
  2. Use bulk datasets for cross-BDC analysis: Much faster than parsing individual filings
  3. Check data availability: Use has_detailed_investments() before parsing
  4. Filter early: Use the filter() method to reduce data before analysis

Next Steps

Now that you can access BDC data, learn how to: