Fund Fees, Expenses and Return History
Fee tables, expense examples and standardised return histories for US registered funds, taken from the XBRL riding on their prospectus filings: management fee, 12b-1 fee, other expenses, gross and net expense ratio, sales loads, portfolio turnover, and the 1-, 5- and 10-year average annual returns including the after-tax and benchmark rows. One row per share class per filing.
edgar_fund_risk_return - the dataset name to pass to the Obscura API.
What one row means
One **share class's fee table and standardised return history** as a US registered fund states it in one prospectus filing - the Risk/Return (`rr:`) inline-XBRL facts riding on a `485BPOS` or a `497`. Management fee, 12b-1 fee, other expenses, acquired-fund fees, gross and net expense ratio, the contractual waiver, sales loads, the SEC's $10,000 expense example at four horizons, the annual-return bar chart, best/worst quarter, and the 1/5/10-year and since-inception average annual returns including the two after-tax rows. Per share class, per filing, dated. **This is what makes `485BPOS` worth claiming.** The form's PROSE is 863 KB per filing → 62 GB across the corpus, past the exporter's practical ceiling and analytically almost worthless. The XBRL riding on the same filing is a few hundred facts. No prospectus text is stored on this table, and none ever should be: ~35 of the instance's 64 `rr:` tags are `…TextBlock` / `…Heading` / `…Narrative` prose containers (`RiskTextBlock` occurs 227 times in one sampled instance) and every one of them is dropped. # SERIES-LEVEL FACTS ARE NOT COPIED ONTO CLASS ROWS `PortfolioTurnoverRate`, `RiskReturnHeading` and the benchmark returns carry only the series dimension - MEASURED on both sampled instances. They live on a **class-less row** (`class_id = ''`), never fanned across the classes. Copying one portfolio-turnover figure onto four class rows would look like four measurements of the same thing, and averaging them would be wrong. The migration CHECKs it rather than trusting the writer. # Not every prospectus carries XBRL, and `497K` never does A `497K` summary prospectus carries no `rr:` facts at all (measured: zero on `0001193125-24-131423`), and a `497` may or may not - `0001137439-24-001011` (26 KB) has none, `0001213900-24-039977` (6.7 MB) has 3 series and 6 classes of them. A filing with no instance produces **zero rows, not null rows**, and is recorded done in the shared `edgar_doc_processed` marker so it is never re-examined. See `docs/datasets/edgar_fund_risk_return.md`.
One row per One row per (filing, series, share class) Risk/Return fact set, keyed by (accession, series_id, class_id). class_id is the EMPTY STRING on the SERIES-LEVEL row, which is where the facts whose XBRL context names only dei:LegalEntityAxis live - portfolio turnover, the risk/return heading and the benchmark returns. Facts whose context also names rr:ProspectusShareClassAxis live on that class's row. A combined prospectus covering 30 funds produces 30 series rows plus one row per share class. The key is the ACCESSION and not the archive path, for the same per-CIK fan-out reason as edgar_fund_series_classes..
Point-in-time availability
Every Obscura dataset carries available_date: the calendar day the publisher made the row
available, day-of, with no session rounding. It is the one column a backtest filters on, and it means the
same thing on every dataset in the catalog.
For edgar_fund_risk_return: filing_join_occurred: available_date is the EDGAR DISSEMINATION day from the bronze object's own day partition; occurred_on is rr:ProspectusDate, the day the prospectus and therefore this fee schedule TAKES EFFECT. It is frequently in the FUTURE relative to publication - T. Rowe Price 0001741773-24-001700 was disseminated 2024-04-25 and its rr:ProspectusDate is 2024-05-01 - so occurred_is_forecast carries that and the usual occurred_on <= available_date monotonicity does not apply to those rows. Both dates are stated by the publisher (one in the daily index, one as a tagged fact), so both are hard: evidence class A + B, CONFIRMED.
Refresh cadence
Obscura refreshes edgar_fund_risk_return every 6 hours - the most frequent scheduled job that re-collects or re-exports it. This is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when a row became public is recorded per row in available_date.
Schema - 51 columns
The full public column list for edgar_fund_risk_return, with the meaning of every field. The same
schema is served unauthenticated at https://api.obscura.trade/v1/catalog/edgar_fund_risk_return.
| Column | Type | Description |
|---|---|---|
| accession | text · not null | PK part. SEC accession number of the prospectus filing, e.g. `0001741773-24-001700`. |
| series_id | text · not null | PK part. SEC series identifier from the context's `dei:LegalEntityAxis` member (`cik0001002624:S000002088Member` → `S000002088`). The namespace prefix is the FILER's own, so the identifier is extracted from the member rather than assumed to sit under a fixed prefix. |
| class_id | text · not null | PK part. SEC class identifier from the context's `rr:ProspectusShareClassAxis` member. The EMPTY STRING (never NULL) on the SERIES-LEVEL row that holds the facts stated without a class dimension. |
| filename | text · not null | The registrant's canonical SEC EDGAR Archives path (`edgar/data/{cik}/{accession}.txt`), derived from the payload rather than from whichever index row was walked. The join key into `edgar_filings`. |
| available_date | date | Public availability date: the EDGAR DISSEMINATION day, from the bronze object's own day partition. The point-in-time anchor and the only date a backtest may key on. |
| occurred_on | date | **The prospectus's own effective date** (`rr:ProspectusDate`) - the day these fees and disclosures take effect. Stated once per filing, on the dimensionless default context. Often AFTER `available_date`: 2024-05-01 on a filing disseminated 2024-04-25. |
| occurred_is_forecast | boolean · not null | TRUE exactly when `occurred_on` has not yet arrived (`occurred_on > available_date`), so monotonicity checks skip the row. The migration CHECKs the biconditional rather than trusting the writer. |
| form_type | text · not null | The submission's form type - `485BPOS` or `497`. `497K` never carries Risk/Return XBRL and so never produces a row here. |
| cik | bigint | Registrant CIK, read from the XBRL context's own `<identifier scheme=".../CIK">` rather than from the SGML header, so it is the entity the FACTS are filed under. |
| risk_return_heading | text | The fund's own heading (`rr:RiskReturnHeading`), e.g. `Elfun Trusts Ticker Symbol: ELFNX`. SERIES-LEVEL - it frequently carries the ticker inline and is an independent cross-check on `edgar_fund_series_classes`. |
| portfolio_turnover | numeric | Annual portfolio turnover (`rr:PortfolioTurnoverRate`); `0.484` = 48.4 %. SERIES-LEVEL - measured stated against the series context on both sampled instances, so it sits on the class-less row and is never copied onto the classes. |
| benchmark_returns | jsonb · not null | SERIES-LEVEL. JSON array of the benchmarks the fund reports against, one entry per `rr:PerformanceMeasureAxis` member that is not one of the two standard after-tax members: `{"members": [...], "name": "S&P 500 Index", "return_1y": …, "return_5y": …, "return_10y": …, "return_since_inception": …}`. An array because funds report one, two or three benchmarks and the set is not fixed. **`members` is retained so the merge is auditable**: filers routinely split ONE benchmark across TWO members, one carrying `rr:AverageAnnualReturnLabel` and the other the returns (`SP500Index1Member` / `SP500Index2Member` on `0001741773-24-001700`), and the two are joined only when their digit-stripped stems match AND exactly one of the group carries a label and one carries returns. |
| management_fee | numeric | Management fee as a fraction of assets (`rr:ManagementFeesOverAssets`); `0.0064` = 64 bp. |
| distribution_12b1_fee | numeric | Rule 12b-1 distribution/service fee (`rr:DistributionAndService12b1FeesOverAssets`). |
| other_expenses | numeric | Other annual operating expenses (`rr:OtherExpensesOverAssets`). |
| acquired_fund_fees | numeric | Acquired fund fees and expenses (`rr:AcquiredFundFeesAndExpensesOverAssets`) - the fees of underlying funds a fund-of-funds holds, which are inside the gross ratio but not inside the management fee. |
| gross_expense_ratio | numeric | **Total annual fund operating expenses BEFORE waivers** (`rr:ExpensesOverAssets`); `0.0080` = 80 bp. |
| net_expense_ratio | numeric | Expenses AFTER fee waivers and reimbursements (`rr:NetExpensesOverAssets`). NULL when there is no waiver - the tag is simply absent, and NULL is not the same claim as "equal to gross". |
| fee_waiver | numeric | The contractual waiver/reimbursement (`rr:FeeWaiverOrReimbursementOverAssets`), as a fraction of assets. |
| max_front_load | numeric | Maximum front-end sales load as a fraction of the offering price (`rr:MaximumSalesChargeImposedOnPurchasesOverOfferingPrice`). NULL on a no-load class, which is a fact and not a gap. |
| max_deferred_load | numeric | Maximum contingent deferred sales charge. Two different tags state it against two different DENOMINATORS, so the value carries its basis instead of being silently coalesced - see `max_deferred_load_basis`. |
| max_deferred_load_basis | text | WHICH tag supplied `max_deferred_load`: `offering_price` (`rr:MaximumDeferredSalesChargeOverOfferingPrice`) or `other` (`rr:MaximumDeferredSalesChargeOverOther`, i.e. against purchase price or redemption proceeds). Recorded because the two are different measures and a silent precedence rule would be unauditable. NULL exactly when `max_deferred_load` is NULL - the migration CHECKs the biconditional. |
| max_account_fee | numeric | Maximum annual account fee in currency units (`rr:MaximumAccountFee`), e.g. `20`. |
| expense_example_1y | numeric | SEC's standard $10,000 expense example at 1 year, in dollars (`rr:ExpenseExampleYear01`). |
| expense_example_3y | numeric | The same example at 3 years (`rr:ExpenseExampleYear03`). |
| expense_example_5y | numeric | The same example at 5 years (`rr:ExpenseExampleYear05`). |
| expense_example_10y | numeric | The same example at 10 years (`rr:ExpenseExampleYear10`). |
| expense_example_no_redemption_1y | numeric | The expense example assuming NO redemption at 1 year (`rr:ExpenseExampleNoRedemptionYear01`). Differs from the redeeming example only where a deferred load applies, which is why both are kept. |
| expense_example_no_redemption_3y | numeric | No-redemption expense example at 3 years. |
| expense_example_no_redemption_5y | numeric | No-redemption expense example at 5 years. |
| expense_example_no_redemption_10y | numeric | No-redemption expense example at 10 years. |
| annual_returns | jsonb · not null | The bar chart, as `{"2014": 0.3194, …, "2023": 0.0308}` from the `rr:AnnualReturnYYYY` tags. A JSON map rather than ten columns because the ten years slide forward every January and fixed columns would need a migration a year. Empty object when the class states no bar chart (a class younger than a full calendar year). |
| best_quarter_return | numeric | Best quarterly return in the bar-chart window (`rr:BarChartHighestQuarterlyReturn`). |
| best_quarter_end | date | The quarter-end date of `best_quarter_return` (`rr:BarChartHighestQuarterlyReturnDate`), e.g. 2020-06-30. |
| worst_quarter_return | numeric | Worst quarterly return in the bar-chart window (`rr:BarChartLowestQuarterlyReturn`). |
| worst_quarter_end | date | The quarter-end date of `worst_quarter_return` (`rr:BarChartLowestQuarterlyReturnDate`). |
| ytd_return | numeric | Year-to-date return quoted beside the bar chart (`rr:BarChartYearToDateReturn`), when the class states one. |
| ytd_return_through | date | The as-of date of `ytd_return` (`rr:BarChartYearToDateReturnDate`). |
| return_1y | numeric | Standardised average annual total return over 1 year, BEFORE tax (`rr:AverageAnnualReturnYear01` on the class's own context, with no `rr:PerformanceMeasureAxis`). |
| return_5y | numeric | Standardised average annual total return over 5 years, before tax. |
| return_10y | numeric | Standardised average annual total return over 10 years, before tax. |
| return_since_inception | numeric | Standardised average annual total return since inception, before tax (`rr:AverageAnnualReturnSinceInception`). The row a class younger than 10 years reports in place of the 10-year figure. |
| return_1y_after_tax | numeric | 1-year return AFTER TAXES ON DISTRIBUTIONS - the same tag under `rr:PerformanceMeasureAxis = rr:AfterTaxesOnDistributionsMember`, which MEASURED carries the share-class dimension too, so it belongs to this class and not to the series. |
| return_5y_after_tax | numeric | 5-year return after taxes on distributions. |
| return_10y_after_tax | numeric | 10-year return after taxes on distributions. |
| return_since_inception_after_tax | numeric | Since-inception return after taxes on distributions. |
| return_1y_after_tax_sale | numeric | 1-year return AFTER TAXES ON DISTRIBUTIONS AND SALE of shares (`rr:AfterTaxesOnDistributionsAndSalesMember`). |
| return_5y_after_tax_sale | numeric | 5-year return after taxes on distributions and sale. |
| return_10y_after_tax_sale | numeric | 10-year return after taxes on distributions and sale. |
| return_since_inception_after_tax_sale | numeric | Since-inception return after taxes on distributions and sale. |
| inception_date | date | Share-class inception date as tagged (`rr:AverageAnnualReturnInceptionDate`), e.g. 1995-12-29. The denominator of every since-inception figure on the row. |
Access edgar_fund_risk_return
Two delivery paths, one identifier. Both require an Obscura account and an active subscription; the catalog entry and the schema above are public.
import obscura
client = obscura.Client("obs_live_…")
df = client.query(
dataset="edgar_fund_risk_return",
symbols=["NVDA", "AAPL"],
start="2024-01-01",
)
Create a free account Browse all 122 datasets
Frequently asked questions
What is in the edgar_fund_risk_return dataset?
Fee tables, expense examples and standardised return histories for US registered funds, taken from the XBRL riding on their prospectus filings: management fee, 12b-1 fee, other expenses, gross and net expense ratio, sales loads, portfolio turnover, and the 1-, 5- and 10-year average annual returns including the after-tax and benchmark rows. One row per share class per filing. One **share class's fee table and standardised return history** as a US registered fund states it in one prospectus filing - the Risk/Return (`rr:`) inline-XBRL facts riding on a `485BPOS` or a `497`. Management fee, 12b-1 fee, other expenses, acquired-fund fees, gross and net expense ratio, the contractual waiver, sales loads, the SEC's $10,000 expense example at four horizons, the annual-return bar chart, best/worst quarter, and the 1/5/10-year and since-inception average annual returns including the two after-tax rows. Per share class, per filing, dated. **This is what makes `485BPOS` worth claiming.** The form's PROSE is 863 KB per filing → 62 GB across the corpus, past the exporter's practical ceiling and analytically almost worthless. The XBRL riding on the same filing is a few hundred facts. No prospectus text is stored on this table, and none ever should be: ~35 of the instance's 64 `rr:` tags are `…TextBlock` / `…Heading` / `…Narrative` prose containers (`RiskTextBlock` occurs 227 times in one sampled instance) and every one of them is dropped. # SERIES-LEVEL FACTS ARE NOT COPIED ONTO CLASS ROWS `PortfolioTurnoverRate`, `RiskReturnHeading` and the benchmark returns carry only the series dimension - MEASURED on both sampled instances. They live on a **class-less row** (`class_id = ''`), never fanned across the classes. Copying one portfolio-turnover figure onto four class rows would look like four measurements of the same thing, and averaging them would be wrong. The migration CHECKs it rather than trusting the writer. # Not every prospectus carries XBRL, and `497K` never does A `497K` summary prospectus carries no `rr:` facts at all (measured: zero on `0001193125-24-131423`), and a `497` may or may not - `0001137439-24-001011` (26 KB) has none, `0001213900-24-039977` (6.7 MB) has 3 series and 6 classes of them. A filing with no instance produces **zero rows, not null rows**, and is recorded done in the shared `edgar_doc_processed` marker so it is never re-examined. See `docs/datasets/edgar_fund_risk_return.md`.
How do I avoid look-ahead bias with edgar_fund_risk_return?
Filter on edgar_fund_risk_return.available_date, the day the publisher made the row public. For this dataset that date is derived as follows - filing_join_occurred: available_date is the EDGAR DISSEMINATION day from the bronze object's own day partition; occurred_on is rr:ProspectusDate, the day the prospectus and therefore this fee schedule TAKES EFFECT. It is frequently in the FUTURE relative to publication - T. Rowe Price 0001741773-24-001700 was disseminated 2024-04-25 and its rr:ProspectusDate is 2024-05-01 - so occurred_is_forecast carries that and the usual occurred_on <= available_date monotonicity does not apply to those rows. Both dates are stated by the publisher (one in the daily index, one as a tagged fact), so both are hard: evidence class A + B, CONFIRMED. A query of the form WHERE available_date <= '<as-of date>' never sees a row before it existed.
In what formats can I get edgar_fund_risk_return?
As a Parquet bulk export (POST https://api.obscura.trade/v1/download) or as JSON from the typed query API (POST https://api.obscura.trade/v1/query), both with dataset="edgar_fund_risk_return". The column schema is public at https://api.obscura.trade/v1/catalog/edgar_fund_risk_return.
Can I filter edgar_fund_risk_return by company or symbol?
Yes. edgar_fund_risk_return carries series_id, the column the API's symbols filter resolves against.
How often is edgar_fund_risk_return updated?
Obscura refreshes edgar_fund_risk_return on a every 6 hours schedule - that is the most frequent scheduled job that re-collects or re-exports the table. It is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when the publisher makes a row available is described by the availability rule above, and is recorded per row in available_date.