diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1ee96d015..d54c0e5a9 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -872,64 +872,78 @@ class Backtesting: self.protections.stop_per_pair(pair, current_time) self.protections.global_stop(current_time) - def check_order_replace(self, trade: LocalTrade, current_time, row: Tuple) -> bool: + def manage_open_orders(self, trade: LocalTrade, current_time, row: Tuple) -> bool: """ - Check if an entry order has to be replaced and do so. If user requested cancellation - and there are no filled orders in the trade will instruct caller to delete the trade. + Check if any open order needs to be cancelled or replaced. Returns True if the trade should be deleted. """ for order in [o for o in trade.orders if o.ft_is_open]: - # only check on new candles for open entry orders - if order.side == trade.entry_side and current_time > order.order_date_utc: - requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price, - default_retval=order.price)( - trade=trade, order=order, pair=trade.pair, current_time=current_time, - proposed_rate=row[OPEN_IDX], current_order_rate=order.price, - entry_tag=trade.enter_tag, side=trade.trade_direction - ) # default value is current order price - - # cancel existing order whenever a new rate is requested (or None) - if requested_rate == order.price: - # assumption: there can't be multiple open entry orders at any given time - return False - else: - del trade.orders[trade.orders.index(order)] - - # place new order if None was not returned - if requested_rate: - self._enter_trade(pair=trade.pair, row=row, trade=trade, - requested_rate=requested_rate, - requested_stake=(order.remaining * order.price), - direction='short' if trade.is_short else 'long') - else: - # assumption: there can't be multiple open entry orders at any given time - return (trade.nr_of_successful_entries == 0) + if self.check_order_cancel(trade, order, current_time): + # delete trade due to order timeout + return True + elif self.check_order_replace(trade, order, current_time, row): + # delete trade due to user request + return True + # default maintain trade return False - def check_order_cancel(self, trade: LocalTrade, current_time) -> bool: + def check_order_cancel(self, trade: LocalTrade, order: Order, current_time) -> bool: """ - Check if an order has been canceled. + Check if current analyzed order has to be canceled. Returns True if the trade should be Deleted (initial order was canceled). """ - for order in [o for o in trade.orders if o.ft_is_open]: - - timedout = self.strategy.ft_check_timed_out(trade, order, current_time) - if timedout: - if order.side == trade.entry_side: - self.timedout_entry_orders += 1 - if trade.nr_of_successful_entries == 0: - # Remove trade due to entry timeout expiration. - return True - else: - # Close additional entry order - del trade.orders[trade.orders.index(order)] - if order.side == trade.exit_side: - self.timedout_exit_orders += 1 - # Close exit order and retry exiting on next signal. + timedout = self.strategy.ft_check_timed_out(trade, order, current_time) + if timedout: + if order.side == trade.entry_side: + self.timedout_entry_orders += 1 + if trade.nr_of_successful_entries == 0: + # Remove trade due to entry timeout expiration. + return True + else: + # Close additional entry order del trade.orders[trade.orders.index(order)] + 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)] return False + def check_order_replace(self, trade: LocalTrade, order: Order, current_time, + row: Tuple) -> bool: + """ + Check if current analyzed entry order has to be replaced and do so. + If user requested cancellation and there are no filled orders in the trade will + instruct caller to delete the trade. + Returns True if the trade should be deleted. + """ + # only check on new candles for open entry orders + if order.side == trade.entry_side and current_time > order.order_date_utc: + requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price, + default_retval=order.price)( + trade=trade, order=order, pair=trade.pair, current_time=current_time, + proposed_rate=row[OPEN_IDX], current_order_rate=order.price, + entry_tag=trade.enter_tag, side=trade.trade_direction + ) # default value is current order price + + # cancel existing order whenever a new rate is requested (or None) + if requested_rate == order.price: + # assumption: there can't be multiple open entry orders at any given time + return False + else: + del trade.orders[trade.orders.index(order)] + + # place new order if None was not returned + if requested_rate: + self._enter_trade(pair=trade.pair, row=row, trade=trade, + requested_rate=requested_rate, + requested_stake=(order.remaining * order.price), + direction='short' if trade.is_short else 'long') + else: + # assumption: there can't be multiple open entry orders at any given time + return (trade.nr_of_successful_entries == 0) + return False + def validate_row( self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]: try: @@ -999,17 +1013,14 @@ class Backtesting: self.dataprovider._set_dataframe_max_index(row_index) for t in list(open_trades[pair]): - # 1. Cancel expired entry/exit orders. - order_cancel = self.check_order_cancel(t, current_time) - # 2. Replace/cancel (user requested) entry orders. - order_replace = self.check_order_replace(t, current_time, row) - if order_cancel or order_replace: - # Close trade due to entry timeout expiration or cancellation. + # 1. Manage currently open orders of active trades + if self.manage_open_orders(t, current_time, row): + # Close trade open_trade_count -= 1 open_trades[pair].remove(t) self.wallets.update() - # 3. Process entries. + # 2. Process entries. # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row @@ -1032,7 +1043,7 @@ class Backtesting: open_trades[pair].append(trade) for trade in list(open_trades[pair]): - # 4. Process entry orders. + # 3. Process entry orders. order = trade.select_order(trade.entry_side, is_open=True) if order and self._get_order_filled(order.price, row): order.close_bt_order(current_time) @@ -1040,11 +1051,11 @@ class Backtesting: LocalTrade.add_bt_trade(trade) self.wallets.update() - # 5. Create exit orders (if any) + # 4. Create exit orders (if any) if not trade.open_order_id: self._get_exit_trade_entry(trade, row) # Place exit order if necessary - # 6. Process exit orders. + # 5. Process exit orders. order = trade.select_order(trade.exit_side, is_open=True) if order and self._get_order_filled(order.price, row): trade.open_order_id = None