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( 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, diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 2e5971c708a..27970930fbb 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -30,25 +30,39 @@ 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): - 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(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 (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) + """ if engine.name == "postgresql": with engine.begin() as connection: if order_id: @@ -165,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) @@ -277,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", "'*'") @@ -296,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://": diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index dabbca65551..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) @@ -1371,6 +1375,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", [ diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 31b58068c6a..2118193faa1 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -360,25 +360,53 @@ 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 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()