Dynamic Universes Moonshot

I’d like to support dynamic universes in several forms, but I haven’t been able to determine the best way to implement them yet.

  1. Point-in-Time Index Universe

A universe that updates historically based on actual index membership at each rebalance date.

Examples:

  • S&P 500 constituents changing quarterly as they did in reality
  • Russell 1000 / Russell 2000 reconstitutions
  • Nasdaq-100 membership changes over time

The universe should automatically reflect the correct constituents before each rebalance, training window, and backtest date.

  1. Point-in-Time Exchange Universe

A universe based on exchange listings that updates historically over time.

Example:

  • All stocks listed on NASDAQ, with membership determined point-in-time (including listings, delistings, transfers, etc.)

This should also update prior to rebalances, training windows, and any historical evaluation date.

  1. Rule-Based / Filter Universe

A universe defined by dynamic screening rules, such as:

  • Average Daily Dollar Volume (ADV) > $5M
  • Price > $10

Ideally this would be a first-class universe definition rather than requiring manual dataframe filtering inside the price-to-features pipeline.

Core Question

What is the recommended / native way to implement these types of dynamic universes so they integrate cleanly with rebalancing, training windows, and historical simulations?

With a Sharadar subscription, you can access S&P 500 historical constituents as described in the docs. You can use the data like this in Moonshot:

from quantrocket.fundamental import get_sharadar_sp500_reindexed_like
...
are_in_sp500 = get_sharadar_sp500_reindexed_like(closes)
signals = signals.where(are_in_sp500)

None of our supported data providers offer historical constituents for indexes other than the S&P 500. If you procure that data elsewhere, you can import it into a custom database and use it in a similar way to what is shown above, except that you would use get_prices_reindexed_like instead of get_sharadar_sp500_reindexed_like.

We don't offer a dataset that tracks when a stock's primary listing switches from one exchange to another. Switching exchanges certainly happens, but it's not very common, so there's not much demand for such a dataset. If you can find that data elsewhere, you can import it as custom data.

Defining dynamic universes differs in Zipline vs Moonshot. In Zipline, you create Pipeline rules:

from zipline.pipeline import Pipeline, EquityPricing
from zipline.pipeline.factors import AverageDollarVolume

adv_over_5m = AverageDollarVolume(window_length=30) > 5e6
prices_over_10 = EquityPricing.close.latest > 10

pipe = Pipeline(
    ...
    screen=adv_over_5m & prices_over_10
)

But in Moonshot, you accomplish the same thing by performing matrix operations on DataFrames inside your Moonshot methods (prices_to_signals, prices_to_features, etc.):

adv_over_5m = (closes * volumes).rolling(30).mean() > 5e6
prices_over_10 = closes > 10

signals = signals.where(adv_over_5m & prices_over_10)

I am not sure what you mean by "first-class universe definition." In both backtesters, you define rules to dynamically filter the initial universe, but the syntax differs.