# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from AlgorithmImports import *
constituent_data = []
###
### Alpha model for ETF constituents, where we generate insights based on the weighting
### of the ETF constituent
###
class ETFConstituentAlphaModel(AlphaModel):
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
pass
###
### Creates new insights based on constituent data and their weighting
### in their respective ETF
###
def update(self, algorithm: QCAlgorithm, data: Slice) -> list[Insight]:
global constituent_data
insights = []
for constituent in constituent_data:
if constituent.symbol not in data.bars and \
constituent.symbol not in data.quote_bars:
continue
insight_direction = InsightDirection.UP if constituent.weight is not None and constituent.weight >= 0.01 else InsightDirection.DOWN
insights.append(Insight(
algorithm.utc_time,
constituent.symbol,
timedelta(days=1),
InsightType.PRICE,
insight_direction,
float(1 * int(insight_direction)),
1.0,
weight=float(0 if constituent.weight is None else constituent.weight)
))
return insights
###
### Generates targets for ETF constituents, which will be set to the weighting
### of the constituent in their respective ETF
###
class ETFConstituentPortfolioModel(PortfolioConstructionModel):
def __init__(self) -> None:
self.has_added = False
###
### Securities changed, detects if we've got new additions to the universe
### so that we don't try to trade every loop
###
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
self.has_added = len(changes.added_securities) != 0
###
### Creates portfolio targets based on the insights provided to us by the alpha model.
### Emits portfolio targets setting the quantity to the weight of the constituent
### in its respective ETF.
###
def create_targets(self, algorithm: QCAlgorithm, insights: list[Insight]) -> list[PortfolioTarget]:
if not self.has_added:
return []
final_insights = []
for insight in insights:
final_insights.append(PortfolioTarget(insight.symbol, float(0 if insight.weight is None else insight.weight)))
self.has_added = False
return final_insights
###
### Executes based on ETF constituent weighting
###
class ETFConstituentExecutionModel(ExecutionModel):
###
### Liquidates if constituents have been removed from the universe
###
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
for change in changes.removed_securities:
algorithm.liquidate(change.symbol)
###
### Creates orders for constituents that attempts to add
### the weighting of the constituent in our portfolio. The
### resulting algorithm portfolio weight might not be equal
### to the leverage of the ETF (1x, 2x, 3x, etc.)
###
def execute(self, algorithm: QCAlgorithm, targets: list[IPortfolioTarget]) -> None:
for target in targets:
algorithm.set_holdings(target.symbol, target.quantity)
###
### Tests ETF constituents universe selection with the algorithm framework models (Alpha, PortfolioConstruction, Execution)
###
class ETFConstituentUniverseFrameworkRegressionAlgorithm(QCAlgorithm):
###
### Initializes the algorithm, setting up the framework classes and ETF constituent universe settings
###
def initialize(self) -> None:
self.set_start_date(2020, 12, 1)
self.set_end_date(2021, 1, 31)
self.set_cash(100000)
self.set_alpha(ETFConstituentAlphaModel())
self.set_portfolio_construction(ETFConstituentPortfolioModel())
self.set_execution(ETFConstituentExecutionModel())
self.universe_settings.resolution = Resolution.HOUR
universe = self.add_universe(self.universe.etf("SPY", self.universe_settings, self.filter_etf_constituents))
historical_data = self.history(universe, 1, flatten=True)
if len(historical_data) < 200:
raise ValueError(f"Unexpected universe DataCollection count {len(historical_data)}! Expected > 200")
###
### Filters ETF constituents
###
### ETF constituents
### ETF constituent Symbols that we want to include in the algorithm
def filter_etf_constituents(self, constituents: list[ETFConstituentUniverse]) -> list[Symbol]:
global constituent_data
constituent_data_local = [i for i in constituents if i.weight and i.weight >= 0.001]
constituent_data = list(constituent_data_local)
return [i.symbol for i in constituent_data_local]