---
title: "Portfolio look-through: aggregate holdings, sectors and countries across funds"
description: "Aggregate underlying holdings, sector and country exposure across every fund a client holds. Data shape, a Python aggregation example and caching notes."
canonical: https://fundfactsapi.com/use-cases/portfolio-look-through
published: 2026-09-04
updated: 2026-10-06
publisher: FundFacts API
---

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

> Aggregate underlying holdings, sector and country exposure across every fund a client holds. Data shape, a Python aggregation example and caching notes.

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](https://fundfactsapi.com/glossary/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` items are `{ name, weight }`, 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 a normalised `name`: holdings carry a name and a weight, no security identifier, and two funds can label the same company differently ("Apple Inc" and "APPLE INC"), so normalising 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](https://fundfactsapi.com/blog/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](https://fundfactsapi.com/glossary/sector-exposure) and [country exposure](https://fundfactsapi.com/glossary/country-exposure).

## FAQ

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

The largest positions, not every holding. topHoldings carries the ten largest lines for most funds and up to 50 where the fund house's product data lists more (Amundi, UBS, J.P. Morgan, Franklin Templeton); Spanish funds read from the CNMV register get the full portfolio of their latest semi-annual report. Where an ETF issuer publishes a full holdings file, the API reads every line of it (for the position count and, where the file carries the columns, the sector, country and asset weights) but keeps only the ten heaviest in topHoldings. keyFacts.holdings gives the total number of positions where stated, so you can show how much of the fund the listed positions cover.

### How do I match the same company across different funds?

Holdings carry a name and a weight, not a security identifier, so 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.

## About FundFacts API

FundFacts turns any fund or ETF ISIN into one dated factsheet (fees and KID costs, the 1–7 risk indicator, holdings, sector and country exposure, returns) read from the fund house's own documents: as a client-ready PDF under your firm's name for advisers and wealth managers, as JSON through its API for developers, or inside ChatGPT and Claude through its MCP server.

FundFacts works inside ChatGPT and Claude as a connector (a remote MCP server): ask about any fund or ETF by ISIN, ticker or name and the assistant answers from FundFacts' dated figures, fees, risk, holdings, exposure and returns, with fund cards, side-by-side comparisons, holdings overlap and portfolio look-through.

- Free API key, no card: https://fundfactsapi.com/signup
- Documentation: https://fundfactsapi.com/docs (OpenAPI: https://fundfactsapi.com/openapi.json)
- MCP server: https://fundfactsapi.com/api/mcp (set-up for every client: https://fundfactsapi.com/mcp-server)
- Pricing: https://fundfactsapi.com/pricing
- Site index for AI assistants: https://fundfactsapi.com/llms.txt
- Informational data, not investment advice.
