ZipLime/institutional-portfolio-13f
ZipLime Institutional Fund Portfolios (PIT) [!CAUTION] A 13F is stale by construction. It reports a quarter that ended up to 45 days earlier, and amendments restate it for months afterwards — the longest in this dataset arrived 452 days late. Use knowledge_date, never event_date, to decide what a strategy could act on. Every Form 13F holdings report filed with the U.S. SEC since structured filing began, turned into point-in-time portfolios: what each institutional manager held… See the full description on the dataset page: https://huggingface.co/datasets/ZipLime/institutional-portfolio-13f.
ZipLime Institutional Fund Portfolios (PIT)
[!CAUTION] A 13F is stale by construction. It reports a quarter that ended up to 45 days earlier, and amendments restate it for months afterwards — the longest in this dataset arrived 452 days late. Useknowledge_date, neverevent_date, to decide what a strategy could act on.
Every Form 13F holdings report filed with the U.S. SEC since structured filing began, turned into point-in-time portfolios: what each institutional manager held, as it became known.
The deterministic recipe lives in `recipe/` in the same repository and revision as the published data. Nothing here was assembled by hand — see PIPELINE.md for the method, including the parts that are approximate.
Configs
Covering 2013 Q2 to 2026 Q2 across 16,521 managers and 31,000 securities, current to the last filing accepted on 2026-09-03.
from datasets import load_dataset
positions = load_dataset("ZipLime/institutional-portfolio-13f", "positions", split="train")Or straight from the Delta table, which keeps the exact dtypes:
import polars as pl
positions = pl.read_delta("data/positions/fund_portfolios.delta")Pin a repository revision for a reproducible research result.
The one thing to know before backtesting
A 13F tells you what a manager held at a quarter end, not what they hold. The gap is structural and large:
quarter ends 30 Jun -> report due 14 Aug -> amended into NovemberOn 1 August you are still holding the March portfolio. Between 14 August and 14 November you hold June's. Median disclosure lag in this dataset is 45 days; amendments run to a median of 135 days and a maximum of 452.
Two consequences the data itself carries:
- Confidential treatment hides positions, then reveals them. A manager can ask the SEC to withhold a position while it is being built; when the request expires the position appears in a
NEW HOLDINGSamendment months later. Berkshire's Q1 2025 report showed 36 positions when filed and 39 after the 14 August amendment. Query at the earlier date and the withheld position is correctly absent. - Only long US-listed equity is here. No shorts, no cash, no bonds, no foreign listings. For Berkshire that means the Japanese trading houses and the operating businesses are invisible; this is the US public equity book, not the company.
Point-in-time view
data/positions/fund_portfolios.delta is the table a ziplime bundle mounts. manifest.json describes it: bundle_storage_data.table_uri points at the table and schema lists its columns.
system entity_id, event_date, knowledge_date, knowledge_estimated
values manager_cik, manager_name, cusip, issuer_name, title_of_class,
put_call, shares, value_usd, weight_pct, portfolio_value_usd,
position_count, disclosure_lag_days, was_confidential,
value_flagged, logical_position_id, revision, amendment_statusentity_id is the manager's CIK — the portfolio's owner. Securities are keyed by CUSIP, which is how 13F identifies them; resolving CUSIP to a tradeable asset is the consumer's join.
The as-of query has three steps and the order is load-bearing:
import polars as pl
positions = pl.read_delta("data/positions/fund_portfolios.delta")
# 1. Restrict to what was knowable at the simulation timestamp.
visible = positions.filter(pl.col("knowledge_date") <= simulation_time)
# 2. Resolve revisions: the newest state of each logical position.
latest = visible.sort(
["logical_position_id", "revision", "knowledge_date", "position_event_id"]
).unique(subset=["logical_position_id"], keep="last", maintain_order=True)
# 3. Only now drop what is no longer in force.
portfolio = latest.filter(pl.col("operation") == "UPSERT")- Filtering by
knowledge_datefirst keeps an amendment filed aftersimulation_timefrom deciding which row survives. - Sorting by
revisionbeforeknowledge_datematters because an estimated knowledge date is an upper bound, pushed to the end of the filing day. - Dropping non-
UPSERTrows last matters because a retraction is itself a revision; filtering it out earlier resurrects the position it retracted.
`recipe/build_positions.py` implements exactly this as as_of(positions, at, manager_cik=...), which also picks the latest quarter the manager had reported by that moment and recomputes weights on the resolved set.
Amendments
A 13F-HR/A states which kind it is, and the two are not interchangeable:
Treating one as the other either drops most of a portfolio or double counts it. 22,060 position events come from amendments that declare no type; pass include_unresolved=True to see them.
Reporting units — the largest trap in this data
Form 13F reported holding value in thousands of dollars until an amendment required whole dollars for filings made from January 2023. Filers adopted that on their own schedule: Renaissance Technologies was still filing in thousands six quarters later, and 622 of 10,117 filers were in mid-2024.
A filing read in the wrong unit is wrong by a factor of a thousand while every weight in it stays correct, so nothing downstream notices. The unit is therefore inferred from the filing's own numbers — value divided by shares is a price per share — and then reconciled against the manager's own value history, because a portfolio does not move three orders of magnitude between quarters. 2,376 filings had their unit corrected that way.
What neither signal settles is published, not silently decided: 682 filings (0.24%) across 381 managers carry value_flagged on their positions and appear in quality/filing_flags.parquet. Filter on `value_flagged` before summing notional across managers.
Coverage and known gaps
Other things worth knowing:
- 13F-NT are notices. 84,028 submissions report that a manager has nothing to disclose. They are kept in
filingsand produce no positions. - 14,466 filings (4.5%) disagree with the totals their own cover page declares. Filers copy a previous quarter's summary or miscount by one; the filed rows are kept and the disagreement is published in
control_rows_delta. - 66 filings put share counts in the value column. Flagged
value_quality = "value_equals_shares", values unusable. - Reporting periods before 2013 appear because a handful of filers typed a wrong
PERIODOFREPORT. They are retained as filed. - A quarter is not a holding period. Anything bought and sold inside one quarter never appears at all.
Using it with ziplime
manifest.json declares what a bundle needs: data_type: PIT_DATA, the storage class, and bundle_storage_data.table_uri pointing at the Delta table. Ingest reads that table; resolving positions to ziplime assets means mapping CUSIP to the asset database.
Not every manager's 13F is a portfolio worth cloning. `recipe/managers.py` carries a curated registry with a representative flag and the reason: a long-only concentrated book like Berkshire's is substantially the whole position, while a multi-strategy platform's long equity slice is a fragment of a hedge whose other legs are not filed, and a quantitative shop's quarter-end snapshot is not a portfolio anyone held.
How the data was verified
Every published figure comes from the filing it cites; nothing is inferred by a model. Three checks carry the most weight, all documented in PIPELINE.md:
- Two independent sources agree. History is loaded from SEC's quarterly Form 13F Data Sets, but the same filings were also parsed one by one from their XML. On the overlap the two routes produced 25,366 identical positions — zero disagreements in shares, zero in value once the unit normalisation is applied.
- Acceptance time is read, not taken on trust. It comes from each submission's SGML header, which is stamped in Eastern time and converted here. EDGAR's submission index was checked against those headers on 972 filings: 118 (12%) disagreed, every one by exactly four or five hours — Eastern time labelled UTC, in the direction that creates look-ahead. The index is not used.
- Publication gates.
recipe/quality.pyruns before every publication and fails the build on duplicate event IDs, negative values, non-monotonic revisions, position revisions visible before what they supersede, and a systematic rate of unit discontinuity. This revision passes all of them (data/quality_report.json).
Updates
Runs execute on Hugging Face Jobs from `jobs/run.py`, a self-contained UV script stored in this repository, so the code that produced a revision ships with that revision. The order is fixed:
restore -> lint -> tests -> update -> rebuild positions -> validate -> publishNothing is uploaded unless every gate before it passed, so a broken run leaves the previous revision as the latest valid release. New submissions are found through EDGAR's quarterly index — one request enumerates a quarter — and only those not already stored are fetched.
License, source terms, and disclaimer
The ZipLime-authored recipe, documentation, schema and database compilation are offered under the Apache License 2.0, to the extent ZipLime has rights to license them. The underlying filings are created by institutional investment managers and made available through SEC EDGAR; this project does not claim that those filings are U.S. Government works. Source URLs and accession numbers are retained on every row for verification. See NOTICE for scope.
CUSIP identifiers appear as filed in public SEC documents. CUSIP Global Services asserts rights in the CUSIP database; consider whether your use requires a licence from them.
Use of SEC systems must follow its developer resources and Fair Access guidance. This dataset is provided for research, without warranty. It is not investment advice, and the extraction is imperfect in the ways documented above.
