quantlab.universe.IndexMembershipFetcher#
- class quantlab.universe.IndexMembershipFetcher(cache_dir)[source]#
Bases:
ABCBase class for rebuilding the dates each stock was in an index.
The result is a set of membership intervals: one row per stay in the index, with the date the stock joined and the date it left (null while it is still a member). An index is described by data: a subclass sets the class constants below and implements
fetch_anchor, and inherits the change-log parse, the rebuild algorithm and the caching offetch_changes.Two sources are combined. The anchor is a snapshot of today’s members and is trusted for who is a member now. The change log is Wikipedia’s dated table of additions and removals and is trusted for when membership changed.
reconstruct_intervalsreplays the log forward and reconciles it with the anchor.A subclass sets
ANCHOR_URL,CHANGES_URL,PIT_COVERAGE_START(the earliest date the change log covers; point-in-time queries before it are refused),CACHE_FILENAME(the per-index change-log snapshot undercache_dir),INDEX_LABEL,CATEGORY,EXPECTED_SOURCE_HEADER(the flattened header that identifies the change-log table) andDATE_HEADER.CHANGES_TABLE_ATTRSis optional.Three safety checks live on this base so every index gets them. The change-log table is picked by matching its header against
EXPECTED_SOURCE_HEADERand its columns are read by name, so a reordered header raises instead of quietly swapping additions and removals. A live table with fewer rows than the cached snapshot is treated as a parse failure, because the log only grows. On any fetch or parse failure the cached snapshot is returned and the cache file is left as it was.- Parameters:
- cache_dirstr
Directory holding the cached change-log snapshot.
- Attributes:
- changes_are_stalebool
Whether the last
fetch_changesresult came from the cache instead of a live fetch.- changes_source_asofstr or None
The date or time the last
fetch_changesresult is current to.
Examples
>>> fetcher = SP500MembershipFetcher(cache_dir="data/reference/_cache") >>> intervals = fetcher.build_intervals() # downloads anchor and change log >>> intervals.columns ['symbol', 'start_date', 'end_date', 'end_date_is_inferred']
- ADDED_TICKER_HEADER = 'Added Ticker'#
The two ticker columns consumed. Both live sources word them identically; they are still validated against
EXPECTED_SOURCE_HEADER.
- CATEGORY#
The catalog category this index is stored under. Typed as the literal so an unknown token is a type error rather than a runtime surprise.
- CHANGES_TABLE_ATTRS = None#
Optional
pd.read_html(attrs=...)pre-filter for a page that marks its change-log table with an id or class.
- DATE_HEADER#
The
EXPECTED_SOURCE_HEADERentry carrying the effective date.
- EXPECTED_SOURCE_HEADER#
Exact flattened header (see
_flatten_header) of the change-log table. It both selects the table and names the columns read from it.
- build_intervals()[source]#
Fetch the anchor and change log and return the rebuilt intervals.
- Returns:
- pl.DataFrame
The frame from
reconstruct_intervals.
Examples
>>> fetcher = Nasdaq100MembershipFetcher(cache_dir="data/reference/_cache") >>> intervals = fetcher.build_intervals() # downloads anchor and change log >>> intervals.columns ['symbol', 'start_date', 'end_date', 'end_date_is_inferred']
- abstractmethod fetch_anchor()[source]#
Return today’s index members as a
[symbol, date_added]frame.date_addedmay be entirely null when the source has no such column;reconstruct_intervalsthen usesPIT_COVERAGE_STARTfor those symbols.Examples
A subclass whose source is a CSV with
SymbolandDate addedcolumns:def fetch_anchor(self) -> pl.DataFrame: response = requests.get(self.ANCHOR_URL, timeout=30) response.raise_for_status() data = pl.read_csv(io.StringIO(response.text)) data = data.rename({"Symbol": "symbol", "Date added": "date_added"}) return data.select(["symbol", "date_added"])
- fetch_changes()[source]#
Fetch the change log, validate it and update the cached snapshot.
On success the parsed table is written to
CACHE_FILENAMEunder the cache directory, atomically so an interrupted write cannot leave a half-written file. On any fetch or parse failure, or when the live table has fewer rows than the cached one, the cached snapshot is returned instead, the cache file is left as it was, andchanges_are_staleis set so the catalog can refuse to save the result.- Returns:
- pl.DataFrame
A frame with columns
effective_date,added_tickerandremoved_ticker.
- Raises:
- RuntimeError
If the live fetch failed and no cached snapshot exists.
Examples
>>> fetcher = SP500MembershipFetcher(cache_dir="data/reference/_cache") >>> changes = fetcher.fetch_changes() # downloads from Wikipedia >>> changes.columns ['effective_date', 'added_ticker', 'removed_ticker'] >>> fetcher.changes_are_stale False
- reconstruct_intervals(anchor, changes)[source]#
Replay the change log forward and reconcile it with the anchor.
The anchor is trusted for who is a member today; the change log is trusted for when membership changed. Three disagreements between them are resolved, each with a warning:
A stay still open at the end of the log whose symbol is not in the anchor is closed at the last date the log covers and marked
end_date_is_inferred=True, because the log never says when it ended.A symbol whose last event is a removal but which the anchor still lists is re-opened from that removal date, because the log is missing a re-addition.
An anchor member with no event in the log is opened at its
date_added, or atPIT_COVERAGE_STARTwhen that is null.
A removal with no earlier addition opens at
PIT_COVERAGE_START, and a duplicate addition keeps the earlier start date.- Parameters:
- anchorpl.DataFrame
A
[symbol, date_added]frame as returned byfetch_anchor.- changespl.DataFrame
A frame as returned by
fetch_changes, withNonewhere a row has no ticker on one side.
- Returns:
- pl.DataFrame
A frame with columns
symbol,start_date,end_date(null for a current member) andend_date_is_inferred.
Examples
>>> fetcher = SP500MembershipFetcher(cache_dir="/tmp/cache") >>> anchor = pl.DataFrame( ... {"symbol": ["AAA", "BBB"], "date_added": ["1999-01-01", None]} ... ) >>> changes = pl.DataFrame( ... { ... "effective_date": ["2010-05-03", "2015-09-21"], ... "added_ticker": ["BBB", "DDD"], ... "removed_ticker": ["ZZZ", None], ... } ... ) >>> fetcher.reconstruct_intervals(anchor, changes).sort("symbol") shape: (4, 4) ┌────────┬────────────┬────────────┬──────────────────────┐ │ symbol ┆ start_date ┆ end_date ┆ end_date_is_inferred │ │ --- ┆ --- ┆ --- ┆ --- │ │ str ┆ str ┆ str ┆ bool │ ╞════════╪════════════╪════════════╪══════════════════════╡ │ AAA ┆ 1999-01-01 ┆ null ┆ false │ │ BBB ┆ 2010-05-03 ┆ null ┆ false │ │ DDD ┆ 2015-09-21 ┆ 2015-09-21 ┆ true │ │ ZZZ ┆ 1976-07-01 ┆ 2010-05-03 ┆ false │ └────────┴────────────┴────────────┴──────────────────────┘