Portfolio look-through

Portfolio look-through: aggregate holdings, sectors and countries across funds

Look-through means opening each fund a client holds and aggregating the underlying holdings, sectors and countries into one view of what the portfolio actually owns. This page explains why it matters, which data you need per fund and how to compute it from FundFacts API responses.

The problem

A client with six funds may believe they are diversified. If four of those funds are global equity trackers, the same ten companies sit at the top of each one, and the portfolio's effective exposure to a single stock or sector is far higher than any one fund suggests. Look-through analysis makes that visible by weighting each fund's holdings by the fund's weight in the portfolio and summing across funds.

The obstacle is data. Each fund house publishes holdings in its own format: a spreadsheet for ETFs, a top-ten table in the factsheet for active funds, a sector breakdown that uses one classification scheme here and another there. Reconciling this by hand for every client review is not sustainable.

What you need from the data

For each fund in the portfolio:

  • Top holdings with weights, ideally with the underlying security's ISIN so the same company can be matched across funds
  • Sector exposure as label and weight
  • Country or region exposure as label and weight
  • Asset allocation (equity, bonds, cash) for multi-asset funds
  • Credit quality and maturity buckets for bond funds
  • The date the holdings were published, so the aggregate can carry an honest as-of

You also need the weights on a comparable basis: percent of fund net assets, not percent of the equity sleeve.

How it works with FundFacts API

Load each ISIN once, then aggregate in your own code. The sector, geography, assetAllocation and creditQuality arrays share a { label, weight } shape, and topHoldings adds name and an optional isin, so one aggregation function works across all of them.

python
import os
from collections import defaultdict
import requests
API = "https://fundfactsapi.com/api/v1/funds/{isin}"
HEADERS = {"Authorization": f"Bearer {os.environ['FUNDFACTS_API_KEY']}"}
portfolio = {"IE00B4L5Y983": 0.40, "IE00B6YX5C33": 0.35, "LU0395794307": 0.25}
_cache: dict[str, dict] = {}
def load(isin: str) -> dict:
if isin not in _cache:
r = requests.get(API.format(isin=isin), headers=HEADERS, timeout=240)
r.raise_for_status()
_cache[isin] = r.json()
return _cache[isin]
def aggregate(key: str) -> dict[str, float]:
total: dict[str, float] = defaultdict(float)
for isin, w in portfolio.items():
for row in load(isin)["data"].get(key) or []:
total[row["label"]] += w * row["weight"]
return dict(sorted(total.items(), key=lambda kv: -kv[1]))
sectors = aggregate("sector")
countries = aggregate("geography")

For holdings, key on isin when present and fall back to a normalised name otherwise. Two funds can label the same company differently ("Apple Inc" and "APPLE INC"), and matching on the security ISIN avoids double counting.

The in-memory cache above means a fund held by many clients is fetched once per run. See the ETF holdings API guide for the shape of each array.

Design notes

Partial coverage. Many active funds publish only their top ten holdings. Sum the weights you have and show the remainder as "other / not disclosed". Never scale the top ten up to 100 percent; it misrepresents concentration.

Caching. Holdings are refreshed on a 24-hour cycle. Store the whole data object with its expiresAt and recompute the aggregate from cache; only re-load a fund when expiresAt has passed. A client review can then run entirely from local data.

Quota. Batch the refresh, honour X-RateLimit-Remaining, and back off on 429. Cold ISINs take one to three minutes on first load, so pre-load a new client's funds when the account is created rather than when the adviser opens the report.

Classification drift. Sector labels are normalised from the documents each fund house publishes, but a granular scheme in one fund and a coarse scheme in another can still leave "Information Technology" and "Technology" side by side. Keep a small mapping table for your own display taxonomy.

As-of dates. Show the oldest dataAsOf across the funds in the portfolio on the aggregate view. A look-through built from holdings of different dates is still useful, but the reader should know it.

Disclaimers. Look-through describes exposure; it does not say whether that exposure is appropriate. Label the output as informational and not investment advice. For more on the exposure fields see sector exposure and country exposure.

Frequently asked questions

Does the API return every holding or only the top ten?

It returns what the fund house publishes. ETF issuers usually publish full holdings files, so topHoldings can be long; many active fund managers only disclose their ten largest positions in the factsheet. keyFacts.holdings gives the total number of positions where stated, so you can show how much of the fund the disclosed list covers.

How do I match the same company across different funds?

Use the isin field on each holding when it is present; it identifies the security regardless of how the fund house spells the name. When it is absent, normalise the name by upper-casing, trimming and stripping suffixes such as Inc, PLC or SA before comparing. Keep a small alias table for the handful of names that still fail to match.

Can I aggregate bond funds alongside equity funds?

Yes. Bond funds populate creditQuality and maturity instead of, or in addition to, sector. Aggregate each dimension only across funds that report it, and show the share of the portfolio covered by each chart. The assetAllocation array is the right place to start for a mixed portfolio because it splits every fund into equity, bonds and cash.

How fresh are the holdings?

Each fund is re-read from the fund house's documents on a 24-hour cycle, but the documents themselves are updated on the issuer's schedule: daily for most ETFs, monthly or quarterly for many active funds. The dataAsOf field carries the publication date, and your aggregate should display the oldest one so readers know the effective date of the view.

Build your portfolio look-through on FundFacts API

Free plan with 10 requests a day to prototype; paid plans add the AI companion kit that scaffolds the integration for you.