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.
pythonimport osfrom collections import defaultdictimport requestsAPI = "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.