---
title: "ETF holdings lookthrough — aggregate sector/geo exposure"
method: GET
path: "/api/portfolio/lookthrough"
tags: ["Portfolio"]
---

# ETF holdings lookthrough — aggregate sector/geo exposure

`GET /api/portfolio/lookthrough`

## Request

- Base URL: `https://www.oxfordledge.com` (the document declares no server; this is the origin it was published from)
- URL: `https://www.oxfordledge.com/api/portfolio/lookthrough`
- Auth: none declared

Aggregate sector + geo exposure with ETF lookthrough.

See ``services.portfolio_lookthrough`` for the computation. This
handler validates input + calls compute + returns the envelope.

Status codes:
  - 200: Successful compute (possibly with empty exposure dicts if
    all positions were cash or all data missing).
  - 401: Unauthenticated.
  - 402: Below Plus tier.
  - 422: Invalid input (parse failure, length mismatch, weights
    sum to > 1.0 + tolerance, ticker count > MAX_POSITIONS, etc).
  - 500: Unexpected compute failure (Sentry-captured upstream).

## Query parameters

- `tickers` string: Comma-separated tickers, e.g. 'AAPL,VTI,QQQ'.
- `weights` string: Comma-separated fractional weights aligned with tickers, e.g. '0.4,0.3,0.3'. Must sum to ≤ 1.0 (cash drag implied by remainder).

## Response `200`

Successful Response

- PortfolioLookthroughResponse: `GET /api/portfolio/lookthrough` — ETF holdings lookthrough. ETF-1 (BUILDER + DATA_CZAR, 2026-05-27). Computes aggregate sector + geo exposure for a portfolio after expanding ETF positions to their underlying holdings (SEC 13F-HR / NPORT-P sourced). Required fields are the ones the FE de-references without a null check (the M1-2 incident class). Optional fields cover the partial-data branches (some ETFs opaque, no levered/inverse in the portfolio, etc.). ``sector_exposure`` and ``geo_exposure`` are dicts (``{label: fractional_weight}``) where weights sum to 1.0 across non-cash buckets. ``cash_weight`` is the leftover cash fraction in [0, 1]. ``coverage_pct`` is the fraction of non-cash weight we successfully expanded via lookthrough (vs. treated as opaque / direct).
  - `sector_exposure` object: Sector → fractional weight; sums to 1.0 across risky positions. Key 'Unknown' bucket holds the weight whose sector lookup failed.
  - `geo_exposure` object: Country → fractional weight; sums to 1.0 across risky positions. 'United States' canonicalized.
  - `top_underlying_holdings` UnderlyingHolding[]: Top 25 underlying tickers by weight; expanded from ETF holdings + user direct holdings, merged.
    - `ticker` string, nullable
    - `weight` number, nullable
    - `name` string, nullable
  - `cash_weight` number, required: Fraction of portfolio in cash sentinels.
  - `opaque_etfs` string[]: ETF tickers we couldn't lookthrough (no PG data).
  - `levered_inverse_etfs` string[]: Levered / inverse ETFs flagged in input; not expanded (3× synthetic ≠ underlying for lookthrough).
  - `coverage_pct` number, required: Fraction of non-cash weight expanded via lookthrough vs. treated as direct/opaque. 1.0 = every ETF had data; 0.0 = nothing expanded.
  - `position_count` integer, required: Distinct ticker count in the input portfolio.
  - `etf_count` integer, required: ETF position count (subset of position_count).
  - `error` string, nullable

## Other responses

- `401`: Unauthorized
- `402`: Payment Required
- `422`: Unprocessable Entity
- `500`: Internal Server Error

---

[API](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api.md) · [All operations](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/revisions/e832a1260f0b?raw)
