# SEC N-PORT Holdings Diff - Fund Position Changes (`datadeltas/sec-nport-holdings`) Actor

Period-over-period holdings changes for any SEC-registered mutual fund, ETF or closed-end fund, straight from Form N-PORT on EDGAR: new positions, exits, buys and sells with balance, USD value and portfolio weight before and after.

- **URL**: https://apify.com/datadeltas/sec-nport-holdings.md
- **Developed by:** [Daniel Matthee](https://apify.com/datadeltas) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 holding change detecteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## SEC N-PORT Fund Holdings Diff — what mutual funds and ETFs bought and sold

**Period-over-period holdings changes for any SEC-registered fund** — mutual
funds, ETFs and closed-end funds — built by diffing a fund's two most recent
Form N-PORT filings on EDGAR. One row per portfolio action: **new position**,
**exited**, **increased**, **decreased**, with balance, USD value and
percent-of-net-assets on both sides.

Unlike a 13F feed, this covers the **whole portfolio**: corporate and government
bonds, mortgage- and asset-backed securities, bank loans, derivatives, repos,
foreign holdings and **short positions**. Public domain data, no API key, no
scraping of a paywalled site — the actor reads SEC EDGAR directly and identifies
itself with the `userAgent` you set, per SEC's fair-access policy.

### Read this first: cadence

**N-PORT holdings are public four times a year, roughly 55–60 days late.** Funds
file quarterly and only the quarter-end month's holdings are public. Measured on
real filings: holdings as of 30 Jun 2026 were filed 28 Aug 2026 (59 days); a
fund with a February fiscal year reported 31 May 2026 holdings on 24 Jul 2026
(54 days). Filings arrive in four clusters a year, and **funds do not share a
calendar** — a fund's periods depend on its own fiscal year.

So this is **ownership research, not a trading signal**. A `new-position` row
may describe a purchase two to five months old. What it is good for: knowing who
owns what, tracking manager behaviour and drift, and finding every fund that
bought or sold a specific bond or stock last quarter.

Run it on a **weekly** Apify Schedule. Most weeks a watched fund has no new
filing, the run costs one small JSON fetch per registrant and emits nothing —
that is a successful run, and it is what keeps the bill at cents.

### Example output

One real record (SPDR S\&P 500 ETF Trust, 2026-06-30 vs 2026-03-31):

```json
{
  "registrantCik": "0000884394",
  "registrantName": "State Street(R) SPDR(R) S&P 500(R) ETF Trust",
  "seriesId": null,
  "fundName": "State Street(R) SPDR(R) S&P 500(R) ETF Trust",
  "fundTickers": [],
  "accessionNumber": "0001410368-26-089410",
  "reportPeriod": "2026-06-30",
  "previousAccessionNumber": "0001410368-26-055357",
  "previousPeriod": "2026-03-31",
  "changeType": "new-position",
  "holdingName": "Marvell Technology Inc",
  "holdingTitle": "Marvell Technology Inc",
  "cusip": "573874104",
  "isin": "US5738741041",
  "otherId": null,
  "otherIdType": null,
  "lei": "549300LTSA1J5FJ7NF61",
  "units": "NS",
  "payoffProfile": "Long",
  "balanceBefore": null,
  "balanceAfter": 10607672.0,
  "balanceChange": 10607672.0,
  "balanceChangePct": null,
  "valUSDBefore": null,
  "valUSDAfter": 3159919412.08,
  "valUSDChange": 3159919412.08,
  "pctValBefore": null,
  "pctValAfter": 0.404501334428,
  "assetCat": "EC",
  "issuerCat": "CORP",
  "invCountry": "US",
  "filedAt": "2026-08-28",
  "filingUrl": "https://www.sec.gov/Archives/edgar/data/884394/000141036826089410/0001410368-26-089410-index.htm",
  "detectedAt": "2026-09-01T07:52:11Z"
}
```

Both source filings are named on every row, so any number can be checked against
EDGAR in one click.

### Input options

| Input | Type | Default | What it does |
|---|---|---|---|
| `tickers` | array | – | Share-class tickers: `VFIAX`, `VOO`, `BND`, `AGG`, `FXAIX`. Resolved to a fund series through SEC's own public fund-ticker file. Works for ETFs too. |
| `seriesIds` | array | – | SEC series ids (`S000016548`). N-PORT is filed **per series**, so this is the precise way to watch one fund inside a big trust. |
| `ciks` | array | – | Registrant (trust) CIKs, e.g. `0000884394` for the SPDR S\&P 500 ETF Trust. Watches **every** series the trust files for. |
| `changeTypes` | array | all four | `new-position`, `exited`, `increased`, `decreased`. |
| `minChangePct` | integer | **5** | Minimum relative balance move for `increased`/`decreased`. See below — the default matters. |
| `minValUSD` | integer | 0 | Floor on the larger absolute before/after USD value. Cuts the long tail. |
| `assetCategories` | array | all | N-PORT `assetCat` allow-list. `EC` alone gives a clean equity-ownership feed. |
| `maxItems` | integer | 1000 | Cap per run. Split fairly between the funds you watch; the overflow carries to the next run with no duplicates. |
| `emitInitialSnapshot` | boolean | false | Only for a fund with exactly one public N-PORT and nothing to diff against. |
| `userAgent` | string | placeholder | SEC requires a contact string. **Set your own.** |

At least one of `tickers` / `seriesIds` / `ciks` is required — there is no
whole-market default, because thousands of series file and each diff reads two
multi-megabyte documents.

#### Why `minChangePct` defaults to 5

A raw balance diff is mostly noise, for two reasons we measured on real filings:

- **Index funds re-scale everything, every quarter.** Creations and redemptions
  move the share count of *every* line. The S\&P 500 ETF Trust had **494 of 504
  positions changed and zero flat** between two real quarters — and 461 of those
  moves were under 5%.
- **Bond principal amounts amortise.** Mortgage pools pay down monthly. Of one
  large bond fund's 5,210 sub-5% moves, **4,658 were mortgage-backed**.

Neither is a portfolio decision. At the default, that ETF's quarter becomes
**52 meaningful rows instead of 513**. Set `minChangePct: 0` if you want the raw
diff — it is your call, and it is priced per row.

`new-position` and `exited` are **never** thresholded.

### Three ways buyers use it

1. **"Which funds bought this bond?"** — Watch a panel of credit funds by
   ticker, filter `assetCategories` to `DBT` / `ABS-MBS` / `LON`, and you get a
   quarterly feed of fixed-income ownership changes that no 13F product can
   produce, because 13F does not report debt at all.
2. **"Did my manager drift?"** — Watch the mutual funds you or your clients hold.
   New positions, exits and material trims land as structured rows with
   portfolio weights, so style drift and concentration changes show up without
   reading a shareholder report.
3. **"What changed in the index?"** — Watch an index ETF's series. Index adds and
   deletes surface as `new-position` / `exited` rows against the fund's own
   filing (Marvell in, Honeywell out, in the real quarter above).

Search terms buyers use: *SEC N-PORT scraper, mutual fund holdings API, ETF
holdings change, fund portfolio diff, NPORT-P parser, who owns this bond, fund
ownership data, mutual fund position changes*.

**Sister actor:** `sec-13f-diff` does the same thing for institutional
*managers'* quarterly 13F filings — long US equities only, but it covers hedge
funds and family offices that file no N-PORT. The two together are the full
public picture of US ownership.

### How the diff works

- **Change types are decided on `balance`** — the share count, principal amount
  or contract count the fund reported. A value move at a flat balance is a price
  move, not a portfolio action, and is never emitted.
- **Positions are aggregated** per `(identifier, units, payoffProfile)` before
  diffing, because a filer can report one position over several rows. A
  security's long and short legs stay separate, and shares (`NS`) are never
  mixed with principal amounts (`PA`) or contracts (`NC`).
- **Identity is ISIN, then CUSIP, then issuer name + security title.** Filers
  write `000000000` in `cusip` when there is none, and their `other` identifiers
  are internal trade ids that change every quarter — neither is ever used as
  identity.
- **State is the last-diffed accession number per fund series.** A run skips any
  series whose newest filing was already diffed, without fetching a single
  document. `maxItems` overflow is lossless: over-cap changes carry to the next
  run with zero duplicate rows.
- Warehouse dedupe key: `accessionNumber` + `isin`/`cusip` + `units` +
  `payoffProfile` + `changeType`.

### Honest limitations

- **Quarterly and ~60 days late.** Stated above; it is the nature of the filing,
  not of this actor.
- **Derivatives, FX forwards and repos churn.** Filers identify them by
  per-trade internal ids and roll them constantly, so they have no stable
  identity across periods. Use `assetCategories` to exclude them (`EC` for
  equities only, or list the debt categories you want).
- **Corporate actions look like an exit plus a new position.** When a CUSIP
  changes — a spin-off, a redomicile, a share-class reorganisation — the feed
  reports it honestly as what the filing says, not as a continuation.
- **Numbers are the filer's own**, never recomputed. `pctVal` is a percent of
  net assets as filed and can sum well past 100 for a levered fund.
- **`NPORT-P/A` amendments are ignored** by default.
- **Funds without a series** (unit investment trusts such as SPY) are absent
  from SEC's fund-ticker file and must be watched by registrant CIK.
- **A big fund family is expensive to watch whole.** One real trust filed 94
  separate N-PORT reports for a single quarter. Name the funds you want.

### Pricing (pay per event)

| Event | Price (USD) | Charged |
|---|---|---|
| Actor start | $0.005 | Flat, per run |
| Holding change detected | $0.004 / $0.003 / $0.002 | Per emitted row, volume-tiered: first 500 in a run, then 501–2000, then beyond |
| Fund series checked | $0.002 | Per watched series whose newest filing was checked, emitted or not |

A 10-series weekly schedule costs about **$0.025** on a quiet run — roughly
**$1.30 a year** — plus the four real diffs per series. The tiering keeps a
10,000-holding bond fund's quarter affordable.

### Maintenance

Schema checked against live EDGAR filings; a missing field logs a named warning
and skips one row rather than failing the run. Issues and field requests via the
Apify actor's issues tab.

### Changelog

- **0.1** — first release. NPORT-P diff per fund series; ticker/series/CIK
  targeting via SEC's own public files; relative-move floor; fair per-series
  `maxItems` split with lossless carry-over.

# Actor input Schema

## `tickers` (type: `array`):

Share-class tickers of the funds you want — mutual funds or ETFs (e.g. VFIAX, VOO, BND, AGG, FXAIX). Resolved to a fund series via SEC's own public company\_tickers\_mf.json, no API key. A ticker that is not in that file (SPY, for example — a unit investment trust with no series) has to be watched by registrant CIK instead.

## `seriesIds` (type: `array`):

SEC series ids (e.g. S000002839). N-PORT is filed per SERIES, so this is the precise way to watch one fund inside a big trust — PIMCO Funds filed 94 separate N-PORT reports for one quarter.

## `ciks` (type: `array`):

SEC CIK numbers of the fund registrants (trusts), any zero-padding — e.g. 0000884394 for the SPDR S\&P 500 ETF Trust. Watches EVERY fund series the trust files for, which for a large family is dozens of funds and dozens of multi-MB documents. Narrow with tickers or seriesIds unless you really want the whole trust.

## `changeTypes` (type: `array`):

Which portfolio actions count. Default: all four.

## `minChangePct` (type: `integer`):

Floor on |balanceAfter - balanceBefore| / |balanceBefore| for increased/decreased rows. new-position and exited are never filtered. Default 5%, and it matters: an index fund's share count moves on EVERY line each quarter because creations and redemptions scale the whole portfolio (measured on SPY: 494 of 504 positions moved, none flat, 461 of them by under 5%), and a bond fund's principal amounts fall monthly as mortgage pools amortise (4,658 of one fund's 5,210 sub-5% moves were ABS-MBS). Neither is a portfolio decision. Set 0 for the raw, unfiltered diff.

## `minValUSD` (type: `integer`):

Optional floor applied to the larger ABSOLUTE before/after USD value — use it to cut a fund's long tail. Absolute, because short positions and sold protection carry a negative valUSD. 0 = no floor.

## `assetCategories` (type: `array`):

Optional allow-list of N-PORT assetCat codes. Empty = all. Use EC alone for a clean equity-ownership feed: derivatives, FX forwards and repos have no stable identity across periods (filers key them on per-trade internal ids), so they churn.

## `maxItems` (type: `integer`):

Cap on changes emitted in a single run (cost control). Biggest positions come first; anything over the cap carries over to the next run with no duplicates, and the cap is split fairly between the funds you watch so one bond fund cannot starve the rest.

## `emitInitialSnapshot` (type: `boolean`):

Only applies when a fund series has exactly ONE public N-PORT on file and there is nothing to diff against. Off (default): the filing is recorded and nothing is emitted. On: every holding is emitted as new-position with null before-values — which for a bond fund is over 10,000 rows. Funds with two or more filings always produce a real period-over-period diff on the first run.

## `userAgent` (type: `string`):

SEC's fair-access policy requires a User-Agent identifying who you are, e.g. 'my-company research you@example.com'. Please set your own contact here.

## Actor input object example

```json
{
  "tickers": [
    "VFIAX",
    "BND"
  ],
  "changeTypes": [
    "new-position",
    "exited",
    "increased",
    "decreased"
  ],
  "minChangePct": 5,
  "minValUSD": 0,
  "assetCategories": [],
  "maxItems": 1000,
  "emitInitialSnapshot": false,
  "userAgent": "apify-actor-sec-nport-holdings daniel@apexwebstudio.co.nz"
}
```

# Actor output Schema

## `records` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "tickers": [
        "VFIAX",
        "BND"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datadeltas/sec-nport-holdings").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "tickers": [
        "VFIAX",
        "BND",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("datadeltas/sec-nport-holdings").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "tickers": [
    "VFIAX",
    "BND"
  ]
}' |
apify call datadeltas/sec-nport-holdings --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datadeltas/sec-nport-holdings"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/aYv76rn7gCf7Ap04q/builds/oXUVcLbibcLHIQWRs/openapi.json
