Skip to content
10 changes: 5 additions & 5 deletions freqtrade/commands/db_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions freqtrade/exchange/exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -1301,13 +1301,19 @@ 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)
price = safe_value_fallback(order, self._ft_has["stop_price_prop"], "price")
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,
Expand Down
42 changes: 30 additions & 12 deletions freqtrade/persistence/migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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", "'*'")

Expand All @@ -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://":
Expand Down
57 changes: 57 additions & 0 deletions tests/exchange/test_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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",
[
Expand Down
34 changes: 31 additions & 3 deletions tests/persistence/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
Loading