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