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

View File

@ -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