Define Universe
SkillDev toolsDefine point-in-time tradeable universes with liquidity filters. Use when constructing the investable asset set that avoids survivorship and liquidity bias.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the Define Universe skill
What this skill tells your AI
The instructions your AI receives, as published by ml4t/skills in data/define-universe/SKILL.md and read by ahel’s review.
Using today's index constituents for a historical backtest introduces survivorship bias - you only trade winners that stayed in the index, inflating returns by 1-2% per year.
The Problem
The S&P 500 today contains companies that survived and grew. The 2010 index contained firms since acquired, delisted, or bankrupt. A backtest on current members never sees these failures and overstates performance.
The Pattern
WRONG
import polars as pl
# Current constituents applied to historical backtest
sp500_today = ["AAPL", "MSFT", "GOOGL", ...] # 2024 list
prices = pl.read_parquet("prices.parquet").filter(
pl.col("symbol").is_in(sp500_today)
)
# Backtest from 2010 - but these are 2024 survivors
CORRECT
import polars as pl
def get_universe(
prices: pl.DataFrame,
as_of: str,
min_price: float = 5.0,
min_avg_dollar_volume: float = 5_000_000,
min_history_days: int = 252,
lookback_days: int = 63,
) -> list[str]:
"""Point-in-time universe: only assets tradeable on as_of date."""
cutoff = pl.lit(as_of).str.to_date()
candidates = (
prices.filter(pl.col("timestamp") <= cutoff)
.group_by("symbol")
.agg(
last_price=pl.col("close").last(),
avg_dollar_vol=(pl.col("close") * pl.col("volume"))
.tail(lookback_days).mean(),
n_days=pl.col("timestamp").n_unique(),
last_trade=pl.col("timestamp").max(),
)
.filter(
(pl.col("last_price") >= min_price)
& (pl.col("avg_dollar_vol") >= min_avg_dollar_volume)
& (pl.col("n_days") >= min_history_days)
& (pl.col("last_trade") == cutoff) # Must be actively trading
)
)
return candidates["symbol"].to_list()
universe_2015 = get_universe(all_prices, as_of="2015-01-02")
universe_2020 = get_universe(all_prices, as_of="2020-01-02")
Handling Delistings
DELISTING_RETURNS = {
"bankruptcy": -1.0,
"acquisition": 0.0, # Use actual tender premium if available
"going_private": 0.0,
}
def apply_delisting_returns(returns: pl.DataFrame, delistings: pl.DataFrame):
"""Replace last return with delisting return for removed assets."""
return returns.join(delistings, on=["symbol", "timestamp"], how="left").with_columns(
pl.when(pl.col("delist_type").is_not_null())
.then(pl.col("delist_return"))
.otherwise(pl.col("ret"))
.alias("ret")
)
Guardrails
- Free data sources (Yahoo Finance, etc.) almost always have survivorship bias - they only cover current tickers
- CRSP is the gold standard for survivorship-free US equities (includes delisting returns)
- A universe that never changes is a red flag - real indices reconstitute quarterly
- Penny stocks and micro-caps pass through if you skip liquidity filters, dominating signals with noise
Production Implementation
from ml4t.data import DataManager
dm = DataManager()
panel = dm.batch_load_universe(
"sp500",
start="2015-01-01",
end="2024-12-31",
provider="yahoo",
)
Checklist
- Universe is point-in-time (no future constituents)
- Liquidity filter applied (price, volume, history)
- Delistings handled with terminal returns
- Rebalance schedule defined (quarterly typical)
- Data source is survivorship-free or bias is documented
Signals
- GitHub stars
- 20
- Forks
- 11
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
ml4t-define-universe- Source
- github.com/ml4t/skills