Backtest order timeout continued.

This commit is contained in:
Rokas Kupstys 2022-01-22 15:11:33 +02:00 committed by Matthias
parent 15698dd1ca
commit 9140679bf4

View File

@ -63,6 +63,8 @@ class Backtesting:
LoggingMixin.show_output = False
self.config = config
self.results: Dict[str, Any] = {}
self.trade_id_counter: int = 0
self.order_id_counter: int = 0
config['dry_run'] = True
self.run_ids: Dict[str, str] = {}
@ -409,7 +411,6 @@ class Backtesting:
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
# call the custom exit price,with default value as previous closerate
current_profit = trade.calc_profit_ratio(closerate)
order_closed = True
if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
# Custom exit pricing only for sell-signals
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
@ -417,7 +418,6 @@ class Backtesting:
pair=trade.pair, trade=trade,
current_time=sell_row[DATE_IDX],
proposed_rate=closerate, current_profit=current_profit)
order_closed = self._get_order_filled(closerate, sell_row)
# Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['sell']
@ -440,20 +440,26 @@ class Backtesting:
):
trade.sell_reason = sell_row[EXIT_TAG_IDX]
trade.close(closerate, show_msg=False)
self.order_id_counter += 1
order = Order(
ft_is_open=order_closed,
id=self.order_id_counter,
ft_trade_id=trade.id,
order_date=sell_row[DATE_IDX].to_pydatetime(),
order_update_date=sell_row[DATE_IDX].to_pydatetime(),
ft_is_open=True,
ft_pair=trade.pair,
order_id=str(self.order_id_counter),
symbol=trade.pair,
ft_order_side="buy",
side="buy",
order_type="market",
status="closed",
ft_order_side="sell",
side="sell",
order_type=self.strategy.order_types['sell'],
status="open",
price=closerate,
average=closerate,
amount=trade.amount,
filled=trade.amount,
cost=trade.amount * closerate
filled=0,
remaining=trade.amount,
cost=trade.amount * closerate,
)
trade.orders.append(order)
return trade
@ -494,10 +500,13 @@ class Backtesting:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])(
pair=pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate
order_type = self.strategy.order_types['buy']
propose_rate = row[OPEN_IDX]
if order_type == 'limit':
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])(
pair=pair, current_time=current_time,
proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
max_stake_amount = self.wallets.get_available_stake_amount()
@ -507,7 +516,7 @@ class Backtesting:
try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
except DependencyException:
return trade
return None
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
@ -522,7 +531,6 @@ class Backtesting:
# If not pos adjust, trade is None
return trade
order_type = self.strategy.order_types['buy']
time_in_force = self.strategy.order_time_in_force['sell']
# Confirm trade entry:
if not pos_adjust:
@ -533,16 +541,21 @@ class Backtesting:
return None
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1
amount = round(stake_amount / propose_rate, 8)
if trade is None:
# Enter trade
self.trade_id_counter += 1
trade = LocalTrade(
id=self.trade_id_counter,
open_order_id=self.order_id_counter,
pair=pair,
open_rate=propose_rate,
open_rate_requested=propose_rate,
open_date=current_time,
stake_amount=stake_amount,
amount=amount,
amount_requested=amount,
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
@ -554,29 +567,28 @@ class Backtesting:
order_filled = self._get_order_filled(propose_rate, row)
order = Order(
order_date=current_time,
id=self.order_id_counter,
ft_trade_id=trade.id,
ft_is_open=not order_filled,
ft_pair=trade.pair,
order_id=str(self.order_id_counter),
symbol=trade.pair,
ft_order_side="buy",
side="buy",
order_type="market",
status="closed",
order_type=order_type,
status="open",
order_date=current_time,
order_filled_date=current_time,
order_update_date=current_time,
price=propose_rate,
average=propose_rate,
amount=amount,
filled=amount if order_filled else 0,
remaining=0 if order_filled else amount,
cost=stake_amount + trade.fee_open
filled=0,
remaining=amount,
cost=stake_amount + trade.fee_open,
)
if not order_filled:
trade.open_order_id = 'buy'
trade.orders.append(order)
if pos_adjust:
trade.recalc_trade_from_orders()
trade.recalc_trade_from_orders()
return trade
@ -589,6 +601,8 @@ class Backtesting:
for pair in open_trades.keys():
if len(open_trades[pair]) > 0:
for trade in open_trades[pair]:
if trade.open_order_id:
continue
sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
@ -638,7 +652,7 @@ class Backtesting:
# Indexes per pair, so some pairs are allowed to have a missing start.
indexes: Dict = defaultdict(int)
tmp = start_date + timedelta(minutes=self.timeframe_min)
current_time = start_date + timedelta(minutes=self.timeframe_min)
open_trades: Dict[str, List[LocalTrade]] = defaultdict(list)
open_trade_count = 0
@ -647,7 +661,7 @@ class Backtesting:
(end_date - start_date) / timedelta(minutes=self.timeframe_min)))
# Loop timerange and get candle for each pair at that point in time
while tmp <= end_date:
while current_time <= end_date:
open_trade_count_start = open_trade_count
self.check_abort()
for i, pair in enumerate(data):
@ -662,48 +676,21 @@ class Backtesting:
continue
# Waits until the time-counter reaches the start of the data for this pair.
if row[DATE_IDX] > tmp:
if row[DATE_IDX] > current_time:
continue
row_index += 1
indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index)
# Check order filling
for open_trade in list(open_trades[pair]):
# TODO: should open orders be stored in a separate list?
if open_trade.open_order_id:
order = open_trade.select_order(is_open=True)
if order is None:
continue
if self._get_order_filled(order.price, row):
open_trade.open_order_id = None
order.ft_is_open = False
order.filled = order.price
order.remaining = 0
timeout = self.config['unfilledtimeout'].get(order.side, 0)
if 0 < timeout <= (tmp - order.order_date).seconds / 60:
open_trade.open_order_id = None
order.ft_is_open = False
order.filled = 0
order.remaining = 0
if order.side == 'buy':
# Close trade due to buy timeout expiration.
open_trade_count -= 1
open_trades[pair].remove(open_trade)
LocalTrade.trades_open.remove(open_trade)
# trades.append(trade_entry) # TODO: Needed or not?
elif order.side == 'sell':
# Close sell order and retry selling on next signal.
del open_trade.orders[open_trade.orders.index(order)]
# 1. Process buys.
# without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected
# don't open on the last row
if (
(position_stacking or len(open_trades[pair]) == 0)
and self.trade_slot_available(max_open_trades, open_trade_count_start)
and tmp != end_date
and current_time != end_date
and row[BUY_IDX] == 1
and row[SELL_IDX] != 1
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
@ -717,29 +704,60 @@ class Backtesting:
open_trade_count += 1
# logger.debug(f"{pair} - Emulate creation of new trade: {trade}.")
open_trades[pair].append(trade)
LocalTrade.add_bt_trade(trade)
for trade in list(open_trades[pair]):
# TODO: This could be avoided with a separate list
if trade.open_order_id:
continue
# also check the buying candle for sell conditions.
trade_entry = self._get_sell_trade_entry(trade, row)
# Sell occurred
if trade_entry:
# 2. Process buy orders.
order = trade.select_order('buy', is_open=True)
if order and self._get_order_filled(order.price, row):
order.order_filled_date = row[DATE_IDX]
trade.open_order_id = None
order.filled = order.amount
order.status = 'closed'
order.ft_is_open = False
LocalTrade.add_bt_trade(trade)
# 3. Create sell orders (if any)
if not trade.open_order_id:
self._get_sell_trade_entry(trade, row) # Place sell order if necessary
# 4. Process sell orders.
order = trade.select_order('sell', is_open=True)
if order and self._get_order_filled(order.price, row):
trade.open_order_id = None
order.order_filled_date = trade.close_date = row[DATE_IDX]
order.filled = order.amount
order.status = 'closed'
order.ft_is_open = False
trade.close(order.price, show_msg=False)
# logger.debug(f"{pair} - Backtesting sell {trade}")
open_trade_count -= 1
open_trades[pair].remove(trade)
LocalTrade.close_bt_trade(trade)
trades.append(trade_entry)
trades.append(trade)
if enable_protections:
self.protections.stop_per_pair(pair, row[DATE_IDX])
self.protections.global_stop(tmp)
self.protections.global_stop(current_time)
# 5. Cancel expired buy/sell orders.
for order in [o for o in trade.orders if o.ft_is_open]:
timeout = self.config['unfilledtimeout'].get(order.side, 0)
if 0 < timeout <= (current_time - order.order_date).seconds / 60:
trade.open_order_id = None
order.ft_is_open = False
order.filled = 0
order.remaining = 0
if order.side == 'buy':
# Close trade due to buy timeout expiration.
open_trade_count -= 1
open_trades[pair].remove(trade)
elif order.side == 'sell':
# Close sell order and retry selling on next signal.
del trade.orders[trade.orders.index(order)]
# Move time one configured time_interval ahead.
self.progress.increment()
tmp += timedelta(minutes=self.timeframe_min)
current_time += timedelta(minutes=self.timeframe_min)
trades += self.handle_left_open(open_trades, data=data)
self.wallets.update()