QuantConnect/Lean · error · AssertionError

Expected 3 liquidation events, found {self.liquidated}

Error message

Expected 3 liquidation events, found {self.liquidated}

What it means

Asserts self.liquidated == 2 (one future liquidation + one FOP exercise). NOTE: the message text says 'Expected 3 liquidation events' but the code checks '!= 2' — the message is wrong; the intended count is 2. Failing means too few or too many SELL/FILLED order events fired.

Source

Thrown at Algorithm.Python/FuturesAndFuturesOptionsExpiryTimeAndLiquidationRegressionAlgorithm.py:102

        self.liquidated += 1
        if order_event.symbol.security_type == SecurityType.FUTURE_OPTION and self.expected_liquidation_time != self.time:
            raise AssertionError(f"Expected to liquidate option {order_event.symbol} at {self.expected_liquidation_time}, instead liquidated at {self.time}")

        if order_event.symbol.security_type == SecurityType.FUTURE and \
            (self.expected_liquidation_time - timedelta(minutes=1)) != self.time and \
            self.expected_liquidation_time != self.time:

            raise AssertionError(f"Expected to liquidate future {order_event.symbol} at {self.expected_liquidation_time} (+1 minute), instead liquidated at {self.time}")

    def on_end_of_algorithm(self):
        if not self.invested:
            raise AssertionError("Never invested in ES futures and FOPs")

        if self.delistings_received != 4:
            raise AssertionError(f"Expected 4 delisting events received, found: {self.delistings_received}")

        if self.liquidated != 2:
            raise AssertionError(f"Expected 3 liquidation events, found {self.liquidated}")

View on GitHub (pinned to d2c3659f87)

Solutions

  1. Log each order_event that increments self.liquidated (symbol + security_type) to see what is extra/missing.
  2. If an extra underlying-future liquidation appears, investigate why the breakeven assumption broke.
  3. Trust the code (== 2) over the message text ('3') when reconciling.
  4. Update the count if engine behaviour intentionally changed, and fix the misleading message.

Example fix

// before
if self.liquidated != 2:
    raise AssertionError(f"Expected 3 liquidation events, found {self.liquidated}")
// after - correct the message
if self.liquidated != 2:
    raise AssertionError(f"Expected 2 liquidation events, found {self.liquidated}")
Defensive patterns

Strategy: validation

Validate before calling

# Track liquidation events with detail
if order_event.direction == OrderDirection.SELL and order_event.status == OrderStatus.FILLED:
    self.liquidated += 1
    self.debug(f"Liquidation #{self.liquidated}: {order_event.symbol.security_type} {order_event.symbol}")

Type guard

def is_liquidation_fill(order_event) -> bool:
    return order_event.direction == OrderDirection.SELL and order_event.status == OrderStatus.FILLED

Prevention

When it happens

Trigger: Extra liquidation events from an unexpected underlying future sale; missing the FOP exercise event; duplicate fills inflating the counter.

Common situations: Engine changes introducing an additional auto-liquidation (the very case the comment says should NOT happen); data changes causing the FOP not to exercise; order-event duplication after a refactor.

Related errors


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