TER, ongoing charges and fund fees: how to compare fund costs programmatically
TER vs OCF vs total cost of ownership, why 0.20 % and 1.50 % end up 30 % apart over 25 years, and how to parse and rank fund fees from API data.Published 5 August 2026 · 2 min read · by FundFacts APIFees are the one input to fund returns that is known in advance, which makes them the most useful field for screening. This article untangles the acronyms, shows the long-term arithmetic and gives you code to parse, normalise and rank fund costs from the headlineMetrics.ter field of a fund data API.
The acronyms
| Term | Where you see it | What it includes |
|---|---|---|
| TER – Total Expense Ratio | Factsheets, ETF product pages | Management fee + administration, custody, audit, legal, regulatory costs, as a % of average assets |
| OCF / Ongoing charges | UCITS KIID, UK documents | Essentially the same as TER; the regulatory name |
| Management fee / AMC | Prospectus | Only the manager's fee — a subset of TER |
| Transaction costs | PRIIPs KID | Trading costs inside the fund, disclosed separately |
| Performance fee | Prospectus, KID | Charged only above a hurdle; excluded from TER |
| Entry / exit charges | KID, platform | One-off, often waived by platforms |
Rule of thumb: TER ≈ OCF ≈ "the running cost". It is what headlineMetrics.ter contains, as published: "0.20%".
Why small differences matter
Fees compound in reverse. On a €100,000 investment growing at 6 % gross:
| TER | Value after 25 years | Fees paid (incl. lost growth) |
|---|---|---|
| 0.10 % | €419,000 | €10,000 |
| 0.20 % | €409,000 | €20,000 |
| 0.75 % | €358,000 | €71,000 |
| 1.50 % | €300,000 | €129,000 |
A 1.3-percentage-point fee gap becomes a 27 % difference in final wealth. This is why fee ranking is the first filter in most fund screeners.
typescriptexport function finalValue(principal: number, grossReturn: number, ter: number, years: number) {return principal * Math.pow(1 + grossReturn - ter, years);}finalValue(100_000, 0.06, 0.0020, 25); // ≈ 409,000
Parsing the fee from API data
Formatted metrics are returned as strings so the unit is never lost. Convert once at the edge:
typescriptexport function pct(s: string | null | undefined): number | null {if (!s) return null;const m = /-?\d+(\.\d+)?/.exec(s.replace(",", "."));return m ? Number(m[0]) : null;}const ter = pct(fund.data.headlineMetrics.ter); // "0.20%" -> 0.2
Then ranking a universe is a sort:
typescriptconst ranked = funds.map((f) => ({ isin: f.isin, name: f.name, ter: pct(f.data.headlineMetrics.ter) })).filter((f) => f.ter != null).sort((a, b) => a.ter! - b.ter!);
Fee is not the whole cost: tracking difference
For index funds the fee you *pay* is the tracking difference — the gap between the fund's return and the index's. Securities-lending income can offset part of the TER; sampling and cash drag can add to it. Compare both:
typescriptconst oneYear = fund.data.annualisedReturns.find((r) => r.label === "1 Year");const trackingDifference = oneYear && oneYear.index != null ? oneYear.fund! - oneYear.index : null;// e.g. TER 0.20 %, tracking difference +0.13 % -> lending income more than covered the fee
Comparing share classes
The same fund often has a cheap institutional class and an expensive retail class with different ISINs. When you build a comparison, group by fund name and show the cheapest accessible class — the keyFacts.manager, benchmarkName and investmentObjective fields are identical across classes, which makes grouping straightforward.
What to display
- TER as published (
headlineMetrics.ter). - Cost in currency per year on the client's amount — €20 per €10,000 lands better than 0.20 %.
- Tracking difference for index products.
- A note on other costs (platform fee, transaction costs, FX) so the TER is not mistaken for the total cost of ownership.
Try it on your funds
Every response from FundFacts API includes headlineMetrics.ter alongside the performance series, so a fee-versus-return scatter plot for a whole universe is one loop. Create a free key or see the field reference.