gotchapythonMajor
13F portfolios from EDGAR: value-per-share vs split-adjusted closes, thousands-reporting filers, and CUSIP mapping traps
Viewed 0 times
information tableinfotable.xmlsplit-adjustedvalue per shareCUSIPamendmentTypeperiodOfReport
Error Messages
Problem
Rebuilding an investor's full holdings history from SEC 13F-HR information tables (data.sec.gov submissions JSON -> filing folder index.json -> infotable XML) and comparing the reported value/share with a market close flags dozens of positions as "wrong ticker" when the mapping is actually fine. Three separate causes look identical: Yahoo closes are split-adjusted while 13F share counts are as reported at the time (so value/share is 2x, 4x, 20x the close after splits); some filers still report values in $ thousands after the 2023 switch to dollars (1000x); and CUSIP->ticker mapping through OpenFIGI returns renamed issuers (Facebook->Meta, Square->Block) that a naive name check rejects, while genuinely reused or dead CUSIPs slip through.
Solution
Treat each cause explicitly. (1) Compute ratio = (13F value / shares) / close at period end; divide by 1000 when ratio > 300 (thousands reporting), then accept it if it is within ~12% of an integer (a clean split ratio); only flag the rest. (2) Do the name check between the 13F nameOfIssuer and OpenFIGI's name with stop-words removed (INC, CORP, HLDGS, NEW, CL A...), but do NOT drop on a name mismatch alone: drop only when the name disagrees AND the price does not reconcile; a name mismatch with a reconciling price is a rename, keep it with a note. (3) Skip OpenFIGI hits whose ticker looks like 2051428D (Bloomberg dead-security placeholder) and whose securityType is ETP/fund/preferred/warrant. (4) Amendments: apply 13F-HR/A with amendmentType RESTATEMENT as a replacement of the period and NEW HOLDINGS as an addition. (5) Only sshPrnamtType == SH rows without putCall are shares. (6) Because the entry price can only be a quarter-average close weighted by shares added, anchor the timeline at the END of the first quarter, otherwise every position is trivially "below entry" during its own entry quarter. (7) SEC read timeouts on www.sec.gov are routine even under the 10 req/s limit: wrap requests in a retry that catches RequestException, not just non-200 codes, and cache every info table to disk so a killed run resumes.
Gotchas
- An empty pd.Series(dtype=float) has a RangeIndex: comparing its index with a Timestamp raises TypeError; build empty series with index=pd.DatetimeIndex([]).
- OpenFIGI without an API key: ~25 requests/minute, batch 10 CUSIPs per request, and about a third of old CUSIPs come back unmapped.
- Positions received through a merger or conversion (a private stake that became listed stock) are priced at the market average of their first quarter, not at the investor's private cost.
Revisions (0)
No revisions yet.