Merge branch 'develop' into pr/Axel-CH/10062

This commit is contained in:
Matthias
2024-10-01 20:51:39 +02:00
60 changed files with 1545 additions and 696 deletions
+2 -2
View File
@@ -38,7 +38,7 @@ def mock_trade_1(fee, is_short: bool):
trade = Trade(
pair="ETH/BTC",
stake_amount=0.001,
amount=123.0,
amount=50.0,
amount_requested=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
@@ -201,7 +201,7 @@ def mock_trade_4(fee, is_short: bool):
trade = Trade(
pair="ETC/BTC",
stake_amount=0.001,
amount=123.0,
amount=0.0,
amount_requested=124.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
+1 -1
View File
@@ -224,7 +224,7 @@ def mock_trade_usdt_4(fee, is_short: bool):
trade = Trade(
pair="NEO/USDT",
stake_amount=20.0,
amount=10.0,
amount=0.0,
amount_requested=10.01,
fee_open=fee.return_value,
fee_close=fee.return_value,
+1 -1
View File
@@ -3569,7 +3569,7 @@ def test_cancel_order_with_result(
mocker.patch(f"{EXMS}.exchange_has", return_value=True)
api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=corder)
api_mock.fetch_order = MagicMock(return_value={})
api_mock.fetch_order = MagicMock(return_value={"id": "1234"})
exchange = get_patched_exchange(mocker, default_conf, api_mock, exchange=exchange_name)
res = exchange.cancel_order_with_result("1234", "ETH/BTC", 1234)
assert isinstance(res, dict)
+39 -4
View File
@@ -6,6 +6,7 @@ import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import RetryableOrderError, TemporaryError
from freqtrade.exchange.common import API_RETRY_COUNT
from freqtrade.exchange.exchange import timeframe_to_minutes
from tests.conftest import EXMS, get_patched_exchange, log_has
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@@ -551,6 +552,7 @@ def test__set_leverage_okx(mocker, default_conf):
@pytest.mark.usefixtures("init_persistence")
def test_fetch_stoploss_order_okx(default_conf, mocker):
default_conf["dry_run"] = False
mocker.patch("freqtrade.exchange.common.time.sleep")
api_mock = MagicMock()
api_mock.fetch_order = MagicMock()
@@ -569,10 +571,10 @@ def test_fetch_stoploss_order_okx(default_conf, mocker):
with pytest.raises(RetryableOrderError):
exchange.fetch_stoploss_order("1234", "ETH/BTC")
assert api_mock.fetch_order.call_count == 1
assert api_mock.fetch_open_orders.call_count == 1
assert api_mock.fetch_closed_orders.call_count == 1
assert api_mock.fetch_canceled_orders.call_count == 1
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_open_orders.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_closed_orders.call_count == API_RETRY_COUNT + 1
assert api_mock.fetch_canceled_orders.call_count == API_RETRY_COUNT + 1
api_mock.fetch_order.reset_mock()
api_mock.fetch_open_orders.reset_mock()
@@ -610,6 +612,39 @@ def test_fetch_stoploss_order_okx(default_conf, mocker):
assert dro_mock.call_count == 1
def test_fetch_stoploss_order_okx_exceptions(default_conf_usdt, mocker):
default_conf_usdt["dry_run"] = False
api_mock = MagicMock()
ccxt_exceptionhandlers(
mocker,
default_conf_usdt,
api_mock,
"okx",
"fetch_stoploss_order",
"fetch_order",
retries=API_RETRY_COUNT + 1,
order_id="12345",
pair="ETH/USDT",
)
# Test 2nd part of the function
api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound())
api_mock.fetch_closed_orders = MagicMock(return_value=[])
api_mock.fetch_canceled_orders = MagicMock(return_value=[])
ccxt_exceptionhandlers(
mocker,
default_conf_usdt,
api_mock,
"okx",
"fetch_stoploss_order",
"fetch_open_orders",
retries=API_RETRY_COUNT + 1,
order_id="12345",
pair="ETH/USDT",
)
@pytest.mark.parametrize(
"sl1,sl2,sl3,side", [(1501, 1499, 1501, "sell"), (1499, 1501, 1499, "buy")]
)
+1 -1
View File
@@ -553,7 +553,7 @@ def test_enter_positions_global_pairlock(
@pytest.mark.parametrize("is_short", [False, True])
def test_handle_protections(mocker, default_conf_usdt, fee, is_short):
default_conf_usdt["protections"] = [
default_conf_usdt["_strategy_protections"] = [
{"method": "CooldownPeriod", "stop_duration": 60},
{
"method": "StoplossGuard",
+2 -2
View File
@@ -1299,7 +1299,7 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad
# While this test IS a copy of test_backtest_pricecontours, it's needed to ensure
# results do not carry-over to the next run, which is not given by using parametrize.
patch_exchange(mocker)
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"stop_duration": 3,
@@ -1358,7 +1358,7 @@ def test_backtest_pricecontours(
default_conf, mocker, testdatadir, protections, contour, expected
) -> None:
if protections:
default_conf["protections"] = protections
default_conf["_strategy_protections"] = protections
default_conf["enable_protections"] = True
patch_exchange(mocker)
+98 -3
View File
@@ -2212,7 +2212,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
@pytest.mark.parametrize(
"pairlists,trade_mode,result",
"pairlists,trade_mode,result,coin_market_calls",
[
(
[
@@ -2222,6 +2222,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT"],
1,
),
(
[
@@ -2231,6 +2232,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT", "ADA/USDT"],
1,
),
(
[
@@ -2240,6 +2242,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
1,
),
(
[
@@ -2249,6 +2252,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
1,
),
(
[
@@ -2257,6 +2261,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT", "XRP/USDT"],
1,
),
(
[
@@ -2265,6 +2270,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"spot",
["BTC/USDT", "ETH/USDT"],
1,
),
(
[
@@ -2273,6 +2279,7 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"futures",
["ETH/USDT:USDT"],
1,
),
(
[
@@ -2281,11 +2288,34 @@ def test_FullTradesFilter(mocker, default_conf_usdt, fee, caplog) -> None:
],
"futures",
["ETH/USDT:USDT", "ADA/USDT:USDT"],
1,
),
(
[
# MarketCapPairList as generator - futures, 1 category
{"method": "MarketCapPairList", "number_assets": 2, "categories": ["layer-1"]}
],
"futures",
["ETH/USDT:USDT", "ADA/USDT:USDT"],
["layer-1"],
),
(
[
# MarketCapPairList as generator - futures, 1 category
{
"method": "MarketCapPairList",
"number_assets": 2,
"categories": ["layer-1", "protocol"],
}
],
"futures",
["ETH/USDT:USDT", "ADA/USDT:USDT"],
["layer-1", "protocol"],
),
],
)
def test_MarketCapPairList_filter(
mocker, default_conf_usdt, trade_mode, markets, pairlists, result
mocker, default_conf_usdt, trade_mode, markets, pairlists, result, coin_market_calls
):
test_value = [
{"symbol": "btc"},
@@ -2309,8 +2339,16 @@ def test_MarketCapPairList_filter(
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
mocker.patch(
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_categories_list",
return_value=[
{"category_id": "layer-1"},
{"category_id": "protocol"},
{"category_id": "defi"},
],
)
gcm_mock = mocker.patch(
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
return_value=test_value,
)
@@ -2319,6 +2357,15 @@ def test_MarketCapPairList_filter(
pm = PairListManager(exchange, default_conf_usdt)
pm.refresh_pairlist()
if isinstance(coin_market_calls, int):
assert gcm_mock.call_count == coin_market_calls
else:
assert gcm_mock.call_count == len(coin_market_calls)
for call in coin_market_calls:
assert any(
"category" in c.kwargs and c.kwargs["category"] == call
for c in gcm_mock.call_args_list
)
assert pm.whitelist == result
@@ -2376,6 +2423,33 @@ def test_MarketCapPairList_timing(mocker, default_conf_usdt, markets, time_machi
assert markets_mock.call_count == 3
def test_MarketCapPairList_filter_special_no_pair_from_coingecko(
mocker,
default_conf_usdt,
markets,
):
default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList", "number_assets": 2}]
mocker.patch.multiple(
EXMS,
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
)
# Simulate no pair returned from coingecko
gcm_mock = mocker.patch(
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_markets",
return_value=[],
)
exchange = get_patched_exchange(mocker, default_conf_usdt)
pm = PairListManager(exchange, default_conf_usdt)
pm.refresh_pairlist()
assert gcm_mock.call_count == 1
assert pm.whitelist == []
def test_MarketCapPairList_exceptions(mocker, default_conf_usdt):
exchange = get_patched_exchange(mocker, default_conf_usdt)
default_conf_usdt["pairlists"] = [{"method": "MarketCapPairList"}]
@@ -2391,6 +2465,27 @@ def test_MarketCapPairList_exceptions(mocker, default_conf_usdt):
):
PairListManager(exchange, default_conf_usdt)
# Test invalid coinmarkets list
mocker.patch(
"freqtrade.plugins.pairlist.MarketCapPairList.FtCoinGeckoApi.get_coins_categories_list",
return_value=[
{"category_id": "layer-1"},
{"category_id": "protocol"},
{"category_id": "defi"},
],
)
default_conf_usdt["pairlists"] = [
{
"method": "MarketCapPairList",
"number_assets": 20,
"categories": ["layer-1", "defi", "layer250"],
}
]
with pytest.raises(
OperationalException, match="Category layer250 not in coingecko category list."
):
PairListManager(exchange, default_conf_usdt)
@pytest.mark.parametrize(
"pairlists,expected_error,expected_warning",
+71 -11
View File
@@ -3,14 +3,17 @@ from datetime import datetime, timedelta, timezone
import pytest
from freqtrade import constants
from freqtrade.enums import ExitType
from freqtrade.exceptions import OperationalException
from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.trade_model import Order
from freqtrade.plugins.protectionmanager import ProtectionManager
from tests.conftest import get_patched_freqtradebot, log_has_re
AVAILABLE_PROTECTIONS = ["CooldownPeriod", "LowProfitPairs", "MaxDrawdown", "StoplossGuard"]
def generate_mock_trade(
pair: str,
fee: float,
@@ -88,19 +91,76 @@ def generate_mock_trade(
def test_protectionmanager(mocker, default_conf):
default_conf["protections"] = [
{"method": protection} for protection in constants.AVAILABLE_PROTECTIONS
default_conf["_strategy_protections"] = [
{"method": protection} for protection in AVAILABLE_PROTECTIONS
]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
for handler in freqtrade.protections._protection_handlers:
assert handler.name in constants.AVAILABLE_PROTECTIONS
assert handler.name in AVAILABLE_PROTECTIONS
if not handler.has_global_stop:
assert handler.global_stop(datetime.now(timezone.utc), "*") is None
if not handler.has_local_stop:
assert handler.stop_per_pair("XRP/BTC", datetime.now(timezone.utc), "*") is None
@pytest.mark.parametrize(
"protconf,expected",
[
([], None),
([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
(
[
{
"method": "StoplossGuard",
"lookback_period_candles": 20,
"lookback_period": 2000,
"stop_duration": 10,
}
],
r"Protections must specify either `lookback_period`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"stop_duration_candles": 10,
}
],
r"Protections must specify either `stop_duration`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"unlock_at": "20:02",
}
],
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
None,
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
"Invalid date format for unlock_at: 55:102.",
),
],
)
def test_validate_protections(protconf, expected):
if expected:
with pytest.raises(OperationalException, match=expected):
ProtectionManager.validate_protections(protconf)
else:
ProtectionManager.validate_protections(protconf)
@pytest.mark.parametrize(
"timeframe,expected_lookback,expected_stop,protconf",
[
@@ -196,7 +256,7 @@ def test_protections_init(default_conf, timeframe, expected_lookback, expected_s
@pytest.mark.usefixtures("init_persistence")
def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
# Active for both sides (long and short)
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{"method": "StoplossGuard", "lookback_period": 60, "stop_duration": 40, "trade_limit": 3}
]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
@@ -268,7 +328,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog, is_short):
@pytest.mark.parametrize("only_per_side", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair, only_per_side):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "StoplossGuard",
"lookback_period": 60,
@@ -379,7 +439,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod(mocker, default_conf, fee, caplog):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"stop_duration": 60,
@@ -425,7 +485,7 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog):
@pytest.mark.usefixtures("init_persistence")
def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machine):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "CooldownPeriod",
"unlock_at": "05:00",
@@ -509,7 +569,7 @@ def test_CooldownPeriod_unlock_at(mocker, default_conf, fee, caplog, time_machin
@pytest.mark.parametrize("only_per_side", [False, True])
@pytest.mark.usefixtures("init_persistence")
def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "LowProfitPairs",
"lookback_period": 400,
@@ -599,7 +659,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog, only_per_side):
@pytest.mark.usefixtures("init_persistence")
def test_MaxDrawdown(mocker, default_conf, fee, caplog):
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{
"method": "MaxDrawdown",
"lookback_period": 1000,
@@ -812,7 +872,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
def test_protection_manager_desc(
mocker, default_conf, protectionconf, desc_expected, exception_expected
):
default_conf["protections"] = [protectionconf]
default_conf["_strategy_protections"] = [protectionconf]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
short_desc = str(freqtrade.protections.short_desc())
+2 -2
View File
@@ -1269,7 +1269,7 @@ def test_api_mix_tag(botclient, fee):
@pytest.mark.parametrize(
"is_short,current_rate,open_trade_value",
[(True, 1.098e-05, 15.0911775), (False, 1.099e-05, 15.1668225)],
[(True, 1.098e-05, 6.134625), (False, 1.099e-05, 6.165375)],
)
def test_api_status(
botclient, mocker, ticker, fee, markets, is_short, current_rate, open_trade_value
@@ -1294,7 +1294,7 @@ def test_api_status(
assert_response(rc)
assert len(rc.json()) == 4
assert rc.json()[0] == {
"amount": 123.0,
"amount": 50.0,
"amount_requested": 123.0,
"close_date": None,
"close_timestamp": None,
+1 -1
View File
@@ -173,7 +173,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf) -> None:
telegram_mock.reset_mock()
default_conf["dry_run"] = True
default_conf["whitelist"] = {"method": "VolumePairList", "config": {"number_assets": 20}}
default_conf["protections"] = [
default_conf["_strategy_protections"] = [
{"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}
]
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
+7 -9
View File
@@ -75,15 +75,13 @@ class StrategyTestV3(IStrategy):
protection_cooldown_lookback = IntParameter([0, 50], default=30)
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
# @property
# def protections(self):
# prot = []
# if self.protection_enabled.value:
# prot.append({
# "method": "CooldownPeriod",
# "stop_duration_candles": self.protection_cooldown_lookback.value
# })
# return prot
@property
def protections(self):
prot = []
if self.protection_enabled.value:
# Workaround to simplify tests. This will not work in real scenarios.
prot = self.config.get("_strategy_protections", {})
return prot
bot_started = False
+2 -61
View File
@@ -812,65 +812,6 @@ def test_validate_whitelist(default_conf):
validate_config_consistency(conf)
@pytest.mark.parametrize(
"protconf,expected",
[
([], None),
([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None),
([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None),
(
[
{
"method": "StoplossGuard",
"lookback_period_candles": 20,
"lookback_period": 2000,
"stop_duration": 10,
}
],
r"Protections must specify either `lookback_period`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"stop_duration_candles": 10,
}
],
r"Protections must specify either `stop_duration`.*",
),
(
[
{
"method": "StoplossGuard",
"lookback_period": 20,
"stop_duration": 10,
"unlock_at": "20:02",
}
],
r"Protections must specify either `unlock_at`, `stop_duration` or.*",
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "20:02"}],
None,
),
(
[{"method": "StoplossGuard", "lookback_period_candles": 20, "unlock_at": "55:102"}],
"Invalid date format for unlock_at: 55:102.",
),
],
)
def test_validate_protections(default_conf, protconf, expected):
conf = deepcopy(default_conf)
conf["protections"] = protconf
if expected:
with pytest.raises(OperationalException, match=expected):
validate_config_consistency(conf)
else:
validate_config_consistency(conf)
def test_validate_ask_orderbook(default_conf, caplog) -> None:
conf = deepcopy(default_conf)
conf["exit_pricing"]["use_order_book"] = True
@@ -1533,8 +1474,8 @@ def test_process_deprecated_protections(default_conf, caplog):
assert not log_has(message, caplog)
config["protections"] = []
process_temporary_deprecated_settings(config)
assert log_has(message, caplog)
with pytest.raises(ConfigurationError, match=message):
process_temporary_deprecated_settings(config)
def test_flat_vars_to_nested_dict(caplog):
+7 -6
View File
@@ -362,7 +362,8 @@ def test_sync_wallet_dry(mocker, default_conf_usdt, fee):
assert len(freqtrade.wallets._wallets) == 5
assert len(freqtrade.wallets._positions) == 0
bal = freqtrade.wallets.get_all_balances()
assert bal["NEO"].total == 10
# NEO trade is not filled yet.
assert bal["NEO"].total == 0
assert bal["XRP"].total == 10
assert bal["LTC"].total == 2
usdt_bal = bal["USDT"]
@@ -410,11 +411,11 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee):
def test_check_exit_amount(mocker, default_conf, fee):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=50.0)
create_mock_trades(fee, is_short=None)
trade = Trade.session.scalars(select(Trade)).first()
assert trade.amount == 123
assert trade.amount == 50.0
assert freqtrade.wallets.check_exit_amount(trade) is True
assert update_mock.call_count == 0
@@ -423,7 +424,7 @@ def test_check_exit_amount(mocker, default_conf, fee):
update_mock.reset_mock()
# Reduce returned amount to below the trade amount - which should
# trigger a wallet update and return False, triggering "order refinding"
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=100)
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=40)
assert freqtrade.wallets.check_exit_amount(trade) is False
assert update_mock.call_count == 1
assert total_mock.call_count == 2
@@ -433,12 +434,12 @@ def test_check_exit_amount_futures(mocker, default_conf, fee):
default_conf["trading_mode"] = "futures"
default_conf["margin_mode"] = "isolated"
freqtrade = get_patched_freqtradebot(mocker, default_conf)
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=50)
create_mock_trades(fee, is_short=None)
trade = Trade.session.scalars(select(Trade)).first()
trade.trading_mode = "futures"
assert trade.amount == 123
assert trade.amount == 50
assert freqtrade.wallets.check_exit_amount(trade) is True
assert total_mock.call_count == 0