From 479d51ec7f3378b7a8a3b43bae00c50455c365c6 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 22 Sep 2026 12:17:45 -0500 Subject: [PATCH 1/4] Bring the 13F listing files and demo algorithms back to the marketplace norm Measured across the 85 published dataset pages, Data Summary rendered at 10,716 bytes against a maximum of 1,056 and Accessing Data at 8,148 against 4,744, and the 13F was the only repo using ### subsections in its listing files. The 20-field table dropped from Data Summary is exactly what the Data Point Attributes data-tree already publishes from the XML docs on SEC13FHolding, so it duplicated the page against itself. The value-scale and amendment warnings were written twice, once in each listing file, and now live only in the documentation. The partial-coverage disclosure survives as the Asset Coverage footnote, so the page still states that it reaches 97.0 percent of the reported lines rather than every reported name. The demo algorithms went from 81 and 140 non-blank lines to 65 and 111, against medians of 38 and 78 across the onboarded repos. Only prose and one duplicate log were cut: the trading logic is unchanged, because the demos cannot be run locally past 2021 and an unverified behaviour change is not worth the saving. vendorName is the registry's "Securities and Exchange Commission", which is what products 9 and 149 carry; the file said "U.S. Securities and Exchange Commission", which matches no vendor. --- SEC13FAlgorithm.cs | 60 +++----------- SEC13FAlgorithm.py | 44 +++------- listing-about-whales.md | 142 ++------------------------------ listing-documentation-whales.md | 137 ++++++++---------------------- 4 files changed, 66 insertions(+), 317 deletions(-) diff --git a/SEC13FAlgorithm.cs b/SEC13FAlgorithm.cs index ea6be54..27d6f18 100644 --- a/SEC13FAlgorithm.cs +++ b/SEC13FAlgorithm.cs @@ -25,37 +25,25 @@ namespace QuantConnect.DataLibrary.Tests { /// - /// Example algorithm using the SEC Form 13F institutional holdings dataset as a source of alpha. - /// It follows one manager, Pershing Square, through seven of the names it reports: it holds - /// them all when the first quarter arrives, and from then on only those the manager added to. - /// - /// The dataset publishes what each manager filed and nothing else, so the change this trades on - /// is worked out here: a point is every position reported for the security on one filing date, - /// the manager's lines are picked out by CIK, and the quarter they describe is PeriodEnd. - /// - /// The 13F symbols returned by AddData are signals, not tradeable securities, so every name is - /// added twice: once as the tradeable equity and once as the custom data subscribed on it. + /// Example algorithm using the SEC Form 13F institutional holdings dataset as a source of + /// alpha. It follows one manager, Pershing Square, through seven of the names it reports: it + /// holds them all when the first quarter arrives, and from then on only those the manager + /// added to. No filing states a change, so the comparison between two reported quarters is + /// worked out here. /// public class SEC13FAlgorithm : QCAlgorithm { /// - /// Pershing Square Capital Management, and Pershing Square Inc., which has reported the same - /// positions since the June 2026 quarter while the former files only a notice. A manager is - /// followed by CIK, and a change of reporting entity is a change of CIK. + /// Pershing Square Capital Management, and Pershing Square Inc., which has reported the + /// same positions since the June 2026 quarter. A change of reporting entity is a change + /// of CIK. /// private static readonly HashSet Managers = [1336528, 2026053]; - /// The shares the manager reported for each equity, by the quarter they describe. private readonly Dictionary> _sharesByEquity = []; - - /// The newest quarter the managers have reported, for any name. private DateTime _latestPeriod; - private bool _rebalance; - /// - /// Initialise the data and resolution required, as well as the cash and start-end dates. - /// public override void Initialize() { // Two filings fall in this window: the March 2026 quarter, filed on 15 May, and the June @@ -72,24 +60,18 @@ public override void Initialize() } } - /// - /// OnData event is the primary entry point for your algorithm. Each new data point is here. - /// - /// Slice object keyed by symbol containing the data public override void OnData(Slice slice) { foreach (var (dataSymbol, point) in slice.Get()) { - // The data symbol carries the equity it was subscribed on as its underlying. - var equity = dataSymbol.Underlying; - // One point per filing date, carrying every position every manager reported for the - // security that day. An amendment would restate lines already counted and an option - // line states the shares under the contracts, so both are left out of the share count. + // security that day. An amendment restates lines already counted and an option line + // states the shares under the contracts, so both are left out of the share count. foreach (var holding in point.OfType().Where(holding => Managers.Contains(holding.ManagerCik) && holding.FormType == "13F-HR" && holding.AmountType == "SH" && !holding.PutCall.HasValue)) { + var equity = dataSymbol.Underlying; var shares = _sharesByEquity[equity]; shares[holding.PeriodEnd] = shares.GetValueOrDefault(holding.PeriodEnd) + (holding.Amount ?? 0); _latestPeriod = holding.PeriodEnd > _latestPeriod ? holding.PeriodEnd : _latestPeriod; @@ -109,17 +91,6 @@ public override void OnData(Slice slice) _rebalance = false; - // The manager's trades, which no filing states: the change between two reported quarters. - foreach (var (equity, shares) in _sharesByEquity.Where(kvp => kvp.Value.Count > 1)) - { - var (previous, latest) = (shares.Values.ElementAt(shares.Count - 2), shares.Values.Last()); - - // A quarter the manager opened the position in reports no shares before it. - var change = previous > 0 ? $" ({latest / previous - 1:+0.0%;-0.0%})" : string.Empty; - Log($"{Time:yyyy-MM-dd} {equity.Value}: {previous:N0} -> {latest:N0} shares{change} " + - $"between {shares.Keys.ElementAt(shares.Count - 2):yyyy-MM-dd} and {shares.Keys.Last():yyyy-MM-dd}"); - } - // With one quarter known, hold what the manager holds. With two, hold what it added to. var selected = _sharesByEquity .Select(kvp => (Equity: kvp.Key, Quarters: QuartersOf(kvp.Value))) @@ -136,7 +107,6 @@ public override void OnData(Slice slice) return; } - Log($"{Time:yyyy-MM-dd} holding {string.Join(", ", selected.Select(symbol => symbol.Value))}"); SetHoldings(selected.Select(symbol => new PortfolioTarget(symbol, 1m / selected.Count)).ToList(), liquidateExistingHoldings: true); } @@ -145,8 +115,8 @@ public override void OnData(Slice slice) /// The shares reported for one equity, oldest quarter first, with a closing zero for a name /// the manager has stopped reporting. A position sold out of has no line in the new quarter, /// so its newest period stays behind the newest the manager reported anywhere; taken for the - /// name's own latest quarter, it would go on being compared with the quarter before it and - /// held forever. Not being reported is a report of no shares. + /// name's own latest quarter, it would be compared with the quarter before it and held + /// forever. Not being reported is a report of no shares. /// private List QuartersOf(SortedDictionary shares) { @@ -159,10 +129,6 @@ private List QuartersOf(SortedDictionary shares) return quarters; } - /// - /// Order fill event handler. - /// - /// Order event details public override void OnOrderEvent(OrderEvent orderEvent) { if (orderEvent.Status == OrderStatus.Filled) diff --git a/SEC13FAlgorithm.py b/SEC13FAlgorithm.py index f7aae52..0353570 100644 --- a/SEC13FAlgorithm.py +++ b/SEC13FAlgorithm.py @@ -18,18 +18,11 @@ class SEC13FAlgorithm(QCAlgorithm): '''Example algorithm using the SEC Form 13F institutional holdings dataset as a source of alpha. It follows one manager, Pershing Square, through seven of the names it reports: it holds them - all when the first quarter arrives, and from then on only those the manager added to. - - The dataset publishes what each manager filed and nothing else, so the change this trades on is - worked out here: a point is every position reported for the security on one filing date, the - manager's lines are picked out by CIK, and the quarter they describe is period_end. - - The 13F symbols returned by add_data are signals, not tradeable securities, so every name is - added twice: once as the tradeable equity and once as the custom data subscribed on it.''' + all when the first quarter arrives, and from then on only those the manager added to. No filing + states a change, so the comparison between two reported quarters is worked out here.''' # Pershing Square Capital Management, and Pershing Square Inc., which has reported the same - # positions since the June 2026 quarter while the former files only a notice. A manager is - # followed by CIK, and a change of reporting entity is a change of CIK. + # positions since the June 2026 quarter. A change of reporting entity is a change of CIK. MANAGERS = {1336528, 2026053} def initialize(self) -> None: @@ -39,12 +32,8 @@ def initialize(self) -> None: self.set_end_date(2026, 8, 31) self.set_cash(100000) - # The shares the manager reported for each equity, by the quarter they describe. self._shares_by_equity = {} - - # The newest quarter the managers have reported, for any name. self._latest_period = datetime.min - self._rebalance = False for ticker in ["META", "UBER", "QSR", "MSFT", "BN", "HTZ", "AMZN"]: @@ -54,17 +43,15 @@ def initialize(self) -> None: def on_data(self, slice: Slice) -> None: for data_symbol, point in slice.get(SEC13FHoldings).items(): - # The data symbol carries the equity it was subscribed on as its underlying. - equity = data_symbol.underlying - # One point per filing date, carrying every position every manager reported for the - # security that day. An amendment would restate lines already counted and an option - # line states the shares under the contracts, so both are left out of the share count. + # security that day. An amendment restates lines already counted and an option line + # states the shares under the contracts, so both are left out of the share count. for holding in point: if (holding.manager_cik not in self.MANAGERS or holding.form_type != "13F-HR" or holding.amount_type != "SH" or holding.put_call is not None): continue + equity = data_symbol.underlying shares = self._shares_by_equity[equity] shares[holding.period_end] = shares.get(holding.period_end, 0) + (holding.amount or 0) self._latest_period = max(self._latest_period, holding.period_end) @@ -81,26 +68,18 @@ def on_data(self, slice: Slice) -> None: self._rebalance = False - # With one quarter known, hold what the manager holds. With two, hold what it added to. selected = [] for equity, shares in self._shares_by_equity.items(): - periods = sorted(shares) - quarters = [shares[period] for period in periods] - - # The manager's trades, which no filing states: the change between two reported quarters. - if len(quarters) > 1: - # A quarter the manager opened the position in reports no shares before it. - change = f" ({quarters[-1] / quarters[-2] - 1:+.1%})" if quarters[-2] > 0 else "" - self.log(f"{self.time:%Y-%m-%d} {equity.value}: {quarters[-2]:,.0f} -> {quarters[-1]:,.0f} shares" - f"{change} between {periods[-2]:%Y-%m-%d} and {periods[-1]:%Y-%m-%d}") + quarters = [shares[period] for period in sorted(shares)] # A position sold out of has no line in the new quarter, so its newest period stays # behind the newest the manager reported anywhere. Taken for the name's own latest - # quarter, it would go on being compared with the quarter before it and held forever. - # Not being reported is a report of no shares. - if periods and periods[-1] < self._latest_period: + # quarter, it would be compared with the quarter before it and held forever. Not + # being reported is a report of no shares. + if shares and max(shares) < self._latest_period: quarters.append(0) + # With one quarter known, hold what the manager holds. With two, hold what it added to. if quarters and (quarters[0] > 0 if len(quarters) == 1 else quarters[-1] > quarters[-2]): selected.append(equity) @@ -108,7 +87,6 @@ def on_data(self, slice: Slice) -> None: self.liquidate() return - self.log(f"{self.time:%Y-%m-%d} holding {', '.join(equity.value for equity in selected)}") self.set_holdings([PortfolioTarget(equity, 1 / len(selected)) for equity in selected], liquidate_existing_holdings=True) diff --git a/listing-about-whales.md b/listing-about-whales.md index c4007b8..f40e1b7 100644 --- a/listing-about-whales.md +++ b/listing-about-whales.md @@ -67,142 +67,18 @@ The following table describes the dataset properties: | Property | Value | | --- | --- | | Start Date | May 2013 | -| Asset Coverage | 8,520 US Equities | +| Asset Coverage\* | 8,520 US Equities | | Data Density | Sparse | -| Resolution | Daily\* | +| Resolution\*\* | Daily | | Timezone | America/New_York | -\* Positions are reported quarterly, but the managers of one quarter file across roughly fifty -different days and several quarters are live at once, so publication is close to continuous rather -than quarterly. In the week of 3 to 7 August 2026, 1,462 filings carrying positions produced 34,002 -security days across 8,520 securities. +\* Positions are reported by CUSIP, which is licensed and is not published, and resolve to a LEAN +`Symbol` for 97.0 percent of the reported lines, so the dataset covers most reported positions +rather than every reported name. -The history begins on 2013-05-20, the first filing date in the SEC structured data set, whose first -archive covers the second quarter of 2013. Anything earlier exists only as raw filings in the EDGAR -full index and is not part of this dataset. - -Each record in a point carries the following fields, exactly as the manager filed them: - -| Property | Meaning | -| --- | --- | -| `AccessionNumber` | EDGAR accession number of the submission the line was reported on | -| `ManagerCik` | Central Index Key of the filing manager, its stable identity across name changes | -| `ManagerName` | The manager's name as its most recent cover page states it, without commas, null for an unknown CIK | -| `PeriodEnd` | End of the quarter the position is reported for, the SEC PERIODOFREPORT | -| `FormType` | 13F-HR for a holdings report, 13F-HR/A for an amendment | -| `AmendmentType` | On an amendment, whether it restates the whole report or only adds holdings | -| `AmendmentNumber` | Sequence number of the amendment, null on an original filing | -| `TitleOfClass` | Class of the security as the manager titled it, such as COM or CL A | -| `Amount` | Size of the position: a share count when `AmountType` is SH, a principal amount when PRN | -| `AmountType` | SH for shares, PRN for a principal amount | -| `ReportedValue` | Market value exactly as the manager stated it, in the unit the filing used | -| `ValueScale` | The power of ten that turns `ReportedValue` into dollars: 3, 0 or -3 | -| `MarketValue` | `ReportedValue` in whole dollars, derived from the two above and never stored | -| `PutCall` | Call or Put when the line is an option on the security, null when it is the security | -| `InvestmentDiscretion` | SOLE, DFND or OTR, as the filing states it | -| `OtherManager` | The other managers sharing the position, as the cover page numbers them, separated by semicolons; empty when there are none | -| `VotingSole` | Shares over which the manager holds sole voting authority | -| `VotingShared` | Shares over which the manager shares voting authority | -| `VotingNone` | Shares over which the manager holds no voting authority | -| `ConfidentialOmitted` | True when the submission withheld other positions under confidential treatment | -| `DateReported` | For a previously confidential filing, the date it was originally made | - -The CUSIP is not published, being licensed. It resolves the security and stops there. - -Option and debt lines are published as what they are rather than folded away. One quarter of the -source carries 61,400 call lines, 57,443 put lines and 15,698 PRN lines, and a share count that -swallowed any of them would misstate the position, so `AmountType` and `PutCall` say what each line -is and the algorithm decides what to count. - -`ConfidentialOmitted` is a point-in-time feature rather than a data quality flag. A manager can ask -the SEC to withhold specific positions temporarily, so a filing marked this way is incomplete by -design and the withheld positions appear in a later filing. - -### The reported value is published as filed - -`ReportedValue` is the number the manager wrote. The SEC asked for thousands of dollars before 2023 -and whole dollars after, and filers on both sides of that change ignore the instruction: in Apple's -December 2019 quarter 85 of 5,365 lines were already in dollars, and scaled as thousands they made -88 percent of the total, an implied 2,577 dollars a share against a 293.65 close. - -`ValueScale` is the one reading in a record the SEC does not publish. It is the power of ten that -turns the reported number into dollars, decided per line from the filing's period and from how the -implied price compares with the security's close on the quarter's last trading day: 3 where the -filing states thousands, 0 where it states dollars, and -3 for a line that overstated its value a -thousandfold, which three SPY lines of the March 2023 quarter did, carrying 16 percent of that -quarter's total with them. - -It is carried beside the reported number rather than multiplied into it, so what the manager filed -stays readable and this one judgement stays separable from it. `MarketValue` applies it. - -### Amendments are published as filed - -An amendment is a record like any other, with its `FormType`, `AmendmentType` and `AmendmentNumber` -saying what it is. It does not replace the filing it restates and nothing is netted against -anything. Deciding that a restatement supersedes an earlier figure is a judgement about the data, -and the filer has already declared which kind of amendment it made, so the algorithm can apply it. -Most amendments restate the whole report, which is what `RESTATEMENT` in `AmendmentType` means; -`NEW HOLDINGS` adds to the original. - -### A manager can change CIK - -A manager whose positions are reported on another manager's filing files a 13F-NT, a notice that -carries no positions and contributes no records. Pershing Square Capital Management (CIK 1336528) -reported its own positions through the March 2026 quarter and has filed a notice since, while -Pershing Square Inc. (CIK 2026053) reports them. Followed by the first CIK alone, the fund appears -to sell everything in one quarter, so follow every CIK the manager has reported under. - -### Coverage is partial by design - -Holdings are keyed by CUSIP in the source and resolved to a LEAN `Symbol` before publication, so no -identifier from the source is shipped. The resolution runs in four steps, each picking up what the -one before it could not reach: - -1. The CUSIP itself, looked up in LEAN's security database. The database repeats some identifiers - across the listings one company has had, such as the old and the new Alcoa, so every row carrying - the CUSIP is tried and the one trading under its own ticker on the filing date is kept. -2. The US ISIN, built arithmetically from the same CUSIP. It reaches issuers whose CUSIP column in - that database is blank. -3. The ticker the SEC's own Form N-PORT filings report for the CUSIP, taken back to a `Symbol` - through the map files. This step needs no security database at all and it is what reaches the - foreign domiciled issuers whose identifier is really a CINS, for which a constructed US ISIN is - wrong by construction. Alphabet is one of them. -4. For an option line, the security the option is written on. A manager reporting options names them - by the option's own CUSIP, which carries the underlying's six character issuer and issue 90 for - calls or 95 for puts and appears in no security database. The position is still published as the - option line it is, with its side in `PutCall`; a line that names no side takes the one its CUSIP - states. Where the issuer has several funds, as iShares does, every fund's options share one CUSIP, - and each line goes to the one fund whose quarter-end close its implied price matches, or is dropped. - -A security's file holds what managers reported under its CUSIP, which is not always the common -stock: filers put preferred shares, units and convertibles under the common's CUSIP, and -`TitleOfClass` is the only field that says so. `ValueScale` takes the values 3, 0 and -3, so a -filing stated in millions is not brought to dollars. - -A fund's reported ticker in step 3 is free text, so that step keeps a match only when the reported -prices do not say otherwise. A group is dropped when three or more of its prices are not the -security's own quarter-end close, in dollars or in thousands, which keeps Centerra Gold, Enerflex, -B2Gold and DeFi Technologies, whose Toronto or fund-reported tickers are CG, EFX, BTO and DEFI, off -Carlyle, Equifax, a John Hancock fund and the Hashdex DEFI ETF. A CUSIP whose issue number carries -letters, as a company's debt does, is rejected outright when that issuer already has stock in the -security database: Apple's 3.45% 2045 bond reached AAPL through a fund administrator's reported -ticker and added the bond's principal amount to Apple's share count as ten thousand shares, and a -bond near par against a stock near the same number agrees with a price test by coincidence. The -iBonds ETFs, whose CUSIPs also carry letters, are themselves in the security database and resolve at -step 1 without ever reaching that rule. - -Step 4 is only taken when the issuer has exactly one equity issue in the database. iShares writes -seventy equity issues under 464287 and SPDR eleven under 81369Y, and an option CUSIP there names one -of them without saying which; filing the position under the wrong fund would be worse than not -publishing it. In the week of 3 to 7 August 2026 that step recovered 2,947 of the 6,083 lines -reported on an option CUSIP and left the ambiguous rest out. - -The real limit is step 3's own history: N-PORT begins in late 2019, so a security that stopped -trading before then is not reachable through it at any depth and can only be resolved if the first -two steps already found it. Measured over the whole chain, 97.0 percent of the reported lines in the -week of 3 to 7 August 2026 and 95.0 percent of those in the fourth quarter of 2020 resolved to a -security and were published. The product therefore covers most of the reported positions but not -every reported name, and it says so rather than implying full coverage. +\*\* Positions are reported quarterly, but the managers of one quarter file across roughly fifty +different days and several quarters are live at once, so publication is close to continuous. In the +week of 3 to 7 August 2026, 1,462 filings produced 34,002 security days. ## Example Applications @@ -229,7 +105,7 @@ Examples include the following strategies: | --- | --- | | name | SEC Whales | | url | sec-whales | -| vendorName | U.S. Securities and Exchange Commission | +| vendorName | Securities and Exchange Commission | | website | https://www.sec.gov | | history | May 2013 | | reach | 8,520 US Equities | diff --git a/listing-documentation-whales.md b/listing-documentation-whales.md index cefe341..2ec57c7 100644 --- a/listing-documentation-whales.md +++ b/listing-documentation-whales.md @@ -38,14 +38,9 @@ public class SEC13FDataAlgorithm : QCAlgorithm To get the current Form 13F data, index the current [Slice](https://www.quantconnect.com/docs/v2/writing-algorithms/key-concepts/time-modeling/timeslices) with the dataset **Symbol**. **Slice** objects deliver unique events to your algorithm as they -happen, but the **Slice** may not contain data for your dataset at every time step. Positions are -reported quarterly, but the managers of one quarter file across roughly fifty different days, so -publication is close to continuous: a widely held name such as AAPL carries filings on 58 of the 66 -weekdays of the fourth quarter of 2020. Check that the **Slice** contains the data you want before -you index it. - -A point is a `SEC13FHoldings` collection holding every position reported for that security on that -filing date, so iterate it rather than reading a single value off it. +happen, so check that the **Slice** contains the data you want before you index it. A point is a +`SEC13FHoldings` collection holding every position reported for that security on that filing date, +so iterate it rather than reading a single value off it; the collection's own `Value` is zero. ```python def on_data(self, slice: Slice) -> None: @@ -67,130 +62,64 @@ public override void OnData(Slice slice) } ``` -To iterate through all of the dataset objects in the current **Slice**, call the **Get** method. A -filing deadline puts thousands of securities into the same day, so this is the usual shape when you -subscribe to more than one name. - -```python -def on_data(self, slice: Slice) -> None: - for dataset_symbol, holdings in slice.get(SEC13FHoldings).items(): - for holding in holdings: - self.log(f"{dataset_symbol} {holding.manager_cik}: {holding.amount}") -``` -```csharp -public override void OnData(Slice slice) -{ - foreach (var kvp in slice.Get()) - { - var datasetSymbol = kvp.Key; - foreach (SEC13FHolding holding in kvp.Value) - { - Log($"{datasetSymbol} {holding.ManagerCik}: {holding.Amount}"); - } - } -} -``` - -A point's `Time` is the filing date and its `EndTime` is midnight that night. LEAN emits a point at -its end time, so a day's filings reach your algorithm at 00:00 the following day, after EDGAR has -finished listing that day at about 22:05 ET, so a backtest never reads a filing before it existed. -A filing whose EDGAR index came days late is added to history under its filing date. +A point's `Time` is the filing date, not the quarter the position describes. Read `PeriodEnd` +whenever you need the quarter, because one day of filings can carry several different ones and the +gap between the two dates runs from zero days to several years. -The filing date is not the quarter the position describes, and the gap between them cannot be -derived: measured across 11,761 filings in one window it runs minimum 0 days, p10 16, median 42, p90 -48, maximum 6,596, and 10.4 percent of filings arrive later than the 45 day deadline. A point that -arrives today therefore carries positions from a quarter that ended typically 45 to 135 days ago, -with late amendments arriving years later, and one day of filings can carry several different -reported quarters. Read `PeriodEnd` whenever you need the quarter a position describes rather than -the day it arrived. - -### Nothing is aggregated - -The dataset publishes what each manager filed. It states no holder count, no total shares and no -total value, because no 13F filing states any of them. Counting is the algorithm's job and it is a -few lines: +The dataset states no holder count, no total shares and no total value, because no 13F filing states +any of them. Counting is the algorithm's job. To iterate through all of the dataset objects in the +current **Slice**, call the **Get** method, which is the usual shape once you subscribe to more than +one name: ```python def on_data(self, slice: Slice) -> None: for dataset_symbol, holdings in slice.get(SEC13FHoldings).items(): managers = {holding.manager_cik for holding in holdings if holding.put_call is None and holding.amount_type == "SH"} - shares = sum(holding.amount or 0 for holding in holdings - if holding.put_call is None and holding.amount_type == "SH") - self.log(f"{dataset_symbol}: {len(managers)} managers filed, {shares} shares") + self.log(f"{dataset_symbol}: {len(managers)} managers filed") ``` ```csharp public override void OnData(Slice slice) { foreach (var kvp in slice.Get()) { - var shareLines = kvp.Value.Cast() + var managers = kvp.Value.Cast() .Where(holding => !holding.PutCall.HasValue && holding.AmountType == "SH") - .ToList(); - var managers = shareLines.Select(holding => holding.ManagerCik).Distinct().Count(); - var shares = shareLines.Sum(holding => holding.Amount.GetValueOrDefault()); - Log($"{kvp.Key}: {managers} managers filed, {shares} shares"); + .Select(holding => holding.ManagerCik).Distinct().Count(); + Log($"{kvp.Key}: {managers} managers filed"); } } ``` -Two things to know when you count. A manager files once per quarter on a day of its own choosing, -so breadth builds up across filing dates rather than appearing on any single one: accumulate across -points instead of reading one day. And a single manager can report the same security on more than -one line when the investment discretion differs, which the rules allow, so records outnumber -managers and counting distinct `ManagerCik` is not the same as counting records. - -Filter on `AmountType` and `PutCall` before you add anything up. A PRN line is a principal amount of -debt, not a share count, and an option line states the shares underlying the contracts rather than a -holding of the security. Adding either into a share total misstates the position. - -### Reading the value +Filter on `AmountType` and `PutCall` first, as that example does: a PRN line is a principal amount +of debt and an option line states the shares underlying the contracts, so adding either into a share +total misstates the position. A manager files once per quarter on a day of its own choosing, so +breadth builds up across filing dates rather than appearing on any single one, and one manager can +report the same security on several lines when the discretion differs, so distinct `ManagerCik` is +not the same as records. One that stops appearing has not necessarily sold: a fund whose positions +move to another reporting entity files a 13F-NT, which carries none, and the new entity reports +them under its own CIK. `ReportedValue` is the number the manager wrote, in whatever unit the filing used, and `ValueScale` -is the power of ten that turns it into dollars: 3 for a filing stating thousands, 0 for one stating -dollars, and -3 for a line that overstated its value a thousandfold. `MarketValue` applies the scale -and is what you want in almost every case. - -```python -value = holding.market_value # dollars -raw = holding.reported_value # as filed, with holding.value_scale beside it -``` -```csharp -var value = holding.MarketValue; // dollars -var raw = holding.ReportedValue; // as filed, with holding.ValueScale beside it -``` - -The collection's own `Value` is zero and carries no meaning: LEAN builds the collection and sets -only its symbol and timestamps, so there is nothing for a single number to be. Read the records. - -### Amendments and confidential filings - -`FormType` is 13F-HR for a holdings report and 13F-HR/A for an amendment, with `AmendmentType` -saying whether the amendment restates the whole report or only adds holdings, and -`AmendmentNumber` its sequence. An amendment is published beside the filing it restates and -replaces nothing, so if your strategy wants a restatement to supersede an earlier figure it has to -apply it. Amendments are rare: 24 of the 1,462 filings carrying positions in the week of 3 August -2026. - -`ConfidentialOmitted` is true when the submission withheld other positions under confidential -treatment. Such a filing is incomplete by design and the withheld positions surface in a later -filing, so treat a flagged record as a floor rather than the full picture. `DateReported` carries -the date a previously confidential filing was originally made, and is null on the roughly 998 -filings in a thousand that were never confidential. - -### Empty values - -A field the filing left empty is null, and null is different from a reported zero: a manager -reporting no shared voting authority files a zero, while one that withheld the figure files nothing. -Guard the arithmetic. +is the power of ten that turns it into dollars, which `MarketValue` applies for you. A field the +filing left empty is null, which is not a reported zero, so guard the arithmetic. ```python +value = holding.market_value # dollars +raw = holding.reported_value # as filed, with holding.value_scale beside it shares = holding.amount or 0 ``` ```csharp +var value = holding.MarketValue; // dollars +var raw = holding.ReportedValue; // as filed, with holding.ValueScale beside it var shares = holding.Amount.GetValueOrDefault(); ``` +An amendment, which `FormType` marks 13F-HR/A, is published beside the filing it corrects and +replaces nothing, so a strategy that wants a restatement to supersede an earlier figure has to +apply it. `ConfidentialOmitted` marks a submission that withheld other positions under confidential +treatment, which makes that record a floor rather than the full picture. + ## Historical Data To get historical Form 13F data, call the **History** method with the From 0acacaa99b3f4e1652dfc248675b50fbc5c6f331 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 22 Sep 2026 13:22:22 -0500 Subject: [PATCH 2/4] Publish the cumulative asset coverage and restore what the listing cuts dropped Asset Coverage said 8,520 US Equities, which is the number of securities that received a line in one week of August 2026, not what the dataset holds. The licensing card then read "Every position reported on Form 13F since 2013 ... across 8,520 US Equities", which is false by roughly a factor of two. The published output and the delivery tar both carry 18,877 ticker files, counted from disk, and that is now the figure in all three places, with the same footnote USPTO uses for the one other equity-linked dataset we onboarded. The count is of ticker files, which is one per point-in-time ticker, so it is slightly above the number of distinct securities the run itself logged; it is the number that can be verified from what ships. Three things the previous commit cut too far, found by reviewing that diff against the source: - The 97.0 percent resolution rate was one week's figure presented as the dataset-wide rate. It now carries its qualifier and the 95.0 percent of the fourth quarter of 2020 beside it, along with the reason they differ: the ticker crosswalk begins in late 2019, so the names most likely to be missing are the ones that delisted before then, which is a skew rather than noise. - Accessing Data said an amendment is published beside the filing it corrects. That is only true of a RESTATEMENT. A NEW HOLDINGS amendment supplements the original, so a reader who dropped the original on every 13F-HR/A would lose positions. - AmendmentType's summary described the two kinds without naming them, and the page tells the reader to apply the rule itself. The literals are now in the summary, which the data-tree publishes on both tabs. "1,462 filings" is gone from the Data Summary footnote: the count excludes 13F-NT notices, and once the sentence saying so was cut it named a set the reader could not identify. --- SEC13FHolding.cs | 6 +++--- listing-about-whales.md | 17 +++++++++-------- listing-documentation-whales.md | 6 +++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/SEC13FHolding.cs b/SEC13FHolding.cs index f9dc156..8fd371d 100644 --- a/SEC13FHolding.cs +++ b/SEC13FHolding.cs @@ -89,9 +89,9 @@ public class SEC13FHolding : BaseData public string FormType { get; set; } /// - /// For an amendment, whether it restates the whole report or only adds holdings. The - /// distinction decides whether the amendment replaces the original filing or supplements - /// it, and the SEC leaves it to the filer to declare. Empty on an original filing. + /// For an amendment, whether it restates the whole report or only adds holdings: + /// RESTATEMENT replaces the original filing and NEW HOLDINGS supplements it. The SEC + /// leaves the distinction to the filer to declare. Empty on an original filing. /// public string AmendmentType { get; set; } diff --git a/listing-about-whales.md b/listing-about-whales.md index f40e1b7..7d686ef 100644 --- a/listing-about-whales.md +++ b/listing-about-whales.md @@ -67,18 +67,19 @@ The following table describes the dataset properties: | Property | Value | | --- | --- | | Start Date | May 2013 | -| Asset Coverage\* | 8,520 US Equities | +| Asset Coverage\* | 18,877 US Equities | | Data Density | Sparse | | Resolution\*\* | Daily | | Timezone | America/New_York | -\* Positions are reported by CUSIP, which is licensed and is not published, and resolve to a LEAN -`Symbol` for 97.0 percent of the reported lines, so the dataset covers most reported positions -rather than every reported name. +\* The coverage includes all assets since the start date. It increases over time. Positions are +reported by CUSIP, which is licensed and not published, and resolve to a LEAN `Symbol` for 97.0 +percent of the lines of one recent week and 95.0 percent of the fourth quarter of 2020; the +crosswalk that reaches the hardest names begins in late 2019, so a security delisted before then +is the likeliest to be missing. \*\* Positions are reported quarterly, but the managers of one quarter file across roughly fifty -different days and several quarters are live at once, so publication is close to continuous. In the -week of 3 to 7 August 2026, 1,462 filings produced 34,002 security days. +different days and several quarters are live at once, so publication is close to continuous. ## Example Applications @@ -108,7 +109,7 @@ Examples include the following strategies: | vendorName | Securities and Exchange Commission | | website | https://www.sec.gov | | history | May 2013 | -| reach | 8,520 US Equities | +| reach | 18,877 US Equities | | shortDescription | Ownership filings from the SEC as their holders filed them, starting with Form 13F institutional holdings | | priceCTA | Free in Cloud | | delivery | cloud only | @@ -120,7 +121,7 @@ Licensing card: ```html

Free access to SEC Whales in QuantConnect Cloud for use in backtesting or live trading.

    -
  • Every position reported on Form 13F since 2013, as each manager filed it, across 8,520 US Equities
  • +
  • Every position reported on Form 13F since 2013, as each manager filed it, across 18,877 US Equities
  • Manager, reported quarter, share or principal amount, value, option side, discretion and voting authority per position
  • Curated, clean data
diff --git a/listing-documentation-whales.md b/listing-documentation-whales.md index 2ec57c7..3fbff8c 100644 --- a/listing-documentation-whales.md +++ b/listing-documentation-whales.md @@ -115,9 +115,9 @@ var raw = holding.ReportedValue; // as filed, with holding.ValueScale besid var shares = holding.Amount.GetValueOrDefault(); ``` -An amendment, which `FormType` marks 13F-HR/A, is published beside the filing it corrects and -replaces nothing, so a strategy that wants a restatement to supersede an earlier figure has to -apply it. `ConfidentialOmitted` marks a submission that withheld other positions under confidential +An amendment, which `FormType` marks 13F-HR/A, is published beside the filing it amends and +replaces nothing. `AmendmentType` says which kind it is: `RESTATEMENT` replaces the original +filing, `NEW HOLDINGS` only adds to it, and applying either is the algorithm's job. `ConfidentialOmitted` marks a submission that withheld other positions under confidential treatment, which makes that record a floor rather than the full picture. ## Historical Data From d520c68967337c640aeda9c8aea9af0498d8f501 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 22 Sep 2026 13:33:59 -0500 Subject: [PATCH 3/4] Cut the remaining listing sections to the size the other datasets use Measured against the 85 published dataset pages, Introduction was the heaviest of all of them and About the Provider the heaviest of its section, while Historical Data was nearly twice the heaviest of ours. Every section is now inside the range the marketplace occupies. What went is either evidence written for a maintainer rather than a user -- the line by line agreement between EDGAR's daily index and the quarterly batches, the per quarter counts of option and debt lines -- or a field list that the Data Point Attributes data-tree already publishes from the XML summaries. Example Applications drops to four strategies, which is what the other repos carry. The measured facts a user needs all survive: the reporting lag and the share of filings that miss the deadline, the rule that nothing is aggregated, the shape of a point, and the difference between the Series, the flattened frame and the typed history. --- listing-about-whales.md | 57 ++++++++++----------------------- listing-documentation-whales.md | 40 +++++++---------------- 2 files changed, 29 insertions(+), 68 deletions(-) diff --git a/listing-about-whales.md b/listing-about-whales.md index 7d686ef..f617936 100644 --- a/listing-about-whales.md +++ b/listing-about-whales.md @@ -1,53 +1,34 @@ ## Introduction SEC Whales is the ownership side of the SEC's filings: who holds what, reported by the holders -themselves. The US Securities and Exchange Commission requires a large holder to say so, and the -filings that carry those disclosures are published through EDGAR as they are made. This product -collects them, one source at a time, and publishes each filing as it was filed. - -It ships today with Form 13F institutional holdings. Every institutional investment manager -exercising discretion over at least 100 million dollars must file a Form 13F within 45 days of the -end of a calendar quarter, listing the covered securities it holds, from the second quarter of 2013 -to the present. Further ownership filings are added to the product as they are built. +themselves. It collects those disclosures one source at a time and publishes each filing as it was +made. It ships today with Form 13F institutional holdings, which every manager exercising +discretion over at least 100 million dollars must file within 45 days of the end of a calendar +quarter, from the second quarter of 2013 to the present. Nothing is summed, counted or averaged. The holders of a name, the shares institutions hold between them, quarter over quarter change and concentration are all derivable from a day's records, and no -filing states any of them, so they are left to the algorithm rather than invented here. Counting -distinct filer CIKs across the records is a line of code and is what the demonstration algorithms -do. - -The reporting lag is the product, not an inconvenience to be hidden. A 13F position is typically 45 -to 135 days old by the time it reaches the public record, with late amendments arriving years later, -so a record is stamped with the filing date and carries the quarter it describes in `PeriodEnd`. -Measured across 11,761 filings in one window, the lag from the reported quarter end to the filing -date runs minimum 0 days, p10 16, median 42, p90 48, maximum 6,596, with 10.4 percent of filings -arriving later than the 45 day deadline. Delivering a position on the quarter end it describes would -inject every day of that gap as look-ahead, so LEAN delivers it when the dataset could first publish -it. - -A record's `Time` is its filing date and its `EndTime` is midnight that night. LEAN emits a point at -its end time, so a day's filings all reach the algorithm at 00:00 the following day, after EDGAR has -finished listing that day at about 22:05 ET, so a backtest never reads a filing before it existed. -A filing whose EDGAR index came days late is added to history under its filing date. +filing states any of them, so they are left to the algorithm rather than invented here. + +The reporting lag is the product, not an inconvenience to be hidden. A position is typically 45 to +135 days old when it reaches the public record, late amendments arrive years later, and 10.4 +percent of filings miss the deadline, so a record is stamped with the date it was filed and carries +the quarter it describes in `PeriodEnd`. Delivering it on that quarter end would inject every day +of the gap as look-ahead. An algorithm receives one `SEC13FHoldings` point per security per filing date, holding every -position reported for that security that day. Several managers file on the same day, and a single +position reported for that security that day. Several managers file on the same day, and one manager can report the same security on more than one line when the investment discretion differs, -which the rules allow and Berkshire Hathaway does with Moody's. Those records stay apart, because -folding them together would state a number no filing contains. +which Berkshire Hathaway does with Moody's. Those records stay apart. ## About the Provider The [U.S. Securities and Exchange Commission](https://www.sec.gov) is the federal agency that regulates the US securities markets. Form 13F is filed through EDGAR, the SEC's electronic filing -system, and the agency's Division of Economic and Risk Analysis republishes those filings as -structured, tab separated data sets in three month batches. The history is built from those data -sets. The daily job reads each day's filings from EDGAR itself, the information tables the data sets -are extracted from, because a batch arrives up to three months after its first filing: compared line -by line, 48 of 48 filings carry the same lines in both, and on six sample days EDGAR's daily index -lists exactly the filings the data set holds. The data is public domain, needs no account and no API -key, and the only access requirement is the descriptive User-Agent header that SEC policy asks of -all automated readers. +system, and the agency republishes those filings as structured, tab separated data sets in three +month batches. The history is built from those data sets, and the daily job reads each day's +filings from EDGAR itself, because a batch arrives up to three months after its first filing. The +data is public domain and needs no account and no API key. ## Getting Started @@ -93,12 +74,8 @@ Examples include the following strategies: - Following one manager through `ManagerCik`, reading what a single fund reported quarter after quarter rather than what the market did in aggregate, which is what the demonstration algorithms do with Pershing Square. -- Screening out thinly followed names, requiring a minimum number of reporting managers before a - security is tradeable. - Reading the reported put and call lines alongside the share positions to see whether managers are hedging a name rather than simply owning it. -- Separating sole from defined discretion with `InvestmentDiscretion`, which is the difference - between a manager's own book and the assets it merely directs. ## Meta diff --git a/listing-documentation-whales.md b/listing-documentation-whales.md index 3fbff8c..017c1b6 100644 --- a/listing-documentation-whales.md +++ b/listing-documentation-whales.md @@ -129,44 +129,28 @@ dataset **Symbol**. If there is no data in the period you request, the history r # pandas Series, one entry per filing date, each holding the list of that day's positions history_series = self.history(self._dataset_symbol, timedelta(days=60), Resolution.DAILY) -# DataFrame, one row per reported position +# DataFrame, one row per reported position, columns named after the record's fields in lower case history_df = self.history(SEC13FHoldings, self._dataset_symbol, timedelta(days=60), Resolution.DAILY, flatten=True) -# Dataset objects, one per filing date +# SEC13FHoldings objects, one per filing date, each carrying its records history_bars = self.history[SEC13FHoldings](self._dataset_symbol, timedelta(days=60), Resolution.DAILY) ``` ```csharp var history = History(_datasetSymbol, TimeSpan.FromDays(60), Resolution.Daily); ``` -The three shapes differ more than usual for this dataset, because a point is a collection. - -Without `flatten`, Python gives you a **Series** and not a DataFrame: one entry per filing date, -indexed by symbol and time, each entry holding the list of that day's positions. Reading a column -off it will not work, because it has none. - -With `flatten=True` you get a DataFrame with **one row per reported position**, indexed by time and -symbol, whose columns are the record's fields in lower case: `accessionnumber`, `managercik`, -`managername`, `periodend`, `formtype`, `amendmenttype`, `amendmentnumber`, `titleofclass`, -`amount`, `amounttype`, `reportedvalue`, `valuescale`, `marketvalue`, `putcall`, `investmentdiscretion`, `othermanager`, -`votingsole`, `votingshared`, `votingnone`, `confidentialomitted`, `datereported`. This is the form -to use for anything cross-sectional. Sixty days of AAPL history is one Series of 39 entries or a -DataFrame of 6,333 rows, which is the difference the flag makes. - -Note that a reported zero occasionally comes back as `NaN` in the flattened frame rather than as -`0`, which is LEAN's pandas conversion rather than a gap in the data. Treat the two alike: - -```python -shares = history_df["votingshared"].fillna(0) -``` - -The typed history and the C# history give you the `SEC13FHoldings` objects themselves, one per -filing date, each carrying its records. That is the form that keeps the day's positions grouped. +The three shapes differ more than usual for this dataset, because a point is a collection. Without +`flatten`, Python gives you a **Series** and not a DataFrame, so reading a column off it will not +work. With `flatten=True` you get one row per reported position, which is the form to use for +anything cross-sectional: sixty days of AAPL history is one Series of 39 entries or a DataFrame of +6,333 rows. A reported zero occasionally comes back there as `NaN` rather than as `0`, which is +LEAN's pandas conversion rather than a gap in the data, so treat the two alike with +`history_df["votingshared"].fillna(0)`. Ask for a time span rather than a bar count. A bar count is read in daily bars and the dataset -publishes on filing dates only, so what comes back depends on how widely the security is held rather -than on the count you asked for. AAPL carries filings on 58 of the 66 weekdays of the fourth quarter -of 2020, while a thinly held name carries them on a handful of days a year. +publishes on filing dates only, so what comes back depends on how widely the security is held +rather than on the count you asked for. AAPL carries filings on 58 of the 66 weekdays of the fourth +quarter of 2020, while a thinly held name carries them on a handful of days a year. For more information about historical data, see [History Requests](https://www.quantconnect.com/docs/v2/writing-algorithms/historical-data/history-requests). From e60aa5a10f3c73bf10a9aabf267ff0e9951d847b Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Tue, 22 Sep 2026 14:00:23 -0500 Subject: [PATCH 4/4] Correct the two documentation numbers that running the snippets disproved Every code block on the page was run in local LEAN against the published data, the classic and framework examples in both languages and the remaining fragments through a harness that executes each one verbatim. Two numbers did not survive it. Historical Data claimed that sixty days of AAPL history is a Series of 39 entries or a DataFrame of 6,333 rows. Those are the figures for a history call anchored on 1 December 2020, which nothing in the documentation tells the reader to make. Read at the start of the window Requesting Data sets up, the same call returns 36 entries and 4,144 rows, confirmed identically by the Python and the C# harness. The sentence now names the anchor so the number can be reproduced. The Requesting Data snippets carried "Coverage runs 2013-05-20 to 2026-05-29". The published history ends on 2026-09-18, and the end date moves every day the job runs, so the comment now gives the start alone. Checked and left alone: AAPL carries filings on 58 of the 66 weekdays of the fourth quarter of 2020, which is exact. --- listing-documentation-whales.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/listing-documentation-whales.md b/listing-documentation-whales.md index 017c1b6..c7d54e6 100644 --- a/listing-documentation-whales.md +++ b/listing-documentation-whales.md @@ -8,7 +8,7 @@ can access the data later in your algorithm. class SEC13FDataAlgorithm(QCAlgorithm): def initialize(self) -> None: - # Coverage runs 2013-05-20 to 2026-05-29 + # Coverage starts 2013-05-20 self.set_start_date(2020, 10, 1) self.set_end_date(2020, 12, 31) self.set_cash(100000) @@ -23,7 +23,7 @@ public class SEC13FDataAlgorithm : QCAlgorithm public override void Initialize() { - // Coverage runs 2013-05-20 to 2026-05-29 + // Coverage starts 2013-05-20 SetStartDate(2020, 10, 1); SetEndDate(2020, 12, 31); SetCash(100000); @@ -142,10 +142,10 @@ var history = History(_datasetSymbol, TimeSpan.FromDays(60), Res The three shapes differ more than usual for this dataset, because a point is a collection. Without `flatten`, Python gives you a **Series** and not a DataFrame, so reading a column off it will not work. With `flatten=True` you get one row per reported position, which is the form to use for -anything cross-sectional: sixty days of AAPL history is one Series of 39 entries or a DataFrame of -6,333 rows. A reported zero occasionally comes back there as `NaN` rather than as `0`, which is -LEAN's pandas conversion rather than a gap in the data, so treat the two alike with -`history_df["votingshared"].fillna(0)`. +anything cross-sectional: sixty days of AAPL history read at the start of the fourth quarter of +2020 is one Series of 36 entries or a DataFrame of 4,144 rows. A reported zero occasionally comes +back there as `NaN` rather than as `0`, which is LEAN's pandas conversion rather than a gap in the +data, so treat the two alike with `history_df["votingshared"].fillna(0)`. Ask for a time span rather than a bar count. A bar count is read in daily bars and the dataset publishes on filing dates only, so what comes back depends on how widely the security is held