History of sec_13f_holdings
The changes to the source code of this entry in the repository, newest first, each with the differences it made to the entry's files. They are also listed on GitLab.
[entries] Add the date when each entry was added, and remove icon_hash beff7c9
The dates are those of the commits that first added the entries' files (2020-08-11 for the entries of the initial commit); renamed entries keep the date of their first name. Changed: none
diff --git a/entries/sec_13f_holdings/__init__.py b/entries/sec_13f_holdings/__init__.py
index 3316e7e..4796611 100644
--- a/entries/sec_13f_holdings/__init__.py
+++ b/entries/sec_13f_holdings/__init__.py
@@ -53,7 +53,7 @@ url = ['https://www.sec.gov/data-research/sec-markets-data/form-13f-data-sets',
'https://www.sec.gov/files/form_13f_readme.pdf']
citation = [('U.S. Securities and Exchange Commission, "Form 13F Data Sets." (2014-2026)',
'https://www.sec.gov/data-research/sec-markets-data/form-13f-data-sets')]
-icon_hash = None
+date_added = '2026-10-31'
upstream_license = ("Public domain (US government work)", "https://www.usa.gov/government-copyright")
upstream_prefix = 'https://www.sec.gov/files'
[entries] Prefix the properties in the descriptions with (Node), (Edge) or (Graph) bc3dbbd
The lists of properties at the end of the descriptions marked edge properties with "Edge" but left node properties unmarked, or grouped them in "Nodes: `a`, ...; `b`, ..." items, so the kind of a property was often unclear. Every item now starts with (Node), (Edge) or (Graph), followed by the property names, a colon and the explanation, one item per property or group of properties; qualifications (kinds of nodes, networks) follow the names in parentheses. Properties that were only named got a short explanation, and a few items that named the wrong kind of property were corrected.
diff --git a/entries/sec_13f_holdings/__init__.py b/entries/sec_13f_holdings/__init__.py
index b5f38ad..3316e7e 100644
--- a/entries/sec_13f_holdings/__init__.py
+++ b/entries/sec_13f_holdings/__init__.py
@@ -30,24 +30,24 @@ The nodes are the managers (filers), identified by their Central Index Key (CIK)
The networks have the following properties:
-- `is_manager`: whether the node is a manager (otherwise it is a security).
-- `cik` (managers): the Central Index Key of the manager (0 for securities).
-- `cusip` (securities): the CUSIP number of the security (empty for managers).
-- `name`: the name of the manager (from its last report of the quarter), or the name of the issuer of the security (the most common spelling in the reports).
-- `city`, `state_or_country`, `zip_code` (managers): the address of the manager.
-- `file_number` (managers): the Form 13F file number of the manager.
-- `figi` (securities): the Financial Instrument Global Identifier of the security, when given (from 2023 on).
-- Edge `value`: the market value of the holding, in US dollars: the reports filed before January 3, 2023 (here, until `2022q3`) give it in thousands of dollars, converted here to dollars, and the later ones in dollars (a few managers still report thousands in the later networks, e.g. about 10 % of the holdings of Apple stock in `2022q4`, which are kept as reported).
-- Edge `shares`: the number of shares, or the principal amount for debt securities.
-- Edge `shares_type`: `SH` (shares) or `PRN` (principal amount).
-- Edge `put_call`: `Put` or `Call` if the holding is an option (the underlying security is given), otherwise empty.
-- Edge `title_of_class`: the title of the class of the security (e.g. `COM`, `CL A`).
-- Edge `discretion`: the investment discretion: `SOLE`, `DFND` (shared-defined) or `OTR` (shared-other).
-- Edge `other_managers`: the sequence numbers of the other managers included in the report with whom the holding is shared (as given, empty if none).
-- Edge `voting_sole`, `voting_shared`, `voting_none`: the number of shares for which the manager has sole, shared, or no voting authority.
-- Edge `accession`: the accession number of the filing; `filing_date`: its date (`YYYY-MM-DD`).
-- Edge `amendment_type`: empty for original reports, or `RESTATEMENT` or `NEW HOLDINGS` for amendments; `amendment_number`: the number of the amendment (0 for original reports).
-- `period` (graph): the date of the end of the period of the reports."""
+- (Node) `is_manager`: whether the node is a manager (otherwise it is a security).
+- (Node) `cik` (managers): the Central Index Key of the manager (0 for securities).
+- (Node) `cusip` (securities): the CUSIP number of the security (empty for managers).
+- (Node) `name`: the name of the manager (from its last report of the quarter), or the name of the issuer of the security (the most common spelling in the reports).
+- (Node) `city`, `state_or_country`, `zip_code` (managers): the address of the manager.
+- (Node) `file_number` (managers): the Form 13F file number of the manager.
+- (Node) `figi` (securities): the Financial Instrument Global Identifier of the security, when given (from 2023 on).
+- (Edge) `value`: the market value of the holding, in US dollars: the reports filed before January 3, 2023 (here, until `2022q3`) give it in thousands of dollars, converted here to dollars, and the later ones in dollars (a few managers still report thousands in the later networks, e.g. about 10 % of the holdings of Apple stock in `2022q4`, which are kept as reported).
+- (Edge) `shares`: the number of shares, or the principal amount for debt securities.
+- (Edge) `shares_type`: `SH` (shares) or `PRN` (principal amount).
+- (Edge) `put_call`: `Put` or `Call` if the holding is an option (the underlying security is given), otherwise empty.
+- (Edge) `title_of_class`: the title of the class of the security (e.g. `COM`, `CL A`).
+- (Edge) `discretion`: the investment discretion: `SOLE`, `DFND` (shared-defined) or `OTR` (shared-other).
+- (Edge) `other_managers`: the sequence numbers of the other managers included in the report with whom the holding is shared (as given, empty if none).
+- (Edge) `voting_sole`, `voting_shared`, `voting_none`: the number of shares for which the manager has sole, shared, or no voting authority.
+- (Edge) `accession`: the accession number of the filing; `filing_date`: its date (`YYYY-MM-DD`).
+- (Edge) `amendment_type`: empty for original reports, or `RESTATEMENT` or `NEW HOLDINGS` for amendments; `amendment_number`: the number of the amendment (0 for original reports).
+- (Node) `period` (graph): the date of the end of the period of the reports."""
tags = ['Economic', 'Commerce', 'Bipartite', 'Multigraph', 'Weighted', 'Metadata']
url = ['https://www.sec.gov/data-research/sec-markets-data/form-13f-data-sets',
'https://www.sec.gov/files/form_13f_readme.pdf']
[entries] sec_13f_holdings: read the data set of 2025q2 from its subdirectory e747a2f
Unlike the others, the archive of 2025q2 (01jun2025-31aug2025_form13f.zip) has its files in a directory, 01JUN2025-31AUG2025_form13f/.
diff --git a/entries/sec_13f_holdings/__init__.py b/entries/sec_13f_holdings/__init__.py
index de25737..b5f38ad 100644
--- a/entries/sec_13f_holdings/__init__.py
+++ b/entries/sec_13f_holdings/__init__.py
@@ -81,7 +81,9 @@ for _y, _q, _w in [(2024, 1, "01mar2024-31may2024"), (2024, 2, "01jun2024-31aug2
_datasets["2026q2"] = "datastandardsinnovation/data/form-13f-data-sets/01jun2026-31aug2026_form13f.zip"
_period_end = {1: ("31-MAR", "03-31"), 2: ("30-JUN", "06-30"),
3: ("30-SEP", "09-30"), 4: ("31-DEC", "12-31")}
-files = [(tuple(f"{os.path.basename(p)}:{t}.tsv"
+# (the files of a data set are in a directory of its archive)
+_subdir = {"2025q2": "01JUN2025-31AUG2025_form13f/"}
+files = [(tuple(f"{os.path.basename(p)}:{_subdir.get(q, '')}{t}.tsv"
for t in ["SUBMISSION", "COVERPAGE", "INFOTABLE"]),
q, None) for q, p in _datasets.items()]
[entries] New entry: sec_13f_holdings b5dca57
diff --git a/entries/sec_13f_holdings/__init__.py b/entries/sec_13f_holdings/__init__.py
new file mode 100644
index 0000000..de25737
--- /dev/null
+++ b/entries/sec_13f_holdings/__init__.py
@@ -0,0 +1,225 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2026 Tiago de Paula Peixoto <tiago@skewed.de>
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from .. import *
+import urllib.request
+import process_entry
+
+title = "Institutional investment manager holdings (SEC Form 13F, 2013-2026)"
+description = r"""Bipartite networks of the institutional investment managers of the United States and the securities that they hold, from the Form 13F reports filed with the US Securities and Exchange Commission (SEC). Every institutional investment manager that exercises investment discretion over at least 100 million dollars in "13(f) securities" (exchange-traded stocks, shares of closed-end funds, convertible bonds, options, ...) must report its holdings of these securities at the end of each calendar quarter, within 45 days. The SEC publishes the information tables of these reports as quarterly data sets, extracted from the XML filings "as filed", without corrections.
+
+There is one network per quarter, from the first quarter of 2013 to the second quarter of 2026 (54 networks), named by the quarter (e.g. `2025q4`), of the holdings at the end of the quarter: it contains all the 13F holdings reports (form `13F-HR`, including the combination reports) and their amendments (`13F-HR/A`) for the period ending on the last day of the quarter, filed within the quarterly data set of the SEC that covers their deadline (until 2023, the data set of the filings of the following calendar quarter; after the SEC changed the periods of its data sets in 2024, the data set of the three months following the end of the quarter, e.g. December to February for the fourth quarter, and January to February 2024 for the fourth quarter of 2023). The reports filed later (late filers, later amendments), which are in the following data sets, are not included. The first network, `2013q1`, may be incomplete: the data sets of the SEC, extracted from the XML information tables introduced in 2013, start with the filings of April to June 2013, which contain the reports for the first quarter of 2013 filed then.
+
+The nodes are the managers (filers), identified by their Central Index Key (CIK) at the SEC, and the securities, identified by their CUSIP number (upper-cased). Each edge is one row of the information table of a report, from the manager to the security, so that the network is a multigraph: a manager reports several rows for the same security when its holdings differ in the kind of investment discretion, the other managers with which it is shared, or whether they are options (puts and calls), and the amendments add or restate rows (see `amendment_type`; a restatement replaces the original report, whose rows are still in the network). The values are as reported by the managers, who also include sometimes securities that are not on the official list of 13(f) securities; the reports that the SEC granted confidential treatment are not included.
+
+The networks have the following properties:
+
+- `is_manager`: whether the node is a manager (otherwise it is a security).
+- `cik` (managers): the Central Index Key of the manager (0 for securities).
+- `cusip` (securities): the CUSIP number of the security (empty for managers).
+- `name`: the name of the manager (from its last report of the quarter), or the name of the issuer of the security (the most common spelling in the reports).
+- `city`, `state_or_country`, `zip_code` (managers): the address of the manager.
+- `file_number` (managers): the Form 13F file number of the manager.
+- `figi` (securities): the Financial Instrument Global Identifier of the security, when given (from 2023 on).
+- Edge `value`: the market value of the holding, in US dollars: the reports filed before January 3, 2023 (here, until `2022q3`) give it in thousands of dollars, converted here to dollars, and the later ones in dollars (a few managers still report thousands in the later networks, e.g. about 10 % of the holdings of Apple stock in `2022q4`, which are kept as reported).
+- Edge `shares`: the number of shares, or the principal amount for debt securities.
+- Edge `shares_type`: `SH` (shares) or `PRN` (principal amount).
+- Edge `put_call`: `Put` or `Call` if the holding is an option (the underlying security is given), otherwise empty.
+- Edge `title_of_class`: the title of the class of the security (e.g. `COM`, `CL A`).
+- Edge `discretion`: the investment discretion: `SOLE`, `DFND` (shared-defined) or `OTR` (shared-other).
+- Edge `other_managers`: the sequence numbers of the other managers included in the report with whom the holding is shared (as given, empty if none).
+- Edge `voting_sole`, `voting_shared`, `voting_none`: the number of shares for which the manager has sole, shared, or no voting authority.
+- Edge `accession`: the accession number of the filing; `filing_date`: its date (`YYYY-MM-DD`).
+- Edge `amendment_type`: empty for original reports, or `RESTATEMENT` or `NEW HOLDINGS` for amendments; `amendment_number`: the number of the amendment (0 for original reports).
+- `period` (graph): the date of the end of the period of the reports."""
+tags = ['Economic', 'Commerce', 'Bipartite', 'Multigraph', 'Weighted', 'Metadata']
+url = ['https://www.sec.gov/data-research/sec-markets-data/form-13f-data-sets',
+ 'https://www.sec.gov/files/form_13f_readme.pdf']
+citation = [('U.S. Securities and Exchange Commission, "Form 13F Data Sets." (2014-2026)',
+ 'https://www.sec.gov/data-research/sec-markets-data/form-13f-data-sets')]
+icon_hash = None
+upstream_license = ("Public domain (US government work)", "https://www.usa.gov/government-copyright")
+
+upstream_prefix = 'https://www.sec.gov/files'
+# The quarterly data sets of the SEC, by the quarter of the reports that
+# they contain: until 2023, data sets of the filings of a calendar quarter,
+# which contain the reports of the previous quarter (e.g. 2014q1 for the
+# period ending 2013-12-31); from 2024, data sets of the three months after
+# the end of each quarter (the filings of January and February 2024 for the
+# fourth quarter of 2023, then of March to May for the first quarter, etc.)
+_ds = "structureddata/data/form-13f-data-sets"
+_datasets = {}
+for _y in range(2013, 2024):
+ for _q in range(1, 5):
+ if (_y, _q) == (2013, 1):
+ continue
+ _r = (_y, _q - 1) if _q > 1 else (_y - 1, 4)
+ _datasets[f"{_r[0]}q{_r[1]}"] = f"{_ds}/{_y}q{_q}_form13f.zip"
+_datasets["2023q4"] = f"{_ds}/01jan2024-29feb2024_form13f.zip"
+for _y, _q, _w in [(2024, 1, "01mar2024-31may2024"), (2024, 2, "01jun2024-31aug2024"),
+ (2024, 3, "01sep2024-30nov2024"), (2024, 4, "01dec2024-28feb2025"),
+ (2025, 1, "01mar2025-31may2025"), (2025, 2, "01jun2025-31aug2025"),
+ (2025, 3, "01sep2025-30nov2025"), (2025, 4, "01dec2025-28feb2026"),
+ (2026, 1, "01mar2026-31may2026")]:
+ _datasets[f"{_y}q{_q}"] = f"{_ds}/{_w}_form13f.zip"
+_datasets["2026q2"] = "datastandardsinnovation/data/form-13f-data-sets/01jun2026-31aug2026_form13f.zip"
+_period_end = {1: ("31-MAR", "03-31"), 2: ("30-JUN", "06-30"),
+ 3: ("30-SEP", "09-30"), 4: ("31-DEC", "12-31")}
+files = [(tuple(f"{os.path.basename(p)}:{t}.tsv"
+ for t in ["SUBMISSION", "COVERPAGE", "INFOTABLE"]),
+ q, None) for q, p in _datasets.items()]
+
+# The SEC refuses downloads without a User-Agent that declares a contact (its
+# fair access policy), and limits the rate of requests
+_user_agent = "netzschleuder postmaster@skewed.de"
+
+def _open(url):
+ pace_request(url)
+ req = urllib.request.Request(url, headers={"User-Agent": _user_agent})
+ return urllib.request.urlopen(req, timeout=DOWNLOAD_TIMEOUT)
+
+def fetch_upstream(force=False):
+ name = __name__.split(".")[-1]
+ paths = list(_datasets.values())
+ if process_entry._check_health:
+ for p in [paths[0], paths[-1]]:
+ with _open(f"{upstream_prefix}/{p}") as f:
+ check_not_html(os.path.basename(p), f.read(1 << 10))
+ return
+ for p in paths:
+ fname = os.path.basename(p)
+ if not force and os.path.exists(f"cache/upstream/{name}/{fname}"):
+ continue
+ for tries in range(4):
+ try:
+ with _open(f"{upstream_prefix}/{p}") as f:
+ total = f.headers.get("Content-Length")
+ head = f.read(1 << 10)
+ check_not_html(fname, head)
+ with open_upstream_file(name, fname, "wb") as output:
+ output.write(head)
+ n = len(head) + copy_download(f, output, len(head),
+ tries=0, label=fname) - len(head)
+ if total is not None and n != int(total):
+ raise OSError(f"incomplete download of {fname}")
+ break
+ except OSError as e:
+ if tries == 3:
+ raise
+ print(f"retrying {fname} after error: {e!r}")
+ time.sleep(30)
+
+def _date(x):
+ "DD-MON-YYYY as YYYY-MM-DD"
+ return datetime.strptime(x, "%d-%b-%Y").strftime("%Y-%m-%d") if x else ""
+
+# For checks only: if set, only this many rows are read of each file
+_check_rows = None
+
+@cache_network()
+@coerce_props()
+@annotate()
+def parse(alts=None):
+ import polars as pl
+ name = __name__.split(".")[-1]
+ for fnames, alt, fmt in files:
+ if alts is not None and alt not in alts:
+ continue
+ year, quarter = int(alt[:4]), int(alt[5])
+ tabs = []
+ for fn in fnames:
+ with open_upstream_file(name, fn, "rb") as f:
+ tabs.append(pl.read_csv(f.read(), separator="\t", n_rows=_check_rows,
+ infer_schema=False, quote_char=None))
+ sub, cover, info = tabs
+ del tabs
+ period = f"{_period_end[quarter][0]}-{year}"
+ sub = (sub.filter(pl.col("SUBMISSIONTYPE").str.starts_with("13F-HR") &
+ (pl.col("PERIODOFREPORT") == period))
+ .join(cover, on="ACCESSION_NUMBER", how="left"))
+ info = (info.join(sub.select("ACCESSION_NUMBER", "CIK", "FILING_DATE",
+ "AMENDMENTTYPE", "AMENDMENTNO"),
+ on="ACCESSION_NUMBER")
+ .with_columns(pl.col("CUSIP").str.to_uppercase().str.strip_chars(),
+ pl.col("CIK").cast(pl.Int64)))
+
+ # the managers, by CIK, with the data of their last report
+ mgr = (sub.with_columns(pl.col("CIK").cast(pl.Int64),
+ pl.col("FILING_DATE").str.to_date("%d-%b-%Y").alias("fd"))
+ .filter(pl.col("CIK").is_in(info["CIK"].unique().implode()))
+ .sort(["CIK", "fd", "ACCESSION_NUMBER"])
+ .unique(subset="CIK", keep="last", maintain_order=True))
+ # the securities, by CUSIP, with the most common spelling of the
+ # name of their issuer
+ sec = (info.group_by("CUSIP", "NAMEOFISSUER").len()
+ .sort(["CUSIP", "len", "NAMEOFISSUER"], descending=[False, True, False])
+ .unique(subset="CUSIP", keep="first", maintain_order=True)
+ .join(info.filter(pl.col("FIGI").is_not_null())
+ .group_by("CUSIP").agg(pl.col("FIGI").first())
+ if "FIGI" in info.columns else
+ pl.DataFrame({"CUSIP": [], "FIGI": []}, schema={"CUSIP": pl.String, "FIGI": pl.String}),
+ on="CUSIP", how="left"))
+ n_m = len(mgr)
+ info = (info.join(mgr.select("CIK").with_row_index("s"), on="CIK")
+ .join(sec.select("CUSIP").with_row_index("t"), on="CUSIP"))
+
+ g = Graph(directed=False)
+ g.add_vertex(n_m + len(sec))
+ g.add_edge_list(numpy.column_stack((info["s"].to_numpy().astype("int64"),
+ info["t"].to_numpy().astype("int64") + n_m)))
+ g.vp.is_manager = g.new_vp("bool", vals=numpy.arange(g.num_vertices()) < n_m)
+ g.vp.cik = g.new_vp("int64_t", vals=numpy.concatenate(
+ (mgr["CIK"].to_numpy(), numpy.zeros(len(sec), dtype="int64"))))
+ g.vp.cusip = g.new_vp("string", vals=[""] * n_m + sec["CUSIP"].to_list())
+ g.vp.name = g.new_vp("string", vals=[" ".join((x or "").split()) for x in
+ mgr["FILINGMANAGER_NAME"].to_list() +
+ sec["NAMEOFISSUER"].to_list()])
+ for k, c in [("city", "FILINGMANAGER_CITY"),
+ ("state_or_country", "FILINGMANAGER_STATEORCOUNTRY"),
+ ("zip_code", "FILINGMANAGER_ZIPCODE"),
+ ("file_number", "FORM13FFILENUMBER")]:
+ g.vp[k] = g.new_vp("string", vals=mgr[c].fill_null("").to_list() +
+ [""] * len(sec))
+ g.vp.figi = g.new_vp("string", vals=[""] * n_m +
+ sec["FIGI"].cast(pl.String).fill_null("").to_list())
+ del mgr, sec, sub, cover
+
+ # the values are in thousands of dollars in the filings made before
+ # January 3, 2023, and in dollars after
+ unit = 1000. if (year, quarter) <= (2022, 3) else 1.
+ num = lambda c: info[c].str.replace_all(",", "").cast(pl.Float64, strict=False)
+ g.ep.value = g.new_ep("double", vals=(num("VALUE") * unit).to_numpy())
+ g.ep.shares = g.new_ep("double", vals=num("SSHPRNAMT").to_numpy())
+ for k, c in [("shares_type", "SSHPRNAMTTYPE"), ("put_call", "PUTCALL"),
+ ("title_of_class", "TITLEOFCLASS"),
+ ("discretion", "INVESTMENTDISCRETION"),
+ ("other_managers", "OTHERMANAGER"),
+ ("accession", "ACCESSION_NUMBER"),
+ ("amendment_type", "AMENDMENTTYPE")]:
+ g.ep[k] = g.new_ep("string", vals=info[c].fill_null("").to_list())
+ for k, c in [("voting_sole", "VOTING_AUTH_SOLE"),
+ ("voting_shared", "VOTING_AUTH_SHARED"),
+ ("voting_none", "VOTING_AUTH_NONE"),
+ ("amendment_number", "AMENDMENTNO")]:
+ g.ep[k] = g.new_ep("int64_t", vals=num(c).fill_null(0).cast(pl.Int64).to_numpy())
+ g.ep.filing_date = g.new_ep("string", vals=info["FILING_DATE"].str.to_date("%d-%b-%Y")
+ .cast(pl.String).to_list())
+ g.gp.period = g.new_gp("string", f"{year}-{_period_end[quarter][1]}")
+ del info
+ yield alt, g