diff --git a/freqtrade/exchange/krakenfutures.py b/freqtrade/exchange/krakenfutures.py index 971fbe8cb..16e9b6ac4 100644 --- a/freqtrade/exchange/krakenfutures.py +++ b/freqtrade/exchange/krakenfutures.py @@ -17,7 +17,7 @@ from freqtrade.exceptions import ( from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.exchange_types import CcxtBalances, CcxtOrder, FtHas -from freqtrade.misc import safe_value_fallback, safe_value_nested +from freqtrade.misc import safe_value_nested from freqtrade.util.datetime_helpers import dt_from_ts @@ -156,14 +156,13 @@ class Krakenfutures(Exchange): Kraken Futures' /orders/status endpoint does not include execution data, so CCXT sets price/average to the limitPrice (the order's limit, not the - actual fill price). For closed/filled orders we fetch trades from /fills - and compute the VWAP average. + actual fill price). For closed/filled orders we ALWAYS fetch trades and + compute VWAP because CCXT's average field is unreliable. + + See: https://github.com/ccxt/ccxt/issues/27979 """ - if ( - order.get("average") is None - and order.get("status") in ("canceled", "closed") - and safe_value_fallback(order, "filled", default_value=0) > 0 - ): + filled = self._safe_float(order.get("filled")) or 0.0 + if order.get("status") in ("canceled", "closed") and filled > 0: trades = self.get_trades_for_order( order["id"], order["symbol"], since=dt_from_ts(order["timestamp"]) ) @@ -171,6 +170,9 @@ class Krakenfutures(Exchange): total_amount = sum(t["amount"] for t in trades) if total_amount: order["average"] = sum(t["price"] * t["amount"] for t in trades) / total_amount + trade_costs = [t["cost"] for t in trades if t.get("cost") is not None] + if trade_costs: + order["cost"] = sum(trade_costs) return order def get_trades_for_order( diff --git a/tests/exchange/test_krakenfutures.py b/tests/exchange/test_krakenfutures.py index 472c9a798..efdad60a3 100644 --- a/tests/exchange/test_krakenfutures.py +++ b/tests/exchange/test_krakenfutures.py @@ -153,6 +153,7 @@ def test_krakenfutures_adjust_order_computes_average_from_trades(mocker, default result = ex._adjust_krakenfutures_order(order) assert result["average"] == pytest.approx(67850.0) + assert result["cost"] == pytest.approx(27.14) def test_krakenfutures_adjust_order_skips_open_orders(mocker, default_conf): @@ -174,8 +175,27 @@ def test_krakenfutures_adjust_order_skips_open_orders(mocker, default_conf): trades_mock.assert_not_called() -def test_krakenfutures_adjust_order_preserves_existing_average(mocker, default_conf): - """Don't overwrite average when already present.""" +def test_krakenfutures_adjust_order_handles_none_filled(mocker, default_conf): + """Don't crash or fetch trades when filled is None.""" + ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures") + + order = { + "id": "abc", + "symbol": "BTC/USD:USD", + "status": "closed", + "filled": None, + "average": None, + "timestamp": 1771354195241, + } + trades_mock = mocker.patch.object(ex, "get_trades_for_order") + + result = ex._adjust_krakenfutures_order(order) + assert result["average"] is None + trades_mock.assert_not_called() + + +def test_krakenfutures_adjust_order_recomputes_existing_average(mocker, default_conf): + """Recompute average from fills even when CCXT already provided one.""" ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures") order = { @@ -186,11 +206,30 @@ def test_krakenfutures_adjust_order_preserves_existing_average(mocker, default_c "average": 67843.0, "timestamp": 1771354195241, } - trades_mock = mocker.patch.object(ex, "get_trades_for_order") + trades = [ + { + "amount": 0.0002, + "price": 67800.0, + "cost": 13.56, + "takerOrMaker": "taker", + "symbol": "BTC/USD:USD", + "fee": None, + }, + { + "amount": 0.0002, + "price": 67900.0, + "cost": 13.58, + "takerOrMaker": "taker", + "symbol": "BTC/USD:USD", + "fee": None, + }, + ] + trades_mock = mocker.patch.object(ex, "get_trades_for_order", return_value=trades) result = ex._adjust_krakenfutures_order(order) - assert result["average"] == 67843.0 - trades_mock.assert_not_called() + assert result["average"] == pytest.approx(67850.0) + assert result["cost"] == pytest.approx(27.14) + trades_mock.assert_called_once() def test_krakenfutures_adjust_order_no_trades_found(mocker, default_conf):