Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 13 additions & 47 deletions SEC13FAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,37 +25,25 @@
namespace QuantConnect.DataLibrary.Tests
{
/// <summary>
/// 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.
/// </summary>
public class SEC13FAlgorithm : QCAlgorithm
{
/// <summary>
/// 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.
/// </summary>
private static readonly HashSet<int> Managers = [1336528, 2026053];

/// <summary>The shares the manager reported for each equity, by the quarter they describe.</summary>
private readonly Dictionary<Symbol, SortedDictionary<DateTime, decimal>> _sharesByEquity = [];

/// <summary>The newest quarter the managers have reported, for any name.</summary>
private DateTime _latestPeriod;

private bool _rebalance;

/// <summary>
/// Initialise the data and resolution required, as well as the cash and start-end dates.
/// </summary>
public override void Initialize()
{
// Two filings fall in this window: the March 2026 quarter, filed on 15 May, and the June
Expand All @@ -72,24 +60,18 @@ public override void Initialize()
}
}

/// <summary>
/// OnData event is the primary entry point for your algorithm. Each new data point is here.
/// </summary>
/// <param name="slice">Slice object keyed by symbol containing the data</param>
public override void OnData(Slice slice)
{
foreach (var (dataSymbol, point) in slice.Get<SEC13FHoldings>())
{
// 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<SEC13FHolding>().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;
Expand All @@ -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)))
Expand All @@ -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);
}
Expand All @@ -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.
/// </summary>
private List<decimal> QuartersOf(SortedDictionary<DateTime, decimal> shares)
{
Expand All @@ -159,10 +129,6 @@ private List<decimal> QuartersOf(SortedDictionary<DateTime, decimal> shares)
return quarters;
}

/// <summary>
/// Order fill event handler.
/// </summary>
/// <param name="orderEvent">Order event details</param>
public override void OnOrderEvent(OrderEvent orderEvent)
{
if (orderEvent.Status == OrderStatus.Filled)
Expand Down
44 changes: 11 additions & 33 deletions SEC13FAlgorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"]:
Expand All @@ -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)
Expand All @@ -81,34 +68,25 @@ 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)

if not selected:
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)

Expand Down
6 changes: 3 additions & 3 deletions SEC13FHolding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ public class SEC13FHolding : BaseData
public string FormType { get; set; }

/// <summary>
/// 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.
/// </summary>
public string AmendmentType { get; set; }

Expand Down
Loading
Loading