tests: increase coverage - krakenfutures edge paths and ft_has overrides
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user