diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c188adbe2..26bb95257 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -373,7 +373,10 @@ class FreqtradeBot(LoggingMixin): "Order is older than 5 days. Assuming order was fully cancelled.") fo = order.to_ccxt_object() fo['status'] = 'canceled' - self.handle_cancel_order(fo, order.trade, constants.CANCEL_REASON['TIMEOUT']) + self.handle_cancel_order( + fo, order.order_id, order.trade, + constants.CANCEL_REASON['TIMEOUT'] + ) except ExchangeError as e: @@ -440,13 +443,6 @@ class FreqtradeBot(LoggingMixin): if fo and fo['status'] == 'open': # Assume this as the open stoploss order trade.stoploss_order_id = order.order_id - elif order.ft_order_side == trade.exit_side: - if fo and fo['status'] == 'open': - # Assume this as the open order - trade.open_order_id = order.order_id - elif order.ft_order_side == trade.entry_side: - if fo and fo['status'] == 'open': - trade.open_order_id = order.order_id if fo: logger.info(f"Found {order} for trade {trade}.") self.update_trade_state(trade, order.order_id, fo, @@ -612,7 +608,8 @@ class FreqtradeBot(LoggingMixin): # Walk through each pair and check if it needs changes for trade in Trade.get_open_trades(): # If there is any open orders, wait for them to finish. - if trade.open_order_id is None: + # TODO Remove to allow mul open orders + if not trade.has_open_orders: # Do a wallets update (will be ratelimited to once per hour) self.wallets.update(False) try: @@ -846,7 +843,6 @@ class FreqtradeBot(LoggingMixin): open_rate_requested=enter_limit_requested, open_date=open_date, exchange=self.exchange.id, - open_order_id=order_id, strategy=self.strategy.get_strategy_name(), enter_tag=enter_tag, timeframe=timeframe_to_minutes(self.config['timeframe']), @@ -867,7 +863,6 @@ class FreqtradeBot(LoggingMixin): trade.is_open = True trade.fee_open_currency = None trade.open_rate_requested = enter_limit_requested - trade.open_order_id = order_id trade.orders.append(order_obj) trade.recalc_trade_from_orders() @@ -1077,7 +1072,7 @@ class FreqtradeBot(LoggingMixin): trades_closed = 0 for trade in trades: - if trade.open_order_id is None and not self.wallets.check_exit_amount(trade): + if not trade.has_open_orders and not self.wallets.check_exit_amount(trade): logger.warning( f'Not enough {trade.safe_base_currency} in wallet to exit {trade}. ' 'Trying to recover.') @@ -1095,7 +1090,7 @@ class FreqtradeBot(LoggingMixin): logger.warning( f'Unable to handle stoploss on exchange for {trade.pair}: {exception}') # Check if we can sell our current pair - if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): + if not trade.has_open_orders and trade.is_open and self.handle_trade(trade): trades_closed += 1 except DependencyException as exception: @@ -1214,7 +1209,6 @@ class FreqtradeBot(LoggingMixin): """ logger.debug('Handling stoploss on exchange %s ...', trade) - stoploss_order = None try: @@ -1237,7 +1231,7 @@ class FreqtradeBot(LoggingMixin): self.handle_protections(trade.pair, trade.trade_direction) return True - if trade.open_order_id or not trade.is_open: + 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 # as the Amount on the exchange is tied up in another trade. # The trade can be closed already (sell-order fill confirmation came in this iteration) @@ -1321,27 +1315,33 @@ class FreqtradeBot(LoggingMixin): Timeout setting takes priority over limit order adjustment request. :return: None """ - for trade in Trade.get_open_order_trades(): - try: - if not trade.open_order_id: + for trade in Trade.get_open_trades(): + for open_order in trade.open_orders: + try: + order = self.exchange.fetch_order(open_order.order_id, trade.pair) + + except (ExchangeError): + logger.info( + 'Cannot query order for %s due to %s', trade, traceback.format_exc() + ) continue - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) - not_closed = order['status'] == 'open' or fully_cancelled - order_obj = trade.select_order_by_order_id(trade.open_order_id) + fully_cancelled = self.update_trade_state(trade, open_order.order_id, order) + not_closed = order['status'] == 'open' or fully_cancelled - if not_closed: - if fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( - trade, order_obj, datetime.now(timezone.utc))): - self.handle_cancel_order(order, trade, constants.CANCEL_REASON['TIMEOUT']) - else: - self.replace_order(order, order_obj, trade) + if not_closed: + if fully_cancelled or ( + open_order and self.strategy.ft_check_timed_out( + trade, open_order, datetime.now(timezone.utc) + ) + ): + self.handle_cancel_order( + order, open_order.order_id, trade, constants.CANCEL_REASON['TIMEOUT'] + ) + else: + self.replace_order(order, open_order, trade) - def handle_cancel_order(self, order: Dict, trade: Trade, reason: str) -> None: + def handle_cancel_order(self, order: Dict, order_id: str, trade: Trade, reason: str) -> None: """ Check if current analyzed order timed out and cancel if necessary. :param order: Order dict grabbed with exchange.fetch_order() @@ -1349,9 +1349,9 @@ class FreqtradeBot(LoggingMixin): :return: None """ if order['side'] == trade.entry_side: - self.handle_cancel_enter(trade, order, reason) + self.handle_cancel_enter(trade, order, order_id, reason) else: - canceled = self.handle_cancel_exit(trade, order, reason) + canceled = self.handle_cancel_exit(trade, order, order_id, reason) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: @@ -1406,7 +1406,7 @@ class FreqtradeBot(LoggingMixin): cancel_reason = constants.CANCEL_REASON['USER_CANCEL'] if order_obj.price != adjusted_entry_price: # cancel existing order if new price is supplied or None - self.handle_cancel_enter(trade, order, cancel_reason, + self.handle_cancel_enter(trade, order, order_obj.order_id, cancel_reason, replacing=replacing) if adjusted_entry_price: # place new order only if new price is supplied @@ -1434,25 +1434,28 @@ class FreqtradeBot(LoggingMixin): :return: None """ - for trade in Trade.get_open_order_trades(): - if not trade.open_order_id: - continue - try: - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue + for trade in Trade.get_open_trades(): + for open_order in trade.open_orders: + try: + order = self.exchange.fetch_order(open_order.order_id, trade.pair) + except (ExchangeError): + logger.info("Can't query order for %s due to %s", trade, traceback.format_exc()) + continue - if order['side'] == trade.entry_side: - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + if order['side'] == trade.entry_side: + self.handle_cancel_enter( + trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED'] + ) - elif order['side'] == trade.exit_side: - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + elif order['side'] == trade.exit_side: + self.handle_cancel_exit( + trade, order, open_order.order_id, constants.CANCEL_REASON['ALL_CANCELLED'] + ) Trade.commit() def handle_cancel_enter( - self, trade: Trade, order: Dict, reason: str, - replacing: Optional[bool] = False + self, trade: Trade, order: Dict, order_id: str, + reason: str, replacing: Optional[bool] = False ) -> bool: """ entry cancel - cancel order @@ -1461,7 +1464,7 @@ class FreqtradeBot(LoggingMixin): """ was_trade_fully_canceled = False side = trade.entry_side.capitalize() - if not trade.open_order_id: + if not trade.has_open_orders: logger.warning(f"No open order for {trade}.") return False @@ -1474,16 +1477,16 @@ class FreqtradeBot(LoggingMixin): if filled_val > 0 and minstake and filled_stake < minstake: logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"Order {order_id} for {trade.pair} not cancelled, " f"as the filled amount of {filled_val} would result in an unexitable trade.") return False - corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + corder = self.exchange.cancel_order_with_result(order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. # Simply bailing here is the only safe way - as this order will then be # handled in the next iteration. if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES: - logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") + logger.warning(f"Order {order_id} for {trade.pair} not cancelled.") return False else: # Order was cancelled already, so we can reuse the existing dict @@ -1503,14 +1506,12 @@ class FreqtradeBot(LoggingMixin): was_trade_fully_canceled = True reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: - self.update_trade_state(trade, trade.open_order_id, corder) - trade.open_order_id = None + self.update_trade_state(trade, order_id, corder) logger.info(f'{side} Order timeout for {trade}.') else: # update_trade_state (and subsequently recalc_trade_from_orders) will handle updates # to the trade object - self.update_trade_state(trade, trade.open_order_id, corder) - trade.open_order_id = None + self.update_trade_state(trade, order_id, corder) logger.info(f'Partial {trade.entry_side} order timeout for {trade}.') reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" @@ -1520,7 +1521,10 @@ class FreqtradeBot(LoggingMixin): reason=reason) return was_trade_fully_canceled - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool: + def handle_cancel_exit( + self, trade: Trade, order: Dict, order_id: str, + reason: str + ) -> bool: """ exit order cancel - cancel order and update trade :return: True if exit order was cancelled, false otherwise @@ -1538,7 +1542,7 @@ class FreqtradeBot(LoggingMixin): reason = constants.CANCEL_REASON['PARTIALLY_FILLED'] if minstake and filled_rem_stake < minstake: logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, as " + f"Order {order_id} for {trade.pair} not cancelled, as " f"the filled amount of {filled_amt} would result in an unexitable trade.") reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] @@ -1555,7 +1559,7 @@ class FreqtradeBot(LoggingMixin): order['id'], trade.pair, trade.amount) except InvalidOrderException: logger.exception( - f"Could not cancel {trade.exit_side} order {trade.open_order_id}") + f"Could not cancel {trade.exit_side} order {order_id}") return False # Set exit_reason for fill message @@ -1564,14 +1568,12 @@ class FreqtradeBot(LoggingMixin): # Order might be filled above in odd timing issues. if order.get('status') in ('canceled', 'cancelled'): trade.exit_reason = None - trade.open_order_id = None else: trade.exit_reason = exit_reason_prev cancelled = True else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] trade.exit_reason = None - trade.open_order_id = None self.update_trade_state(trade, order['id'], order) @@ -1705,7 +1707,6 @@ class FreqtradeBot(LoggingMixin): order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit) trade.orders.append(order_obj) - trade.open_order_id = order['id'] trade.exit_order_status = '' trade.close_rate_requested = limit trade.exit_reason = exit_reason @@ -1713,7 +1714,7 @@ class FreqtradeBot(LoggingMixin): self._notify_exit(trade, order_type, sub_trade=bool(sub_trade_amt), order=order_obj) # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): - self.update_trade_state(trade, trade.open_order_id, order) + self.update_trade_state(trade, order_obj.order_id, order) Trade.commit() return True @@ -1927,11 +1928,11 @@ class FreqtradeBot(LoggingMixin): trade.amount, abs_tol=constants.MATH_CLOSE_PREC) if order.ft_order_side == trade.exit_side: # Exit notification - if send_msg and not stoploss_order and not trade.open_order_id: + if send_msg and not stoploss_order and order.order_id not in trade.open_orders_ids: self._notify_exit(trade, '', fill=True, sub_trade=sub_trade, order=order) if not trade.is_open: self.handle_protections(trade.pair, trade.trade_direction) - elif send_msg and not trade.open_order_id and not stoploss_order: + elif send_msg and order.order_id not in trade.open_orders_ids and not stoploss_order: # Enter fill self._notify_enter(trade, order, order.order_type, fill=True, sub_trade=sub_trade) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5be3b4531..4b267b315 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -593,7 +593,6 @@ class Backtesting: """ if order and self._get_order_filled(order.ft_price, row): order.close_bt_order(current_date, trade) - trade.open_order_id = None if not (order.ft_order_side == trade.exit_side and order.safe_amount == trade.amount): self._call_adjust_stop(current_date, trade, order.ft_price) # pass @@ -862,7 +861,6 @@ class Backtesting: self.trade_id_counter += 1 trade = LocalTrade( id=self.trade_id_counter, - open_order_id=self.order_id_counter, pair=pair, base_currency=base_currency, stake_currency=self.config['stake_currency'], @@ -924,8 +922,7 @@ class Backtesting: ) order._trade_bt = trade trade.orders.append(order) - if not self._try_close_open_order(order, trade, current_time, row): - trade.open_order_id = str(self.order_id_counter) + self._try_close_open_order(order, trade, current_time, row) trade.recalc_trade_from_orders() return trade @@ -937,7 +934,7 @@ class Backtesting: """ for pair in open_trades.keys(): for trade in list(open_trades[pair]): - if trade.open_order_id and trade.nr_of_successful_entries == 0: + if trade.has_open_orders and trade.nr_of_successful_entries == 0: # Ignore trade if entry-order did not fill yet continue exit_row = data[pair][-1] @@ -1014,13 +1011,11 @@ class Backtesting: else: # Close additional entry order del trade.orders[trade.orders.index(order)] - trade.open_order_id = None return False if order.side == trade.exit_side: self.timedout_exit_orders += 1 # Close exit order and retry exiting on next signal. del trade.orders[trade.orders.index(order)] - trade.open_order_id = None return False return None @@ -1048,7 +1043,6 @@ class Backtesting: return False else: del trade.orders[trade.orders.index(order)] - trade.open_order_id = None self.canceled_entry_orders += 1 # place new order if result was not None @@ -1059,7 +1053,7 @@ class Backtesting: order.safe_remaining * order.ft_price / trade.leverage), direction='short' if trade.is_short else 'long') # Delete trade if no successful entries happened (if placing the new order failed) - if trade.open_order_id is None and trade.nr_of_successful_entries == 0: + if not trade.has_open_orders and trade.nr_of_successful_entries == 0: return True self.replaced_entry_orders += 1 else: @@ -1144,7 +1138,7 @@ class Backtesting: self.wallets.update() # 4. Create exit orders (if any) - if not trade.open_order_id: + if not trade.has_open_orders: self._check_trade_exit(trade, row) # Place exit order if necessary # 5. Process exit orders. diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1270b85ac..69d37530f 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -157,7 +157,7 @@ def migrate_trades_and_orders_table( fee_open, fee_open_cost, fee_open_currency, fee_close, fee_close_cost, fee_close_currency, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, - stake_amount, amount, amount_requested, open_date, close_date, open_order_id, + 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, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, @@ -174,7 +174,7 @@ def migrate_trades_and_orders_table( {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, open_rate, {open_rate_requested} open_rate_requested, close_rate, {close_rate_requested} close_rate_requested, close_profit, - stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id, + stake_amount, amount, {amount_requested}, open_date, close_date, {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, @@ -272,6 +272,13 @@ def set_sqlite_to_wal(engine): def fix_old_dry_orders(engine): with engine.begin() as connection: + + # Update current dry-run Orders where + # - current Order is open + # - current Trade is closed + # - current Order trade_id not equal to current Trade.id + # - current Order not stoploss + stmt = update(Order).where( Order.ft_is_open.is_(True), tuple_(Order.ft_trade_id, Order.order_id).not_in( @@ -285,12 +292,13 @@ def fix_old_dry_orders(engine): ).values(ft_is_open=False) connection.execute(stmt) + # Close dry-run orders for closed trades. stmt = update(Order).where( Order.ft_is_open.is_(True), - tuple_(Order.ft_trade_id, Order.order_id).not_in( + Order.ft_trade_id.not_in( select( - Trade.id, Trade.open_order_id - ).where(Trade.open_order_id.is_not(None)) + Trade.id + ).where(Trade.is_open.is_(True)) ), 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 cb5816d4e..22444bb49 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -352,7 +352,6 @@ class LocalTrade: amount_requested: Optional[float] = None open_date: datetime close_date: Optional[datetime] = None - open_order_id: Optional[str] = None # absolute value of the stop loss stop_loss: float = 0.0 # percentage value of the stop loss @@ -494,6 +493,32 @@ class LocalTrade: except IndexError: return '' + @property + def open_orders(self) -> List[Order]: + """ + All open orders for this trade excluding stoploss orders + """ + 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: + """ + True if there are open orders for this trade excluding stoploss orders + """ + open_orders_wo_sl = [ + o for o in self.orders + if o.ft_order_side not in ['stoploss'] and o.ft_is_open + ] + return len(open_orders_wo_sl) > 0 + + @property + def open_orders_ids(self) -> List[str]: + open_orders_ids_wo_sl = [ + oo.order_id for oo in self.open_orders + if oo.ft_order_side not in ['stoploss'] + ] + return open_orders_ids_wo_sl + def __init__(self, **kwargs): for key in kwargs: setattr(self, key, kwargs[key]) @@ -512,8 +537,8 @@ class LocalTrade: ) def to_json(self, minified: bool = False) -> Dict[str, Any]: - filled_orders = self.select_filled_or_open_orders() - orders = [order.to_json(self.entry_side, minified) for order in filled_orders] + filled_or_open_orders = self.select_filled_or_open_orders() + orders_json = [order.to_json(self.entry_side, minified) for order in filled_or_open_orders] return { 'trade_id': self.id, @@ -589,11 +614,11 @@ class LocalTrade: 'is_short': self.is_short, 'trading_mode': self.trading_mode, 'funding_fees': self.funding_fees, - 'open_order_id': self.open_order_id, 'amount_precision': self.amount_precision, 'price_precision': self.price_precision, 'precision_mode': self.precision_mode, - 'orders': orders, + 'orders': orders_json, + 'has_open_orders': self.has_open_orders, } @staticmethod @@ -711,24 +736,13 @@ class LocalTrade: if self.is_open: payment = "SELL" if self.is_short else "BUY" logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') - # condition to avoid reset value when updating fees - if self.open_order_id == order.order_id: - self.open_order_id = None - else: - logger.warning( - f'Got different open_order_id {self.open_order_id} != {order.order_id}') + self.recalc_trade_from_orders() elif order.ft_order_side == self.exit_side: if self.is_open: payment = "BUY" if self.is_short else "SELL" # * On margin shorts, you buy a little bit more than the amount (amount + interest) logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') - # condition to avoid reset value when updating fees - if self.open_order_id == order.order_id: - self.open_order_id = None - else: - logger.warning( - f'Got different open_order_id {self.open_order_id} != {order.order_id}') elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): self.stoploss_order_id = None @@ -761,7 +775,6 @@ class LocalTrade: self.close_date = self.close_date or datetime.utcnow() self.is_open = False self.exit_order_status = 'closed' - self.open_order_id = None self.recalc_trade_from_orders(is_closing=True) if show_msg: logger.info(f"Marking {self} as closed as the trade is fulfilled " @@ -1309,7 +1322,6 @@ class Trade(ModelBase, LocalTrade): open_date: Mapped[datetime] = mapped_column( nullable=False, default=datetime.utcnow) # type: ignore close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore - open_order_id: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) # type: ignore # absolute value of the stop loss stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore # percentage value of the stop loss @@ -1466,14 +1478,6 @@ class Trade(ModelBase, LocalTrade): # raise an exception. return Trade.session.scalars(query) - @staticmethod - def get_open_order_trades() -> List['Trade']: - """ - Returns all open trades - NOTE: Not supported in Backtesting. - """ - return cast(List[Trade], Trade.get_trades(Trade.open_order_id.isnot(None)).all()) - @staticmethod def get_open_trades_without_assigned_fees(): """ @@ -1772,7 +1776,6 @@ class Trade(ModelBase, LocalTrade): is_short=data["is_short"], trading_mode=data["trading_mode"], funding_fees=data["funding_fees"], - open_order_id=data["open_order_id"], ) for order in data["orders"]: diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index aad06644a..97f6251bc 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -308,7 +308,7 @@ class TradeSchema(BaseModel): min_rate: Optional[float] = None max_rate: Optional[float] = None - open_order_id: Optional[str] = None + has_open_orders: bool orders: List[OrderSchema] leverage: Optional[float] = None @@ -333,8 +333,6 @@ class OpenTradeSchema(TradeSchema): total_profit_fiat: Optional[float] = None total_profit_ratio: Optional[float] = None - open_order: Optional[str] = None - class TradeResponse(BaseModel): trades: List[TradeSchema] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e0bb05b11..0abac3975 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -26,7 +26,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.exchange.types import Tickers from freqtrade.loggers import bufferHandler from freqtrade.misc import decimals_per_coin -from freqtrade.persistence import KeyStoreKeys, KeyValueStore, Order, PairLocks, Trade +from freqtrade.persistence import KeyStoreKeys, KeyValueStore, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -171,13 +171,20 @@ class RPC: else: results = [] for trade in trades: - order: Optional[Order] = None current_profit_fiat: Optional[float] = None total_profit_fiat: Optional[float] = None + + # prepare open orders details + oo_details: Optional[str] = "" + oo_details_lst = [ + f'({oo.order_type} {oo.side} rem={oo.safe_remaining:.8f})' + for oo in trade.open_orders + if oo.ft_order_side not in ['stoploss'] + ] + oo_details = ', '.join(oo_details_lst) + total_profit_abs = 0.0 total_profit_ratio: Optional[float] = None - if trade.open_order_id: - order = trade.select_order_by_order_id(trade.open_order_id) # calculate profit and send message to user if trade.is_open: try: @@ -234,7 +241,6 @@ class RPC: profit_pct=round(current_profit * 100, 2), profit_abs=current_profit_abs, profit_fiat=current_profit_fiat, - total_profit_abs=total_profit_abs, total_profit_fiat=total_profit_fiat, total_profit_ratio=total_profit_ratio, @@ -243,10 +249,7 @@ class RPC: stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), stoploss_entry_dist=stoploss_entry_dist, stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8), - open_order=( - f'({order.order_type} {order.side} rem={order.safe_remaining:.8f})' if - order else None - ), + open_orders=oo_details )) results.append(trade_dict) return results @@ -288,18 +291,22 @@ class RPC: profit_str += f" ({fiat_profit:.2f})" fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit - open_order = (trade.select_order_by_order_id( - trade.open_order_id) if trade.open_order_id else None) + + active_attempt_side_symbols = [ + '*' if (oo and oo.ft_order_side == trade.entry_side) else '**' + for oo in trade.open_orders + ] + + # exemple: '*.**.**' trying to enter, exit and exit with 3 different orders + active_attempt_side_symbols_str = '.'.join(active_attempt_side_symbols) detail_trade = [ f'{trade.id} {direction_str}', - trade.pair + ('*' if (open_order - and open_order.ft_order_side == trade.entry_side) else '') - + ('**' if (open_order and - open_order.ft_order_side == trade.exit_side is not None) else ''), + trade.pair + active_attempt_side_symbols_str, shorten_date(dt_humanize(trade.open_date, only_distance=True)), profit_str ] + if self._config.get('position_adjustment_enable', False): max_entry_str = '' if self._config.get('max_entry_position_adjustment', -1) > 0: @@ -780,21 +787,25 @@ class RPC: def __exec_force_exit(self, trade: Trade, ordertype: Optional[str], amount: Optional[float] = None) -> bool: - # Check if there is there is an open order - fully_canceled = False - if trade.open_order_id: - order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) + # Check if there is there are open orders + trade_entry_cancelation_registry = [] + for oo in trade.open_orders: + trade_entry_cancelation_res = {'order_id': oo.order_id, 'cancel_state': False} + order = self._freqtrade.exchange.fetch_order(oo.order_id, trade.pair) if order['side'] == trade.entry_side: fully_canceled = self._freqtrade.handle_cancel_enter( - trade, order, CANCEL_REASON['FORCE_EXIT']) + trade, order, oo.order_id, CANCEL_REASON['FORCE_EXIT']) + trade_entry_cancelation_res['cancel_state'] = fully_canceled + trade_entry_cancelation_registry.append(trade_entry_cancelation_res) if order['side'] == trade.exit_side: # Cancel order - so it is placed anew with a fresh price. - self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT']) + self._freqtrade.handle_cancel_exit( + trade, order, oo.order_id, CANCEL_REASON['FORCE_EXIT']) - if not fully_canceled: - if trade.open_order_id is not None: + if all(tocr['cancel_state'] is False for tocr in trade_entry_cancelation_registry): + if trade.has_open_orders: # Order cancellation failed, so we can't exit. return False # Get current rate and execute sell @@ -893,10 +904,10 @@ class RPC: if trade: is_short = trade.is_short if not self._freqtrade.strategy.position_adjustment_enable: - raise RPCException(f'position for {pair} already open - id: {trade.id}') - if trade.open_order_id is not None: - raise RPCException(f'position for {pair} already open - id: {trade.id} ' - f'and has open order {trade.open_order_id}') + raise RPCException(f"position for {pair} already open - id: {trade.id}") + if trade.has_open_orders: + raise RPCException(f"position for {pair} already open - id: {trade.id} " + f"and has open order {','.join(trade.open_orders_ids)}") else: if Trade.get_open_trade_count() >= self._config['max_open_trades']: raise RPCException("Maximum number of trades is reached.") @@ -933,16 +944,18 @@ class RPC: if not trade: logger.warning('cancel_open_order: Invalid trade_id received.') raise RPCException('Invalid trade_id.') - if not trade.open_order_id: + if not trade.has_open_orders: logger.warning('cancel_open_order: No open order for trade_id.') raise RPCException('No open order for trade_id.') - try: - order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) - except ExchangeError as e: - logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True) - raise RPCException("Order not found.") - self._freqtrade.handle_cancel_order(order, trade, CANCEL_REASON['USER_CANCEL']) + for open_order in trade.open_orders: + try: + order = self._freqtrade.exchange.fetch_order(open_order.order_id, trade.pair) + except ExchangeError as e: + logger.info(f"Cannot query order for {trade} due to {e}.", exc_info=True) + raise RPCException("Order not found.") + self._freqtrade.handle_cancel_order( + order, open_order.order_id, trade, CANCEL_REASON['USER_CANCEL']) Trade.commit() def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: @@ -958,9 +971,9 @@ class RPC: raise RPCException('invalid argument') # Try cancelling regular order if that exists - if trade.open_order_id: + for open_order in trade.open_orders: try: - self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) + self._freqtrade.exchange.cancel_order(open_order.order_id, trade.pair) c_count += 1 except (ExchangeError): pass diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 78af78410..be27c38f4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -647,10 +647,10 @@ class Telegram(RPCHandler): ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) lines.append("*Stoploss distance:* `{stoploss_current_dist:.8g}` " "`({stoploss_current_dist_ratio:.2%})`") - if r['open_order']: + if r.get('open_orders'): lines.append( - "*Open Order:* `{open_order}`" - + "- `{exit_order_status}`" if r['exit_order_status'] else "") + "*Open Order:* `{open_orders}`" + + ("- `{exit_order_status}`" if r['exit_order_status'] else "")) lines_detail = self._prepare_order_details( r['orders'], r['quote_currency'], r['is_open']) diff --git a/tests/conftest.py b/tests/conftest.py index 732fffd8f..4372534ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2601,7 +2601,6 @@ def open_trade(): pair='ETH/BTC', open_rate=0.00001099, exchange='binance', - open_order_id='123456789', amount=90.99181073, fee_open=0.0, fee_close=0.0, @@ -2613,7 +2612,7 @@ def open_trade(): Order( ft_order_side='buy', ft_pair=trade.pair, - ft_is_open=False, + ft_is_open=True, ft_amount=trade.amount, ft_price=trade.open_rate, order_id='123456789', @@ -2639,7 +2638,6 @@ def open_trade_usdt(): pair='ADA/USDT', open_rate=2.0, exchange='binance', - open_order_id='123456789_exit', amount=30.0, fee_open=0.0, fee_close=0.0, diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index e50ed0e59..a2276ae16 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -46,7 +46,6 @@ def mock_trade_1(fee, is_short: bool): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, exchange='binance', - open_order_id=f'dry_run_buy_{direc(is_short)}_12345', strategy='StrategyTestV3', timeframe=5, is_short=is_short @@ -210,7 +209,6 @@ def mock_trade_4(fee, is_short: bool): is_open=True, open_rate=0.123, exchange='binance', - open_order_id=f'prod_buy_{direc(is_short)}_12345', strategy='StrategyTestV3', timeframe=5, is_short=is_short, @@ -327,7 +325,6 @@ def mock_trade_6(fee, is_short: bool): exchange='binance', strategy='SampleStrategy', enter_tag='TEST2', - open_order_id=f"prod_sell_{direc(is_short)}_6", timeframe=5, is_short=is_short ) @@ -411,7 +408,6 @@ def short_trade(fee): # close_profit_abs=-0.6925113200000013, exchange='binance', is_open=True, - open_order_id=None, strategy='DefaultStrategy', timeframe=5, exit_reason='sell_signal', @@ -502,7 +498,6 @@ def leverage_trade(fee): close_profit_abs=2.5983135000000175, exchange='kraken', is_open=False, - open_order_id='dry_run_leverage_buy_12368', strategy='DefaultStrategy', timeframe=5, exit_reason='sell_signal', diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index fc4776148..d73a53605 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -66,7 +66,6 @@ def mock_trade_usdt_1(fee, is_short: bool): close_profit_abs=-4.09, exchange='binance', strategy='SampleStrategy', - open_order_id=f'prod_exit_1_{direc(is_short)}', timeframe=5, is_short=is_short, ) @@ -123,7 +122,6 @@ def mock_trade_usdt_2(fee, is_short: bool): close_profit_abs=3.9875, exchange='binance', is_open=False, - open_order_id=f'12366_{direc(is_short)}', strategy='StrategyTestV2', timeframe=5, enter_tag='TEST1', @@ -231,7 +229,6 @@ def mock_trade_usdt_4(fee, is_short: bool): is_open=True, open_rate=2.0, exchange='binance', - open_order_id=f'prod_buy_12345_{direc(is_short)}', strategy='StrategyTestV2', timeframe=5, is_short=is_short, @@ -340,7 +337,6 @@ def mock_trade_usdt_6(fee, is_short: bool): open_rate=10.0, exchange='binance', strategy='SampleStrategy', - open_order_id=f'prod_exit_6_{direc(is_short)}', timeframe=5, is_short=is_short, ) @@ -378,7 +374,6 @@ def mock_trade_usdt_7(fee, is_short: bool): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=2.0, exchange='binance', - open_order_id=None, strategy='StrategyTestV2', timeframe=5, is_short=is_short, diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 13b3f89bf..1e87d3940 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -15,6 +15,7 @@ from freqtrade.persistence import Trade, init_db from freqtrade.persistence.base import ModelBase from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids from freqtrade.persistence.models import PairLock +from freqtrade.persistence.trade_model import Order from tests.conftest import log_has @@ -217,6 +218,23 @@ def test_migrate_new(mocker, default_conf, fee, caplog): {amount}, 0, {amount * 0.00258580} + ), + ( + -- Order without reference trade + 2, + 'buy', + 'ETC/BTC', + 1, + 'dry_buy_order55', + 'canceled', + 'ETC/BTC', + 'limit', + 'buy', + 0.00258580, + {amount}, + {amount}, + 0, + {amount * 0.00258580} ) """ engine = create_engine('sqlite://') @@ -238,9 +256,10 @@ def test_migrate_new(mocker, default_conf, fee, caplog): # Run init to test migration init_db(default_conf['db_url']) - trades = Trade.session.scalars(select(Trade).filter(Trade.id == 1)).all() + trades = Trade.session.scalars(select(Trade)).all() assert len(trades) == 1 trade = trades[0] + assert trade.id == 1 assert trade.fee_open == fee.return_value assert trade.fee_close == fee.return_value assert trade.open_rate_requested is None @@ -281,12 +300,18 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'dry_buy_order22' assert orders[1].ft_order_side == 'buy' - assert orders[1].ft_is_open is False + assert orders[1].ft_is_open is True assert orders[2].order_id == 'dry_stop_order_id11X' assert orders[2].ft_order_side == 'stoploss' assert orders[2].ft_is_open is False + orders1 = Order.session.scalars(select(Order)).all() + assert len(orders1) == 5 + order = orders1[4] + assert order.ft_trade_id == 2 + assert order.ft_is_open is False + def test_migrate_too_old(mocker, default_conf, fee, caplog): """ diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 8f5accdb4..0f0057d1e 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -10,7 +10,8 @@ from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException from freqtrade.persistence import LocalTrade, Order, Trade, init_db from freqtrade.util import dt_now -from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re +from tests.conftest import (create_mock_trades, create_mock_trades_usdt, + create_mock_trades_with_leverage, log_has, log_has_re) spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES @@ -457,15 +458,14 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ leverage=lev, trading_mode=trading_mode ) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.close_profit is None assert trade.close_date is None - trade.open_order_id = enter_order['id'] oobj = Order.parse_from_ccxt_object(enter_order, 'ADA/USDT', entry_side) trade.orders.append(oobj) trade.update_trade(oobj) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == open_rate assert trade.close_profit is None assert trade.close_date is None @@ -476,13 +476,12 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ caplog) caplog.clear() - trade.open_order_id = enter_order['id'] time_machine.move_to("2022-03-31 21:45:05 +00:00") oobj = Order.parse_from_ccxt_object(exit_order, 'ADA/USDT', exit_side) trade.orders.append(oobj) trade.update_trade(oobj) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.close_rate == close_rate assert pytest.approx(trade.close_profit) == profit assert trade.close_date is not None @@ -511,11 +510,10 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, leverage=1.0, ) - trade.open_order_id = 'mocked_market_buy' oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') trade.orders.append(oobj) trade.update_trade(oobj) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == 2.0 assert trade.close_profit is None assert trade.close_date is None @@ -526,11 +524,10 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog.clear() trade.is_open = True - trade.open_order_id = 'mocked_market_sell' oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') trade.orders.append(oobj) trade.update_trade(oobj) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.close_rate == 2.2 assert pytest.approx(trade.close_profit) == 0.094513715710723 assert trade.close_date is not None @@ -580,7 +577,6 @@ def test_calc_open_close_trade_price( ) entry_order = limit_order[trade.entry_side] exit_order = limit_order[trade.exit_side] - trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' oobj = Order.parse_from_ccxt_object(entry_order, 'ADA/USDT', trade.entry_side) oobj._trade_live = trade @@ -678,7 +674,6 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): leverage=1.0, ) - trade.open_order_id = 'something' oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') trade.update_trade(oobj) assert trade.calc_close_trade_value(trade.close_rate) == 0.0 @@ -697,7 +692,7 @@ def test_update_open_order(limit_buy_order_usdt): trading_mode=margin ) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.close_profit is None assert trade.close_date is None @@ -705,7 +700,7 @@ def test_update_open_order(limit_buy_order_usdt): oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') trade.update_trade(oobj) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.close_profit is None assert trade.close_date is None @@ -778,7 +773,6 @@ def test_calc_open_trade_value( is_short=is_short, trading_mode=trading_mode ) - trade.open_order_id = 'open_trade' oobj = Order.parse_from_ccxt_object( limit_buy_order_usdt, 'ADA/USDT', 'sell' if is_short else 'buy') trade.update_trade(oobj) # Buy @ 2.0 @@ -833,7 +827,6 @@ def test_calc_close_trade_price( trading_mode=trading_mode, funding_fees=funding_fees ) - trade.open_order_id = 'close_trade' assert round(trade.calc_close_trade_value(rate=close_rate), 8) == result @@ -1156,7 +1149,6 @@ def test_calc_profit( trading_mode=trading_mode, funding_fees=funding_fees ) - trade.open_order_id = 'something' profit_res = trade.calculate_profit(close_rate) assert pytest.approx(profit_res.profit_abs) == round(profit, 8) @@ -1352,6 +1344,24 @@ def test_get_open_lev(fee, use_db): Trade.use_db = True +@pytest.mark.parametrize('is_short', [True, False]) +@pytest.mark.parametrize('use_db', [True, False]) +@pytest.mark.usefixtures("init_persistence") +def test_get_open_orders(fee, is_short, use_db): + Trade.use_db = use_db + Trade.reset_trades() + + create_mock_trades_usdt(fee, is_short, use_db) + # Trade.commit() + trade = Trade.get_trades_proxy(pair="XRP/USDT")[0] + # assert trade.id == 3 + assert len(trade.orders) == 2 + assert len(trade.open_orders) == 0 + assert not trade.has_open_orders + + Trade.use_db = True + + @pytest.mark.usefixtures("init_persistence") def test_to_json(fee): @@ -1367,7 +1377,6 @@ def test_to_json(fee): open_rate=0.123, exchange='binance', enter_tag=None, - open_order_id='dry_run_buy_12345', precision_mode=1, amount_precision=8.0, price_precision=7.0, @@ -1383,7 +1392,6 @@ def test_to_json(fee): 'is_open': None, 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), - 'open_order_id': 'dry_run_buy_12345', 'close_date': None, 'close_timestamp': None, 'open_rate': 0.123, @@ -1438,6 +1446,7 @@ def test_to_json(fee): 'price_precision': 7.0, 'precision_mode': 1, 'orders': [], + 'has_open_orders': False, } # Simulate dry_run entries @@ -1505,7 +1514,6 @@ def test_to_json(fee): 'is_open': None, 'max_rate': None, 'min_rate': None, - 'open_order_id': None, 'open_rate_requested': None, 'open_trade_value': 12.33075, 'exit_reason': None, @@ -1524,6 +1532,7 @@ def test_to_json(fee): 'price_precision': 8.0, 'precision_mode': 2, 'orders': [], + 'has_open_orders': False, } @@ -2066,7 +2075,6 @@ def test_Trade_object_idem(): 'total_open_trades_stakes', 'get_closed_trades_without_assigned_fees', 'get_open_trades_without_assigned_fees', - 'get_open_order_trades', 'get_trades', 'get_trades_query', 'get_exit_reason_performance', @@ -2676,7 +2684,7 @@ def test_recalc_trade_from_orders_dca(data) -> None: assert len(trade.orders) == idx + 1 if idx < len(data) - 1: assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.amount == result[0] assert trade.open_rate == result[1] assert trade.stake_amount == result[2] @@ -2690,4 +2698,4 @@ def test_recalc_trade_from_orders_dca(data) -> None: assert not trade.is_open trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 70341b37e..49700b7f4 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -42,7 +42,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'strategy': ANY, 'enter_tag': ANY, 'timeframe': 5, - 'open_order_id': ANY, 'close_date': None, 'close_timestamp': None, 'open_rate': 1.098e-05, @@ -75,7 +74,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_current_dist_pct': -10.01, 'stoploss_entry_dist': -0.00010402, 'stoploss_entry_dist_ratio': -0.10376381, - 'open_order': None, + 'open_orders': '', 'realized_profit': 0.0, 'realized_profit_ratio': None, 'total_profit_abs': -4.09e-06, @@ -91,6 +90,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'amount_precision': 8.0, 'price_precision': 8.0, 'precision_mode': 2, + 'has_open_orders': False, 'orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', @@ -128,7 +128,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_pct': 0.0, 'profit_abs': 0.0, 'total_profit_abs': 0.0, - 'open_order': '(limit buy rem=91.07468123)', + 'open_orders': '(limit buy rem=91.07468123)', + 'has_open_orders': True, }) response_unfilled['orders'][0].update({ 'is_open': True, @@ -146,7 +147,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: results = rpc._rpc_trade_status() # Reuse above object, only remaining changed. response_unfilled['orders'][0].update({ - 'remaining': None + 'remaining': None, }) assert results[0] == response_unfilled @@ -165,6 +166,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: response.update({ 'max_stake_amount': 0.001, 'total_profit_ratio': pytest.approx(-0.00409153), + 'has_open_orders': False, }) assert results[0] == response @@ -779,7 +781,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: 'amount': amount, 'remaining': amount, 'filled': 0.0, - 'id': trade.orders[0].order_id, + 'id': trade.orders[-1].order_id, } ) cancel_order_3 = mocker.patch( @@ -791,7 +793,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: 'amount': amount, 'remaining': amount, 'filled': 0.0, - 'id': trade.orders[0].order_id, + 'id': trade.orders[-1].order_id, } ) msg = rpc._rpc_force_exit('3') @@ -800,7 +802,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert cancel_order_3.call_count == 1 assert cancel_order_mock.call_count == 0 - trade = Trade.session.scalars(select(Trade).filter(Trade.id == '2')).first() + trade = Trade.session.scalars(select(Trade).filter(Trade.id == '4')).first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( @@ -829,7 +831,7 @@ def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: assert msg == {'result': 'Created exit order for trade 4.'} assert cancel_order_4.call_count == 1 assert cancel_order_mock.call_count == 0 - assert trade.amount == amount + assert pytest.approx(trade.amount) == amount def test_performance_handle(default_conf_usdt, ticker, fee, mocker) -> None: @@ -1097,7 +1099,8 @@ def test_rpc_force_entry(mocker, default_conf, ticker, fee, limit_buy_order_open trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05) assert trade.stake_amount == 0.05 assert trade.buy_tag == 'force_entry' - assert trade.open_order_id == 'mocked_limit_buy' + + assert trade.open_orders_ids[-1] == 'mocked_limit_buy' freqtradebot.strategy.position_adjustment_enable = True with pytest.raises(RPCException, match=r'position for LTC/BTC already open.*open order.*'): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2b6e1d530..89cb47830 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1026,7 +1026,6 @@ def test_api_performance(botclient, fee): exchange='binance', stake_amount=1, open_rate=0.245441, - open_order_id="123456", is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, @@ -1043,7 +1042,6 @@ def test_api_performance(botclient, fee): stake_amount=1, exchange='binance', open_rate=0.412, - open_order_id="123456", is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, @@ -1066,11 +1064,11 @@ def test_api_performance(botclient, fee): @pytest.mark.parametrize( - 'is_short,current_rate,open_order_id,open_trade_value', - [(True, 1.098e-05, 'dry_run_buy_short_12345', 15.0911775), - (False, 1.099e-05, 'dry_run_buy_long_12345', 15.1668225)]) + 'is_short,current_rate,open_trade_value', + [(True, 1.098e-05, 15.0911775), + (False, 1.099e-05, 15.1668225)]) def test_api_status(botclient, mocker, ticker, fee, markets, is_short, - current_rate, open_order_id, open_trade_value): + current_rate, open_trade_value): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -1111,7 +1109,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, - 'open_order': None, 'open_rate': 0.123, 'pair': 'ETH/BTC', 'base_currency': 'ETH', @@ -1144,7 +1141,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, "is_short": is_short, 'max_rate': ANY, 'min_rate': ANY, - 'open_order_id': open_order_id, 'open_rate_requested': ANY, 'open_trade_value': open_trade_value, 'exit_reason': None, @@ -1162,6 +1158,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'price_precision': None, 'precision_mode': None, 'orders': [ANY], + 'has_open_orders': True, } mocker.patch(f'{EXMS}.get_rate', @@ -1291,7 +1288,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): exchange='binance', stake_amount=1, open_rate=0.245441, - open_order_id="123456", open_date=datetime.now(timezone.utc), is_open=False, is_short=False, @@ -1352,7 +1348,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'is_short': False, 'max_rate': None, 'min_rate': None, - 'open_order_id': '123456', 'open_rate_requested': None, 'open_trade_value': 0.24605460, 'exit_reason': None, @@ -1369,6 +1364,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'amount_precision': None, 'price_precision': None, 'precision_mode': None, + 'has_open_orders': False, 'orders': [], } diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d40af508e..886012535 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -872,7 +872,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade.is_short = is_short assert trade assert trade.is_open is True - assert trade.open_order_id == '22' + assert trade.has_open_orders + assert '22' in trade.open_orders_ids # Test calling with price open_order['id'] = '33' @@ -898,7 +899,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade = Trade.session.scalars(select(Trade)).all()[2] trade.is_short = is_short assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == 10 assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8) assert pytest.approx(trade.liquidation_price) == liq_price @@ -916,7 +917,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_order, trade = Trade.session.scalars(select(Trade)).all()[3] trade.is_short = is_short assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == 0.5 assert trade.stake_amount == round(order['average'] * order['filled'] / leverage, 8) @@ -1118,7 +1119,6 @@ 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.open_order_id = None trade.stoploss_order_id = None trade.is_open = True trades = [trade] @@ -1163,7 +1163,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.open_order_id = None trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1174,7 +1173,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # should do nothing and return false stop_order_dict.update({'id': "102"}) trade.is_open = True - trade.open_order_id = None trade.stoploss_order_id = "102" trade.orders.append( Order( @@ -1198,7 +1196,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ # should set a stoploss immediately and return False caplog.clear() trade.is_open = True - trade.open_order_id = None trade.stoploss_order_id = "102" canceled_stoploss_order = MagicMock(return_value={'id': '103_1', 'status': 'canceled'}) @@ -1223,7 +1220,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.open_order_id = None trade.stoploss_order_id = "104" trade.orders.append(Order( ft_order_side='stoploss', @@ -1351,7 +1347,6 @@ def test_handle_stoploss_on_exchange_partial( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.open_order_id = None trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1410,7 +1405,6 @@ 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.open_order_id = None trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1468,8 +1462,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': enter_order['id']}, - {'id': exit_order['id']}, + enter_order, + exit_order, ]), get_fee=fee, ) @@ -1485,7 +1479,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.open_order_id = None trade.stoploss_order_id = "100" trade.orders.append( Order( @@ -1663,7 +1656,6 @@ def test_handle_stoploss_on_exchange_trailing( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.open_order_id = None trade.stoploss_order_id = '100' trade.stoploss_last_update = dt_now() - timedelta(minutes=20) trade.orders.append( @@ -1793,7 +1785,6 @@ 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.open_order_id = None trade.stoploss_order_id = "abcd" trade.stop_loss = 0.2 trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None) @@ -1873,8 +1864,8 @@ def test_handle_stoploss_on_exchange_custom_stop( 'last': 1.9 }), create_order=MagicMock(side_effect=[ - {'id': enter_order['id']}, - {'id': exit_order['id']}, + enter_order, + exit_order, ]), get_fee=fee, ) @@ -1907,7 +1898,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.open_order_id = None trade.stoploss_order_id = '100' trade.stoploss_last_update = dt_now() - timedelta(minutes=601) trade.orders.append( @@ -2045,7 +2035,6 @@ 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.open_order_id = None trade.stoploss_order_id = '100' trade.stoploss_last_update = dt_now() trade.orders.append( @@ -2152,7 +2141,6 @@ def test_exit_positions(mocker, default_conf_usdt, limit_order, is_short, caplog order_id = '123' trade = Trade( - open_order_id=order_id, pair='ETH/USDT', fee_open=0.001, fee_close=0.001, @@ -2198,7 +2186,6 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog order_id = '123' trade = Trade( - open_order_id=order_id, pair='ETH/USDT', fee_open=0.001, fee_close=0.001, @@ -2217,9 +2204,9 @@ def test_exit_positions_exception(mocker, default_conf_usdt, limit_order, caplog ft_amount=trade.amount, ft_price=trade.open_rate, order_id=order_id, + ft_is_open=False, )) - trade.open_order_id = None Trade.session.add(trade) Trade.commit() freqtrade.wallets.update() @@ -2248,7 +2235,6 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca order_id = order['id'] trade = Trade( - open_order_id=order_id, fee_open=0.001, fee_close=0.001, open_rate=0.01, @@ -2272,19 +2258,17 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_order, is_short, ca # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) caplog.clear() - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.amount == order['amount'] - trade.open_order_id = order_id mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=0.01) assert trade.amount == 30.0 # test amount modified by fee-logic freqtrade.update_trade_state(trade, order_id) assert trade.amount == 29.99 - assert trade.open_order_id is None + assert not trade.has_open_orders trade.is_open = True - trade.open_order_id = None # Assert we call handle_trade() if trade is feasible for execution freqtrade.update_trade_state(trade, order_id) @@ -2328,7 +2312,6 @@ def test_update_trade_state_withorderdict( open_date=dt_now(), fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id=order_id, is_open=True, leverage=1, is_short=is_short, @@ -2361,15 +2344,15 @@ def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit # TODO: should not be magicmock trade = MagicMock() - trade.open_order_id = '123' trade.amount = 123 + open_order_id = '123' # Test raise of OperationalException exception mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=DependencyException() ) - freqtrade.update_trade_state(trade, trade.open_order_id) + freqtrade.update_trade_state(trade, open_order_id) assert log_has('Could not update trade amount: ', caplog) @@ -2379,13 +2362,13 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) -> # TODO: should not be magicmock trade = MagicMock() - trade.open_order_id = '123' + open_order_id = '123' # Test raise of OperationalException exception grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) - freqtrade.update_trade_state(trade, trade.open_order_id) + freqtrade.update_trade_state(trade, open_order_id) assert grm_mock.call_count == 0 - assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) + assert log_has(f'Unable to fetch order {open_order_id}: ', caplog) @pytest.mark.parametrize("is_short", [False, True]) @@ -2413,7 +2396,6 @@ def test_update_trade_state_sell( fee_open=0.0025, fee_close=0.0025, open_date=dt_now(), - open_order_id=open_order['id'], is_open=True, interest_rate=0.0005, leverage=1, @@ -2425,7 +2407,7 @@ def test_update_trade_state_sell( order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', exit_side(is_short)) trade.orders.append(order) assert order.status == 'open' - freqtrade.update_trade_state(trade, trade.open_order_id, l_order) + freqtrade.update_trade_state(trade, trade.open_orders_ids[-1], l_order) assert trade.amount == l_order['amount'] # Wallet needs to be updated after closing a limit-sell order to reenable buying assert wallet_mock.call_count == 1 @@ -2475,7 +2457,7 @@ def test_handle_trade( patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short, exit_tag='sell_signal1') assert freqtrade.handle_trade(trade) is True - assert trade.open_order_id == exit_order['id'] + assert trade.open_orders_ids[-1] == exit_order['id'] # Simulate fulfilled LIMIT_SELL order for trade trade.orders[-1].ft_is_open = False @@ -2706,7 +2688,7 @@ def test_manage_open_orders_entry_usercustom( ) -> None: old_order = limit_sell_order_old if is_short else limit_buy_order_old - old_order['id'] = open_trade.open_order_id + old_order['id'] = open_trade.open_orders_ids[0] default_conf_usdt["unfilledtimeout"] = {"entry": 1400, "exit": 30} @@ -2741,7 +2723,11 @@ def test_manage_open_orders_entry_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() nb_trades = len(trades) assert nb_trades == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 1 @@ -2750,7 +2736,11 @@ def test_manage_open_orders_entry_usercustom( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() nb_trades = len(trades) assert nb_trades == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 1 @@ -2761,7 +2751,11 @@ def test_manage_open_orders_entry_usercustom( assert cancel_order_wr_mock.call_count == 1 assert rpc_mock.call_count == 2 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() nb_trades = len(trades) assert nb_trades == 0 assert freqtrade.strategy.check_entry_timeout.call_count == 1 @@ -2774,7 +2768,7 @@ def test_manage_open_orders_entry( ) -> None: old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) - open_trade.open_order_id = old_order['id'] + order = Order.parse_from_ccxt_object(old_order, 'mocked', 'buy') open_trade.orders[0] = order limit_buy_cancel = deepcopy(old_order) @@ -2801,7 +2795,11 @@ def test_manage_open_orders_entry( assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 2 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() nb_trades = len(trades) assert nb_trades == 0 # Custom user entry-timeout is never called @@ -2817,7 +2815,7 @@ def test_adjust_entry_cancel( ) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) old_order = limit_sell_order_old if is_short else limit_buy_order_old - old_order['id'] = open_trade.open_order_id + old_order['id'] = open_trade.open_orders[0].order_id limit_buy_cancel = deepcopy(old_order) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) @@ -2840,7 +2838,10 @@ def test_adjust_entry_cancel( freqtrade.strategy.adjust_entry_price = MagicMock(return_value=None) freqtrade.manage_open_orders() trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_trade_id == Trade.id) + ).all() + assert len(trades) == 0 assert len(Order.session.scalars(select(Order)).all()) == 0 assert log_has_re( @@ -2859,7 +2860,7 @@ def test_adjust_entry_maintain_replace( ) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) old_order = limit_sell_order_old if is_short else limit_buy_order_old - old_order['id'] = open_trade.open_order_id + old_order['id'] = open_trade.open_orders_ids[0] limit_buy_cancel = deepcopy(old_order) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) @@ -2868,7 +2869,8 @@ def test_adjust_entry_maintain_replace( fetch_ticker=ticker_usdt, fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_mock, - get_fee=fee + get_fee=fee, + _dry_is_price_crossed=MagicMock(return_value=False), ) open_trade.is_short = is_short @@ -2882,7 +2884,10 @@ def test_adjust_entry_maintain_replace( freqtrade.strategy.adjust_entry_price = MagicMock(return_value=old_order['price']) freqtrade.manage_open_orders() trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 1 assert len(Order.get_open_orders()) == 1 # Entry adjustment is called @@ -2891,9 +2896,16 @@ def test_adjust_entry_maintain_replace( # Check that order is replaced freqtrade.get_valid_enter_price_and_stake = MagicMock(return_value={100, 10, 1}) freqtrade.strategy.adjust_entry_price = MagicMock(return_value=1234) + freqtrade.manage_open_orders() + + assert freqtrade.strategy.adjust_entry_price.call_count == 1 + trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 1 nb_all_orders = len(Order.session.scalars(select(Order)).all()) assert nb_all_orders == 2 @@ -2917,6 +2929,8 @@ def test_check_handle_cancelled_buy( cancel_order_mock = MagicMock() patch_exchange(mocker) old_order.update({"status": "canceled", 'filled': 0.0}) + old_order['side'] = 'buy' if is_short else 'sell' + old_order['id'] = open_trade.open_orders[0].order_id mocker.patch.multiple( EXMS, fetch_ticker=ticker_usdt, @@ -2925,7 +2939,6 @@ def test_check_handle_cancelled_buy( get_fee=fee ) freqtrade = FreqtradeBot(default_conf_usdt) - open_trade.orders = [] open_trade.is_short = is_short Trade.session.add(open_trade) Trade.commit() @@ -2935,10 +2948,13 @@ def test_check_handle_cancelled_buy( assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 2 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 0 - assert log_has_re( - f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog) + exit_name = 'Buy' if is_short else 'Sell' + assert log_has_re(f"{exit_name} order cancelled on exchange for Trade.*", caplog) @pytest.mark.parametrize("is_short", [False, True]) @@ -2966,10 +2982,7 @@ def test_manage_open_orders_buy_exception( freqtrade.manage_open_orders() assert cancel_order_mock.call_count == 0 assert rpc_mock.call_count == 1 - trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() - nb_trades = len(trades) - assert nb_trades == 1 + assert len(open_trade.open_orders) == 1 @pytest.mark.parametrize("is_short", [False, True]) @@ -2978,7 +2991,7 @@ def test_manage_open_orders_exit_usercustom( is_short, open_trade_usdt, caplog ) -> None: default_conf_usdt["unfilledtimeout"] = {"entry": 1440, "exit": 1440, "exit_timeout_count": 1} - open_trade_usdt.open_order_id = limit_sell_order_old['id'] + if is_short: limit_sell_order_old['side'] = 'buy' open_trade_usdt.is_short = is_short @@ -3035,13 +3048,10 @@ def test_manage_open_orders_exit_usercustom( assert rpc_mock.call_count == 2 assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_entry_timeout.call_count == 0 - trade = Trade.session.scalars(select(Trade)).first() - # cancelling didn't succeed - order-id remains open. - assert trade.open_order_id is not None # 2nd canceled trade - Fail execute exit caplog.clear() - open_trade_usdt.open_order_id = limit_sell_order_old['id'] + mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit', side_effect=DependencyException) @@ -3051,7 +3061,6 @@ def test_manage_open_orders_exit_usercustom( et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') caplog.clear() # 2nd canceled trade ... - open_trade_usdt.open_order_id = limit_sell_order_old['id'] # If cancelling fails - no emergency exit! with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False): @@ -3069,7 +3078,7 @@ def test_manage_open_orders_exit( ) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() - limit_sell_order_old['id'] = open_trade_usdt.open_order_id + limit_sell_order_old['id'] = '123456789_exit' limit_sell_order_old['side'] = 'buy' if is_short else 'sell' patch_exchange(mocker) mocker.patch.multiple( @@ -3111,7 +3120,7 @@ def test_check_handle_cancelled_exit( cancel_order_mock = MagicMock() limit_sell_order_old.update({"status": "canceled", 'filled': 0.0}) limit_sell_order_old['side'] = 'buy' if is_short else 'sell' - limit_sell_order_old['id'] = open_trade_usdt.open_order_id + limit_sell_order_old['id'] = open_trade_usdt.open_orders[0].order_id patch_exchange(mocker) mocker.patch.multiple( @@ -3148,7 +3157,8 @@ def test_manage_open_orders_partial( open_trade.is_short = is_short open_trade.leverage = leverage open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' - limit_buy_order_old_partial['id'] = open_trade.open_order_id + + limit_buy_order_old_partial['id'] = open_trade.orders[0].order_id limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy' limit_buy_canceled = deepcopy(limit_buy_order_old_partial) limit_buy_canceled['status'] = 'canceled' @@ -3172,11 +3182,13 @@ def test_manage_open_orders_partial( assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 3 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + ).all() assert len(trades) == 1 assert trades[0].amount == 23.0 assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount / leverage assert trades[0].stake_amount != prior_stake + assert not trades[0].has_open_orders @pytest.mark.parametrize("is_short", [False, True]) @@ -3188,8 +3200,8 @@ def test_manage_open_orders_partial_fee( open_trade.is_short = is_short open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' rpc_mock = patch_RPCManager(mocker) - limit_buy_order_old_partial['id'] = open_trade.open_order_id - limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id + limit_buy_order_old_partial['id'] = open_trade.orders[0].order_id + limit_buy_order_old_partial_canceled['id'] = open_trade.open_orders_ids[0] limit_buy_order_old_partial['side'] = 'sell' if is_short else 'buy' limit_buy_order_old_partial_canceled['side'] = 'sell' if is_short else 'buy' @@ -3220,12 +3232,14 @@ def test_manage_open_orders_partial_fee( assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 3 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 1 # Verify that trade has been updated assert trades[0].amount == (limit_buy_order_old_partial['amount'] - limit_buy_order_old_partial['remaining']) - 0.023 - assert trades[0].open_order_id is None + assert not trades[0].has_open_orders assert trades[0].fee_updated(open_trade.entry_side) assert pytest.approx(trades[0].fee_open) == 0.001 @@ -3239,8 +3253,8 @@ def test_manage_open_orders_partial_except( open_trade.is_short = is_short open_trade.orders[0].ft_order_side = 'sell' if is_short else 'buy' rpc_mock = patch_RPCManager(mocker) - limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id - limit_buy_order_old_partial['id'] = open_trade.open_order_id + limit_buy_order_old_partial_canceled['id'] = open_trade.open_orders_ids[0] + limit_buy_order_old_partial['id'] = open_trade.open_orders_ids[0] if is_short: limit_buy_order_old_partial['side'] = 'sell' cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) @@ -3271,13 +3285,14 @@ def test_manage_open_orders_partial_except( assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 3 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_(open_trade.open_order_id))).all() + select(Trade) + ).all() assert len(trades) == 1 # Verify that trade has been updated assert trades[0].amount == (limit_buy_order_old_partial['amount'] - limit_buy_order_old_partial['remaining']) - assert trades[0].open_order_id is None + assert not trades[0].has_open_orders assert trades[0].fee_open == fee() @@ -3335,32 +3350,34 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order, is_ l_order['filled'] = 0.0 l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_enter(trade, l_order, reason) + assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() caplog.clear() l_order['filled'] = 0.01 - assert not freqtrade.handle_cancel_enter(trade, l_order, reason) + assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason) assert cancel_order_mock.call_count == 0 assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unexitable.*", caplog) caplog.clear() cancel_order_mock.reset_mock() l_order['filled'] = 2 - assert not freqtrade.handle_cancel_enter(trade, l_order, reason) + assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason) assert cancel_order_mock.call_count == 1 # Order remained open for some reason (cancel failed) cancel_buy_order['status'] = 'open' cancel_order_mock = MagicMock(return_value=cancel_buy_order) - trade.open_order_id = 'some_open_order' + mocker.patch(f'{EXMS}.cancel_order_with_result', cancel_order_mock) - assert not freqtrade.handle_cancel_enter(trade, l_order, reason) + assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) # min_pair_stake empty should not crash mocker.patch(f'{EXMS}.get_min_pair_stake_amount', return_value=None) - assert not freqtrade.handle_cancel_enter(trade, limit_order[entry_side(is_short)], reason) + assert not freqtrade.handle_cancel_enter( + trade, limit_order[entry_side(is_short)], trade.open_orders_ids[0], reason + ) @pytest.mark.parametrize("is_short", [False, True]) @@ -3381,7 +3398,9 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho trade = mock_trade_usdt_4(fee, is_short) Trade.session.add(trade) Trade.commit() - assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) + assert freqtrade.handle_cancel_enter( + trade, limit_buy_order_canceled_empty, trade.open_orders_ids[0], reason + ) assert cancel_order_mock.call_count == 0 assert log_has_re( f'{trade.entry_side.capitalize()} order fully cancelled. ' @@ -3418,7 +3437,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order l_order['filled'] = 0.0 l_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_enter(trade, l_order, reason) + assert freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() @@ -3426,7 +3445,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order order = deepcopy(l_order) order['status'] = 'canceled' mocker.patch(f'{EXMS}.fetch_order', return_value=order) - assert not freqtrade.handle_cancel_enter(trade, l_order, reason) + assert not freqtrade.handle_cancel_enter(trade, l_order, trade.open_orders_ids[0], reason) assert cancel_order_mock.call_count == 1 @@ -3456,7 +3475,6 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short, amount=amount * leverage, exchange='binance', open_rate=entry_price, - open_order_id="sell_123456", open_date=dt_now() - timedelta(days=2), fee_open=fee.return_value, fee_close=fee.return_value, @@ -3509,26 +3527,26 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short, 'status': "open"} reason = CANCEL_REASON['TIMEOUT'] send_msg_mock.reset_mock() - assert freqtrade.handle_cancel_exit(trade, order, reason) + assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 assert trade.close_rate is None assert trade.exit_reason is None - assert trade.open_order_id is None + assert not trade.has_open_orders send_msg_mock.reset_mock() # Partial exit - below exit threshold order['amount'] = amount * leverage order['filled'] = amount * 0.99 * leverage - assert not freqtrade.handle_cancel_exit(trade, order, reason) + assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason) # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 assert (send_msg_mock.call_args_list[0][0][0]['reason'] == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']) - assert not freqtrade.handle_cancel_exit(trade, order, reason) + assert not freqtrade.handle_cancel_exit(trade, order, order['id'], reason) assert (send_msg_mock.call_args_list[0][0][0]['reason'] == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']) @@ -3540,7 +3558,7 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee, is_short, send_msg_mock.reset_mock() order['filled'] = amount * 0.5 * leverage - assert freqtrade.handle_cancel_exit(trade, order, reason) + assert freqtrade.handle_cancel_exit(trade, order, order['id'], reason) assert send_msg_mock.call_count == 1 assert (send_msg_mock.call_args_list[0][0][0]['reason'] == CANCEL_REASON['PARTIALLY_FILLED']) @@ -3556,17 +3574,16 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: # TODO: should not be magicmock trade = MagicMock() - trade.open_order_id = '125' + order_id = '125' reason = CANCEL_REASON['TIMEOUT'] order = {'remaining': 1, 'id': '125', 'amount': 1, 'status': "open"} - assert not freqtrade.handle_cancel_exit(trade, order, reason) + assert not freqtrade.handle_cancel_exit(trade, order, order_id, reason) # mocker.patch(f'{EXMS}.cancel_order_with_result', return_value=order) # assert not freqtrade.handle_cancel_exit(trade, order, reason) - # assert trade.open_order_id == '125' @pytest.mark.parametrize("is_short, open_rate, amt", [ @@ -4013,7 +4030,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( freqtrade.exit_positions(trades) assert trade assert trade.stoploss_order_id == '123' - assert trade.open_order_id is None + assert not trade.has_open_orders # Assuming stoploss on exchange is hit # stoploss_order_id should become None @@ -4308,7 +4325,6 @@ def test__safe_exit_amount(default_conf_usdt, fee, caplog, mocker, amount_wallet amount=amount, exchange='binance', open_rate=0.245441, - open_order_id="123456", fee_open=fee.return_value, fee_close=fee.return_value, ) @@ -4651,7 +4667,6 @@ def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fe open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4679,7 +4694,6 @@ def test_get_real_amount_quote_dust(default_conf_usdt, trades_for_order, buy_ord open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4703,7 +4717,6 @@ def test_get_real_amount_no_trade(default_conf_usdt, buy_order_fee, caplog, mock open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4756,7 +4769,6 @@ def test_get_real_amount( fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.245441, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4803,8 +4815,7 @@ def test_get_real_amount_multi( exchange='binance', fee_open=fee.return_value, fee_close=fee.return_value, - open_rate=0.245441, - open_order_id="123456" + open_rate=0.245441 ) # Fake markets entry to enable fee parsing @@ -4849,7 +4860,6 @@ def test_get_real_amount_invalid_order(default_conf_usdt, trades_for_order, buy_ fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.245441, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4871,7 +4881,6 @@ def test_get_real_amount_fees_order(default_conf_usdt, market_buy_order_usdt_dou fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.245441, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4898,7 +4907,6 @@ def test_get_real_amount_wrong_amount(default_conf_usdt, trades_for_order, buy_o open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4923,7 +4931,6 @@ def test_get_real_amount_wrong_amount_rounding(default_conf_usdt, trades_for_ord fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.245441, - open_order_id="123456" ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -4942,7 +4949,6 @@ def test_get_real_amount_open_trade_usdt(default_conf_usdt, fee, mocker): open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456" ) order = { 'id': 'mocked_order', @@ -5002,8 +5008,7 @@ def test_get_real_amount_in_point(default_conf_usdt, buy_order_fee, fee, mocker, exchange='binance', fee_open=fee.return_value, fee_close=fee.return_value, - open_rate=0.245441, - open_order_id="123456" + open_rate=0.245441 ) limit_buy_order_usdt['amount'] = amount freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) @@ -5045,7 +5050,6 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker, caplog, open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456" ) order = Order( ft_order_side='buy', @@ -5083,8 +5087,7 @@ def test_apply_fee_conditional_multibuy(default_conf_usdt, fee, mocker, caplog, exchange='binance', open_rate=0.245441, fee_open=fee.return_value, - fee_close=fee.return_value, - open_order_id="123456" + fee_close=fee.return_value ) # One closed order order = Order( @@ -5558,7 +5561,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap return_value={'status': 'open'}) def reset_open_orders(trade): - trade.open_order_id = None + trade.stoploss_order_id = None trade.is_short = is_short @@ -5570,7 +5573,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap # No open order trade = trades[1] reset_open_orders(trade) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.stoploss_order_id is None freqtrade.handle_insufficient_funds(trade) @@ -5580,7 +5583,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 # No change to orderid - as update_trade_state is mocked - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.stoploss_order_id is None caplog.clear() @@ -5589,7 +5592,9 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap # Open buy order trade = trades[3] reset_open_orders(trade) - assert trade.open_order_id is None + + # This part in not relevant anymore + # assert not trade.has_open_orders assert trade.stoploss_order_id is None freqtrade.handle_insufficient_funds(trade) @@ -5598,7 +5603,7 @@ 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.open_order_id is not None + assert trade.has_open_orders assert trade.stoploss_order_id is None caplog.clear() @@ -5607,7 +5612,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap # Open stoploss order trade = trades[4] reset_open_orders(trade) - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.stoploss_order_id is None freqtrade.handle_insufficient_funds(trade) @@ -5616,7 +5621,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_fo.call_count == 1 assert mock_uts.call_count == 2 # stoploss_order_id is "refound" and added to the trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.stoploss_order_id is not None caplog.clear() @@ -5626,7 +5631,8 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap # Open sell order trade = trades[5] reset_open_orders(trade) - assert trade.open_order_id is None + # This part in not relevant anymore + # assert not trade.has_open_orders assert trade.stoploss_order_id is None freqtrade.handle_insufficient_funds(trade) @@ -5635,7 +5641,7 @@ 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 # sell-orderid is "refound" and added to the trade - assert trade.open_order_id == order['id'] + assert trade.open_orders_ids[0] == order['id'] assert trade.stoploss_order_id is None caplog.clear() @@ -5662,10 +5668,7 @@ def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_shor exit_order, ]) - order_id = entry_order['id'] - trade = Trade( - open_order_id=order_id, pair='ETH/USDT', fee_open=0.001, fee_close=0.001, @@ -5989,7 +5992,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == 11 assert trade.stake_amount == 110 @@ -5999,7 +6002,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == 11 assert trade.stake_amount == 110 assert not trade.fee_updated('buy') @@ -6009,7 +6012,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == 11 assert trade.stake_amount == 110 assert not trade.fee_updated('buy') @@ -6037,7 +6040,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: assert len(orders) == 2 trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id == '651' + assert '651' in trade.open_orders_ids assert trade.open_rate == 11 assert trade.amount == 10 assert trade.stake_amount == 110 @@ -6074,7 +6077,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Assert trade is as expected trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id == '651' + assert '651' in trade.open_orders_ids assert trade.open_rate == 11 assert trade.amount == 10 assert trade.stake_amount == 110 @@ -6111,7 +6114,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Assert trade is as expected (averaged dca) trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert pytest.approx(trade.open_rate) == 9.90909090909 assert trade.amount == 22 assert pytest.approx(trade.stake_amount) == 218 @@ -6153,7 +6156,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Assert trade is as expected (averaged dca) trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert pytest.approx(trade.open_rate) == 8.729729729729 assert trade.amount == 37 assert trade.stake_amount == 323 @@ -6191,7 +6194,7 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Assert trade is as expected (averaged dca) trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.is_open assert trade.amount == 22 assert trade.stake_amount == 192.05405405405406 @@ -6268,7 +6271,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == bid assert trade.stake_amount == bid * amount @@ -6278,7 +6281,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == bid assert trade.stake_amount == bid * amount assert not trade.fee_updated(trade.entry_side) @@ -6288,7 +6291,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.open_rate == bid assert trade.stake_amount == bid * amount assert not trade.fee_updated(trade.entry_side) @@ -6323,7 +6326,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.amount == 50 assert trade.open_rate == 11 assert trade.stake_amount == 550 @@ -6365,7 +6368,7 @@ def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.amount == 50 assert trade.open_rate == 11 assert trade.stake_amount == 550 @@ -6465,7 +6468,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: assert trade if idx < len(data) - 1: assert trade.is_open is True - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.amount == result[0] assert trade.open_rate == result[1] assert trade.stake_amount == result[2] @@ -6478,7 +6481,7 @@ def test_position_adjust3(mocker, default_conf_usdt, fee, data) -> None: trade = Trade.session.scalars(select(Trade)).first() assert trade - assert trade.open_order_id is None + assert not trade.has_open_orders assert trade.is_open is False diff --git a/tests/test_integration.py b/tests/test_integration.py index 520cb4244..ee1d4bbb3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -103,7 +103,6 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, trade.orders.append(oobj) trade.stoploss_order_id = f"stop{idx}" - trade.open_order_id = None n = freqtrade.exit_positions(trades) assert n == 2 @@ -194,8 +193,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati for trade in trades: assert pytest.approx(trade.stake_amount) == result1 - # Reset trade open order id's - trade.open_order_id = None + trades = Trade.get_open_trades() assert len(trades) == 5 bals = freqtrade.wallets.get_all_balances() @@ -386,7 +384,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) assert len(Trade.get_trades().all()) == 1 trade: Trade = Trade.get_trades().first() assert len(trade.orders) == 1 - assert trade.open_order_id is not None + assert trade.has_open_orders assert pytest.approx(trade.stake_amount) == 60 assert trade.open_rate == 1.96 assert trade.stop_loss_pct == -0.1 @@ -399,7 +397,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 1 - assert trade.open_order_id is not None + assert trade.has_open_orders assert pytest.approx(trade.stake_amount) == 60 # Cancel order and place new one @@ -407,7 +405,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 2 - assert trade.open_order_id is not None + assert trade.has_open_orders # Open rate is not adjusted yet assert trade.open_rate == 1.96 assert trade.stop_loss_pct == -0.1 @@ -421,7 +419,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 2 - assert trade.open_order_id is None + assert not trade.has_open_orders # Open rate is not adjusted yet assert trade.open_rate == 1.99 assert pytest.approx(trade.stake_amount) == 60 @@ -438,7 +436,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 3 - assert trade.open_order_id is not None + assert trade.has_open_orders assert trade.open_rate == 1.99 assert trade.orders[-1].price == 1.96 assert trade.orders[-1].cost == 120 * leverage @@ -449,7 +447,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 4 - assert trade.open_order_id is not None + assert trade.has_open_orders assert trade.open_rate == 1.99 assert pytest.approx(trade.stake_amount) == 60 assert trade.orders[-1].price == 1.95 @@ -463,7 +461,7 @@ def test_dca_order_adjust(default_conf_usdt, ticker_usdt, leverage, fee, mocker) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 4 - assert trade.open_order_id is None + assert not trade.has_open_orders assert pytest.approx(trade.open_rate) == 1.963153456 assert trade.orders[-1].price == 1.95 assert pytest.approx(trade.orders[-1].cost) == 120 * leverage @@ -522,7 +520,11 @@ def test_dca_order_adjust_entry_replace_fails( freqtrade.enter_positions() trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_not(None))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 1 mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=False) @@ -539,14 +541,22 @@ def test_dca_order_adjust_entry_replace_fails( assert freqtrade.strategy.adjust_trade_position.call_count == 1 trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_not(None))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 2 # We now have 2 orders open freqtrade.strategy.adjust_entry_price = MagicMock(return_value=2.05) freqtrade.manage_open_orders() trades = Trade.session.scalars( - select(Trade).filter(Trade.open_order_id.is_not(None))).all() + select(Trade) + .where(Order.ft_is_open.is_(True)) + .where(Order.ft_order_side != "stoploss") + .where(Order.ft_trade_id == Trade.id) + ).all() assert len(trades) == 2 assert len(Order.get_open_orders()) == 2 # Entry adjustment is called