freqtrade/freqtrade · error · InvalidOrderException

Could not cancel order. Message: {e}

Error message

Could not cancel order. Message: {e}

What it means

freqtrade wraps ccxt.InvalidOrder from _api.cancel_order() into InvalidOrderException('Could not cancel order'). The exchange refused the cancel because the order is in a terminal state (already filled or canceled) or the id is invalid for that pair. Freqtrade's own cancel handling treats this as 'order effectively gone' in most paths.

Source

Thrown at freqtrade/exchange/exchange.py:1801

    def cancel_order(self, order_id: str, pair: str, params: dict | None = None) -> dict[str, Any]:
        if self._config["dry_run"]:
            try:
                order = self.fetch_dry_run_order(order_id)

                order.update({"status": "canceled", "filled": 0.0, "remaining": order["amount"]})
                return order
            except InvalidOrderException:
                return {}

        if params is None:
            params = {}
        try:
            order = self._api.cancel_order(order_id, pair, params=params)
            self._log_exchange_response("cancel_order", order)
            order = self._order_contracts_to_amount(order)
            return order
        except ccxt.InvalidOrder as e:
            raise InvalidOrderException(f"Could not cancel order. Message: {e}") from e
        except ccxt.DDoSProtection as e:
            raise DDosProtection(e) from e
        except (ccxt.OperationFailed, ccxt.ExchangeError) as e:
            raise TemporaryError(
                f"Could not cancel order due to {e.__class__.__name__}. Message: {e}"
            ) from e
        except ccxt.BaseError as e:
            raise OperationalException(e) from e

    def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict:
        if self.get_option("stoploss_query_requires_stop_flag"):
            params = params or {}
            params["stop"] = True
        return self.cancel_order(order_id, pair, params)

    def is_cancel_order_result_suitable(self, corder) -> TypeGuard[CcxtOrder]:
        if not isinstance(corder, dict):
            return False

View on GitHub (pinned to 1c8edfe4d1)

Solutions

  1. Treat the exception as 'order no longer open': re-fetch the order (fetch_order) to learn its final state (filled/canceled) instead of retrying the cancel.
  2. Use freqtrade's handle_insufficient_funds / cancel-order-then-verify flow: catch InvalidOrderException and return {} (freqtrade's own safe_cancel_order does exactly this).
  3. If racing during volatility, shorten the gap between checking and canceling, or rely on freqtrade's built-in order handling rather than strategy-level cancels.
  4. Clean stale order ids after restarts so the bot never cancels orders it no longer owns.

Example fix

# before
order = exchange.cancel_order(order_id, pair)  # raises if already filled/canceled

# after (mirrors freqtrade's own pattern)
from freqtrade.exceptions import InvalidOrderException
try:
    order = exchange.cancel_order(order_id, pair)
except InvalidOrderException:
    # Order already gone - fetch its final state
    try:
        order = exchange.fetch_order(order_id, pair)
    except Exception:
        order = {"status": "canceled", "id": order_id}
Defensive patterns

Strategy: try-catch

Validate before calling

corder = next((o for o in exchange.fetch_open_orders(pair) if o["id"] == order_id), None)
if corder is None:
    # nothing to cancel - avoid InvalidOrderException
    pass

Type guard

from freqtrade.exceptions import InvalidOrderException

def order_gone_after_cancel_error(e: BaseException) -> bool:
    """InvalidOrderException during cancel means the order is (already) not cancellable."""
    return isinstance(e, InvalidOrderException)

Try / catch

from freqtrade.exceptions import InvalidOrderException

try:
    exchange.cancel_order(order_id, pair)
except InvalidOrderException:
    # already filled or canceled - reconcile state via fetch
    order = exchange.fetch_order(order_id, pair)
    if order.get("status") == "closed":
        ...  # handle the fill instead of the cancel

Prevention

When it happens

Trigger: Calling cancel_order / cancel_stoploss_order for an order that was just filled (race: fill happened between your check and the cancel), an order already canceled manually on the exchange website, or an order id belonging to another pair.

Common situations: High-volatility races where stoploss-on-exchange fills as freqtrade tries to cancel it; user manually cancels orders in the exchange UI while the bot runs; freqtrade restarted with stale order ids in its DB.

Related errors


AI-assisted analysis of freqtrade/freqtrade@1c8edfe4d1 (2026-08-15). Data as JSON: /api/errors/234ebba8d763503b. Report an issue: GitHub.