Files
freqtrade/tests/exchange/test_krakenfutures.py
T

991 lines
36 KiB
Python

"""Tests for Kraken Futures exchange class"""
from __future__ import annotations
from copy import deepcopy
from datetime import UTC, datetime
from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (
DDosProtection,
ExchangeError,
InvalidOrderException,
OperationalException,
TemporaryError,
)
from freqtrade.exchange.krakenfutures import Krakenfutures
from tests.conftest import EXMS, get_patched_exchange
ExchangeBase = Krakenfutures.__mro__[1] # freqtrade.exchange.exchange.Exchange
# --- _ft_has and OHLCV tests ---
def test_krakenfutures_ft_has_overrides():
"""Test that _ft_has contains Kraken Futures stoploss settings."""
ft_has = Krakenfutures._ft_has
assert ft_has["stoploss_on_exchange"] is True
assert ft_has["stoploss_order_types"] == {"limit": "limit", "market": "market"}
assert ft_has["stoploss_query_requires_stop_flag"] is True
assert ft_has["stop_price_param"] == "triggerPrice"
assert ft_has["stop_price_type_field"] == "triggerSignal"
def test_krakenfutures_ohlcv_candle_limit_uses_ccxt_limit(mocker, default_conf):
"""Test that OHLCV candle limit follows CCXT feature limit."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
assert isinstance(ex, Krakenfutures)
mocker.patch.object(ex, "features", return_value=2000)
assert ex.ohlcv_candle_limit("1m", candle_type=CandleType.FUTURES) == 2000
# --- _order_contracts_to_amount trigger price fix tests ---
def test_krakenfutures_order_contracts_fixes_missing_trigger_price(mocker, default_conf):
"""Extract triggerPrice from info.order.priceTriggerOptions when CCXT misses it."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"triggerPrice": None,
"stopPrice": None,
"info": {
"order": {
"type": "TRIGGER_ORDER",
"priceTriggerOptions": {
"triggerPrice": 71641,
"triggerSignal": "LAST_PRICE",
},
},
"status": "TRIGGER_PLACED",
},
}
result = ex._order_contracts_to_amount(order)
assert result["triggerPrice"] == 71641.0
assert result["stopPrice"] == 71641.0
def test_krakenfutures_order_contracts_preserves_existing_trigger_price(mocker, default_conf):
"""Don't overwrite triggerPrice when CCXT already parsed it correctly."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"triggerPrice": 70000.0,
"stopPrice": 70000.0,
"info": {
"order": {
"priceTriggerOptions": {
"triggerPrice": 71641,
},
},
},
}
result = ex._order_contracts_to_amount(order)
assert result["triggerPrice"] == 70000.0
assert result["stopPrice"] == 70000.0
def test_krakenfutures_order_contracts_no_trigger_options(mocker, default_conf):
"""Regular (non-trigger) orders should pass through unchanged."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"triggerPrice": None,
"stopPrice": None,
"info": {
"order": {
"type": "lmt",
"orderId": "abc",
},
"status": "placed",
},
}
result = ex._order_contracts_to_amount(order)
assert result["triggerPrice"] is None
assert result["stopPrice"] is None
# --- _adjust_krakenfutures_order average price tests ---
def test_krakenfutures_adjust_order_computes_average_from_trades(mocker, default_conf):
"""Compute VWAP average price from trades when CCXT returns None."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": 0.0004,
"average": None,
"timestamp": 1771354195241,
}
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,
},
]
mocker.patch.object(ex, "get_trades_for_order", return_value=trades)
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):
"""Don't fetch trades for open orders."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "open",
"filled": 0,
"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_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 = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": 0.0004,
"average": 67843.0,
"timestamp": 1771354195241,
}
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"] == 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):
"""Leave average as None when no trades are found."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
order = {
"id": "abc",
"symbol": "BTC/USD:USD",
"status": "closed",
"filled": 0.0004,
"average": None,
"timestamp": 1771354195241,
}
mocker.patch.object(ex, "get_trades_for_order", return_value=[])
result = ex._adjust_krakenfutures_order(order)
assert result["average"] is None
# --- get_trades_for_order fee enrichment tests ---
def test_krakenfutures_get_trades_enriches_fees(mocker, default_conf):
"""Calculate fees from market fee schedule when CCXT returns fee: None."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
raw_trades = [
{
"amount": 0.0004,
"price": 67843.0,
"cost": 27.14,
"order": "abc",
"symbol": "BTC/USD:USD",
"takerOrMaker": "taker",
"fee": {"cost": None, "currency": None},
},
]
mocker.patch.object(
ExchangeBase,
"get_trades_for_order",
return_value=raw_trades,
)
# Re-patch markets property with fee rates for BTC/USD:USD
kf_markets = {"BTC/USD:USD": {"taker": 0.0005, "maker": 0.0002, "quote": "USD"}}
mocker.patch.object(type(ex), "markets", PropertyMock(return_value=kf_markets))
result = ex.get_trades_for_order("abc", "BTC/USD:USD", since=MagicMock())
assert len(result) == 1
assert result[0]["fee"]["cost"] == pytest.approx(27.14 * 0.0005)
assert result[0]["fee"]["currency"] == "USD"
assert result[0]["fee"]["rate"] == 0.0005
def test_krakenfutures_get_trades_uses_maker_rate(mocker, default_conf):
"""Use maker fee rate when fillType is maker."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
raw_trades = [
{
"amount": 0.0004,
"price": 67843.0,
"cost": 27.14,
"order": "abc",
"symbol": "BTC/USD:USD",
"takerOrMaker": "maker",
"fee": None,
},
]
mocker.patch.object(
ExchangeBase,
"get_trades_for_order",
return_value=raw_trades,
)
kf_markets = {"BTC/USD:USD": {"taker": 0.0005, "maker": 0.0002, "quote": "USD"}}
mocker.patch.object(type(ex), "markets", PropertyMock(return_value=kf_markets))
result = ex.get_trades_for_order("abc", "BTC/USD:USD", since=MagicMock())
assert result[0]["fee"]["cost"] == pytest.approx(27.14 * 0.0002)
assert result[0]["fee"]["rate"] == 0.0002
def test_krakenfutures_get_trades_preserves_existing_fees(mocker, default_conf):
"""Don't overwrite fees if CCXT already provided them."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
existing_fee = {"cost": 0.01, "currency": "USD", "rate": 0.0005}
raw_trades = [
{
"amount": 0.0004,
"price": 67843.0,
"cost": 27.14,
"order": "abc",
"symbol": "BTC/USD:USD",
"takerOrMaker": "taker",
"fee": existing_fee,
},
]
mocker.patch.object(
ExchangeBase,
"get_trades_for_order",
return_value=raw_trades,
)
kf_markets = {"BTC/USD:USD": {"taker": 0.0005, "maker": 0.0002, "quote": "USD"}}
mocker.patch.object(type(ex), "markets", PropertyMock(return_value=kf_markets))
result = ex.get_trades_for_order("abc", "BTC/USD:USD", since=MagicMock())
# Should keep existing fee, not recalculate
assert result[0]["fee"] == existing_fee
# --- fetch_order fallback tests ---
def test_krakenfutures_fetch_order_falls_back_to_closed_orders(mocker, default_conf):
"""Fallback to fetch_closed_orders when fetch_order can't find the order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(
ex._api,
"fetch_closed_orders",
return_value=[{"id": "abc", "symbol": "BTC/USD:USD", "status": "closed"}],
create=True,
)
closed_fetch.__name__ = "fetch_closed_orders"
res = ex.fetch_order("abc", "BTC/USD:USD")
assert res["id"] == "abc"
def test_krakenfutures_fetch_order_falls_back_to_canceled_orders(mocker, default_conf):
"""Fallback to fetch_canceled_orders when closed orders don't contain the order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.ExchangeError("UUID too large"))
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api,
"fetch_canceled_orders",
return_value=[{"id": "def", "symbol": "BTC/USD:USD", "status": "canceled"}],
create=True,
)
canceled_fetch.__name__ = "fetch_canceled_orders"
res = ex.fetch_order("def", "BTC/USD:USD")
assert res["id"] == "def"
def test_krakenfutures_fetch_order_returns_direct_ccxt_result(mocker, default_conf):
"""Use direct CCXT fetch_order result when available."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
ccxt_order = {"id": "live-123", "symbol": "BTC/USD:USD", "status": "open"}
converted = {"id": "live-123", "status": "open"}
mocker.patch.object(ex._api, "fetch_order", return_value=ccxt_order)
converter = mocker.patch.object(ex, "_order_contracts_to_amount", return_value=converted)
fallback = mocker.patch.object(ex, "_fetch_order_fallback")
res = ex.fetch_order("live-123", "BTC/USD:USD")
assert res == converted
converter.assert_called_once_with(ccxt_order)
fallback.assert_not_called()
def test_krakenfutures_fetch_order_strips_stop_from_status_query(mocker, default_conf):
"""Direct CCXT fetch_order status lookup should not receive stop/trigger params."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
ccxt_order = {"id": "order-1", "symbol": "BTC/USD:USD", "status": "open"}
fetch_order = mocker.patch.object(ex._api, "fetch_order", return_value=ccxt_order)
# Simulate call from base fetch_stoploss_order which adds stop=True
ex.fetch_order("order-1", "BTC/USD:USD", params={"stop": True})
# stop should be stripped from the direct CCXT status call
fetch_order.assert_called_once_with("order-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")
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(InvalidOrderException, match="Order not found in any endpoint"):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_invalid_order_maps_exception(mocker, default_conf):
"""Map ccxt.InvalidOrder to InvalidOrderException."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.InvalidOrder("bad order"))
with pytest.raises(InvalidOrderException, match="bad order"):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_ddos_maps_exception(mocker, default_conf):
"""Map ccxt.DDoSProtection to DDosProtection."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.DDoSProtection("ratelimit"))
with pytest.raises(DDosProtection):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_baseerror_maps_exception(mocker, default_conf):
"""Map generic ccxt.BaseError to OperationalException."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.BaseError("unexpected"))
with pytest.raises(OperationalException):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_fallback_returns_none(mocker, default_conf):
"""Return None when order is not found in any endpoint."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api, "fetch_canceled_orders", return_value=[], create=True
)
canceled_fetch.__name__ = "fetch_canceled_orders"
res = ex._fetch_order_fallback("abc", "BTC/USD:USD", {})
assert res is None
def test_krakenfutures_fetch_order_fallback_returns_open_order_first(mocker, default_conf):
"""Return immediately when order is found in open orders."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(
ex._api,
"fetch_open_orders",
return_value=[{"id": "abc", "symbol": "BTC/USD:USD", "status": "open"}],
create=True,
)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api, "fetch_canceled_orders", return_value=[], create=True
)
canceled_fetch.__name__ = "fetch_canceled_orders"
res = ex._fetch_order_fallback("abc", "BTC/USD:USD", {})
assert res is not None
assert res["id"] == "abc"
open_fetch.assert_called_once()
closed_fetch.assert_not_called()
canceled_fetch.assert_not_called()
def test_krakenfutures_fetch_order_dry_run(mocker, default_conf):
"""Test fetch_order uses dry_run order in dry_run mode."""
conf = dict(default_conf)
conf["dry_run"] = True
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
dry_order = {"id": "dry-123", "status": "open"}
mocker.patch.object(ex, "fetch_dry_run_order", return_value=dry_order)
res = ex.fetch_order("dry-123", "BTC/USD:USD")
assert res["id"] == "dry-123"
def test_krakenfutures_fetch_order_finds_stoploss_via_stop_param(mocker, default_conf):
"""Test fetch_order finds stoploss orders via closed orders fallback with stop=True."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
# With stop=True, CCXT queries trigger history endpoint
closed_fetch = mocker.patch.object(
ex._api,
"fetch_closed_orders",
return_value=[
{"id": "trigger-123", "symbol": "BTC/USD:USD", "status": "closed"},
],
create=True,
)
closed_fetch.__name__ = "fetch_closed_orders"
# Simulate what base class fetch_stoploss_order does (adds stop=True)
res = ex.fetch_order("trigger-123", "BTC/USD:USD", params={"stop": True})
assert res["id"] == "trigger-123"
def test_krakenfutures_fetch_order_fallback_passes_stop_to_history(mocker, default_conf):
"""Stoploss query (stop=True) should pass through to closed/canceled endpoints."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_order = {"id": "sl-123", "symbol": "BTC/USD:USD", "status": "closed"}
closed_fetch = mocker.patch.object(
ex._api,
"fetch_closed_orders",
return_value=[closed_order],
create=True,
)
closed_fetch.__name__ = "fetch_closed_orders"
res = ex._fetch_order_fallback("sl-123", "BTC/USD:USD", {"stop": True})
assert res is not None
assert res["id"] == "sl-123"
# Verify stop=True was passed to closed orders (CCXT maps stop→trigger)
closed_fetch.assert_called_once_with("BTC/USD:USD", params={"stop": True})
def test_krakenfutures_fetch_order_fallback_strips_stop_from_open_orders(mocker, default_conf):
"""Open orders query should not receive stop/trigger flags."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
open_fetch = mocker.patch.object(ex._api, "fetch_open_orders", return_value=[], create=True)
open_fetch.__name__ = "fetch_open_orders"
closed_fetch = mocker.patch.object(ex._api, "fetch_closed_orders", return_value=[], create=True)
closed_fetch.__name__ = "fetch_closed_orders"
canceled_fetch = mocker.patch.object(
ex._api, "fetch_canceled_orders", return_value=[], create=True
)
canceled_fetch.__name__ = "fetch_canceled_orders"
ex._fetch_order_fallback("abc", "BTC/USD:USD", {"stop": True})
# stop should be stripped from open orders call
open_fetch.assert_called_once_with(None, params={})
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)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.OrderNotFound("not found"))
open_fetch = mocker.patch.object(
ex._api, "fetch_open_orders", side_effect=ccxt.ExchangeError("service unavailable")
)
open_fetch.__name__ = "fetch_open_orders"
with pytest.raises(TemporaryError):
ex.fetch_order("abc", "BTC/USD:USD", count=0)
def test_krakenfutures_fetch_order_exchangeerror_uses_fallback(mocker, default_conf):
"""ExchangeError from fetch_order should trigger fallback lookup."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
fallback_order = {"id": "abc", "symbol": "BTC/USD:USD", "status": "closed"}
mocker.patch.object(ex._api, "fetch_order", side_effect=ccxt.ExchangeError("temporary"))
fallback = mocker.patch.object(ex, "_fetch_order_fallback", return_value=fallback_order)
result = ex.fetch_order("abc", "BTC/USD:USD", count=0)
assert result == fallback_order
fallback.assert_called_once_with("abc", "BTC/USD:USD", {})
def test_krakenfutures_find_order_in_list_handles_ordernotfound(mocker, default_conf):
"""OrderNotFound in list fetch is treated as a missing order."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_order_not_found(_symbol, params=None):
raise ccxt.OrderNotFound("missing")
assert ex._find_order_in_list(raise_order_not_found, "BTC/USD:USD", {}, "abc") is None
def test_krakenfutures_find_order_in_list_maps_ddos(mocker, default_conf):
"""DDoS errors from list fetch are mapped to DDosProtection."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_ddos(_symbol, params=None):
raise ccxt.DDoSProtection("ratelimit")
with pytest.raises(DDosProtection):
ex._find_order_in_list(raise_ddos, "BTC/USD:USD", {}, "abc")
def test_krakenfutures_find_order_in_list_maps_temporary(mocker, default_conf):
"""OperationFailed/ExchangeError from list fetch map to TemporaryError."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_temp(_symbol, params=None):
raise ccxt.OperationFailed("temporary")
with pytest.raises(TemporaryError):
ex._find_order_in_list(raise_temp, "BTC/USD:USD", {}, "abc")
def test_krakenfutures_find_order_in_list_maps_operational(mocker, default_conf):
"""Unexpected BaseError from list fetch maps to OperationalException."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
def raise_base(_symbol, params=None):
raise ccxt.BaseError("unexpected")
with pytest.raises(OperationalException):
ex._find_order_in_list(raise_base, "BTC/USD:USD", {}, "abc")
# --- Stoploss tests ---
def test_krakenfutures_create_stoploss_uses_trigger_price_type(mocker, default_conf):
"""Test create_stoploss uses triggerPrice, triggerSignal, and reduceOnly."""
api_mock = MagicMock()
api_mock.create_order = MagicMock(return_value={"id": "order-id", "info": {"foo": "bar"}})
conf = deepcopy(default_conf)
conf["dry_run"] = False
conf["trading_mode"] = TradingMode.FUTURES
conf["margin_mode"] = MarginMode.ISOLATED
mocker.patch(f"{EXMS}.amount_to_precision", lambda s, x, y: y)
mocker.patch(f"{EXMS}.price_to_precision", lambda s, x, y, **kwargs: y)
ex = get_patched_exchange(mocker, conf, api_mock, exchange="krakenfutures")
ex.create_stoploss(
pair="ETH/BTC",
amount=1,
stop_price=90000.0,
side="sell",
order_types={"stoploss": "market", "stoploss_price_type": "mark"},
leverage=1.0,
)
call_args = api_mock.create_order.call_args
params = call_args[1].get("params") if call_args[1] else call_args[0][5]
assert params["triggerPrice"] == 90000.0
assert params["triggerSignal"] == "mark"
assert params["reduceOnly"] is True
# --- Funding fees tests ---
def test_krakenfutures_get_funding_fees_futures_success(mocker, default_conf):
"""Use funding fee helper in futures mode."""
conf = dict(default_conf)
conf["trading_mode"] = TradingMode.FUTURES
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
helper = mocker.patch.object(ex, "_fetch_and_calculate_funding_fees", return_value=1.23)
open_date = datetime.now(UTC)
assert ex.get_funding_fees("BTC/USD:USD", 0.1, False, open_date) == 1.23
helper.assert_called_once_with("BTC/USD:USD", 0.1, False, open_date)
def test_krakenfutures_get_funding_fees_futures_exchange_error(mocker, default_conf):
"""Return 0.0 when funding fee retrieval fails."""
conf = dict(default_conf)
conf["trading_mode"] = TradingMode.FUTURES
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
mocker.patch.object(ex, "_fetch_and_calculate_funding_fees", side_effect=ExchangeError("fail"))
assert ex.get_funding_fees("BTC/USD:USD", 0.1, False, None) == 0.0
def test_krakenfutures_get_funding_fees_spot_returns_zero(mocker, default_conf):
"""Return 0.0 outside futures mode without calling the helper."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
helper = mocker.patch.object(ex, "_fetch_and_calculate_funding_fees")
assert ex.get_funding_fees("BTC/USD:USD", 0.1, False, None) == 0.0
helper.assert_not_called()
# --- Balance tests (flex account USD synthesis) ---
def test_krakenfutures_get_balances_flex_account_synthesizes_usd(mocker, default_conf):
"""Test that flex account availableMargin/portfolioValue are synthesized as USD balance."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"EUR": {"free": 100.0, "used": 0.0, "total": 100.0},
"info": {
"accounts": {
"flex": {
"availableMargin": "950.50",
"marginEquity": "1000.00",
"portfolioValue": "1050.00", # Should be ignored, marginEquity preferred
"currencies": {"EUR": {"quantity": "100", "value": "105.00"}},
}
}
},
"free": {"EUR": 100.0},
"used": {"EUR": 0.0},
"total": {"EUR": 100.0},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
# USD should be synthesized from flex account
assert "USD" in balances
assert balances["USD"]["free"] == 950.50
assert balances["USD"]["total"] == 1000.00
# used = total - free = 1000.00 - 950.50 = 49.50
assert balances["USD"]["used"] == 49.50
# EUR should still be present
assert "EUR" in balances
# info, free, total, used dicts should be removed
assert "info" not in balances
assert "free" not in balances
assert "total" not in balances
assert "used" not in balances
def test_krakenfutures_get_balances_no_flex_account(mocker, default_conf):
"""Test that non-flex accounts work without USD synthesis."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
standard_response = {
"USD": {"free": 500.0, "used": 100.0, "total": 600.0},
"info": {"type": "cashAccount"},
"free": {"USD": 500.0},
"used": {"USD": 100.0},
"total": {"USD": 600.0},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=standard_response)
balances = ex.get_balances()
# USD should be preserved as-is
assert balances["USD"]["free"] == 500.0
assert balances["USD"]["total"] == 600.0
# info, free, total, used dicts should be removed
assert "info" not in balances
def test_krakenfutures_get_balances_flex_fallback_chain(mocker, default_conf):
"""Test fallback chain: marginEquity -> portfolioValue -> balanceValue."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
# Test fallback to balanceValue (no marginEquity or portfolioValue)
flex_response = {
"info": {
"accounts": {
"flex": {
"availableMargin": "800.00",
"balanceValue": "850.00",
}
}
},
"free": {},
"used": {},
"total": {},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
assert balances["USD"]["free"] == 800.00
assert balances["USD"]["total"] == 850.00
# used = total - free = 850.00 - 800.00 = 50.00
assert balances["USD"]["used"] == 50.00
def test_krakenfutures_get_balances_flex_zero_free_calculates_used(mocker, default_conf):
"""Test used margin is correct when availableMargin is 0.0."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"info": {
"accounts": {
"flex": {
"availableMargin": "0.00",
"marginEquity": "125.00",
}
}
},
"free": {},
"used": {},
"total": {},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
assert balances["USD"]["free"] == 0.00
assert balances["USD"]["total"] == 125.00
assert balances["USD"]["used"] == 125.00
def test_krakenfutures_get_balances_flex_missing_free_uses_total(mocker, default_conf):
"""When availableMargin is missing, free falls back to total and used is 0.0."""
default_conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"info": {
"accounts": {
"flex": {
"marginEquity": "250.00",
}
}
},
"free": {},
"used": {},
"total": {},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
assert balances["USD"]["free"] == 250.00
assert balances["USD"]["total"] == 250.00
assert balances["USD"]["used"] == 0.00
def test_krakenfutures_get_balances_skips_synthesis_for_non_usd_stake(mocker, default_conf):
"""Test that USD synthesis is skipped when stake_currency is not USD."""
default_conf["stake_currency"] = "EUR"
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
flex_response = {
"EUR": {"free": 100.0, "used": 0.0, "total": 100.0},
"info": {
"accounts": {
"flex": {
"availableMargin": "950.50",
"portfolioValue": "1000.00",
}
}
},
"free": {"EUR": 100.0},
"used": {"EUR": 0.0},
"total": {"EUR": 100.0},
}
mocker.patch.object(ex._api, "fetch_balance", return_value=flex_response)
balances = ex.get_balances()
# USD should NOT be synthesized since stake_currency is EUR
assert "USD" not in balances
# EUR should still be present
assert "EUR" in balances
assert balances["EUR"]["free"] == 100.0
def test_krakenfutures_get_balances_maps_ddos(mocker, default_conf):
"""Map ccxt.DDoSProtection from fetch_balance to DDosProtection."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=ccxt.DDoSProtection("ratelimit"))
with pytest.raises(DDosProtection):
ex.get_balances(count=0)
def test_krakenfutures_get_balances_maps_temporary(mocker, default_conf):
"""Map ccxt.OperationFailed/ExchangeError from fetch_balance to TemporaryError."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=ccxt.OperationFailed("temporary"))
with pytest.raises(TemporaryError):
ex.get_balances(count=0)
def test_krakenfutures_get_balances_maps_operational(mocker, default_conf):
"""Map unexpected ccxt.BaseError from fetch_balance to OperationalException."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=ccxt.BaseError("unexpected"))
with pytest.raises(OperationalException):
ex.get_balances(count=0)
def test_krakenfutures_safe_float():
"""Test _safe_float handles various input types."""
assert Krakenfutures._safe_float("123.45") == 123.45
assert Krakenfutures._safe_float(100) == 100.0
assert Krakenfutures._safe_float(None) is None
assert Krakenfutures._safe_float("invalid") is None
assert Krakenfutures._safe_float({}) is None
# --- Stoploss via base class (stoploss_query_requires_stop_flag) ---
def test_krakenfutures_fetch_stoploss_order_uses_base_class(mocker, default_conf):
"""Base class fetch_stoploss_order should add stop=True and delegate to fetch_order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
expected_order = {"id": "sl-order-1", "status": "open", "info": {}}
fetch_order = mocker.patch.object(ex, "fetch_order", return_value=expected_order)
result = ex.fetch_stoploss_order("sl-order-1", "BTC/USD:USD")
assert result["id"] == "sl-order-1"
# Base class should pass stop=True
fetch_order.assert_called_once()
call_params = fetch_order.call_args[0][2] if len(fetch_order.call_args[0]) > 2 else {}
assert call_params.get("stop") is True
def test_krakenfutures_cancel_stoploss_order_uses_base_class(mocker, default_conf):
"""Base class cancel_stoploss_order should add stop=True and delegate to cancel_order."""
conf = dict(default_conf)
conf["dry_run"] = False
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
expected_order = {"id": "sl-cancel-1", "status": "canceled"}
cancel_order = mocker.patch.object(ex, "cancel_order", return_value=expected_order)
result = ex.cancel_stoploss_order("sl-cancel-1", "BTC/USD:USD")
assert result["id"] == "sl-cancel-1"
# Base class should pass stop=True
cancel_order.assert_called_once()
call_params = cancel_order.call_args[0][2] if len(cancel_order.call_args[0]) > 2 else {}
assert call_params.get("stop") is True