nautechsystems/nautilus_trader · error

Python on_stop failed: {e}

Error message

Python on_stop failed: {e}

What it means

This error wraps any Python exception raised by the strategy's user-implemented `on_stop()` callback. The Rust `DataActor::on_stop` dispatches to the Python instance via `call_method0(py, "on_stop")` (crates/trading/src/python/strategy.rs:342-347) and re-wraps any `PyErr` as `anyhow::anyhow!("Python on_stop failed: {e}")`. It indicates cleanup code executed at strategy shutdown failed, which can leave orders, subscriptions, or timers in an unclean state.

Source

Thrown at crates/trading/src/python/strategy.rs:1066

    fn on_position_changed(&mut self, event: PositionChanged) {
        let _ = self.dispatch_on_position_changed(event);
    }

    fn on_position_closed(&mut self, event: PositionClosed) {
        let _ = self.dispatch_on_position_closed(event);
    }
}

impl DataActor for PyStrategyInner {
    fn on_start(&mut self) -> anyhow::Result<()> {
        Strategy::on_start(self)?;
        self.dispatch_on_start()
            .map_err(|e| anyhow::anyhow!("Python on_start failed: {e}"))
    }

    fn on_stop(&mut self) -> anyhow::Result<()> {
        self.dispatch_on_stop()
            .map_err(|e| anyhow::anyhow!("Python on_stop failed: {e}"))
    }

    fn on_resume(&mut self) -> anyhow::Result<()> {
        self.dispatch_on_resume()
            .map_err(|e| anyhow::anyhow!("Python on_resume failed: {e}"))
    }

    fn on_reset(&mut self) -> anyhow::Result<()> {
        self.dispatch_on_reset()
            .map_err(|e| anyhow::anyhow!("Python on_reset failed: {e}"))
    }

    fn on_dispose(&mut self) -> anyhow::Result<()> {
        self.dispatch_on_dispose()
            .map_err(|e| anyhow::anyhow!("Python on_dispose failed: {e}"))
    }

    fn on_degrade(&mut self) -> anyhow::Result<()> {

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Read the chained Python traceback to find the raising line in `on_stop`.
  2. Guard each cleanup step in `on_stop` with `try/except` so one failing step doesn't abort the rest of shutdown.
  3. Check client/adapter connectivity before calling cancel/close/unsubscribe operations in `on_stop`.
  4. Make `on_stop` idempotent so repeated stop calls or already-released resources don't raise.

Example fix

// before (strategy.py)
def on_stop(self):
    self.cancel_all_orders(self.instrument.id)  # raises if client disconnected

// after
def on_stop(self):
    try:
        self.cancel_all_orders(self.instrument.id)
    except Exception as e:
        self.log.error(f"Failed to cancel orders on stop: {e}")
Defensive patterns

Strategy: try-catch

Try / catch

def on_stop(self):
    for step in (self._cancel_open_orders, self._unsubscribe_all):
        try:
            step()
        except Exception as e:
            self.log.exception(f"on_stop step {step.__name__} failed: {e}")

Prevention

When it happens

Trigger: Calling `strategy.stop()` (or stopping the trader node / Ctrl+C shutdown) when the Python subclass's `on_stop()` raises: e.g. canceling orders through a disconnected client, accessing actors/cache state already torn down, or any bug in user cleanup code.

Common situations: Shutdown-time races (venue/adapter already disconnected when cancel-orders runs), NameError/AttributeError in cleanup logic added after initial development, or double-stop flows where state was already reset.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/45d8cdbd751d0a0c. Report an issue: GitHub.