🚀 AI One-Click Generation of quantconnect Strategy Code

How to Dynamically Manage Your Portfolio with the on_securities_changed Event in QuantConnect

quantconnect | Published on: 2025/12/1
The following content is generated by EasyQuant .

Problem Description

How can I automatically liquidate a stock when it's removed from my trading universe?

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

  1. on_securities_changed: This is the specific event handler for universe changes.
  2. changes.removed_securities: A list of Security objects that have dropped out of the universe.
  3. security.invested: A check to ensure you actually hold the asset before attempting to liquidate it (saves API calls and log noise).
  4. 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 Security object remains accessible in changes.removed_securities immediately, allowing you to check security.invested and security.holdings.
  • Open Orders: self.liquidate cancels 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 using self.transactions.cancel_open_orders(security.symbol).
  • PEP8 Compliance: The code above uses the modern PEP8 snake_case format (on_securities_changed instead of OnSecuritiesChanged), 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.