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.ETF
universe.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 ETF
etf
method arguments:
Argument: |
Argument: |
Argument: |
To select a subset of the ETF constituents, provide a universeFilterFunc
universe_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 History
history
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 History
history
method with the Universe
object and the lookback period.
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}"); } }
# DataFrame object: df_history = self.history(self._universe, 30, Resolution.DAILY, flatten=True) # Series object: series_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: