From 3b55e577c2c629828093dd4e31507313da503dd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Nov 2025 13:00:52 +0100 Subject: [PATCH 01/11] chore: bump version to 2025.11 --- freqtrade/__init__.py | 2 +- ft_client/freqtrade_client/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 641600a4d..055d9545e 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,6 +1,6 @@ """Freqtrade bot""" -__version__ = "2025.11-dev" +__version__ = "2025.11" if "dev" in __version__: from pathlib import Path diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 9182ce800..2da84cd9f 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -1,7 +1,7 @@ from freqtrade_client.ft_rest_client import FtRestClient -__version__ = "2025.11-dev" +__version__ = "2025.11" if "dev" in __version__: from pathlib import Path From eabb7c98ce37a8c2bd6354620fa8bd0a268188be Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Dec 2025 19:28:19 +0100 Subject: [PATCH 02/11] test: simplify some stoploss test setups --- tests/exchange/test_exchange.py | 18 ++--- tests/freqtradebot/test_integration.py | 9 ++- .../freqtradebot/test_stoploss_on_exchange.py | 69 ++++++++++++------- tests/rpc/test_rpc.py | 15 ++-- tests/rpc/test_rpc_apiserver.py | 3 +- 5 files changed, 69 insertions(+), 45 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 63dd0be94..49656512b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3826,37 +3826,29 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): default_conf["dry_run"] = False - mock_prefix = "freqtrade.exchange.gate.Gate" - if exchange_name == "okx": - mock_prefix = "freqtrade.exchange.okx.Okx" - mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value={"for": 123}) - mocker.patch(f"{mock_prefix}.fetch_stoploss_order", return_value={"for": 123}) exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) + mocker.patch.object(exchange, "fetch_stoploss_order", return_value={"for": 123}) res = {"fee": {}, "status": "canceled", "amount": 1234} - mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=res) - mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value=res) + mocker.patch.object(exchange, "cancel_stoploss_order", return_value=res) co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555) assert co == res - mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value="canceled") - mocker.patch(f"{mock_prefix}.cancel_stoploss_order", return_value="canceled") + mocker.patch.object(exchange, "cancel_stoploss_order", return_value="canceled") # Fall back to fetch_stoploss_order co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555) assert co == {"for": 123} exc = InvalidOrderException("") - mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=exc) - mocker.patch(f"{mock_prefix}.fetch_stoploss_order", side_effect=exc) + mocker.patch.object(exchange, "fetch_stoploss_order", side_effect=exc) co = exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=555) assert co["amount"] == 555 assert co == {"id": "_", "fee": {}, "status": "canceled", "amount": 555, "info": {}} with pytest.raises(InvalidOrderException): exc = InvalidOrderException("Did not find order") - mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=exc) - mocker.patch(f"{mock_prefix}.cancel_stoploss_order", side_effect=exc) exchange = get_patched_exchange(mocker, default_conf, exchange=exchange_name) + mocker.patch.object(exchange, "cancel_stoploss_order", side_effect=exc) exchange.cancel_stoploss_order_with_result(order_id="_", pair="TKN/BTC", amount=123) diff --git a/tests/freqtradebot/test_integration.py b/tests/freqtradebot/test_integration.py index 2cb4b6aa8..d55e15afd 100644 --- a/tests/freqtradebot/test_integration.py +++ b/tests/freqtradebot/test_integration.py @@ -53,13 +53,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, cancel_order_mock = MagicMock() mocker.patch.multiple( EXMS, - create_stoploss=stoploss, fetch_ticker=ticker, get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - fetch_stoploss_order=stoploss_order_mock, - cancel_stoploss_order_with_result=cancel_order_mock, ) mocker.patch.multiple( @@ -73,6 +70,12 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True) freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch.multiple( + freqtrade.exchange, + create_stoploss=stoploss, + fetch_stoploss_order=stoploss_order_mock, + cancel_stoploss_order_with_result=cancel_order_mock, + ) freqtrade.strategy.order_types["stoploss_on_exchange"] = True # Switch ordertype to market to close trade immediately freqtrade.strategy.order_types["exit"] = "market" diff --git a/tests/freqtradebot/test_stoploss_on_exchange.py b/tests/freqtradebot/test_stoploss_on_exchange.py index c71621620..1cfedbf7c 100644 --- a/tests/freqtradebot/test_stoploss_on_exchange.py +++ b/tests/freqtradebot/test_stoploss_on_exchange.py @@ -103,7 +103,7 @@ def test_handle_stoploss_on_exchange( trade.is_open = True hanging_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "open"}) - mocker.patch(f"{EXMS}.fetch_stoploss_order", hanging_stoploss_order) + mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False hanging_stoploss_order.assert_called_once_with("13434334", trade.pair) @@ -116,7 +116,7 @@ def test_handle_stoploss_on_exchange( trade.is_open = True canceled_stoploss_order = MagicMock(return_value={"id": "13434334", "status": "canceled"}) - mocker.patch(f"{EXMS}.fetch_stoploss_order", canceled_stoploss_order) + mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", canceled_stoploss_order) stoploss.reset_mock() amount_before = trade.amount @@ -149,7 +149,7 @@ def test_handle_stoploss_on_exchange( "amount": enter_order["amount"], } ) - mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit) + mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_order_hit) freqtrade.strategy.order_filled = MagicMock(return_value=None) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r"STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.", caplog) @@ -158,7 +158,7 @@ def test_handle_stoploss_on_exchange( assert freqtrade.strategy.order_filled.call_count == 1 caplog.clear() - mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError()) + mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError()) trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) assert log_has("Unable to place a stoploss order on exchange.", caplog) @@ -168,8 +168,13 @@ def test_handle_stoploss_on_exchange( # It should try to add stoploss order stop_order_dict.update({"id": "105"}) stoploss.reset_mock() - mocker.patch(f"{EXMS}.fetch_stoploss_order", side_effect=InvalidOrderException()) - mocker.patch(f"{EXMS}.create_stoploss", stoploss) + mocker.patch.multiple( + freqtrade.exchange, + fetch_stoploss_order=MagicMock( + side_effect=InvalidOrderException(), + ), + create_stoploss=stoploss, + ) freqtrade.handle_stoploss_on_exchange(trade) assert len(trade.open_sl_orders) == 1 assert stoploss.call_count == 1 @@ -179,8 +184,7 @@ def test_handle_stoploss_on_exchange( trade.is_open = False trade.open_sl_orders[-1].ft_is_open = False stoploss.reset_mock() - mocker.patch(f"{EXMS}.fetch_order") - mocker.patch(f"{EXMS}.create_stoploss", stoploss) + mocker.patch.multiple(freqtrade.exchange, fetch_order=MagicMock(), create_stoploss=stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.has_open_sl_orders is False assert stoploss.call_count == 0 @@ -644,8 +648,11 @@ def test_handle_stoploss_on_exchange_trailing( stoploss_order_cancel = deepcopy(stoploss_order_hanging) stoploss_order_cancel["status"] = "canceled" - mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging) - mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=stoploss_order_cancel) + mocker.patch.multiple( + freqtrade.exchange, + fetch_stoploss_order=MagicMock(return_value=stoploss_order_hanging), + cancel_stoploss_order=MagicMock(return_value=stoploss_order_cancel), + ) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -671,9 +678,12 @@ def test_handle_stoploss_on_exchange_trailing( return_value={"id": "13434334", "status": "canceled", "fee": {}, "amount": trade.amount} ) stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"}) - mocker.patch(f"{EXMS}.fetch_stoploss_order") - mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock) - mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock) + mocker.patch.multiple( + freqtrade.exchange, + fetch_stoploss_order=MagicMock(), + cancel_stoploss_order=cancel_order_mock, + create_stoploss=stoploss_order_mock, + ) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -711,8 +721,9 @@ def test_handle_stoploss_on_exchange_trailing( } ), ) - mocker.patch( - f"{EXMS}.cancel_stoploss_order_with_result", + mocker.patch.object( + freqtrade.exchange, + "cancel_stoploss_order_with_result", return_value={"id": "so1", "status": "canceled"}, ) assert len(trade.open_sl_orders) == 1 @@ -786,8 +797,12 @@ def test_handle_stoploss_on_exchange_trailing_error( order_date=dt_now(), ) ) - mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException()) - mocker.patch(f"{EXMS}.fetch_stoploss_order", return_value=stoploss_order_hanging) + mocker.patch.object( + freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException() + ) + mocker.patch.object( + freqtrade.exchange, "fetch_stoploss_order", return_value=stoploss_order_hanging + ) time_machine.shift(timedelta(minutes=50)) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog) @@ -799,8 +814,8 @@ def test_handle_stoploss_on_exchange_trailing_error( # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order") - mocker.patch(f"{EXMS}.create_stoploss", side_effect=ExchangeError()) + cancel_mock = mocker.patch.object(freqtrade.exchange, "cancel_stoploss_order") + mocker.patch.object(freqtrade.exchange, "create_stoploss", side_effect=ExchangeError()) time_machine.shift(timedelta(minutes=50)) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 2 @@ -932,8 +947,11 @@ def test_handle_stoploss_on_exchange_custom_stop( cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={"id": "so1", "status": "open"}) - mocker.patch(f"{EXMS}.cancel_stoploss_order", cancel_order_mock) - mocker.patch(f"{EXMS}.create_stoploss", stoploss_order_mock) + mocker.patch.multiple( + freqtrade.exchange, + cancel_stoploss_order=cancel_order_mock, + create_stoploss=stoploss_order_mock, + ) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1054,7 +1072,9 @@ def test_execute_trade_exit_sloe_cancel_exception( mocker, default_conf_usdt, ticker_usdt, fee, caplog ) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException()) + mocker.patch.object( + freqtrade.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException() + ) mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=300)) create_order_mock = MagicMock( side_effect=[ @@ -1114,12 +1134,15 @@ def test_execute_trade_exit_with_stoploss_on_exchange( get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, + ) + freqtrade = FreqtradeBot(default_conf_usdt) + mocker.patch.multiple( + freqtrade.exchange, create_stoploss=stoploss, cancel_stoploss_order=cancel_order, _dry_is_price_crossed=MagicMock(side_effect=[True, False]), ) - freqtrade = FreqtradeBot(default_conf_usdt) freqtrade.strategy.order_types["stoploss_on_exchange"] = True patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 40e2b8266..f288cd0ba 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -386,11 +386,14 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): mocker.patch.multiple( EXMS, markets=PropertyMock(return_value=markets), - cancel_order=cancel_mock, - cancel_stoploss_order=stoploss_mock, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) + mocker.patch.multiple( + freqtradebot.exchange, + cancel_order=cancel_mock, + cancel_stoploss_order=stoploss_mock, + ) freqtradebot.strategy.order_types["stoploss_on_exchange"] = True create_mock_trades(fee, is_short) rpc = RPC(freqtradebot) @@ -426,13 +429,17 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): assert stoploss_mock.call_count == 1 assert res["cancel_order_count"] == 1 - stoploss_mock = mocker.patch(f"{EXMS}.cancel_stoploss_order", side_effect=InvalidOrderException) + stoploss_mock = mocker.patch.object( + freqtradebot.exchange, "cancel_stoploss_order", side_effect=InvalidOrderException + ) res = rpc._rpc_delete("3") assert stoploss_mock.call_count == 1 stoploss_mock.reset_mock() - cancel_mock = mocker.patch(f"{EXMS}.cancel_order", side_effect=InvalidOrderException) + cancel_mock = mocker.patch.object( + freqtradebot.exchange, "cancel_order", side_effect=InvalidOrderException + ) res = rpc._rpc_delete("4") assert cancel_mock.call_count == 1 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f7f99f288..46088ca0c 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1034,8 +1034,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( - EXMS, - markets=PropertyMock(return_value=markets), + ftbot.exchange, cancel_order=cancel_mock, cancel_stoploss_order=stoploss_mock, ) From fc5680d95c771f0d168fdc23fb28dc3e282347c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 11 Dec 2025 06:49:56 +0100 Subject: [PATCH 03/11] test: further test simplifications --- tests/exchange/test_exchange.py | 2 +- .../freqtradebot/test_stoploss_on_exchange.py | 59 +++++++++++-------- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 49656512b..4828e9aa2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4033,7 +4033,7 @@ def test_fetch_order_or_stoploss_order(default_conf, mocker): fetch_order_mock = MagicMock() fetch_stoploss_order_mock = MagicMock() mocker.patch.multiple( - EXMS, + exchange, fetch_order=fetch_order_mock, fetch_stoploss_order=fetch_stoploss_order_mock, ) diff --git a/tests/freqtradebot/test_stoploss_on_exchange.py b/tests/freqtradebot/test_stoploss_on_exchange.py index 1cfedbf7c..a3271e4ea 100644 --- a/tests/freqtradebot/test_stoploss_on_exchange.py +++ b/tests/freqtradebot/test_stoploss_on_exchange.py @@ -256,9 +256,12 @@ def test_handle_stoploss_on_exchange_emergency( stoploss = MagicMock(side_effect=InvalidOrderException()) assert trade.has_open_sl_orders is True Trade.commit() - mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", side_effect=InvalidOrderException()) - mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_cancelled) - mocker.patch(f"{EXMS}.create_stoploss", stoploss) + mocker.patch.multiple( + freqtrade.exchange, + cancel_stoploss_order_with_result=MagicMock(side_effect=InvalidOrderException()), + fetch_stoploss_order=stoploss_order_cancelled, + create_stoploss=stoploss, + ) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.has_open_sl_orders is False assert trade.is_open is False @@ -315,7 +318,7 @@ def test_handle_stoploss_on_exchange_partial( "amount": enter_order["amount"], } ) - mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit) + mocker.patch.multiple(freqtrade.exchange, fetch_stoploss_order=stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is False # Stoploss filled partially ... assert trade.amount == 15 @@ -387,8 +390,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here( "amount": enter_order["amount"], } ) - mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_order_hit) - mocker.patch(f"{EXMS}.cancel_stoploss_order_with_result", stoploss_order_cancel) + mocker.patch.multiple( + freqtrade.exchange, + fetch_stoploss_order=stoploss_order_hit, + cancel_stoploss_order_with_result=stoploss_order_cancel, + ) time_machine.shift(timedelta(minutes=15)) assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -412,20 +418,20 @@ def test_handle_sle_cancel_cant_recreate( mocker.patch.multiple( EXMS, fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}), + get_fee=fee, + ) + freqtrade = FreqtradeBot(default_conf_usdt) + mocker.patch.multiple( + freqtrade.exchange, create_order=MagicMock( side_effect=[ enter_order, exit_order, ] ), - get_fee=fee, - ) - mocker.patch.multiple( - EXMS, fetch_stoploss_order=MagicMock(return_value={"status": "canceled", "id": "100"}), create_stoploss=MagicMock(side_effect=ExchangeError()), ) - freqtrade = FreqtradeBot(default_conf_usdt) patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) freqtrade.enter_positions() @@ -861,20 +867,9 @@ def test_handle_stoploss_on_exchange_custom_stop( mocker.patch.multiple( EXMS, fetch_ticker=MagicMock(return_value={"bid": 1.9, "ask": 2.2, "last": 1.9}), - create_order=MagicMock( - side_effect=[ - enter_order, - exit_order, - ] - ), get_fee=fee, is_cancel_order_result_suitable=MagicMock(return_value=True), ) - mocker.patch.multiple( - EXMS, - create_stoploss=stoploss, - stoploss_adjust=MagicMock(return_value=True), - ) # enabling TSL default_conf_usdt["use_custom_stoploss"] = True @@ -883,6 +878,17 @@ def test_handle_stoploss_on_exchange_custom_stop( default_conf_usdt["minimal_roi"]["0"] = 999999999 freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch.multiple( + freqtrade.exchange, + create_order=MagicMock( + side_effect=[ + enter_order, + exit_order, + ] + ), + create_stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), + ) # enabling stoploss on exchange freqtrade.strategy.order_types["stoploss_on_exchange"] = True @@ -927,8 +933,11 @@ def test_handle_stoploss_on_exchange_custom_stop( x["id"] = order_id return x - mocker.patch(f"{EXMS}.fetch_stoploss_order", MagicMock(fetch_stoploss_order_mock)) - mocker.patch(f"{EXMS}.cancel_stoploss_order", return_value=slo_canceled) + mocker.patch.multiple( + freqtrade.exchange, + fetch_stoploss_order=MagicMock(fetch_stoploss_order_mock), + cancel_stoploss_order=MagicMock(return_value=slo_canceled), + ) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1231,7 +1240,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( "trades": None, } ) - mocker.patch(f"{EXMS}.fetch_stoploss_order", stoploss_executed) + mocker.patch.object(freqtrade.exchange, "fetch_stoploss_order", stoploss_executed) freqtrade.exit_positions(trades) assert trade.has_open_sl_orders is False From 043574f55852e6722160071624daf0d3c2c16a23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Dec 2025 06:52:11 +0100 Subject: [PATCH 04/11] fix: support binance algo orders closes #12610 --- freqtrade/exchange/binance.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b5c6c5d52..8da91a3a8 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -17,7 +17,7 @@ from freqtrade.exchange.binance_public_data import ( download_archive_trades, ) from freqtrade.exchange.common import retrier -from freqtrade.exchange.exchange_types import FtHas, Tickers +from freqtrade.exchange.exchange_types import CcxtOrder, FtHas, Tickers from freqtrade.exchange.exchange_utils_timeframe import timeframe_to_msecs from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.util import FtTTLCache @@ -145,6 +145,20 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e + def fetch_stoploss_order( + self, order_id: str, pair: str, params: dict | None = None + ) -> CcxtOrder: + if self.trading_mode == TradingMode.FUTURES: + params = params or {} + params.update({"stop": True}) + return self.fetch_order(order_id, pair, params) + + def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict: + if self.trading_mode == TradingMode.FUTURES: + params = params or {} + params.update({"stop": True}) + return self.cancel_order(order_id=order_id, pair=pair, params=params) + def get_historic_ohlcv( self, pair: str, From c9ed79d2c8a2eecfa2e9b5fb41dc61e41ce7149e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Dec 2025 06:54:29 +0100 Subject: [PATCH 05/11] chore: bump ccxt to 4.5.27 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 43f79ab25..84080742e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ ft-pandas-ta==0.3.16 ta-lib==0.6.8 technical==1.5.3 -ccxt==4.5.20 +ccxt==4.5.27 cryptography==46.0.3 aiohttp==3.13.2 SQLAlchemy==2.0.44 From 28517085caa14007b1f95c2c1c4888f7337616ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Dec 2025 19:29:21 +0100 Subject: [PATCH 06/11] chore: bump version to 2025.11.1 --- freqtrade/__init__.py | 2 +- ft_client/freqtrade_client/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 055d9545e..bfa1e56b7 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,6 +1,6 @@ """Freqtrade bot""" -__version__ = "2025.11" +__version__ = "2025.11.1" if "dev" in __version__: from pathlib import Path diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 2da84cd9f..871d2d848 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -1,7 +1,7 @@ from freqtrade_client.ft_rest_client import FtRestClient -__version__ = "2025.11" +__version__ = "2025.11.1" if "dev" in __version__: from pathlib import Path From 66235f3198ad800ee78b4d46bda749987771888a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Dec 2025 20:12:34 +0100 Subject: [PATCH 07/11] fix: improve binance stoploss "triggered" behavior --- freqtrade/exchange/binance.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8da91a3a8..c0fc517a7 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -151,7 +151,21 @@ class Binance(Exchange): if self.trading_mode == TradingMode.FUTURES: params = params or {} params.update({"stop": True}) - return self.fetch_order(order_id, pair, params) + order = self.fetch_order(order_id, pair, params) + if self.trading_mode == TradingMode.FUTURES and order.get("status", "open") == "closed": + # Places a real order - which we need to fetch explicitly. + + if new_orderid := order.get("info", {}).get("actualOrderId"): + order1 = self.fetch_order(order_id=new_orderid, pair=pair, params={}) + order1["id_stop"] = order1["id"] + order1["id"] = order_id + order1["type"] = "stoploss" + order1["stopPrice"] = order.get("stopPrice") + order1["status_stop"] = "triggered" + + return order1 + + return order def cancel_stoploss_order(self, order_id: str, pair: str, params: dict | None = None) -> dict: if self.trading_mode == TradingMode.FUTURES: From 1811f9581f0d1ab65c8cc53910b5bbf3fec06edf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Dec 2025 20:22:52 +0100 Subject: [PATCH 08/11] fix: allow set-leverage failures on followup orders --- freqtrade/exchange/exchange.py | 5 +++-- freqtrade/freqtradebot.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a2d1eb859..ba8751171 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1362,8 +1362,9 @@ class Exchange: amount: float, rate: float, leverage: float, - reduceOnly: bool = False, time_in_force: str = "GTC", + reduceOnly: bool = False, + initial_order: bool = True, ) -> CcxtOrder: if self._config["dry_run"]: dry_order = self.create_dry_run_order( @@ -1380,7 +1381,7 @@ class Exchange: rate_for_order = self.price_to_precision(pair, rate) if needs_price else None if not reduceOnly: - self._lev_prep(pair, leverage, side) + self._lev_prep(pair, leverage, side, accept_fail=not initial_order) order = self._api.create_order( pair, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6485b5d11..38f1bd905 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -937,6 +937,7 @@ class FreqtradeBot(LoggingMixin): reduceOnly=False, time_in_force=time_in_force, leverage=leverage, + initial_order=trade is None, ) order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested) order_obj.ft_order_tag = enter_tag @@ -2131,6 +2132,7 @@ class FreqtradeBot(LoggingMixin): leverage=trade.leverage, reduceOnly=self.trading_mode == TradingMode.FUTURES, time_in_force=time_in_force, + initial_order=False, ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") From 5fdc8acbe9dd85b1a4ab608e9fcdee3f694a4d9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 4 Dec 2025 06:56:14 +0100 Subject: [PATCH 09/11] test: switch to BTC/USDC in tests for now --- tests/exchange_online/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index 94ff4ac47..755e6716c 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -515,7 +515,8 @@ EXCHANGES = { ], }, "hyperliquid": { - "pair": "UBTC/USDC", + # TODO: Should be UBTC/USDC - probably needs a fix in ccxt + "pair": "BTC/USDC", "stake_currency": "USDC", "hasQuoteVolume": False, "timeframe": "30m", From f002ce67b10b03341bdfe22d452a9b1d3ab66208 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Dec 2025 06:56:35 +0100 Subject: [PATCH 10/11] chore: bump version to 2025.11.2 --- freqtrade/__init__.py | 2 +- ft_client/freqtrade_client/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index bfa1e56b7..fba2da67c 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,6 +1,6 @@ """Freqtrade bot""" -__version__ = "2025.11.1" +__version__ = "2025.11.2" if "dev" in __version__: from pathlib import Path diff --git a/ft_client/freqtrade_client/__init__.py b/ft_client/freqtrade_client/__init__.py index 871d2d848..22e04552b 100644 --- a/ft_client/freqtrade_client/__init__.py +++ b/ft_client/freqtrade_client/__init__.py @@ -1,7 +1,7 @@ from freqtrade_client.ft_rest_client import FtRestClient -__version__ = "2025.11.1" +__version__ = "2025.11.2" if "dev" in __version__: from pathlib import Path From 0beb76ce48aaa7e47249f21d5f0d1664e5650db0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Dec 2025 20:35:05 +0100 Subject: [PATCH 11/11] fix: function signature mismatch --- freqtrade/exchange/kucoin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 17afbd63a..e1fe469fb 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -44,8 +44,9 @@ class Kucoin(Exchange): amount: float, rate: float, leverage: float, - reduceOnly: bool = False, time_in_force: str = "GTC", + reduceOnly: bool = False, + initial_order: bool = True, ) -> CcxtOrder: res = super().create_order( pair=pair, @@ -56,6 +57,7 @@ class Kucoin(Exchange): leverage=leverage, reduceOnly=reduceOnly, time_in_force=time_in_force, + initial_order=initial_order, ) # Kucoin returns only the order-id. # ccxt returns status = 'closed' at the moment - which is information ccxt invented.