tests: increase coverage - krakenfutures edge paths and ft_has overrides

pull/12706/head
matstedt 4 weeks ago
parent b4116076d0
commit 282a644c57

@ -3,6 +3,7 @@ from datetime import UTC, datetime, timedelta
from math import isnan, nan
from unittest.mock import MagicMock
import ccxt
import pytest
from ccxt import (
DECIMAL_PLACES,
@ -28,8 +29,14 @@ from freqtrade.exchange import (
timeframe_to_resample_freq,
timeframe_to_seconds,
)
from freqtrade.exchange.check_exchange import check_exchange
from freqtrade.exchange.exchange_utils import _exchange_has_helper
from freqtrade.exchange import exchange_utils as exchange_utils_mod
from freqtrade.exchange.check_exchange import _get_ft_has_overrides, check_exchange
from freqtrade.exchange.exchange_utils import (
_build_exchange_list_entry,
_exchange_has_helper,
validate_exchange,
)
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import log_has_re
@ -426,3 +433,66 @@ def test_exchange__exchange_has_helper():
}
missing = _exchange_has_helper(e_mod.has, required)
assert set(missing) == {"fetchOHLCV", "fetchMyTrades", "fetchOrder"}
def test_validate_exchange_uses_ft_has_overrides(monkeypatch):
class DummyExchange:
def __init__(self) -> None:
self.has = {
"fetchOrder": False,
"fetchL2OrderBook": False,
"fetchTicker": True,
"cancelOrder": True,
"createOrder": True,
"fetchBalance": True,
"fetchOHLCV": False,
}
monkeypatch.setattr(ccxt.pro, "dummy", DummyExchange, raising=False)
valid, _, _, _ = validate_exchange("dummy")
assert not valid
valid, _, _, _ = validate_exchange("dummy", {"fetchOrder": True, "fetchOHLCV": True})
assert valid
def test_build_exchange_list_entry_uses_ft_has_overrides(monkeypatch):
class DummyClass:
_supported_trading_mode_margin_pairs = []
@staticmethod
def get_ft_has():
return {"fetchOrder": True}
captured = {}
class DummyExchange:
name = "Dummy"
alias = False
dex = False
def fake_validate_exchange(exchange, ft_has_overrides=None):
captured["ft_has_overrides"] = ft_has_overrides
return True, "", "", DummyExchange()
monkeypatch.setattr(exchange_utils_mod, "validate_exchange", fake_validate_exchange)
res = _build_exchange_list_entry("dummy", {"dummy": {"class": DummyClass}})
assert captured["ft_has_overrides"] == {"fetchOrder": True}
assert res["classname"] == "dummy"
def test_get_ft_has_overrides_returns_override(monkeypatch):
class DummyClass:
@staticmethod
def get_ft_has():
return {"fetchOrder": True}
monkeypatch.setattr(
ExchangeResolver,
"search_all_objects",
lambda *args, **kwargs: [{"name": "dummy", "class": DummyClass}],
)
assert _get_ft_has_overrides("dummy") == {"fetchOrder": True}

@ -6,8 +6,10 @@ from copy import deepcopy
from datetime import UTC, datetime
from unittest.mock import MagicMock
import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import RetryableOrderError, TemporaryError
from freqtrade.exceptions import ExchangeError, RetryableOrderError, TemporaryError
from freqtrade.exchange.exchange import Exchange
from freqtrade.exchange.krakenfutures import Krakenfutures
from tests.conftest import EXMS, get_patched_exchange
@ -76,6 +78,26 @@ def test_krakenfutures_fetch_order_falls_back_to_canceled_orders(mocker, default
assert res["id"] == "def"
def test_krakenfutures_fetch_order_reraises_when_no_fallback(mocker, default_conf):
"""Re-raise when fallback cannot locate the order."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(Exchange, "fetch_order", side_effect=RetryableOrderError("not found"))
mocker.patch.object(ex, "_fetch_order_from_closed_or_canceled", return_value=None)
with pytest.raises(RetryableOrderError):
ex.fetch_order("abc", "BTC/USD:USD")
def test_krakenfutures_fetch_order_from_closed_or_canceled_returns_none(mocker, default_conf):
"""Return None when the exchange does not support order history endpoints."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex, "exchange_has", return_value=False)
res = ex._fetch_order_from_closed_or_canceled("abc", "BTC/USD:USD", {})
assert res is None
def test_krakenfutures_create_stoploss_uses_trigger_price_type(mocker, default_conf):
"""Test create_stoploss uses triggerPrice, triggerSignal, and reduceOnly."""
api_mock = MagicMock()
@ -114,6 +136,15 @@ def test_krakenfutures_validate_stakecurrency_allows_eur(mocker, default_conf):
ex.validate_stakecurrency("EUR")
def test_krakenfutures_validate_stakecurrency_calls_super(mocker, default_conf):
"""Test validate_stakecurrency calls the base implementation for non-EUR."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
base_validate = mocker.patch.object(Exchange, "validate_stakecurrency")
ex.validate_stakecurrency("USD")
assert base_validate.call_count == 1
def test_krakenfutures_get_balances_synth_usd_from_flex(mocker, default_conf):
"""Test get_balances synthesizes USD balance from flex account."""
conf = dict(default_conf)
@ -169,6 +200,65 @@ def test_krakenfutures_get_balances_falls_back_to_ccxt_fetch_balance(mocker, def
assert res["total"]["USD"] == 12.0
def test_krakenfutures_get_balances_returns_for_non_usd_stake(mocker, default_conf):
"""Test get_balances returns early when stake currency is not USD."""
conf = dict(default_conf)
conf["stake_currency"] = "EUR"
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
sample = {"free": {"EUR": 10.0}, "used": {"EUR": 0.0}, "total": {"EUR": 10.0}}
mocker.patch.object(Exchange, "get_balances", return_value=sample)
res = ex.get_balances()
assert res == sample
assert "USD" not in res
def test_krakenfutures_get_balances_returns_when_flex_missing_or_invalid(mocker, default_conf):
"""Return original balances when flex data or USD extraction is missing."""
conf = dict(default_conf)
conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
base_one = {"free": {}, "used": {}, "total": {}}
base_two = {"free": {}, "used": {}, "total": {}}
mocker.patch.object(Exchange, "get_balances", side_effect=[base_one, base_two])
mocker.patch.object(ex, "_get_flex_account", side_effect=[None, {"availableMargin": 1.0}])
mocker.patch.object(ex, "_extract_usd_from_flex", return_value=(None, 1.0))
res = ex.get_balances()
assert res == base_one
res = ex.get_balances()
assert res == base_two
def test_krakenfutures_get_balances_preserves_existing_usd(mocker, default_conf):
"""Keep existing USD free balance if higher than flex-derived value."""
conf = dict(default_conf)
conf["stake_currency"] = "USD"
ex = get_patched_exchange(mocker, conf, exchange="krakenfutures")
sample = {
"USD": {"free": 20.0, "used": 0.0, "total": 20.0},
"free": {"USD": 20.0},
"used": {"USD": 0.0},
"total": {"USD": 20.0},
"info": {
"accounts": {
"flex": {
"availableMargin": 11.0,
"balanceValue": 12.0,
}
}
},
}
mocker.patch.object(Exchange, "get_balances", return_value=sample)
res = ex.get_balances()
assert res["free"]["USD"] == 20.0
def test_krakenfutures_sum_currencies_value_sums_valid_values(mocker, default_conf):
"""Sum currencies values, skipping invalid entries."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
@ -189,6 +279,49 @@ def test_krakenfutures_sum_currencies_value_returns_none_when_empty(mocker, defa
assert ex._sum_currencies_value({"USD": {"value": ""}}) is None
def test_krakenfutures_get_flex_account_fetch_balance_error(mocker, default_conf):
"""Return None when fetch_balance fails while attempting to load flex data."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
mocker.patch.object(ex._api, "fetch_balance", side_effect=Exception("boom"), create=True)
res = ex._get_flex_account({"free": {}, "used": {}, "total": {}}, None)
assert res is None
def test_krakenfutures_extract_flex_from_raw_handles_invalid(mocker, default_conf):
"""Return None for malformed flex account structures."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
assert ex._extract_flex_from_raw(None) is None
assert ex._extract_flex_from_raw({"info": "bad"}) is None
assert ex._extract_flex_from_raw({"info": {"accounts": "bad"}}) is None
def test_krakenfutures_extract_usd_from_flex_fallbacks(mocker, default_conf):
"""Use currencies fallback and fill missing USD values."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
usd_free, usd_total = ex._extract_usd_from_flex(
{"availableMargin": "5.0", "currencies": {"EUR": {"value": "6.0"}}}
)
assert usd_free == 5.0
assert usd_total == 6.0
usd_free, usd_total = ex._extract_usd_from_flex({"availableMargin": "7.0"})
assert usd_free == 7.0
assert usd_total == 7.0
usd_free, usd_total = ex._extract_usd_from_flex({"balanceValue": "9.0"})
assert usd_free == 9.0
assert usd_total == 9.0
def test_krakenfutures_safe_float_invalid_returns_none(mocker, default_conf):
"""Return None for values that cannot be coerced to float."""
ex = get_patched_exchange(mocker, default_conf, exchange="krakenfutures")
assert ex._safe_float("not-a-number") is None
def test_krakenfutures_get_funding_fees_futures_success(mocker, default_conf):
"""Use funding fee helper in futures mode."""
conf = dict(default_conf)
@ -202,6 +335,17 @@ def test_krakenfutures_get_funding_fees_futures_success(mocker, default_conf):
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")

Loading…
Cancel
Save