# 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.
import typing
from AlgorithmImports import *
constituentData = []
###
### Alpha model for ETF constituents, where we generate insights based on the weighting
### of the ETF constituent
###
class ETFConstituentAlphaModel(AlphaModel):
def OnSecuritiesChanged(self, algorithm, changes):
pass
###
### Creates new insights based on constituent data and their weighting
### in their respective ETF
###
def Update(self, algorithm: QCAlgorithm, data: Slice):
insights = []
for constituent in constituentData:
if constituent.Symbol not in data.Bars and \
constituent.Symbol not in data.QuoteBars:
continue
insightDirection = InsightDirection.Up if constituent.Weight is not None and constituent.Weight >= 0.01 else InsightDirection.Down
insights.append(Insight(
algorithm.UtcTime,
constituent.Symbol,
timedelta(days=1),
InsightType.Price,
insightDirection,
float(1 * int(insightDirection)),
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):
self.hasAdded = False
###
### Securities changed, detects if we've got new additions to the universe
### so that we don't try to trade every loop
###
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges):
self.hasAdded = len(changes.AddedSecurities) != 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 CreateTargets(self, algorithm: QCAlgorithm, insights: typing.List[Insight]):
if not self.hasAdded:
return []
finalInsights = []
for insight in insights:
finalInsights.append(PortfolioTarget(insight.Symbol, float(0 if insight.Weight is None else insight.Weight)))
self.hasAdded = False
return finalInsights
###
### Executes based on ETF constituent weighting
###
class ETFConstituentExecutionModel(ExecutionModel):
###
### Liquidates if constituents have been removed from the universe
###
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges):
for change in changes.RemovedSecurities:
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: typing.List[IPortfolioTarget]):
for target in targets:
algorithm.SetHoldings(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):
self.SetStartDate(2020, 12, 1)
self.SetEndDate(2021, 1, 31)
self.SetCash(100000)
self.SetAlpha(ETFConstituentAlphaModel())
self.SetPortfolioConstruction(ETFConstituentPortfolioModel())
self.SetExecution(ETFConstituentExecutionModel())
spy = Symbol.Create("SPY", SecurityType.Equity, Market.USA)
self.UniverseSettings.Resolution = Resolution.Hour
universe = self.AddUniverse(self.Universe.ETF(spy, self.UniverseSettings, self.FilterETFConstituents))
historicalData = self.History(universe, 1)
if len(historicalData) != 1:
raise ValueError(f"Unexpected history count {len(historicalData)}! Expected 1");
for universeDataCollection in historicalData:
if len(universeDataCollection) < 200:
raise ValueError(f"Unexpected universe DataCollection count {len(universeDataCollection)}! Expected > 200");
###
### Filters ETF constituents
###
### ETF constituents
### ETF constituent Symbols that we want to include in the algorithm
def FilterETFConstituents(self, constituents):
global constituentData
constituentDataLocal = [i for i in constituents if i is not None and i.Weight >= 0.001]
constituentData = list(constituentDataLocal)
return [i.Symbol for i in constituentDataLocal]
###
### no-op for performance
###
def OnData(self, data):
pass