Problem Description
Solution
To automatically liquidate a stock when it is removed from your trading universe in QuantConnect, you should utilize the on_securities_changed event handler.
This method is triggered whenever the universe selection model adds or removes assets. The changes parameter passed to this method contains a list of removed_securities. You can iterate through this list, check if you currently hold a position, and then liquidate it.
Implementation Details
on_securities_changed: This is the specific event handler for universe changes.changes.removed_securities: A list ofSecurityobjects that have dropped out of the universe.security.invested: A check to ensure you actually hold the asset before attempting to liquidate it (saves API calls and log noise).self.liquidate: The method to close the position.
Complete Algorithm Example
The following code demonstrates a strategy that selects the top 5 liquid stocks. If a stock drops out of the top 5 (is removed from the universe), the algorithm automatically liquidates the position.
# region imports
from AlgorithmImports import *
# endregion
class UniverseLiquidationAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 6, 1)
self.set_cash(100000)
# Use a coarse selection to dynamically select assets based on dollar volume
self.add_universe(self.coarse_selection_function)
# Initialize a list to track active symbols
self._active_symbols = []
def coarse_selection_function(self, coarse):
# Sort by Dollar Volume and take the top 5
# This ensures the universe changes frequently for demonstration purposes
sorted_by_volume = sorted(coarse, key=lambda x: x.dollar_volume, reverse=True)
return [x.symbol for x in sorted_by_volume[:5]]
def on_data(self, slice: Slice):
# Simple logic: Invest equally in all active symbols
# This ensures we have positions to liquidate when they are removed
for symbol in self._active_symbols:
if not self.portfolio[symbol].invested:
self.set_holdings(symbol, 1.0 / len(self._active_symbols))
def on_securities_changed(self, changes: SecurityChanges):
"""
Event fired when securities are added or removed from the universe.
"""
# 1. Handle Added Securities
for security in changes.added_securities:
self._active_symbols.append(security.symbol)
# Optional: Log the addition
# self.debug(f"Added {security.symbol} to universe")
# 2. Handle Removed Securities (The Solution)
for security in changes.removed_securities:
if security.symbol in self._active_symbols:
self._active_symbols.remove(security.symbol)
# Check if we are currently invested in this security
if security.invested:
# Liquidate the position immediately
self.liquidate(security.symbol, "Removed from Universe")
self.debug(f"Liquidated {security.symbol} because it was removed from the universe.")
Key Considerations
- Data Availability: Even after a security is removed from the universe, the
Securityobject remains accessible inchanges.removed_securitiesimmediately, allowing you to checksecurity.investedandsecurity.holdings. - Open Orders:
self.liquidatecancels open orders by default. If you have specific logic for open orders (e.g., stop losses) that you want to handle differently, you should cancel them explicitly usingself.transactions.cancel_open_orders(security.symbol). - PEP8 Compliance: The code above uses the modern PEP8 snake_case format (
on_securities_changedinstead ofOnSecuritiesChanged), which is the standard for new QuantConnect Python projects.
Q&A: Universe Management in QuantConnect
Q: Does liquidate close both long and short positions?
A: Yes, self.liquidate(symbol) closes any open position for that symbol, regardless of whether it is long or short, and cancels all open orders associated with it.
Q: Will on_securities_changed run before on_data?
A: Yes, in the LEAN engine event flow, on_securities_changed is triggered after universe selection but before the on_data slice is passed to the algorithm. This ensures you can clean up your portfolio before processing new data.
Q: What happens if I don't liquidate a removed security?
A: If you do not liquidate, you will continue to hold the shares. However, you will no longer receive data updates for that symbol in on_data (unless you manually added a subscription for it elsewhere), effectively leaving you with a "zombie" position that is harder to manage.
Q: Can I delay liquidation?
A: Yes. Instead of calling self.liquidate immediately inside on_securities_changed, you could set a flag or add the symbol to a "pending liquidation" list to be processed at a specific time (e.g., Market On Close) within your on_data or a Scheduled Event.