From a42c8faecae10141379cceba9eb410ba8e8dbbe3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 16:11:54 +0100 Subject: [PATCH 1/9] test: add test for immediately triggering stop order part of #12824 --- tests/exchange/test_exchange.py | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index dabbca65551..61cfb2da95b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1371,6 +1371,59 @@ def test_create_dry_run_order_limit_fill( order_closed = exchange.fetch_dry_run_order(order["id"]) +@pytest.mark.parametrize( + "side,price,error", + [ + # order_book_l2_usd spread: + # best ask: 25.566 + # best bid: 25.563 + ("sell", 22.0, False), + ("sell", 25.55, False), + ("sell", 26.00, True), + ("buy", 30.0, False), + ("buy", 25.57, False), + ("buy", 22.57, True), + ], +) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_create_dry_run_order_stoploss( + default_conf_usdt, + mocker, + exchange_name, + order_book_l2_usd, + side, + price, + error, +): + default_conf_usdt["dry_run"] = True + exchange = get_patched_exchange(mocker, default_conf_usdt, exchange=exchange_name) + if not exchange.get_option("stoploss_on_exchange"): + pytest.skip(f"{exchange_name} does not support on exchange stoploss orders") + + mocker.patch.multiple( + EXMS, + exchange_has=MagicMock(return_value=True), + fetch_l2_order_book=order_book_l2_usd, + ) + params = { + "pair": "LTC/USDT", + "amount": 1, + "stop_price": price, + "order_types": {"stoploss": "limit"}, + "side": side, + "leverage": 1.0, + } + if not error: + order = exchange.create_stoploss(**params) + assert isinstance(order, dict) + assert order.get("ft_order_type") == "stoploss" + assert order["status"] == "open" + # assert order["price"] == price + else: + with pytest.raises(InvalidOrderException, match=r".*Stoploss would trigger immediately.*"): + exchange.create_stoploss(**params) + + @pytest.mark.parametrize( "side,rate,amount,endprice", [ From c999de629146926fb025f211d801ed7837865694 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 16:12:13 +0100 Subject: [PATCH 2/9] feat: immediately triggering stoploss orders should cause emergency exits closes #12824 --- freqtrade/exchange/exchange.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 858e50c98a6..8c69f4b3d65 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1301,6 +1301,7 @@ def check_dry_limit_order_filled( Check dry-run limit order fill and update fee (if it filled). """ if order["status"] != "closed" and order.get("ft_order_type") == "stoploss": + # Stoploss branch pair = order["symbol"] if not orderbook and self.exchange_has("fetchL2OrderBook"): orderbook = self.fetch_l2_order_book(pair, 20) @@ -1308,6 +1309,11 @@ def check_dry_limit_order_filled( crossed = self._dry_is_price_crossed( pair, order["side"], price, orderbook, is_stop=True ) + if crossed and immediate: + raise InvalidOrderException( + "Could not create dry stoploss order. Stoploss would trigger immediately." + ) + if crossed: average = self.get_dry_market_fill_price( pair, From 5f06b1d43ea950bbc460dd03b168f3aa3b865838 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 16:14:00 +0100 Subject: [PATCH 3/9] test: update dry_order_fill test --- tests/exchange/test_exchange.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 61cfb2da95b..9b68e37d2ea 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1272,6 +1272,10 @@ def test_check_dry_limit_order_filled_stoploss( "ft_order_type": "stoploss", "stopLossPrice": 24.5, } + if immediate and crossed: + with pytest.raises(InvalidOrderException, match=r".*Stoploss would trigger immediately.*"): + exchange.check_dry_limit_order_filled(order, immediate=immediate) + return result = exchange.check_dry_limit_order_filled(order, immediate=immediate) From 131c1aaf13e3f53894d5ae38ad955da963f3f570 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 18:05:33 +0100 Subject: [PATCH 4/9] chore: clarify set_sequence_id's docstring --- freqtrade/persistence/migrations.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 2e5971c708a..c5f222122f2 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -49,6 +49,17 @@ def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str): def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None, kv_id=None, custom_data_id=None): + """ + Set sequence ids to the given values. + The id's given should be the next id to use, so the current max id + 1 - or current id + if using nextval before migration. + :param engine: SQLAlchemy engine + :param order_id: value to set for orders_id_seq + :param trade_id: value to set for trades_id_seq + :param pairlock_id: value to set for pairlocks_id_seq (optional) + :param kv_id: value to set for KeyValueStore_id_seq (optional) + :param custom_data_id: value to set for trade_custom_data_id_seq (optional) + """ if engine.name == "postgresql": with engine.begin() as connection: if order_id: From ca94909cccf7bc28f7e76055ef5a3bd926b6b17b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 18:09:05 +0100 Subject: [PATCH 5/9] test: improve test for migrate_set_sequence_id's --- tests/persistence/test_migrations.py | 30 +++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 31b58068c6a..1e99060c72c 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -374,11 +374,39 @@ def test_migrate_get_last_sequence_ids(): def test_migrate_set_sequence_ids(): engine = MagicMock() - engine.begin = MagicMock() + # make engine.begin() usable as a context manager that returns `conn` + conn = MagicMock() + engine.begin = MagicMock( + return_value=MagicMock( + __enter__=MagicMock(return_value=conn), + ) + ) engine.name = "postgresql" + set_sequence_ids(engine, 22, 55, 5, 3, 1) + # begin called once and connection.execute invoked for each provided sequence id assert engine.begin.call_count == 1 + assert conn.execute.call_count == 5 + assert ( + conn.execute.call_args_list[0][0][0].text == "ALTER SEQUENCE orders_id_seq RESTART WITH 22" + ) + assert ( + conn.execute.call_args_list[1][0][0].text == "ALTER SEQUENCE trades_id_seq RESTART WITH 55" + ) + assert ( + conn.execute.call_args_list[2][0][0].text + == "ALTER SEQUENCE pairlocks_id_seq RESTART WITH 5" + ) + assert ( + conn.execute.call_args_list[3][0][0].text + == 'ALTER SEQUENCE "KeyValueStore_id_seq" RESTART WITH 3' + ) + assert ( + conn.execute.call_args_list[4][0][0].text + == "ALTER SEQUENCE trade_custom_data_id_seq RESTART WITH 1" + ) + engine.reset_mock() engine.begin.reset_mock() From da62e614d7018a7ec130d71a1923840bb5be6aa6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 18:10:42 +0100 Subject: [PATCH 6/9] chore: improve typesafety for migrations --- freqtrade/persistence/migrations.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index c5f222122f2..1720b4d2969 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -30,7 +30,9 @@ def get_backup_name(tabs: list[str], backup_prefix: str): return table_back_name -def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str): +def get_last_sequence_ids( + engine, trade_back_name: str, order_back_name: str +) -> tuple[int | None, int | None]: order_id: int | None = None trade_id: int | None = None @@ -48,14 +50,21 @@ def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str): return order_id, trade_id -def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None, kv_id=None, custom_data_id=None): +def set_sequence_ids( + engine, + order_id: int | None = None, + trade_id: int | None = None, + pairlock_id: int | None = None, + kv_id: int | None = None, + custom_data_id: int | None = None, +): """ Set sequence ids to the given values. The id's given should be the next id to use, so the current max id + 1 - or current id if using nextval before migration. :param engine: SQLAlchemy engine - :param order_id: value to set for orders_id_seq - :param trade_id: value to set for trades_id_seq + :param order_id: value to set for orders_id_seq (optional) + :param trade_id: value to set for trades_id_seq (optional) :param pairlock_id: value to set for pairlocks_id_seq (optional) :param kv_id: value to set for KeyValueStore_id_seq (optional) :param custom_data_id: value to set for trade_custom_data_id_seq (optional) From 8ed1e6c394891fc2dad86c59cfcc9520cadbce74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 18:24:02 +0100 Subject: [PATCH 7/9] chore: refactor get_last_sequence_ids --- freqtrade/persistence/migrations.py | 21 ++++++++------------- tests/persistence/test_migrations.py | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1720b4d2969..ca3ecf8f201 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -30,24 +30,18 @@ def get_backup_name(tabs: list[str], backup_prefix: str): return table_back_name -def get_last_sequence_ids( - engine, trade_back_name: str, order_back_name: str -) -> tuple[int | None, int | None]: - order_id: int | None = None - trade_id: int | None = None +def get_last_sequence_ids(engine, sequence_name: str, table_back_name: str) -> int | None: + last_id: int | None = None if engine.name == "postgresql": with engine.begin() as connection: - trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0] - order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0] + last_id = connection.execute(text(f"select nextval('{sequence_name}')")).fetchone()[0] with engine.begin() as connection: connection.execute( - text(f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak") + text(f"ALTER SEQUENCE {sequence_name} rename to {table_back_name}_id_seq_bak") ) - connection.execute( - text(f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak") - ) - return order_id, trade_id + + return last_id def set_sequence_ids( @@ -185,7 +179,8 @@ def migrate_trades_and_orders_table( drop_index_on_table(engine, inspector, trade_back_name) - order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name) + order_id = get_last_sequence_ids(engine, "order_id_seq", order_back_name) + trade_id = get_last_sequence_ids(engine, "trades_id_seq", trade_back_name) drop_orders_table(engine, order_back_name) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 1e99060c72c..2118193faa1 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -360,14 +360,14 @@ def test_migrate_get_last_sequence_ids(): engine = MagicMock() engine.begin = MagicMock() engine.name = "postgresql" - get_last_sequence_ids(engine, "trades_bak", "orders_bak") + get_last_sequence_ids(engine, "trades_id_seq", "trades_bak") assert engine.begin.call_count == 2 engine.reset_mock() engine.begin.reset_mock() engine.name = "somethingelse" - get_last_sequence_ids(engine, "trades_bak", "orders_bak") + get_last_sequence_ids(engine, "trades_id_seq", "trades_bak") assert engine.begin.call_count == 0 From e6db0ab00d781241c14baf2015284d16360412bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 18:27:22 +0100 Subject: [PATCH 8/9] fix: pairlocks table migration on postgres --- freqtrade/persistence/migrations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index ca3ecf8f201..27970930fbb 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -292,6 +292,7 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}")) drop_index_on_table(engine, inspector, pairlock_back_name) + pairlock_id = get_last_sequence_ids(engine, "pairlocks_id_seq", pairlock_back_name) side = get_column_def(cols, "side", "'*'") @@ -311,6 +312,8 @@ def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: st ) ) + set_sequence_ids(engine, pairlock_id=pairlock_id) + def set_sqlite_to_wal(engine): if engine.name == "sqlite" and str(engine.url) != "sqlite://": From 18aed385f9a38145a6e2cc5207e58b48666e0344 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Feb 2026 19:28:11 +0100 Subject: [PATCH 9/9] fix: increment latest ID by 1 on migration part of #12825 --- freqtrade/commands/db_commands.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/db_commands.py b/freqtrade/commands/db_commands.py index 475c5050f28..d5acf8f1427 100644 --- a/freqtrade/commands/db_commands.py +++ b/freqtrade/commands/db_commands.py @@ -66,11 +66,11 @@ def start_convert_db(args: dict[str, Any]) -> None: set_sequence_ids( session_target.get_bind(), - trade_id=max_trade_id, - order_id=max_order_id, - pairlock_id=max_pairlock_id, - kv_id=max_kv_id, - custom_data_id=max_custom_data_id, + trade_id=(max_trade_id or 0) + 1, + order_id=(max_order_id or 0) + 1, + pairlock_id=(max_pairlock_id or 0) + 1, + kv_id=(max_kv_id or 0) + 1, + custom_data_id=(max_custom_data_id or 0) + 1, ) logger.info(