diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0eb1c608a..ff04037da 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -432,10 +432,6 @@ class FreqtradeBot(LoggingMixin): try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') - if order.ft_order_side == 'stoploss': - if fo and fo['status'] == 'open': - # Assume this as the open stoploss order - trade.stoploss_order_id = order.order_id if fo: logger.info(f"Found {order} for trade {trade}.") self.update_trade_state(trade, order.order_id, fo, @@ -895,17 +891,15 @@ class FreqtradeBot(LoggingMixin): def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: # First cancelling stoploss on exchange ... - if trade.stoploss_order_id: + for oslo in trade.open_sl_orders: try: - logger.info(f"Cancelling stoploss on exchange for {trade}") + logger.info(f"Cancelling stoploss on exchange for {trade} " + f"order: {oslo.order_id}") co = self.exchange.cancel_stoploss_order_with_result( - trade.stoploss_order_id, trade.pair, trade.amount) - self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) - - # Reset stoploss order id. - trade.stoploss_order_id = None + oslo.order_id, trade.pair, trade.amount) + self.update_trade_state(trade, oslo.order_id, co, stoploss_order=True) except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} " + logger.exception(f"Could not cancel stoploss order {oslo.order_id} " f"for pair {trade.pair}") return trade @@ -1080,7 +1074,7 @@ class FreqtradeBot(LoggingMixin): if ( not trade.has_open_orders - and not trade.stoploss_order_id + and not trade.has_open_sl_orders and not self.wallets.check_exit_amount(trade) ): logger.warning( @@ -1190,8 +1184,6 @@ class FreqtradeBot(LoggingMixin): order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss', trade.amount, stop_price) trade.orders.append(order_obj) - trade.stoploss_order_id = str(stoploss_order['id']) - trade.stoploss_last_update = datetime.now(timezone.utc) return True except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") @@ -1199,13 +1191,11 @@ class FreqtradeBot(LoggingMixin): self.handle_insufficient_funds(trade) except InvalidOrderException as e: - trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') self.emergency_exit(trade, stop_price) except ExchangeError: - trade.stoploss_order_id = None logger.exception('Unable to place a stoploss order on exchange.') return False @@ -1219,27 +1209,28 @@ class FreqtradeBot(LoggingMixin): """ logger.debug('Handling stoploss on exchange %s ...', trade) - stoploss_order = None - try: - # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.fetch_stoploss_order( - trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None - except InvalidOrderException as exception: - logger.warning('Unable to fetch stoploss order: %s', exception) + stoploss_orders = [] + for slo in trade.open_sl_orders: + stoploss_order = None + try: + # First we check if there is already a stoploss on exchange + stoploss_order = self.exchange.fetch_stoploss_order( + slo.order_id, trade.pair) if slo.order_id else None + except InvalidOrderException as exception: + logger.warning('Unable to fetch stoploss order: %s', exception) - if stoploss_order: - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) + if stoploss_order: + stoploss_orders.append(stoploss_order) + self.update_trade_state(trade, slo.order_id, stoploss_order, + stoploss_order=True) - # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) - self._notify_exit(trade, "stoploss", True) - self.handle_protections(trade.pair, trade.trade_direction) - return True + # We check if stoploss order is fulfilled + if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): + trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value + self._notify_exit(trade, "stoploss", True) + self.handle_protections(trade.pair, trade.trade_direction) + return True if trade.has_open_orders or not trade.is_open: # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case @@ -1248,7 +1239,7 @@ class FreqtradeBot(LoggingMixin): return False # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange - if not stoploss_order: + if len(stoploss_orders) == 0: stop_price = trade.stoploss_or_liquidation if self.edge: stoploss = self.edge.get_stoploss(pair=trade.pair) @@ -1262,27 +1253,7 @@ class FreqtradeBot(LoggingMixin): # in which case the trade will be closed - which we must check below. return False - # If stoploss order is canceled for some reason we add it again - if (trade.is_open - and stoploss_order - and stoploss_order['status'] in ('canceled', 'cancelled')): - if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): - return False - else: - logger.warning('Stoploss order was cancelled, but unable to recreate one.') - - # Finally we check if stoploss on exchange should be moved up because of trailing. - # Triggered Orders are now real orders - so don't replace stoploss anymore - if ( - trade.is_open and stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) - ): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) + self.manage_trade_stoploss_orders(trade, stoploss_orders) return False @@ -1318,6 +1289,42 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") + def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: List[Dict]): + """ + Perform required actions acording to existing stoploss orders of trade + :param trade: Corresponding Trade + :param stoploss_orders: Current on exchange stoploss orders + :return: None + """ + # If all stoploss orderd are canceled for some reason we add it again + canceled_sl_orders = [o for o in stoploss_orders + if o['status'] in ('canceled', 'cancelled')] + if ( + trade.is_open and + len(stoploss_orders) > 0 and + len(stoploss_orders) == len(canceled_sl_orders) + ): + if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): + return False + else: + logger.warning('All Stoploss orders are cancelled, but unable to recreate one.') + + active_sl_orders = [o for o in stoploss_orders if o not in canceled_sl_orders] + if len(active_sl_orders) > 0: + last_active_sl_order = active_sl_orders[-1] + # Finally we check if stoploss on exchange should be moved up because of trailing. + # Triggered Orders are now real orders - so don't replace stoploss anymore + if (trade.is_open and + last_active_sl_order.get('status_stop') != 'triggered' and + (self.config.get('trailing_stop', False) or + self.config.get('use_custom_stoploss', False))): + # if trailing stoploss is enabled we check if stoploss value has changed + # in which case we cancel stoploss order and put another one with new + # value immediately + self.handle_trailing_stoploss_on_exchange(trade, last_active_sl_order) + + return + def manage_open_orders(self) -> None: """ Management of open orders on exchange. Unfilled orders might be cancelled if timeout diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index f4d5a7174..b07a05632 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -1,7 +1,7 @@ import logging from typing import List, Optional -from sqlalchemy import inspect, select, text, tuple_, update +from sqlalchemy import inspect, select, text, update from freqtrade.exceptions import OperationalException from freqtrade.persistence.trade_model import Order, Trade @@ -91,8 +91,6 @@ def migrate_trades_and_orders_table( is_stop_loss_trailing = get_column_def( cols, 'is_stop_loss_trailing', f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)') - stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') - stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') min_rate = get_column_def(cols, 'min_rate', 'null') exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) @@ -160,7 +158,7 @@ def migrate_trades_and_orders_table( open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, amount_requested, open_date, close_date, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, - is_stop_loss_trailing, stoploss_order_id, stoploss_last_update, + is_stop_loss_trailing, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, liquidation_price, is_short, @@ -180,7 +178,6 @@ def migrate_trades_and_orders_table( {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, {is_stop_loss_trailing} is_stop_loss_trailing, - {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, case when {exit_reason} = 'sell_signal' then 'exit_signal' when {exit_reason} = 'custom_sell' then 'custom_exit' @@ -279,6 +276,8 @@ def fix_old_dry_orders(engine): with engine.begin() as connection: # Update current dry-run Orders where + # - stoploss order is Open (will be replaced eventually) + # 2nd query: # - current Order is open # - current Trade is closed # - current Order trade_id not equal to current Trade.id @@ -286,11 +285,6 @@ def fix_old_dry_orders(engine): stmt = update(Order).where( Order.ft_is_open.is_(True), - tuple_(Order.ft_trade_id, Order.order_id).not_in( - select( - Trade.id, Trade.stoploss_order_id - ).where(Trade.stoploss_order_id.is_not(None)) - ), Order.ft_order_side == 'stoploss', Order.order_id.like('dry%'), diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7e3cf970f..a90d9ab2d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -177,6 +177,8 @@ class Order(ModelBase): order_date = safe_value_fallback(order, 'timestamp') if order_date: self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc) + elif not self.order_date: + self.order_date = dt_now() self.ft_is_open = True if self.status in NON_OPEN_EXCHANGE_STATES: @@ -376,10 +378,6 @@ class LocalTrade: # percentage value of the initial stop loss initial_stop_loss_pct: Optional[float] = None is_stop_loss_trailing: bool = False - # stoploss order id which is on exchange - stoploss_order_id: Optional[str] = None - # last update time of the stoploss order on exchange - stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price max_rate: Optional[float] = None # Lowest price reached @@ -469,8 +467,8 @@ class LocalTrade: @property def stoploss_last_update_utc(self): - if self.stoploss_last_update: - return self.stoploss_last_update.replace(tzinfo=timezone.utc) + if self.has_open_sl_orders: + return max(o.order_date_utc for o in self.open_sl_orders) return None @property @@ -526,7 +524,7 @@ class LocalTrade: return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss'] @property - def has_open_orders(self) -> int: + def has_open_orders(self) -> bool: """ True if there are open orders for this trade excluding stoploss orders """ @@ -536,6 +534,37 @@ class LocalTrade: ] return len(open_orders_wo_sl) > 0 + @property + def open_sl_orders(self) -> List[Order]: + """ + All open stoploss orders for this trade + """ + return [ + o for o in self.orders + if o.ft_order_side in ['stoploss'] and o.ft_is_open + ] + + @property + def has_open_sl_orders(self) -> bool: + """ + True if there are open stoploss orders for this trade + """ + open_sl_orders = [ + o for o in self.orders + if o.ft_order_side in ['stoploss'] and o.ft_is_open + ] + return len(open_sl_orders) > 0 + + @property + def sl_orders(self) -> List[Order]: + """ + All stoploss orders for this trade + """ + return [ + o for o in self.orders + if o.ft_order_side in ['stoploss'] + ] + @property def open_orders_ids(self) -> List[str]: open_orders_ids_wo_sl = [ @@ -628,11 +657,10 @@ class LocalTrade: 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, - 'stoploss_order_id': self.stoploss_order_id, - 'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT) - if self.stoploss_last_update else None), - 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( - tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, + 'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT) + if self.stoploss_last_update_utc else None), + 'stoploss_last_update_timestamp': int(self.stoploss_last_update_utc.timestamp() * 1000 + ) if self.stoploss_last_update_utc else None, 'initial_stop_loss_abs': self.initial_stop_loss, 'initial_stop_loss_ratio': (self.initial_stop_loss_pct if self.initial_stop_loss_pct else None), @@ -793,7 +821,6 @@ class LocalTrade: logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): - self.stoploss_order_id = None self.close_rate_requested = self.stop_loss self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value if self.is_open and order.safe_filled > 0: @@ -1370,11 +1397,6 @@ class LocalTrade: exit_order_status=data["exit_order_status"], stop_loss=data["stop_loss_abs"], stop_loss_pct=data["stop_loss_ratio"], - stoploss_order_id=data["stoploss_order_id"], - stoploss_last_update=( - datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000, - tz=timezone.utc) - if data["stoploss_last_update_timestamp"] else None), initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss_pct=data["initial_stop_loss_ratio"], min_rate=data["min_rate"], @@ -1481,11 +1503,6 @@ class Trade(ModelBase, LocalTrade): Float(), nullable=True) # type: ignore is_stop_loss_trailing: Mapped[bool] = mapped_column( nullable=False, default=False) # type: ignore - # stoploss order id which is on exchange - stoploss_order_id: Mapped[Optional[str]] = mapped_column( - String(255), nullable=True, index=True) # type: ignore - # last update time of the stoploss order on exchange - stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore # absolute value of the highest reached price max_rate: Mapped[Optional[float]] = mapped_column( Float(), nullable=True, default=0.0) # type: ignore diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 791f70fa0..af3e84873 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -315,7 +315,6 @@ class TradeSchema(BaseModel): stop_loss_abs: Optional[float] = None stop_loss_ratio: Optional[float] = None stop_loss_pct: Optional[float] = None - stoploss_order_id: Optional[str] = None stoploss_last_update: Optional[str] = None stoploss_last_update_timestamp: Optional[int] = None initial_stop_loss_abs: Optional[float] = None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6decd7f7b..2317ee1a9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -979,15 +979,16 @@ class RPC: except (ExchangeError): pass - # cancel stoploss on exchange ... + # cancel stoploss on exchange orders ... if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') - and trade.stoploss_order_id): - try: - self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, - trade.pair) - c_count += 1 - except (ExchangeError): - pass + and trade.has_open_sl_orders): + + for oslo in trade.open_sl_orders: + try: + self._freqtrade.exchange.cancel_stoploss_order(oslo.order_id, trade.pair) + c_count += 1 + except (ExchangeError): + pass trade.delete() self._freqtrade.wallets.update() diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index a2276ae16..9ac43d73d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -266,7 +266,6 @@ def mock_trade_5(fee, is_short: bool): exchange='binance', strategy='SampleStrategy', enter_tag='TEST1', - stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455', timeframe=5, is_short=is_short, stop_loss_pct=0.10, diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index d73a53605..cf3109090 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -282,7 +282,6 @@ def mock_trade_usdt_5(fee, is_short: bool): open_rate=2.0, exchange='binance', strategy='SampleStrategy', - stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}', timeframe=5, is_short=is_short, ) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index f2bb0b2f1..a6a107a5e 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -74,7 +74,7 @@ def test_init_dryrun_db(default_conf, tmpdir): assert Path(filename).is_file() -def test_migrate_new(mocker, default_conf, fee, caplog): +def test_migrate(mocker, default_conf, fee, caplog): """ Test Database migration (starting with new pairformat) """ @@ -277,8 +277,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.exit_reason is None assert trade.strategy is None assert trade.timeframe == '5m' - assert trade.stoploss_order_id == 'dry_stop_order_id222' - assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", @@ -294,9 +292,10 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[0].order_id == 'dry_buy_order' assert orders[0].ft_order_side == 'buy' + # All dry-run stoploss orders will be closed assert orders[-1].order_id == 'dry_stop_order_id222' assert orders[-1].ft_order_side == 'stoploss' - assert orders[-1].ft_is_open is True + assert orders[-1].ft_is_open is False assert orders[1].order_id == 'dry_buy_order22' assert orders[1].ft_order_side == 'buy' diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 5829f8b71..95db7bc0f 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1432,7 +1432,6 @@ def test_to_json(fee): 'stop_loss_abs': None, 'stop_loss_ratio': None, 'stop_loss_pct': None, - 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, @@ -1500,7 +1499,6 @@ def test_to_json(fee): 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, - 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, diff --git a/tests/persistence/test_trade_fromjson.py b/tests/persistence/test_trade_fromjson.py index bb5e77f22..302a81c54 100644 --- a/tests/persistence/test_trade_fromjson.py +++ b/tests/persistence/test_trade_fromjson.py @@ -54,7 +54,6 @@ def test_trade_fromjson(): "stop_loss_abs": 0.1981, "stop_loss_ratio": -0.216, "stop_loss_pct": -21.6, - "stoploss_order_id": null, "stoploss_last_update": "2022-10-18 09:13:42", "stoploss_last_update_timestamp": 1666077222000, "initial_stop_loss_abs": 0.1981, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ca81ea0e6..1f51b30df 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -63,7 +63,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stop_loss_abs': 9.89e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, - 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss_abs': 9.89e-06, @@ -355,7 +354,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): rpc._rpc_delete('200') trades = Trade.session.scalars(select(Trade)).all() - trades[2].stoploss_order_id = '102' trades[2].orders.append( Order( ft_order_side='stoploss', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 9df26ad4e..bba18bcd3 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1174,7 +1174,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'stop_loss_abs': ANY, 'stop_loss_pct': ANY, 'stop_loss_ratio': ANY, - 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss_abs': 0.0, @@ -1378,7 +1377,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, - 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8e17604ab..b3df537f8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1120,12 +1120,11 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho freqtrade.enter_positions() trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short - trade.stoploss_order_id = None trade.is_open = True trades = [trade] freqtrade.exit_positions(trades) - assert trade.stoploss_order_id == '13434334' + assert trade.has_open_sl_orders is True assert stoploss.call_count == 1 assert trade.is_open is True @@ -1164,11 +1163,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert trade.is_open - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "13434334" + assert trade.open_sl_orders[-1].order_id == "13434334" # Second case: when stoploss is set but it is not yet hit # should do nothing and return false @@ -1179,7 +1178,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert freqtrade.handle_stoploss_on_exchange(trade) is False hanging_stoploss_order.assert_called_once_with('13434334', trade.pair) - assert trade.stoploss_order_id == "13434334" + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == "13434334" # Third case: when stoploss was set but it was canceled for some reason # should set a stoploss immediately and return False @@ -1195,12 +1195,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "103_1" + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == "103_1" assert trade.amount == amount_before # Fourth case: when stoploss is set and it is hit - # should unset stoploss_order_id and return true - # as a trade actually happened + # should return true as a trade actually happened caplog.clear() stop_order_dict.update({'id': "103_1"}) @@ -1221,7 +1221,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) - assert trade.stoploss_order_id is None + assert len(trade.open_sl_orders) == 0 assert trade.is_open is False caplog.clear() @@ -1229,26 +1229,27 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange.', caplog) - assert trade.stoploss_order_id is None + assert len(trade.open_sl_orders) == 0 # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order stop_order_dict.update({'id': "105"}) - trade.stoploss_order_id = "105" stoploss.reset_mock() mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.create_stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) + assert len(trade.open_sl_orders) == 1 assert stoploss.call_count == 1 # Sixth case: Closed Trade # Should not create new order - trade.stoploss_order_id = None 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) assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.has_open_sl_orders is False assert stoploss.call_count == 0 @@ -1282,7 +1283,7 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert trade.is_open - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False # emergency exit triggered # Trailing stop should not act anymore @@ -1297,7 +1298,6 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i 'remaining': enter_order['amount'], 'info': {'stopPrice': 22}, }]) - trade.stoploss_order_id = "107" trade.stoploss_last_update = dt_now() - timedelta(hours=1) trade.stop_loss = 24 trade.exit_reason = None @@ -1314,14 +1314,14 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i ) freqtrade.config['trailing_stop'] = True 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) assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert trade.is_open is False assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT) @@ -1356,11 +1356,11 @@ def test_handle_stoploss_on_exchange_partial( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "101" + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "101" assert trade.amount == 30 stop_order_dict.update({'id': "102"}) # Stoploss on exchange is cancelled on exchange, but filled partially. @@ -1380,13 +1380,14 @@ def test_handle_stoploss_on_exchange_partial( # Stoploss filled partially ... assert trade.amount == 15 - assert trade.stoploss_order_id == "102" + assert trade.open_sl_orders[-1].order_id == "102" @pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange_partial_cancel_here( - mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None: + mocker, default_conf_usdt, fee, is_short, limit_order, caplog, time_machine) -> None: stop_order_dict = {'id': "101", "status": "open"} + time_machine.move_to(dt_now()) default_conf_usdt['trailing_stop'] = True stoploss = MagicMock(return_value=stop_order_dict) enter_order = limit_order[entry_side(is_short)] @@ -1414,11 +1415,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "101" + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "101" assert trade.amount == 30 stop_order_dict.update({'id': "102"}) # Stoploss on exchange is open. @@ -1445,13 +1446,14 @@ def test_handle_stoploss_on_exchange_partial_cancel_here( }) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel) - trade.stoploss_last_update = dt_now() - timedelta(minutes=10) + time_machine.shift(timedelta(minutes=15)) assert freqtrade.handle_stoploss_on_exchange(trade) is False # Canceled Stoploss filled partially ... assert log_has_re('Cancelling current stoploss on exchange.*', caplog) - assert trade.stoploss_order_id == "102" + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "102" assert trade.amount == 15 @@ -1478,7 +1480,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, ) mocker.patch.multiple( EXMS, - fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': '100'}), create_stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf_usdt) @@ -1488,7 +1490,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short trade.is_open = True - trade.stoploss_order_id = "100" trade.orders.append( Order( ft_order_side='stoploss', @@ -1503,8 +1504,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, assert trade assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert log_has_re(r'Stoploss order was cancelled, but unable to recreate one.*', caplog) - assert trade.stoploss_order_id is None + assert log_has_re(r'All Stoploss orders are cancelled, but unable to recreate one\.', caplog) + assert trade.has_open_sl_orders is False assert trade.is_open is True @@ -1545,7 +1546,7 @@ def test_create_stoploss_order_invalid_order( caplog.clear() rpc_mock.reset_mock() freqtrade.create_stoploss_order(trade, 200) - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Exiting the trade forcefully", caplog) @@ -1599,14 +1600,13 @@ def test_create_stoploss_order_insufficient_funds( caplog.clear() freqtrade.create_stoploss_order(trade, 200) # stoploss_orderid was empty before - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert mock_insuf.call_count == 1 mock_insuf.reset_mock() - trade.stoploss_order_id = 'stoploss_orderid' freqtrade.create_stoploss_order(trade, 200) # No change to stoploss-orderid - assert trade.stoploss_order_id == 'stoploss_orderid' + assert trade.has_open_sl_orders is False assert mock_insuf.call_count == 1 @@ -1668,7 +1668,7 @@ def test_handle_stoploss_on_exchange_trailing( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = '100' + assert trade.has_open_sl_orders is False trade.stoploss_last_update = dt_now() - timedelta(minutes=20) trade.orders.append( Order( @@ -1678,27 +1678,35 @@ def test_handle_stoploss_on_exchange_trailing( ft_amount=trade.amount, ft_price=trade.stop_loss, order_id='100', + order_date=dt_now() - timedelta(minutes=20), ) ) - stoploss_order_hanging = MagicMock(return_value={ + stoploss_order_hanging = { 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', 'price': hang_price, 'average': 2, + 'fee': {}, + 'amount': 0, 'info': { 'stopPrice': stop_price[0] } - }) + } + stoploss_order_cancel = deepcopy(stoploss_order_hanging) + stoploss_order_cancel['status'] = 'canceled' - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=stoploss_order_cancel) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id == '13434334' + assert len(trade.open_sl_orders) == 1 + + assert trade.open_sl_orders[-1].order_id == '13434334' # price jumped 2x mocker.patch( @@ -1710,14 +1718,17 @@ def test_handle_stoploss_on_exchange_trailing( }) ) - cancel_order_mock = MagicMock() + cancel_order_mock = MagicMock(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) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert len(trade.open_sl_orders) == 1 cancel_order_mock.assert_not_called() stoploss_order_mock.assert_not_called() @@ -1748,14 +1759,21 @@ def test_handle_stoploss_on_exchange_trailing( 'last': bid[1], }) ) + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', + return_value={'id': 'so1', 'status': 'canceled'}) + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == 'so1' + assert freqtrade.handle_trade(trade) is True - assert trade.stoploss_order_id is None + assert trade.is_open is False + assert trade.has_open_sl_orders is False @pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange_trailing_error( - mocker, default_conf_usdt, fee, caplog, limit_order, is_short + mocker, default_conf_usdt, fee, caplog, limit_order, is_short, time_machine ) -> None: + time_machine.move_to(dt_now() - timedelta(minutes=601)) enter_order = limit_order[entry_side(is_short)] exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set @@ -1774,9 +1792,6 @@ def test_handle_stoploss_on_exchange_trailing_error( {'id': exit_order['id']}, ]), get_fee=fee, - ) - mocker.patch.multiple( - EXMS, create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1798,10 +1813,7 @@ def test_handle_stoploss_on_exchange_trailing_error( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = "abcd" trade.stop_loss = 0.2 - trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None) - trade.is_short = is_short stoploss_order_hanging = { 'id': "abcd", @@ -1813,23 +1825,37 @@ def test_handle_stoploss_on_exchange_trailing_error( 'stopPrice': '0.1' } } + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=3, + order_id='abcd', + 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) + 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) # Still try to create order assert stoploss.call_count == 1 + # TODO: Is this actually correct ? This will create a new order every time, + assert len(trade.open_sl_orders) == 2 # Fail creating stoploss order - trade.stoploss_last_update = dt_now() - timedelta(minutes=601) caplog.clear() cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') mocker.patch(f'{EXMS}.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 == 1 + assert cancel_mock.call_count == 2 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) @@ -1850,7 +1876,6 @@ def test_stoploss_on_exchange_price_rounding( price_to_precision=price_mock, ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - open_trade_usdt.stoploss_order_id = '13434334' open_trade_usdt.stop_loss = 222.55 freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {}) @@ -1881,6 +1906,7 @@ def test_handle_stoploss_on_exchange_custom_stop( exit_order, ]), get_fee=fee, + is_cancel_order_result_suitable=MagicMock(return_value=True), ) mocker.patch.multiple( EXMS, @@ -1911,8 +1937,6 @@ def test_handle_stoploss_on_exchange_custom_stop( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = '100' - trade.stoploss_last_update = dt_now() - timedelta(minutes=601) trade.orders.append( Order( ft_order_side='stoploss', @@ -1920,11 +1944,12 @@ def test_handle_stoploss_on_exchange_custom_stop( ft_is_open=True, ft_amount=trade.amount, ft_price=trade.stop_loss, + order_date=dt_now() - timedelta(minutes=601), order_id='100', ) ) - - stoploss_order_hanging = MagicMock(return_value={ + Trade.commit() + slo = { 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', @@ -1933,9 +1958,17 @@ def test_handle_stoploss_on_exchange_custom_stop( 'info': { 'stopPrice': '2.0805' } - }) + } + slo_canceled = deepcopy(slo) + slo_canceled.update({'status': 'canceled'}) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) + def fetch_stoploss_order_mock(order_id, *args, **kwargs): + x = deepcopy(slo) + 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) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1954,7 +1987,6 @@ def test_handle_stoploss_on_exchange_custom_stop( 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) - trade.stoploss_order_id = '100' # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1968,10 +2000,12 @@ def test_handle_stoploss_on_exchange_custom_stop( # setting stoploss_on_exchange_interval to 0 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() assert freqtrade.handle_stoploss_on_exchange(trade) is False - cancel_order_mock.assert_called_once_with('100', 'ETH/USDT') + cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT') # Long uses modified ask - offset, short modified bid + offset stoploss_order_mock.assert_called_once_with( amount=pytest.approx(trade.amount), @@ -2048,7 +2082,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde freqtrade.enter_positions() trade = Trade.session.scalars(select(Trade)).first() trade.is_open = True - trade.stoploss_order_id = '100' + trade.stoploss_last_update = dt_now() trade.orders.append( Order( @@ -4054,7 +4088,17 @@ def test_execute_trade_exit_sloe_cancel_exception( PairLock.session = MagicMock() freqtrade.config['dry_run'] = False - trade.stoploss_order_id = "abcd" + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='abcd', + status='open', + ) + ) freqtrade.execute_trade_exit(trade=trade, limit=1234, exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) @@ -4157,16 +4201,15 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( freqtrade.manage_open_orders() trade = Trade.session.scalars(select(Trade)).first() trades = [trade] - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.exit_positions(trades) assert trade - assert trade.stoploss_order_id == '123' + assert trade.has_open_sl_orders is True assert not trade.has_open_orders # Assuming stoploss on exchange is hit - # stoploss_order_id should become None - # and trade should be sold at the price of stoploss + # trade should be sold at the price of stoploss, with exit_reaeon STOPLOSS_ON_EXCHANGE stoploss_executed = MagicMock(return_value={ "id": "123", "timestamp": 1542707426845, @@ -4188,7 +4231,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert trade.is_open is False assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 4 @@ -5693,7 +5736,6 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap def reset_open_orders(trade): - trade.stoploss_order_id = None trade.is_short = is_short create_mock_trades(fee, is_short=is_short) @@ -5705,7 +5747,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap trade = trades[1] reset_open_orders(trade) assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.handle_insufficient_funds(trade) order = trade.orders[0] @@ -5715,7 +5757,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_uts.call_count == 0 # No change to orderid - as update_trade_state is mocked assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False caplog.clear() mock_fo.reset_mock() @@ -5726,7 +5768,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap # This part in not relevant anymore # assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.handle_insufficient_funds(trade) order = mock_order_4(is_short=is_short) @@ -5734,8 +5776,8 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 # Found open buy order - assert trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_orders is True + assert trade.has_open_sl_orders is False caplog.clear() mock_fo.reset_mock() @@ -5744,16 +5786,16 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap trade = trades[4] reset_open_orders(trade) assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders freqtrade.handle_insufficient_funds(trade) order = mock_order_5_stoploss(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 2 - # stoploss_order_id is "refound" and added to the trade + # stoploss order is "refound" and added to the trade assert not trade.has_open_orders - assert trade.stoploss_order_id is not None + assert trade.has_open_sl_orders is True caplog.clear() mock_fo.reset_mock() @@ -5764,7 +5806,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap reset_open_orders(trade) # This part in not relevant anymore # assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.handle_insufficient_funds(trade) order = mock_order_6_sell(is_short=is_short) @@ -5773,7 +5815,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_uts.call_count == 1 # sell-orderid is "refound" and added to the trade assert trade.open_orders_ids[0] == order['id'] - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False caplog.clear() diff --git a/tests/test_integration.py b/tests/test_integration.py index 94253dffb..522693131 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -49,7 +49,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, stoploss_order_closed['filled'] = stoploss_order_closed['amount'] # Sell first trade based on stoploss, keep 2nd and 3rd trade open - stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open] + stop_orders = [stoploss_order_closed, stoploss_order_open.copy(), stoploss_order_open.copy()] stoploss_order_mock = MagicMock( side_effect=stop_orders) # Sell 3rd trade (not called for the first trade) @@ -100,9 +100,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, stop_order = stop_orders[idx] stop_order['id'] = f"stop{idx}" oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss') + oobj.ft_is_open = True trade.orders.append(oobj) - trade.stoploss_order_id = f"stop{idx}" + assert len(trade.open_sl_orders) == 1 n = freqtrade.exit_positions(trades) assert n == 2 @@ -113,6 +114,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, # Only order for 3rd trade needs to be cancelled assert cancel_order_mock.call_count == 1 + assert stoploss_order_mock.call_count == 3 # Wallets must be updated between stoploss cancellation and selling, and will be updated again # during update_trade_state assert wallets_mock.call_count == 4