QuantConnect/Lean · error · AssertionError

Algorithm should have been invested at the end of the algori

Error message

Algorithm should have been invested at the end of the algorithm

What it means

Assertion in PEP8StyleBasicAlgorithm.on_end_of_algorithm that the portfolio holds a position at algorithm end. The algorithm calls self.set_holdings(self.spy, 1) once in on_data when not invested, so if the strategy never took (and kept) a position, this fails. It is a sanity check that the single intended trade actually executed and stuck.

Source

Thrown at Algorithm.Python/PEP8StyleBasicAlgorithm.py:40

        self.spy = self.add_equity("SPY", Resolution.MINUTE, extended_market_hours=False, fill_forward=True).symbol

        # Test accessing a constant (QCAlgorithm.MaxTagsCount)
        self.debug("MaxTagsCount: " + str(self.MAX_TAGS_COUNT))

    def on_data(self, slice):
        if not self.portfolio.invested:
            self.set_holdings(self.spy, 1)
            self.debug("Purchased Stock")

    def on_order_event(self, order_event):
        self.log(f"{self.time} :: {order_event}")

    def on_end_of_algorithm(self):
        self.log("Algorithm ended!")

        if not self.portfolio.invested:
            raise AssertionError("Algorithm should have been invested at the end of the algorithm")

        # let's do some logging to do more pep8 style testing
        self.log("-----------------------------------------------------------------------------------------")
        self.log(f"{self.spy.value} last price: {self.securities[self.spy].price}")
        self.log(f"{self.spy.value} holdings: "
                 f"{self.securities[self.spy].holdings.quantity}@{self.securities[self.spy].holdings.price}="
                 f"{self.securities[self.spy].holdings.holdings_value}")
        self.log("-----------------------------------------------------------------------------------------")

View on GitHub (pinned to d2c3659f87)

Solutions

  1. Confirm the security has data for the backtest window: check self.securities[self.spy].has_data and that history is non-empty for the start/end dates.
  2. Verify market hours and that the backtest range spans at least one open session for the symbol's exchange.
  3. Check the order events (self.transactions.get_orders()) for rejections or invalid-status fills and inspect their messages.
  4. Ensure set_holdings weight and leverage permit the trade (lower the weight or call self.set_security(self.spy) / adjust BuyingPowerModel).
  5. Confirm the symbol/market resolves (e.g. add_equity('SPY') maps to the expected market) and that data is present on disk for that range.

Example fix

# before
def on_data(self, slice):
    if not self.portfolio.invested:
        self.set_holdings(self.spy, 1)
        self.debug("Purchased Stock")

# after (guard against no-data and verify the order was accepted)
def on_data(self, slice):
    if not self.portfolio.invested and self.securities[self.spy].has_data:
        ticket = self.set_holdings(self.spy, 1)
        if ticket.status == OrderStatus.INVALID:
            self.error(f"Order invalid: {ticket.tag}")
        else:
            self.debug("Submitted purchase")
Defensive patterns

Strategy: validation

Validate before calling

# Before asserting invested, confirm a fill actually occurred
orders = self.transactions.get_orders(lambda o: o.symbol == self.spy)
filled = any(o.status == OrderStatus.FILLED for o in orders)
if not self.portfolio.invested and not filled:
    self.debug("No SPY fill occurred; will not assert invested")
else:
    assert self.portfolio.invested, "Expected to be invested"

Type guard

def security_has_data(algorithm, symbol):
    """True when the security has received data and can be traded."""
    sec = algorithm.securities[symbol]
    return sec.has_data and sec.is_tradable

Prevention

When it happens

Trigger: self.portfolio.invested is False at on_end_of_algorithm. This happens when on_data's set_holdings call never filled: no minute bars were received for SPY, the order was rejected/invalid, buying power was insufficient, the fill never arrived before backtest end, or the position was opened then fully closed by a later action.

Common situations: Backtest date range has no data for the security (wrong symbol/market, delisted, holiday-only range). Set_holdings with weight 1 fails due to leverage/buying-power limits. The security was added but data feed produced no slices. A fill model or brokerage rejected the order. Market hours meant no fills occurred in the short window.

Related errors


AI-assisted analysis of QuantConnect/Lean@d2c3659f87 (2026-08-13). Data as JSON: /api/errors/4d12ec405ec92dc4. Report an issue: GitHub.