Backtest order timeout continued.
This commit is contained in:
parent
15698dd1ca
commit
9140679bf4
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user