Problem Description
Solution
To create a custom Alpha Model in the QuantConnect Algorithm Framework, you need to define a class that inherits from AlphaModel. This class is responsible for analyzing data and emitting Insight objects, which predict the future direction and magnitude of assets.
Key Components of a Custom Alpha Model
update(self, algorithm, data): This is the core method. It runs every time a newSliceof data is available. It returns a list ofInsightobjects.on_securities_changed(self, algorithm, changes): This method manages the setup and teardown of indicators or data specific to assets as they enter or leave the universe.Insight: The return object containing theSymbol,TimeDelta(duration),InsightDirection(Up, Down, Flat), and optionally magnitude/confidence.
Complete Example: EMA Crossover Alpha
Below is a complete, runnable example of a custom Alpha Model that uses an Exponential Moving Average (EMA) crossover strategy. It includes a helper class to manage indicators for each specific symbol.
# region imports
from AlgorithmImports import *
# endregion
class CustomAlphaModelAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2023, 1, 1)
self.set_cash(100000)
# 1. Select a Universe
self.set_universe_selection(
ManualUniverseSelectionModel([Symbol.create("SPY", SecurityType.EQUITY, Market.USA)])
)
# 2. Set the Custom Alpha Model
self.set_alpha(EmaCrossoverAlphaModel(fast_period=12, slow_period=26))
# 3. Set Portfolio Construction (Equal Weighting)
self.set_portfolio_construction(EqualWeightingPortfolioConstructionModel())
# 4. Set Execution Model (Immediate)
self.set_execution(ImmediateExecutionModel())
class EmaCrossoverAlphaModel(AlphaModel):
"""
Emits 'Up' insights when Fast EMA > Slow EMA, and 'Down' insights otherwise.
"""
def __init__(self, fast_period=12, slow_period=26, resolution=Resolution.DAILY):
self.fast_period = fast_period
self.slow_period = slow_period
self.resolution = resolution
self.prediction_interval = Time.multiply(Extensions.to_time_span(resolution), 1)
self.symbol_data_by_symbol = {}
def update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
"""
Called each time the algorithm receives data.
Returns a list of Insights.
"""
insights = []
for symbol, symbol_data in self.symbol_data_by_symbol.items():
# Ensure indicators are ready and we have data for this symbol
if not symbol_data.is_ready or symbol not in data:
continue
# Logic: Check for Crossover
if symbol_data.fast.current.value > symbol_data.slow.current.value:
# Generate an UP insight with a duration of 1 bar
insights.append(Insight.price(symbol, self.prediction_interval, InsightDirection.UP))
elif symbol_data.fast.current.value < symbol_data.slow.current.value:
# Generate a DOWN insight
insights.append(Insight.price(symbol, self.prediction_interval, InsightDirection.DOWN))
return insights
def on_securities_changed(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
"""
Called when securities are added or removed from the universe.
"""
# Clean up data for removed securities
for security in changes.removed_securities:
if security.symbol in self.symbol_data_by_symbol:
self.symbol_data_by_symbol.pop(security.symbol)
# Initialize data for added securities
for security in changes.added_securities:
if security.symbol not in self.symbol_data_by_symbol:
self.symbol_data_by_symbol[security.symbol] = SymbolData(
algorithm,
security.symbol,
self.fast_period,
self.slow_period,
self.resolution
)
class SymbolData:
"""
Helper class to store indicators and state for a specific symbol.
"""
def __init__(self, algorithm, symbol, fast_period, slow_period, resolution):
self.symbol = symbol
# Define Indicators
self.fast = algorithm.EMA(symbol, fast_period, resolution)
self.slow = algorithm.EMA(symbol, slow_period, resolution)
# Warm up indicators using historical data
history = algorithm.history(symbol, slow_period, resolution)
if not history.empty:
# History requests return a DataFrame; iterate to update indicators
for time, row in history.loc[symbol].iterrows():
if "close" in row:
self.fast.update(time, row["close"])
self.slow.update(time, row["close"])
@property
def is_ready(self):
return self.fast.is_ready and self.slow.is_ready
Implementation Details
- Inheritance: The class must inherit from
AlphaModel. - SymbolData Pattern: Notice the
SymbolDataclass. Since the Alpha Model handles the entire universe, you should not store single variables likeself.emain the main Alpha class. Instead, use a dictionary (self.symbol_data_by_symbol) to map everySymbolto its own set of indicators. - Warm Up: In
on_securities_changed, when a new security is added, we immediately requestalgorithm.historyto warm up the indicators. This ensures the model can emit insights immediately rather than waiting 26 days (in this example) for the EMA to fill up. - Insight Duration:
Insight.pricerequires a duration (how long the prediction is valid). In the example, we useTime.multiply(Extensions.to_time_span(resolution), 1)to make the insight valid for exactly one bar (e.g., one day).
Q&A
Q: Can I emit multiple insights for different symbols in one update?
A: Yes. The update method returns a List[Insight]. You can append as many insights as necessary for different symbols within that list.
Q: What happens if I don't emit an insight for a symbol that previously had one?
A: Insights have an expiration time (the duration you set). If an insight expires and no new insight is emitted to replace it, the Portfolio Construction Model will generally liquidate the position or reduce it to zero, depending on the specific PCM logic.
Q: How do I combine multiple Alpha Models?
A: You can use the CompositeAlphaModel.
self.set_alpha(CompositeAlphaModel(RsiAlphaModel(), EmaCrossoverAlphaModel()))
Q: Do I have to use indicators?
A: No. You can use any logic inside the update method, including alternative data, fundamental data, or machine learning predictions, as long as you return Insight objects.