From bfbae5d04033b157a7a876590e53fd972c1c1408 Mon Sep 17 00:00:00 2001 From: matstedt Date: Sat, 7 Feb 2026 15:44:48 +0100 Subject: [PATCH] fix(krakenfutures): re-introduce improved stoploss lookup for trigger orders --- freqtrade/exchange/krakenfutures.py | 23 ++++++---- tests/exchange/test_krakenfutures.py | 63 ++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/krakenfutures.py b/freqtrade/exchange/krakenfutures.py index c2e92eebe..41fbce19d 100644 --- a/freqtrade/exchange/krakenfutures.py +++ b/freqtrade/exchange/krakenfutures.py @@ -11,7 +11,6 @@ from freqtrade.exceptions import ( ExchangeError, InvalidOrderException, OperationalException, - RetryableOrderError, TemporaryError, ) from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier @@ -145,8 +144,9 @@ class Krakenfutures(Exchange): return self.fetch_dry_run_order(order_id) params = params or {} + status_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")} try: - order = self._api.fetch_order(order_id, pair, params=params) + order = self._api.fetch_order(order_id, pair, params=status_params) self._log_exchange_response("fetch_order", order) return self._order_contracts_to_amount(order) except ccxt.OrderNotFound: @@ -167,7 +167,11 @@ class Krakenfutures(Exchange): if order is not None: return order - raise RetryableOrderError(f"Order not found in any endpoint (pair: {pair} id: {order_id})") + # Order not in status, open, closed, or canceled endpoints - genuinely gone. + # Raise non-retrying InvalidOrderException (Kraken has limited history retention). + raise InvalidOrderException( + f"Order not found in any endpoint (pair: {pair} id: {order_id})" + ) def _fetch_order_fallback( self, order_id: str, pair: str, params: dict[str, Any] @@ -175,8 +179,12 @@ class Krakenfutures(Exchange): """Search open, closed, and canceled order endpoints for order_id.""" order_id_str = str(order_id) - # Open orders: Kraken returns all symbols and includes triggers by default. - order = self._find_order_in_list(self._api.fetch_open_orders, None, params, order_id_str) + # Open orders include triggers by default. Avoid passing trigger flags here + # to prevent endpoint/filter mismatches. + open_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")} + order = self._find_order_in_list( + self._api.fetch_open_orders, None, open_params, order_id_str + ) if order is not None: return order @@ -205,8 +213,9 @@ class Krakenfutures(Exchange): """Fetch orders and return matching order_id, or None.""" try: for order in fetch_fn(symbol, params=params) or []: - if str(order.get("id")) == order_id_str: - return self._order_contracts_to_amount(order) + fixed_order = self._fix_trigger_order_id(order) + if str(fixed_order.get("id")) == order_id_str: + return self._order_contracts_to_amount(fixed_order) except (ccxt.OrderNotFound, ccxt.InvalidOrder) as e: logger.debug(f"{fetch_fn.__name__} failed: {e}") return None diff --git a/tests/exchange/test_krakenfutures.py b/tests/exchange/test_krakenfutures.py index 0501bcebf..356598cc0 100644 --- a/tests/exchange/test_krakenfutures.py +++ b/tests/exchange/test_krakenfutures.py @@ -15,7 +15,6 @@ from freqtrade.exceptions import ( ExchangeError, InvalidOrderException, OperationalException, - RetryableOrderError, TemporaryError, ) from freqtrade.exchange.krakenfutures import Krakenfutures @@ -105,8 +104,23 @@ def test_krakenfutures_fetch_order_returns_direct_ccxt_result(mocker, default_co fallback.assert_not_called() -def test_krakenfutures_fetch_order_reraises_when_no_fallback(mocker, default_conf): - """Re-raise when fallback cannot locate the order.""" +def test_krakenfutures_fetch_stoploss_order_strips_trigger_from_status_query(mocker, default_conf): + """Direct fetch_order status lookup should not receive trigger params.""" + conf = dict(default_conf) + conf["dry_run"] = False + ex = get_patched_exchange(mocker, conf, exchange="krakenfutures") + + ccxt_order = {"id": "trigger-raw-1", "symbol": "BTC/USD:USD", "status": "open"} + fetch_order = mocker.patch.object(ex._api, "fetch_order", return_value=ccxt_order) + + res = ex.fetch_stoploss_order("trigger-raw-1", "BTC/USD:USD") + + assert res["id"] == "trigger-raw-1" + fetch_order.assert_called_once_with("trigger-raw-1", "BTC/USD:USD", params={}) + + +def test_krakenfutures_fetch_order_raises_invalid_when_not_found(mocker, default_conf): + """Raise InvalidOrderException (non-retrying) when order is not in any endpoint.""" conf = dict(default_conf) conf["dry_run"] = False ex = get_patched_exchange(mocker, conf, exchange="krakenfutures") @@ -114,7 +128,7 @@ def test_krakenfutures_fetch_order_reraises_when_no_fallback(mocker, default_con mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found")) mocker.patch.object(ex, "_fetch_order_fallback", return_value=None) - with pytest.raises(RetryableOrderError): + with pytest.raises(InvalidOrderException, match="Order not found in any endpoint"): ex.fetch_order("abc", "BTC/USD:USD", count=0) @@ -222,6 +236,47 @@ def test_krakenfutures_fetch_order_finds_trigger_order(mocker, default_conf): assert res["id"] == "trigger-123" +def test_krakenfutures_fetch_stoploss_order_prefers_open_orders_without_trigger_param( + mocker, default_conf +): + """Stoploss lookup should query open orders without trigger flags and match nested orderId.""" + conf = dict(default_conf) + conf["dry_run"] = False + ex = get_patched_exchange(mocker, conf, exchange="krakenfutures") + + trigger_id = "trigger-open-123" + + mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found")) + + def fetch_open(symbol, params=None): + assert symbol is None + assert params == {} + return [ + { + "id": None, + "symbol": "BTC/USD:USD", + "status": "open", + "info": {"order": {"orderId": trigger_id}}, + } + ] + + open_fetch = mocker.patch.object( + ex._api, "fetch_open_orders", side_effect=fetch_open, create=True + ) + closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True) + canceled_fetch = mocker.patch.object( + ex._api, "fetch_canceled_orders", return_value=[], create=True + ) + + res = ex.fetch_stoploss_order(trigger_id, "BTC/USD:USD") + + assert res["id"] == trigger_id + assert res["status"] == "open" + open_fetch.assert_called_once() + closed_fetch.assert_not_called() + canceled_fetch.assert_not_called() + + def test_krakenfutures_fetch_order_propagates_exchange_errors_from_fallback(mocker, default_conf): """Fallback list fetch should not hide exchange-level failures.""" conf = dict(default_conf)