BT: Refactor open order management.

This commit is contained in:
eSeR1805 2022-05-01 18:06:20 +03:00
parent 9d205132d0
commit 4e43194dfe
No known key found for this signature in database
GPG Key ID: BA53686259B46936
1 changed files with 66 additions and 55 deletions

View File

@ -872,64 +872,78 @@ class Backtesting:
self.protections.stop_per_pair(pair, current_time) self.protections.stop_per_pair(pair, current_time)
self.protections.global_stop(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 Check if any open order needs to be cancelled or replaced.
and there are no filled orders in the trade will instruct caller to delete the trade.
Returns True if the trade should be deleted. Returns True if the trade should be deleted.
""" """
for order in [o for o in trade.orders if o.ft_is_open]: for order in [o for o in trade.orders if o.ft_is_open]:
# only check on new candles for open entry orders if self.check_order_cancel(trade, order, current_time):
if order.side == trade.entry_side and current_time > order.order_date_utc: # delete trade due to order timeout
requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price, return True
default_retval=order.price)( elif self.check_order_replace(trade, order, current_time, row):
trade=trade, order=order, pair=trade.pair, current_time=current_time, # delete trade due to user request
proposed_rate=row[OPEN_IDX], current_order_rate=order.price, return True
entry_tag=trade.enter_tag, side=trade.trade_direction # default maintain trade
) # 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 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). 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:
timedout = self.strategy.ft_check_timed_out(trade, order, current_time) if order.side == trade.entry_side:
if timedout: self.timedout_entry_orders += 1
if order.side == trade.entry_side: if trade.nr_of_successful_entries == 0:
self.timedout_entry_orders += 1 # Remove trade due to entry timeout expiration.
if trade.nr_of_successful_entries == 0: return True
# Remove trade due to entry timeout expiration. else:
return True # Close additional entry order
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)] 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 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( def validate_row(
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]: self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
try: try:
@ -999,17 +1013,14 @@ class Backtesting:
self.dataprovider._set_dataframe_max_index(row_index) self.dataprovider._set_dataframe_max_index(row_index)
for t in list(open_trades[pair]): for t in list(open_trades[pair]):
# 1. Cancel expired entry/exit orders. # 1. Manage currently open orders of active trades
order_cancel = self.check_order_cancel(t, current_time) if self.manage_open_orders(t, current_time, row):
# 2. Replace/cancel (user requested) entry orders. # Close trade
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.
open_trade_count -= 1 open_trade_count -= 1
open_trades[pair].remove(t) open_trades[pair].remove(t)
self.wallets.update() self.wallets.update()
# 3. Process entries. # 2. Process entries.
# without positionstacking, we can only have one open trade per pair. # without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected # max_open_trades must be respected
# don't open on the last row # don't open on the last row
@ -1032,7 +1043,7 @@ class Backtesting:
open_trades[pair].append(trade) open_trades[pair].append(trade)
for trade in list(open_trades[pair]): 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) order = trade.select_order(trade.entry_side, is_open=True)
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time) order.close_bt_order(current_time)
@ -1040,11 +1051,11 @@ class Backtesting:
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
self.wallets.update() self.wallets.update()
# 5. Create exit orders (if any) # 4. Create exit orders (if any)
if not trade.open_order_id: if not trade.open_order_id:
self._get_exit_trade_entry(trade, row) # Place exit order if necessary 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) order = trade.select_order(trade.exit_side, is_open=True)
if order and self._get_order_filled(order.price, row): if order and self._get_order_filled(order.price, row):
trade.open_order_id = None trade.open_order_id = None