Equity

ETF Constituents Universes

Introduction

An ETF constituents universe lets you select a universe of securities in an exchange traded fund. The US ETF Constituents dataset includes 2,650 US ETFs you can use to create your universe.

Create Universes

To add an ETF Constituents universe, call the Universe.ETFuniverse.etf method.

public class ETFConstituentsAlgorithm : QCAlgorithm
{
    public override void Initialize() 
    {
        UniverseSettings.Asynchronous = true;
        AddUniverse(Universe.ETF("SPY"));
    }
}
class ETFConstituentsAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.universe_settings.asynchronous = True        
        self.add_universe(self.universe.etf("SPY"))

The following table describes the ETFetf method arguments:

Argument: etfTickeretf_ticker

The ETF ticker. To view the supported ETFs in the US ETF Constituents dataset, see Supported ETFs.

Data Type: stringstr | Default Value: None

Argument: universeSettingsuniverse_settings

The universe settings. If you don't provide an argument, it uses the algorithm UniverseSettingsuniverse_settings.

Data Type: UniverseSettings | Default Value: nullNone

Argument: universeFilterFuncuniverse_filter_func

A function to select some of the ETF constituents for the universe. If you don't provide an argument, it selects all of the constituents.

Data Type: Func<IEnumerable<ETFConstituentUniverse>, IEnumerable<Symbol>>Callable[[List[ETFConstituentUniverse]], List[Symbol]] | Default Value: nullNone

To select a subset of the ETF constituents, provide a universeFilterFuncuniverse_filter_func argument. The filter function receives ETFConstituentUniverse objects, which represent one of the ETF constituents. ETFConstituentUniverse objects have the following attributes:

public class ETFConstituentsAlgorithm : QCAlgorithm 
{
    private Universe _universe;
    public override void Initialize() 
    {
        UniverseSettings.Asynchronous = true;
        _universe = Universe.ETF("SPY", UniverseSettings, ETFConstituentsFilter);
        AddUniverse(_universe);
    }

    private IEnumerable<Symbol> ETFConstituentsFilter(IEnumerable<ETFConstituentUniverse> constituents)
    {
        // Get the 10 securities with the largest weight in the index
        return constituents.OrderByDescending(c => c.Weight).Take(10).Select(c => c.Symbol);
    }
}
class ETFConstituentsAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.universe_settings.asynchronous = True
        self._universe = self.universe.etf("SPY", self.universe_settings, self._etf_constituents_filter)
        self.add_universe(self._universe)

    def _etf_constituents_filter(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]:
        # Get the 10 securities with the largest weight in the index
        selected = sorted([c for c in constituents if c.weight],
            key=lambda c: c.weight, reverse=True)[:10]
        return [c.symbol for c in selected]

Historical Data

To get historical ETF constituents data, call the Historyhistory method with the Universe object and the lookback period. The return type is a IEnumerable<BaseDataCollection> and you have to cast its items to ETFConstituentUniverse.

To get historical ETF constituents data, call the Historyhistory method with the Universe object and the lookback period. The return type is a multi-index pandas.Series of ETFConstituentUniverse list.

var history = History(_universe, 30, Resolution.Daily);
foreach (var constituents in history)
{
    foreach (ETFConstituentUniverse constituent in constituents)
    {
        Log($"{constituent.Symbol} weight at {constituent.EndTime}: {constituent.Weight}");
    }
}
history = self.history(self._universe, 30, Resolution.DAILY)
for (universe_symbol, time), constituents in history.items():
    for constituent in constituents:
        self.log(f'{constituent.symbol} weight at {constituent.end_time}: {constituent.weight}')

For more information about ETF Constituents data, see US ETF Constituents.

Selection Frequency

Equity universes run on a daily basis by default. To adjust the selection schedule, see Schedule.

Examples

The following examples demonstrate some common practices for ETF constituent universes.

Example 1: Short the Smallest ETF Constituents

A subset of the SPY constituents outperform the SPY while many of the constituents underperform the overall index. In an attempt to exclude the ETF constituents that underperform the index, the following algorithm buys the SPY and shorts the 100 assets in the index with the smallest weight in the ETF.

public class ETFConstituentsUniverseAlgorithm : QCAlgorithm
{
    private Symbol _spy;
    private Dictionary<Symbol, decimal> _etfWeightBySymbol;
    private Universe _universe;

    public override void Initialize()
    {
        SetStartDate(2023, 6, 1);
        SetCash(10000000);
        Settings.MinimumOrderMarginPortfolioPercentage = 0m;
        // To avoid over-trading and high transaction costs, refilter and rebalance weekly.
        UniverseSettings.Schedule.On(DateRules.WeekEnd());
        // Add the SPY ETF.
        _spy = AddEquity("SPY", Resolution.Daily).Symbol;
        // Add a universe of the SPY constituents.
        _universe = AddUniverse(Universe.ETF(_spy, universeFilterFunc: SelectAssets));
        // Create a Scheduled Event to rebalance the portfolio.
        Schedule.On(DateRules.WeekStart(), TimeRules.At(9, 0), Rebalance);
    }

    private IEnumerable<Symbol> SelectAssets(IEnumerable<ETFConstituentUniverse> constituents)
    {
        // Cache the constituent weights in a dictionary for filtering and position sizing.
        _etfWeightBySymbol = constituents
            .Where(c => c.Weight.HasValue)
            .ToDictionary(c => c.Symbol, c => c.Weight.Value);
        // Select the 100 consituents with the smallest weight in the ETF.
        // They should have negative excess return.
        return _etfWeightBySymbol
            .OrderBy(x => x.Value)
            .Take(100)
            .Select(x => x.Key);
    }

    private void Rebalance()
    {
        // Get the ETF weight of all the assets currently in the universe.  
        var weightBySymbol = _universe.Selected
            // To avoid trading errors, skip assets that have no price yet.
            .Where(symbol => Securities[symbol].Price != 0)
            .Select(symbol => new { Symbol = symbol, Weight = _etfWeightBySymbol[symbol] })
            .ToDictionary(x => x.Symbol, x => x.Weight);
        // Buy the SPY to eliminate systematic risk.
        var targets = new List<PortfolioTarget>
        {
            new PortfolioTarget(_spy, 1.0m-weightBySymbol.Sum(kvp => kvp.Value))
        };
        // Sell the 100 ETF constituents with the lowest weight due to expected negative excess return.
        targets.AddRange(weightBySymbol.Select(x => new PortfolioTarget(x.Key, -x.Value)));
        // Place orders to rebalance the portfolio.
        SetHoldings(targets, true);
    }
}
class ETFConstituentsUniverseAlgorithm(QCAlgorithm):
    
    def initialize(self) -> None:
        self.set_start_date(2023, 6, 1)
        self.set_cash(10_000_000)
        self.settings.minimum_order_margin_portfolio_percentage = 0
        # To avoid over-trading and high transaction costs, refilter and rebalance weekly.
        self.universe_settings.schedule.on(self.date_rules.week_end())
        # Add the SPY ETF.
        self._spy = self.add_equity("SPY", Resolution.DAILY).symbol
        # Add a universe of the SPY constituents.
        self._universe = self.add_universe(self.universe.etf(self._spy, universe_filter_func=self._select_assets))
        # Create a Scheduled Event to rebalance the portfolio.
        self.schedule.on(self.date_rules.week_start(), self.time_rules.at(9, 0), self._rebalance)
    
    def _select_assets(self, constituents: List[ETFConstituentUniverse]) -> List[Symbol]:
        # Cache the constituent weights in a dictionary for filtering and position sizing.
        self._etf_weight_by_symbol = {c.symbol: c.weight for c in constituents if c.weight}
        # Select the 100 consituents with the smallest weight in the ETF.
        # They should have negative excess return.
        return [symbol for symbol, _ in sorted(self._etf_weight_by_symbol.items(), key=lambda x: x[1])[:100]]

    def _rebalance(self) -> None:
        # Get the ETF weight of all the assets currently in the universe.        
        weight_by_symbol = {
            symbol: self._etf_weight_by_symbol[symbol] for symbol in self._universe.selected 
            # To avoid trading errors, skip assets that have no price yet.
            if self.securities[symbol].price
        }
        # Buy the SPY to eliminate systematic risk.
        targets = [PortfolioTarget(self._spy, 1-sum(weight_by_symbol.values()))]
        # Sell the 100 ETF constituents with the lowest weight due to expected negative excess return.
        targets.extend(
            [PortfolioTarget(symbol, -weight) for symbol, weight in weight_by_symbol.items()]
        )
        # Place orders to rebalance the portfolio.
        self.set_holdings(targets, True)
        

Example 2: SPY Constituents in an Uptrend

The following example chains an ETF constituents universe and a fundamental universe. It first selects all the constituents of the SPY ETF and then filters them down in the fundamental universe filter to select the assets that are trading above their average price over the last 200 days. The output of the fundamental universe selection method is the output of the chained universe. The Fundamental objects that the fundamental universe filter function recieves contains the prices of the ETF constituents. By chaining the ETF constituents universe into the fundamental universe, you can update the indicators with the price instead of making a history request.

public class ChainedUniverseAlgorithm : QCAlgorithm
{
    private Dictionary<Symbol, SelectionData> _selectionDataBySymbol = new();
    private Universe _universe;

    public override void Initialize()
    {
        SetStartDate(2023, 6, 1);
        // Seed the price of each asset that enters the universe with its last known price so you can trade it 
        // on the same morning it enters the universe without getting warnings.
        SetSecurityInitializer(
            new BrokerageModelSecurityInitializer(BrokerageModel, new FuncSecuritySeeder(GetLastKnownPrices))
        );
        // Add a chained universe that selects the SPY constituents trading above their 200-day SMA.
        var spy = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA);
        _universe = AddUniverse(Universe.ETF(spy), FundamentalSelection);
        // Trade daily at market open since the trading signal is generated on a daily resolution.
        Schedule.On(DateRules.EveryDay(spy), TimeRules.AfterMarketOpen(spy, 1), Rebalance);
    }

    public IEnumerable<Symbol> FundamentalSelection(IEnumerable<Fundamental> fundamental)
    {
        // Create/Update an SMA indicator for each asset that enters the ETF.
        var universeSymbols = new List<Symbol>();
        foreach (var f in fundamental)
        {
            universeSymbols.Add(f.Symbol);
            if (!_selectionDataBySymbol.ContainsKey(f.Symbol))
            {
                _selectionDataBySymbol[f.Symbol] = new SelectionData(this, f.Symbol);
            }
            _selectionDataBySymbol[f.Symbol].Update(Time, f.AdjustedPrice);
        }

        // Remove indicators for assets that are no longer in the ETF to release the computational resources.
        var symbolsToRemove = _selectionDataBySymbol.Keys.Where(symbol => !universeSymbols.Contains(symbol)).ToList();
        foreach (var symbol in symbolsToRemove)
        {
            _selectionDataBySymbol.Remove(symbol);
        }

        // Select the Equities trading above their SMA as we estimate them to be in uptrend and still rising.
        var selected = _selectionDataBySymbol.Where(kvp => kvp.Value.IsAboveSma)
            .Select(kvp => kvp.Key)
            .ToList();

        // Plot the results.
        Plot("Universe", "Possible", fundamental.Count());
        Plot("Universe", "Selected", selected.Count);

        return selected;
    }

    private void Rebalance()
    {
        // For an equal-weighted portfolio with the Equities that are above their SMA.
        var symbols = _universe.Selected.Where(symbol => Securities[symbol].Price > 0);
        if (symbols.Count() == 0)
        {
            return;
        }
        var weight = 1m / symbols.Count();
        SetHoldings(symbols.Select(symbol => new PortfolioTarget(symbol, weight)).ToList(), true);
    }
}

// Define a separate class to contain the SMA indicator.
class SelectionData
{
    private SimpleMovingAverage _sma;
    public bool IsAboveSma { get; private set; }

    public SelectionData(QCAlgorithm algorithm, Symbol symbol, int period = 200)
    {
        // Create the SMA indicator for trend detection and filtering.
        _sma = new SimpleMovingAverage(period);
        // Warm up the SMA so you can immediately use it.
        algorithm.WarmUpIndicator(symbol, _sma, Resolution.Daily);
    }

    public void Update(DateTime time, decimal value)
    {
        IsAboveSma = _sma.Update(time, value) && value > _sma.Current.Value;
    }
}
class ChainedUniverseAlgorithm(QCAlgorithm):

    _selection_data_by_symbol = {}
    
    def initialize(self):
        self.set_start_date(2023, 6, 1)
        # Seed the price of each asset that enters the universe with its last known price so you can trade it 
        # on the same morning it enters the universe without getting warnings.
        self.set_security_initializer(
            BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))
        )
        # Add a chained universe that selects the SPY constituents trading above their 200-day SMA.
        spy = Symbol.create("SPY", SecurityType.EQUITY, Market.USA)
        self._universe = self.add_universe(self.universe.etf(spy), self._fundamental_selection)
        # Trade daily at market open since the trading signal is generated on a daily resolution.
        self.schedule.on(self.date_rules.every_day(spy), self.time_rules.after_market_open(spy, 1), self._rebalance)
    
    def _fundamental_selection(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # Create/Update an SMA indicator for each asset that enters the ETF.
        universe_symbols = []
        for f in fundamental:
            universe_symbols.append(f.symbol)
            if f.symbol not in self._selection_data_by_symbol:
                self._selection_data_by_symbol[f.symbol] = SelectionData(self, f.symbol)
            self._selection_data_by_symbol[f.symbol].update(self.time, f.adjusted_price)
    
        # Remove indicators for assets that are no longer in the ETF to release the computational resources.
        symbols_to_remove = [s for s in self._selection_data_by_symbol.keys() if s not in universe_symbols]
        for symbol in symbols_to_remove:
            self._selection_data_by_symbol.pop(symbol)

        # Select the Equities trading above their SMA as we estimate them to be in uptrend and still rising.
        selected = [
            symbol for symbol, selection_data in self._selection_data_by_symbol.items() if selection_data.is_above_sma
        ]
            
        # Plot the results.
        self.plot("Universe", "Possible", len(list(fundamental)))
        self.plot("Universe", "Selected", len(selected))
    
        return selected

    def _rebalance(self) -> None:
        # Form an equal-weighted portfolio with the Equities that are above their SMA.
        symbols = [symbol for symbol in self._universe.selected if self.securities[symbol].price]
        if not symbols:
            return
        weight = 1 / len(symbols)
        self.set_holdings([PortfolioTarget(symbol, weight) for symbol in symbols], True)
    

# Define a separate class to contain the SMA indicator.
class SelectionData(object):

    def __init__(self, algorithm, symbol, period=200):
        #  Create the SMA indicator for trend detection and filtering.
        self._sma = SimpleMovingAverage(period)
        # Warm up the SMA so you can immediately use it.
        algorithm.warm_up_indicator(symbol, self._sma, Resolution.DAILY)
    
    def update(self, time, value):
        self.is_above_sma = self._sma.update(time, value) and value > self._sma.current.value

Other Examples

For more examples, see the following algorithms:

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: