fix(krakenfutures): re-introduce improved stoploss lookup for trigger orders
This commit is contained in:
@@ -11,7 +11,6 @@ from freqtrade.exceptions import (
|
|||||||
ExchangeError,
|
ExchangeError,
|
||||||
InvalidOrderException,
|
InvalidOrderException,
|
||||||
OperationalException,
|
OperationalException,
|
||||||
RetryableOrderError,
|
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier
|
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)
|
return self.fetch_dry_run_order(order_id)
|
||||||
|
|
||||||
params = params or {}
|
params = params or {}
|
||||||
|
status_params = {k: v for k, v in params.items() if k not in ("trigger", "stop")}
|
||||||
try:
|
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)
|
self._log_exchange_response("fetch_order", order)
|
||||||
return self._order_contracts_to_amount(order)
|
return self._order_contracts_to_amount(order)
|
||||||
except ccxt.OrderNotFound:
|
except ccxt.OrderNotFound:
|
||||||
@@ -167,7 +167,11 @@ class Krakenfutures(Exchange):
|
|||||||
if order is not None:
|
if order is not None:
|
||||||
return order
|
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(
|
def _fetch_order_fallback(
|
||||||
self, order_id: str, pair: str, params: dict[str, Any]
|
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."""
|
"""Search open, closed, and canceled order endpoints for order_id."""
|
||||||
order_id_str = str(order_id)
|
order_id_str = str(order_id)
|
||||||
|
|
||||||
# Open orders: Kraken returns all symbols and includes triggers by default.
|
# Open orders include triggers by default. Avoid passing trigger flags here
|
||||||
order = self._find_order_in_list(self._api.fetch_open_orders, None, params, order_id_str)
|
# 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:
|
if order is not None:
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@@ -205,8 +213,9 @@ class Krakenfutures(Exchange):
|
|||||||
"""Fetch orders and return matching order_id, or None."""
|
"""Fetch orders and return matching order_id, or None."""
|
||||||
try:
|
try:
|
||||||
for order in fetch_fn(symbol, params=params) or []:
|
for order in fetch_fn(symbol, params=params) or []:
|
||||||
if str(order.get("id")) == order_id_str:
|
fixed_order = self._fix_trigger_order_id(order)
|
||||||
return self._order_contracts_to_amount(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:
|
except (ccxt.OrderNotFound, ccxt.InvalidOrder) as e:
|
||||||
logger.debug(f"{fetch_fn.__name__} failed: {e}")
|
logger.debug(f"{fetch_fn.__name__} failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from freqtrade.exceptions import (
|
|||||||
ExchangeError,
|
ExchangeError,
|
||||||
InvalidOrderException,
|
InvalidOrderException,
|
||||||
OperationalException,
|
OperationalException,
|
||||||
RetryableOrderError,
|
|
||||||
TemporaryError,
|
TemporaryError,
|
||||||
)
|
)
|
||||||
from freqtrade.exchange.krakenfutures import Krakenfutures
|
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()
|
fallback.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_krakenfutures_fetch_order_reraises_when_no_fallback(mocker, default_conf):
|
def test_krakenfutures_fetch_stoploss_order_strips_trigger_from_status_query(mocker, default_conf):
|
||||||
"""Re-raise when fallback cannot locate the order."""
|
"""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 = dict(default_conf)
|
||||||
conf["dry_run"] = False
|
conf["dry_run"] = False
|
||||||
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
|
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._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
|
||||||
mocker.patch.object(ex, "_fetch_order_fallback", return_value=None)
|
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)
|
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"
|
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):
|
def test_krakenfutures_fetch_order_propagates_exchange_errors_from_fallback(mocker, default_conf):
|
||||||
"""Fallback list fetch should not hide exchange-level failures."""
|
"""Fallback list fetch should not hide exchange-level failures."""
|
||||||
conf = dict(default_conf)
|
conf = dict(default_conf)
|
||||||
|
|||||||
Reference in New Issue
Block a user