Refactor entry adjustment for backtesting.

This commit is contained in:
eSeR1805 2022-04-30 13:38:17 +03:00
parent f9977c26e7
commit ad0c5d9440
No known key found for this signature in database
GPG Key ID: BA53686259B46936

View File

@ -650,7 +650,7 @@ class Backtesting:
def get_valid_price_and_stake( def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float], self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
direction: LongShort, current_time: datetime, entry_tag: Optional[str], direction: LongShort, current_time: datetime, entry_tag: Optional[str],
trade: Optional[LocalTrade], order_type: str, readjust_req: Optional[bool] = False trade: Optional[LocalTrade], order_type: str
) -> Tuple[float, float, float, float]: ) -> Tuple[float, float, float, float]:
if order_type == 'limit': if order_type == 'limit':
@ -660,13 +660,6 @@ class Backtesting:
proposed_rate=propose_rate, entry_tag=entry_tag, proposed_rate=propose_rate, entry_tag=entry_tag,
side=direction, side=direction,
) # default value is the open rate ) # default value is the open rate
if readjust_req:
propose_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
default_retval=propose_rate)(
pair=pair, current_time=current_time,
proposed_rate=propose_rate, entry_tag=entry_tag,
side=direction
) # default value is open rate or custom rate from before
# We can't place orders higher than current high (otherwise it'd be a stop limit buy) # We can't place orders higher than current high (otherwise it'd be a stop limit buy)
# which freqtrade does not support in live. # which freqtrade does not support in live.
@ -675,7 +668,7 @@ class Backtesting:
else: else:
propose_rate = min(propose_rate, row[HIGH_IDX]) propose_rate = min(propose_rate, row[HIGH_IDX])
pos_adjust = trade is not None and readjust_req is False pos_adjust = trade is not None
leverage = trade.leverage if trade else 1.0 leverage = trade.leverage if trade else 1.0
if not pos_adjust: if not pos_adjust:
try: try:
@ -721,19 +714,24 @@ class Backtesting:
def _enter_trade(self, pair: str, row: Tuple, direction: LongShort, def _enter_trade(self, pair: str, row: Tuple, direction: LongShort,
stake_amount: Optional[float] = None, stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None, trade: Optional[LocalTrade] = None,
readjust_req: Optional[bool] = False) -> Optional[LocalTrade]: requested_rate: Optional[float] = None,
requested_stake: Optional[float] = None) -> Optional[LocalTrade]:
current_time = row[DATE_IDX].to_pydatetime() current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price # let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['entry'] order_type = self.strategy.order_types['entry']
pos_adjust = trade is not None and readjust_req is False pos_adjust = trade is not None and requested_rate is None
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake( propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade, pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
order_type, readjust_req order_type
) )
# replace proposed rate if another rate was requested
propose_rate = requested_rate if requested_rate else propose_rate
stake_amount = requested_stake if requested_stake else stake_amount
if not stake_amount: if not stake_amount:
# In case of pos adjust, still return the original trade # In case of pos adjust, still return the original trade
# If not pos adjust, trade is None # If not pos adjust, trade is None
@ -874,20 +872,36 @@ 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) -> None: def check_order_replace(self, trade: LocalTrade, current_time, row: Tuple) -> bool:
""" """
Check if an entry order has to be replaced and do so. Check if an entry order has to be replaced and do so. If user requested cancellation
Returns None. and there are no filled orders in the trade will instruct caller to delete the trade.
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 order.side == trade.entry_side and current_time > order.order_date_utc: if order.side == trade.entry_side and current_time > order.order_date_utc:
# cancel existing order requested_rate = strategy_safe_wrapper(self.strategy.adjust_entry_price,
del trade.orders[trade.orders.index(order)] 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
# place new order # cancel existing order whenever a new rate is requested (or None)
self._enter_trade(pair=trade.pair, row=row, trade=trade, if requested_rate != order.price:
direction='short' if trade.is_short else 'long', del trade.orders[trade.orders.index(order)]
readjust_req=True)
# 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 check_order_cancel(self, trade: LocalTrade, current_time) -> bool: def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
""" """
@ -983,15 +997,16 @@ class Backtesting:
for t in list(open_trades[pair]): for t in list(open_trades[pair]):
# 1. Cancel expired entry/exit orders. # 1. Cancel expired entry/exit orders.
if self.check_order_cancel(t, current_time): order_cancel = self.check_order_cancel(t, current_time)
# Close trade due to entry timeout expiration. # 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.
open_trade_count -= 1 open_trade_count -= 1
open_trades[pair].remove(t) open_trades[pair].remove(t)
self.wallets.update() self.wallets.update()
else:
self.check_order_replace(t, current_time, row)
# 2. Process entries. # 3. 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
@ -1014,7 +1029,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]):
# 3. Process entry orders. # 4. 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)
@ -1022,11 +1037,11 @@ class Backtesting:
LocalTrade.add_bt_trade(trade) LocalTrade.add_bt_trade(trade)
self.wallets.update() self.wallets.update()
# 4. Create exit orders (if any) # 5. 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
# 5. Process exit orders. # 6. 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