Merge pull request #6235 from freqtrade/backtest_order_timeout
Backtest order timeout
This commit is contained in:
commit
9f47853661
@ -313,6 +313,7 @@ A backtesting result will look like that:
|
|||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| Rejected Buy signals | 3089 |
|
| Rejected Buy signals | 3089 |
|
||||||
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| | |
|
| | |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
| Max balance | 0.01846651 BTC |
|
| Max balance | 0.01846651 BTC |
|
||||||
@ -400,6 +401,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
| Avg. Duration Winners | 4:23:00 |
|
| Avg. Duration Winners | 4:23:00 |
|
||||||
| Avg. Duration Loser | 6:55:00 |
|
| Avg. Duration Loser | 6:55:00 |
|
||||||
| Rejected Buy signals | 3089 |
|
| Rejected Buy signals | 3089 |
|
||||||
|
| Entry/Exit Timeouts | 0 / 0 |
|
||||||
| | |
|
| | |
|
||||||
| Min balance | 0.00945123 BTC |
|
| Min balance | 0.00945123 BTC |
|
||||||
| Max balance | 0.01846651 BTC |
|
| Max balance | 0.01846651 BTC |
|
||||||
@ -429,6 +431,7 @@ It contains some useful key metrics about performance of your strategy on backte
|
|||||||
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
|
||||||
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
|
||||||
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
|
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
|
||||||
|
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
|
||||||
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
|
||||||
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
|
||||||
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
- `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point.
|
||||||
|
@ -62,6 +62,7 @@ This loop will be repeated again and again until the bot is stopped.
|
|||||||
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
|
||||||
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
|
||||||
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
|
||||||
|
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks.
|
||||||
* Generate backtest report output
|
* Generate backtest report output
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
|
@ -389,8 +389,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate.
|
||||||
|
|
||||||
!!! Warning "Backtesting"
|
!!! Warning "Backtesting"
|
||||||
While Custom prices are supported in backtesting (starting with 2021.12), prices will be moved to within the candle's high/low prices.
|
Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range.
|
||||||
This behavior is currently being tested, and might be changed at a later point.
|
Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle.
|
||||||
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices.
|
`custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices.
|
||||||
|
|
||||||
## Custom order timeout rules
|
## Custom order timeout rules
|
||||||
@ -400,7 +400,8 @@ Simple, time-based order-timeouts can be configured either via strategy or in th
|
|||||||
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances.
|
Backtesting fills orders if their price falls within the candle's low/high range.
|
||||||
|
The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing).
|
||||||
|
|
||||||
### Custom order timeout example
|
### Custom order timeout example
|
||||||
|
|
||||||
@ -467,7 +468,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
'sell': 60 * 25
|
'sell': 60 * 25
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
ob = self.dp.orderbook(pair, 1)
|
ob = self.dp.orderbook(pair, 1)
|
||||||
current_price = ob['bids'][0][0]
|
current_price = ob['bids'][0][0]
|
||||||
# Cancel buy order if price is more than 2% above the order.
|
# Cancel buy order if price is more than 2% above the order.
|
||||||
@ -476,7 +478,8 @@ class AwesomeStrategy(IStrategy):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool:
|
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
|
||||||
|
current_time: datetime, **kwargs) -> bool:
|
||||||
ob = self.dp.orderbook(pair, 1)
|
ob = self.dp.orderbook(pair, 1)
|
||||||
current_price = ob['asks'][0][0]
|
current_price = ob['asks'][0][0]
|
||||||
# Cancel sell order if price is more than 2% below the order.
|
# Cancel sell order if price is more than 2% below the order.
|
||||||
|
@ -456,6 +456,7 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'dataformat_ohlcv',
|
'dataformat_ohlcv',
|
||||||
'dataformat_trades',
|
'dataformat_trades',
|
||||||
|
'unfilledtimeout',
|
||||||
]
|
]
|
||||||
|
|
||||||
SCHEMA_MINIMAL_REQUIRED = [
|
SCHEMA_MINIMAL_REQUIRED = [
|
||||||
|
@ -987,18 +987,20 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
|
||||||
|
order_obj = trade.select_order_by_order_id(trade.open_order_id)
|
||||||
|
|
||||||
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
|
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
|
||||||
fully_cancelled
|
fully_cancelled
|
||||||
or self.strategy.ft_check_timed_out(
|
or (order_obj and self.strategy.ft_check_timed_out(
|
||||||
'buy', trade, order, datetime.now(timezone.utc))
|
'buy', trade, order_obj, datetime.now(timezone.utc))
|
||||||
)):
|
))):
|
||||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
|
|
||||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
||||||
fully_cancelled
|
fully_cancelled
|
||||||
or self.strategy.ft_check_timed_out(
|
or (order_obj and self.strategy.ft_check_timed_out(
|
||||||
'sell', trade, order, datetime.now(timezone.utc)))
|
'sell', trade, order_obj, datetime.now(timezone.utc))
|
||||||
):
|
))):
|
||||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
canceled_count = trade.get_exit_order_count()
|
canceled_count = trade.get_exit_order_count()
|
||||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||||
|
@ -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] = {}
|
||||||
@ -231,6 +233,8 @@ class Backtesting:
|
|||||||
PairLocks.reset_locks()
|
PairLocks.reset_locks()
|
||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
self.rejected_trades = 0
|
self.rejected_trades = 0
|
||||||
|
self.timedout_entry_orders = 0
|
||||||
|
self.timedout_exit_orders = 0
|
||||||
self.dataprovider.clear_cache()
|
self.dataprovider.clear_cache()
|
||||||
if enable_protections:
|
if enable_protections:
|
||||||
self._load_protections(self.strategy)
|
self._load_protections(self.strategy)
|
||||||
@ -353,7 +357,10 @@ class Backtesting:
|
|||||||
# use Open rate if open_rate > calculated sell rate
|
# use Open rate if open_rate > calculated sell rate
|
||||||
return sell_row[OPEN_IDX]
|
return sell_row[OPEN_IDX]
|
||||||
|
|
||||||
return close_rate
|
# Use the maximum between close_rate and low as we
|
||||||
|
# cannot sell outside of a candle.
|
||||||
|
# Applies when a new ROI setting comes in place and the whole candle is above that.
|
||||||
|
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# This should not be reached...
|
# This should not be reached...
|
||||||
@ -376,10 +383,15 @@ class Backtesting:
|
|||||||
if stake_amount is not None and stake_amount > 0.0:
|
if stake_amount is not None and stake_amount > 0.0:
|
||||||
pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade)
|
pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade)
|
||||||
if pos_trade is not None:
|
if pos_trade is not None:
|
||||||
|
self.wallets.update()
|
||||||
return pos_trade
|
return pos_trade
|
||||||
|
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
def _get_order_filled(self, rate: float, row: Tuple) -> bool:
|
||||||
|
""" Rate is within candle, therefore filled"""
|
||||||
|
return row[LOW_IDX] <= rate <= row[HIGH_IDX]
|
||||||
|
|
||||||
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
def _get_sell_trade_entry_for_candle(self, trade: LocalTrade,
|
||||||
sell_row: Tuple) -> Optional[LocalTrade]:
|
sell_row: Tuple) -> Optional[LocalTrade]:
|
||||||
|
|
||||||
@ -405,18 +417,21 @@ 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_type = self.strategy.order_types['sell']
|
||||||
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,
|
if order_type == 'limit':
|
||||||
default_retval=closerate)(
|
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
|
||||||
pair=trade.pair, trade=trade,
|
default_retval=closerate)(
|
||||||
current_time=sell_row[DATE_IDX],
|
pair=trade.pair, trade=trade,
|
||||||
proposed_rate=closerate, current_profit=current_profit)
|
current_time=sell_candle_time,
|
||||||
# Use the maximum between close_rate and low as we cannot sell outside of a candle.
|
proposed_rate=closerate, current_profit=current_profit)
|
||||||
closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
|
# We can't place orders lower than current low.
|
||||||
|
# freqtrade does not support this in live, and the order would fill immediately
|
||||||
|
closerate = max(closerate, sell_row[LOW_IDX])
|
||||||
# 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']
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
|
||||||
rate=closerate,
|
rate=closerate,
|
||||||
@ -436,7 +451,28 @@ 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(
|
||||||
|
id=self.order_id_counter,
|
||||||
|
ft_trade_id=trade.id,
|
||||||
|
order_date=sell_candle_time,
|
||||||
|
order_update_date=sell_candle_time,
|
||||||
|
ft_is_open=True,
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
order_id=str(self.order_id_counter),
|
||||||
|
symbol=trade.pair,
|
||||||
|
ft_order_side="sell",
|
||||||
|
side="sell",
|
||||||
|
order_type=order_type,
|
||||||
|
status="open",
|
||||||
|
price=closerate,
|
||||||
|
average=closerate,
|
||||||
|
amount=trade.amount,
|
||||||
|
filled=0,
|
||||||
|
remaining=trade.amount,
|
||||||
|
cost=trade.amount * closerate,
|
||||||
|
)
|
||||||
|
trade.orders.append(order)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -475,13 +511,16 @@ 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])(
|
||||||
# Move rate to within the candle's low/high rate
|
pair=pair, current_time=current_time,
|
||||||
propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX])
|
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
|
||||||
|
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
|
||||||
|
# which freqtrade does not support in live.
|
||||||
|
propose_rate = min(propose_rate, row[HIGH_IDX])
|
||||||
|
|
||||||
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()
|
||||||
@ -489,9 +528,9 @@ class Backtesting:
|
|||||||
pos_adjust = trade is not None
|
pos_adjust = trade is not None
|
||||||
if not pos_adjust:
|
if not pos_adjust:
|
||||||
try:
|
try:
|
||||||
stake_amount = self.wallets.get_trade_stake_amount(pair, None)
|
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
|
||||||
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)(
|
||||||
@ -506,8 +545,7 @@ 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['buy']
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
|
||||||
# Confirm trade entry:
|
# Confirm trade entry:
|
||||||
if not pos_adjust:
|
if not pos_adjust:
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
@ -517,15 +555,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_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,
|
||||||
@ -533,28 +577,36 @@ class Backtesting:
|
|||||||
exchange='backtesting',
|
exchange='backtesting',
|
||||||
orders=[]
|
orders=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||||
|
|
||||||
order = Order(
|
order = Order(
|
||||||
ft_is_open=False,
|
id=self.order_id_counter,
|
||||||
|
ft_trade_id=trade.id,
|
||||||
|
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="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,
|
filled=0,
|
||||||
cost=stake_amount + trade.fee_open
|
remaining=amount,
|
||||||
|
cost=stake_amount + trade.fee_open,
|
||||||
)
|
)
|
||||||
|
if pos_adjust and self._get_order_filled(order.price, row):
|
||||||
|
order.close_bt_order(current_time)
|
||||||
|
else:
|
||||||
|
trade.open_order_id = str(self.order_id_counter)
|
||||||
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
|
||||||
|
|
||||||
@ -567,6 +619,9 @@ 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 and trade.nr_of_successful_buys == 0:
|
||||||
|
# Ignore trade if buy-order did not fill yet
|
||||||
|
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()
|
||||||
@ -587,6 +642,51 @@ class Backtesting:
|
|||||||
self.rejected_trades += 1
|
self.rejected_trades += 1
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def run_protections(self, enable_protections, pair: str, current_time: datetime):
|
||||||
|
if enable_protections:
|
||||||
|
self.protections.stop_per_pair(pair, current_time)
|
||||||
|
self.protections.global_stop(current_time)
|
||||||
|
|
||||||
|
def check_order_cancel(self, trade: LocalTrade, current_time) -> bool:
|
||||||
|
"""
|
||||||
|
Check if an order has been 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(order.side, trade, order, current_time)
|
||||||
|
if timedout:
|
||||||
|
if order.side == 'buy':
|
||||||
|
self.timedout_entry_orders += 1
|
||||||
|
if trade.nr_of_successful_buys == 0:
|
||||||
|
# Remove trade due to buy timeout expiration.
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# Close additional buy order
|
||||||
|
del trade.orders[trade.orders.index(order)]
|
||||||
|
if order.side == 'sell':
|
||||||
|
self.timedout_exit_orders += 1
|
||||||
|
# Close sell order and retry selling on next signal.
|
||||||
|
del trade.orders[trade.orders.index(order)]
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def validate_row(
|
||||||
|
self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]:
|
||||||
|
try:
|
||||||
|
# Row is treated as "current incomplete candle".
|
||||||
|
# Buy / sell signals are shifted by 1 to compensate for this.
|
||||||
|
row = data[pair][row_index]
|
||||||
|
except IndexError:
|
||||||
|
# missing Data for one pair at the end.
|
||||||
|
# Warnings for this are shown during data loading
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Waits until the time-counter reaches the start of the data for this pair.
|
||||||
|
if row[DATE_IDX] > current_time:
|
||||||
|
return None
|
||||||
|
return row
|
||||||
|
|
||||||
def backtest(self, processed: Dict,
|
def backtest(self, processed: Dict,
|
||||||
start_date: datetime, end_date: datetime,
|
start_date: datetime, end_date: datetime,
|
||||||
max_open_trades: int = 0, position_stacking: bool = False,
|
max_open_trades: int = 0, position_stacking: bool = False,
|
||||||
@ -616,7 +716,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
|
||||||
@ -625,35 +725,27 @@ 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):
|
||||||
row_index = indexes[pair]
|
row_index = indexes[pair]
|
||||||
try:
|
row = self.validate_row(data, pair, row_index, current_time)
|
||||||
# Row is treated as "current incomplete candle".
|
if not row:
|
||||||
# Buy / sell signals are shifted by 1 to compensate for this.
|
|
||||||
row = data[pair][row_index]
|
|
||||||
except IndexError:
|
|
||||||
# missing Data for one pair at the end.
|
|
||||||
# Warnings for this are shown during data loading
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Waits until the time-counter reaches the start of the data for this pair.
|
|
||||||
if row[DATE_IDX] > tmp:
|
|
||||||
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)
|
||||||
|
|
||||||
|
# 1. Process buys.
|
||||||
# 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])
|
||||||
@ -661,32 +753,51 @@ class Backtesting:
|
|||||||
trade = self._enter_trade(pair, row)
|
trade = self._enter_trade(pair, row)
|
||||||
if trade:
|
if trade:
|
||||||
# TODO: hacky workaround to avoid opening > max_open_trades
|
# TODO: hacky workaround to avoid opening > max_open_trades
|
||||||
# This emulates previous behaviour - not sure if this is correct
|
# This emulates previous behavior - not sure if this is correct
|
||||||
# Prevents buying if the trade-slot was freed in this candle
|
# Prevents buying if the trade-slot was freed in this candle
|
||||||
open_trade_count_start += 1
|
open_trade_count_start += 1
|
||||||
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]):
|
||||||
# also check the buying candle for sell conditions.
|
# 2. Process buy orders.
|
||||||
trade_entry = self._get_sell_trade_entry(trade, row)
|
order = trade.select_order('buy', is_open=True)
|
||||||
# Sell occurred
|
if order and self._get_order_filled(order.price, row):
|
||||||
if trade_entry:
|
order.close_bt_order(current_time)
|
||||||
|
trade.open_order_id = None
|
||||||
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
trade.close_date = current_time
|
||||||
|
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:
|
self.wallets.update()
|
||||||
self.protections.stop_per_pair(pair, row[DATE_IDX])
|
self.run_protections(enable_protections, pair, current_time)
|
||||||
self.protections.global_stop(tmp)
|
|
||||||
|
# 5. Cancel expired buy/sell orders.
|
||||||
|
if self.check_order_cancel(trade, current_time):
|
||||||
|
# Close trade due to buy timeout expiration.
|
||||||
|
open_trade_count -= 1
|
||||||
|
open_trades[pair].remove(trade)
|
||||||
|
self.wallets.update()
|
||||||
|
|
||||||
# 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()
|
||||||
@ -697,6 +808,8 @@ class Backtesting:
|
|||||||
'config': self.strategy.config,
|
'config': self.strategy.config,
|
||||||
'locks': PairLocks.get_all_locks(),
|
'locks': PairLocks.get_all_locks(),
|
||||||
'rejected_signals': self.rejected_trades,
|
'rejected_signals': self.rejected_trades,
|
||||||
|
'timedout_entry_orders': self.timedout_entry_orders,
|
||||||
|
'timedout_exit_orders': self.timedout_exit_orders,
|
||||||
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -436,6 +436,8 @@ def generate_strategy_stats(pairlist: List[str],
|
|||||||
'dry_run_wallet': starting_balance,
|
'dry_run_wallet': starting_balance,
|
||||||
'final_balance': content['final_balance'],
|
'final_balance': content['final_balance'],
|
||||||
'rejected_signals': content['rejected_signals'],
|
'rejected_signals': content['rejected_signals'],
|
||||||
|
'timedout_entry_orders': content['timedout_entry_orders'],
|
||||||
|
'timedout_exit_orders': content['timedout_exit_orders'],
|
||||||
'max_open_trades': max_open_trades,
|
'max_open_trades': max_open_trades,
|
||||||
'max_open_trades_setting': (config['max_open_trades']
|
'max_open_trades_setting': (config['max_open_trades']
|
||||||
if config['max_open_trades'] != float('inf') else -1),
|
if config['max_open_trades'] != float('inf') else -1),
|
||||||
@ -726,6 +728,9 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
|
||||||
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
|
||||||
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
|
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
|
||||||
|
('Entry/Exit Timeouts',
|
||||||
|
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
|
||||||
|
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # Empty line to improve readability
|
||||||
|
|
||||||
('Min balance', round_coin_value(strat_results['csum_min'],
|
('Min balance', round_coin_value(strat_results['csum_min'],
|
||||||
|
@ -132,6 +132,10 @@ class Order(_DECL_BASE):
|
|||||||
order_filled_date = Column(DateTime, nullable=True)
|
order_filled_date = Column(DateTime, nullable=True)
|
||||||
order_update_date = Column(DateTime, nullable=True)
|
order_update_date = Column(DateTime, nullable=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def order_date_utc(self):
|
||||||
|
return self.order_date.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
||||||
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
|
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
|
||||||
@ -188,6 +192,12 @@ class Order(_DECL_BASE):
|
|||||||
'status': self.status,
|
'status': self.status,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def close_bt_order(self, close_date: datetime):
|
||||||
|
self.order_filled_date = close_date
|
||||||
|
self.filled = self.amount
|
||||||
|
self.status = 'closed'
|
||||||
|
self.ft_is_open = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_orders(orders: List['Order'], order: Dict[str, Any]):
|
def update_orders(orders: List['Order'], order: Dict[str, Any]):
|
||||||
"""
|
"""
|
||||||
@ -635,14 +645,27 @@ class LocalTrade():
|
|||||||
if self.stop_loss_pct is not None and self.open_rate is not None:
|
if self.stop_loss_pct is not None and self.open_rate is not None:
|
||||||
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
|
self.adjust_stop_loss(self.open_rate, self.stop_loss_pct)
|
||||||
|
|
||||||
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
|
def select_order_by_order_id(self, order_id: str) -> Optional[Order]:
|
||||||
|
"""
|
||||||
|
Finds order object by Order id.
|
||||||
|
:param order_id: Exchange order id
|
||||||
|
"""
|
||||||
|
for o in self.orders:
|
||||||
|
if o.order_id == order_id:
|
||||||
|
return o
|
||||||
|
return None
|
||||||
|
|
||||||
|
def select_order(
|
||||||
|
self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]:
|
||||||
"""
|
"""
|
||||||
Finds latest order for this orderside and status
|
Finds latest order for this orderside and status
|
||||||
:param order_side: Side of the order (either 'buy' or 'sell')
|
:param order_side: Side of the order (either 'buy' or 'sell')
|
||||||
:param is_open: Only search for open orders?
|
:param is_open: Only search for open orders?
|
||||||
:return: latest Order object if it exists, else None
|
:return: latest Order object if it exists, else None
|
||||||
"""
|
"""
|
||||||
orders = [o for o in self.orders if o.side == order_side]
|
orders = self.orders
|
||||||
|
if order_side:
|
||||||
|
orders = [o for o in self.orders if o.side == order_side]
|
||||||
if is_open is not None:
|
if is_open is not None:
|
||||||
orders = [o for o in orders if o.ft_is_open == is_open]
|
orders = [o for o in orders if o.ft_is_open == is_open]
|
||||||
if len(orders) > 0:
|
if len(orders) > 0:
|
||||||
|
@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError
|
|||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
|
from freqtrade.persistence.models import LocalTrade, Order
|
||||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||||
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||||
_create_and_merge_informative_pair,
|
_create_and_merge_informative_pair,
|
||||||
@ -862,23 +863,22 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
else:
|
else:
|
||||||
return current_profit > roi
|
return current_profit > roi
|
||||||
|
|
||||||
def ft_check_timed_out(self, side: str, trade: Trade, order: Dict,
|
def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order,
|
||||||
current_time: datetime) -> bool:
|
current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
FT Internal method.
|
FT Internal method.
|
||||||
Check if timeout is active, and if the order is still open and timed out
|
Check if timeout is active, and if the order is still open and timed out
|
||||||
"""
|
"""
|
||||||
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
timeout = self.config.get('unfilledtimeout', {}).get(side)
|
||||||
ordertime = arrow.get(order['datetime']).datetime
|
|
||||||
if timeout is not None:
|
if timeout is not None:
|
||||||
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
|
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
|
||||||
timeout_kwargs = {timeout_unit: -timeout}
|
timeout_kwargs = {timeout_unit: -timeout}
|
||||||
timeout_threshold = current_time + timedelta(**timeout_kwargs)
|
timeout_threshold = current_time + timedelta(**timeout_kwargs)
|
||||||
timedout = (order['status'] == 'open' and order['side'] == side
|
timedout = (order.status == 'open' and order.side == side
|
||||||
and ordertime < timeout_threshold)
|
and order.order_date_utc < timeout_threshold)
|
||||||
if timedout:
|
if timedout:
|
||||||
return True
|
return True
|
||||||
time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout
|
time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout
|
||||||
|
|
||||||
return strategy_safe_wrapper(time_method,
|
return strategy_safe_wrapper(time_method,
|
||||||
default_retval=False)(
|
default_retval=False)(
|
||||||
|
@ -211,7 +211,7 @@ class Wallets:
|
|||||||
|
|
||||||
return stake_amount
|
return stake_amount
|
||||||
|
|
||||||
def get_trade_stake_amount(self, pair: str, edge=None) -> float:
|
def get_trade_stake_amount(self, pair: str, edge=None, update: bool = True) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate stake amount for the trade
|
Calculate stake amount for the trade
|
||||||
:return: float: Stake amount
|
:return: float: Stake amount
|
||||||
@ -219,7 +219,8 @@ class Wallets:
|
|||||||
"""
|
"""
|
||||||
stake_amount: float
|
stake_amount: float
|
||||||
# Ensure wallets are uptodate.
|
# Ensure wallets are uptodate.
|
||||||
self.update()
|
if update:
|
||||||
|
self.update()
|
||||||
val_tied_up = Trade.total_open_trades_stakes()
|
val_tied_up = Trade.total_open_trades_stakes()
|
||||||
available_amount = self.get_available_stake_amount()
|
available_amount = self.get_available_stake_amount()
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ from freqtrade.edge import PairInfo
|
|||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import LocalTrade, Trade, init_db
|
from freqtrade.persistence import LocalTrade, Order, Trade, init_db
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
from freqtrade.worker import Worker
|
from freqtrade.worker import Worker
|
||||||
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
|
from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4,
|
||||||
@ -1985,7 +1985,7 @@ def import_fails() -> None:
|
|||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
def open_trade():
|
def open_trade():
|
||||||
return Trade(
|
trade = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
open_rate=0.00001099,
|
open_rate=0.00001099,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
@ -1997,6 +1997,26 @@ def open_trade():
|
|||||||
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
||||||
is_open=True
|
is_open=True
|
||||||
)
|
)
|
||||||
|
trade.orders = [
|
||||||
|
Order(
|
||||||
|
ft_order_side='buy',
|
||||||
|
ft_pair=trade.pair,
|
||||||
|
ft_is_open=False,
|
||||||
|
order_id='123456789',
|
||||||
|
status="closed",
|
||||||
|
symbol=trade.pair,
|
||||||
|
order_type="market",
|
||||||
|
side="buy",
|
||||||
|
price=trade.open_rate,
|
||||||
|
average=trade.open_rate,
|
||||||
|
filled=trade.amount,
|
||||||
|
remaining=0,
|
||||||
|
cost=trade.open_rate * trade.amount,
|
||||||
|
order_date=trade.open_date,
|
||||||
|
order_filled_date=trade.open_date,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
return trade
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
|
@ -36,6 +36,8 @@ class BTContainer(NamedTuple):
|
|||||||
trailing_stop_positive_offset: float = 0.0
|
trailing_stop_positive_offset: float = 0.0
|
||||||
use_sell_signal: bool = False
|
use_sell_signal: bool = False
|
||||||
use_custom_stoploss: bool = False
|
use_custom_stoploss: bool = False
|
||||||
|
custom_entry_price: Optional[float] = None
|
||||||
|
custom_exit_price: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
def _get_frame_time_from_offset(offset):
|
def _get_frame_time_from_offset(offset):
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
# pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument
|
||||||
import logging
|
import logging
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -534,6 +535,80 @@ tc33 = BTContainer(data=[
|
|||||||
)]
|
)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Test 34: Custom-entry-price below all candles should timeout - so no trade happens.
|
||||||
|
tc34 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # timeout
|
||||||
|
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||||
|
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0,
|
||||||
|
custom_entry_price=4200, trades=[]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 35: Custom-entry-price above all candles should have rate adjusted to "entry candle high"
|
||||||
|
tc35 = BTContainer(data=[
|
||||||
|
# D O H L C V B S
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Timeout
|
||||||
|
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||||
|
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01,
|
||||||
|
custom_entry_price=7200, trades=[
|
||||||
|
BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 36: Custom-entry-price around candle low
|
||||||
|
# Causes immediate ROI exit. This is currently expected behavior (#6261)
|
||||||
|
# https://github.com/freqtrade/freqtrade/issues/6261
|
||||||
|
# But may change at a later point.
|
||||||
|
tc36 = BTContainer(data=[
|
||||||
|
# D O H L C V B S BT
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Enter and immediate ROI
|
||||||
|
[2, 4900, 5250, 4500, 5100, 6172, 0, 0],
|
||||||
|
[3, 5100, 5100, 4650, 4750, 6172, 0, 0],
|
||||||
|
[4, 4750, 4950, 4350, 4750, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.1,
|
||||||
|
custom_entry_price=4952,
|
||||||
|
trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test 37: Custom exit price below all candles
|
||||||
|
# Price adjusted to candle Low.
|
||||||
|
tc37 = BTContainer(data=[
|
||||||
|
# D O H L C V B S BT
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
|
||||||
|
[2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout
|
||||||
|
[3, 5100, 5100, 4950, 4950, 6172, 0, 0],
|
||||||
|
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01,
|
||||||
|
use_sell_signal=True,
|
||||||
|
custom_exit_price=4552,
|
||||||
|
trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 38: Custom exit price above all candles
|
||||||
|
# causes sell signal timeout
|
||||||
|
tc38 = BTContainer(data=[
|
||||||
|
# D O H L C V B S BT
|
||||||
|
[0, 5000, 5050, 4950, 5000, 6172, 1, 0],
|
||||||
|
[1, 5000, 5500, 4951, 5000, 6172, 0, 0],
|
||||||
|
[2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout
|
||||||
|
[3, 5100, 5100, 4950, 4950, 6172, 0, 0],
|
||||||
|
[4, 5000, 5100, 4950, 4950, 6172, 0, 0]],
|
||||||
|
stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0,
|
||||||
|
use_sell_signal=True,
|
||||||
|
custom_exit_price=6052,
|
||||||
|
trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
TESTS = [
|
TESTS = [
|
||||||
tc0,
|
tc0,
|
||||||
tc1,
|
tc1,
|
||||||
@ -569,6 +644,11 @@ TESTS = [
|
|||||||
tc31,
|
tc31,
|
||||||
tc32,
|
tc32,
|
||||||
tc33,
|
tc33,
|
||||||
|
tc34,
|
||||||
|
tc35,
|
||||||
|
tc36,
|
||||||
|
tc37,
|
||||||
|
tc38,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -597,6 +677,10 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None:
|
|||||||
backtesting.required_startup = 0
|
backtesting.required_startup = 0
|
||||||
backtesting.strategy.advise_buy = lambda a, m: frame
|
backtesting.strategy.advise_buy = lambda a, m: frame
|
||||||
backtesting.strategy.advise_sell = lambda a, m: frame
|
backtesting.strategy.advise_sell = lambda a, m: frame
|
||||||
|
if data.custom_entry_price:
|
||||||
|
backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price)
|
||||||
|
if data.custom_exit_price:
|
||||||
|
backtesting.strategy.custom_exit_price = MagicMock(return_value=data.custom_exit_price)
|
||||||
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
@ -521,6 +521,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
|||||||
# Fake 2 trades, so there's not enough amount for the next trade left.
|
# Fake 2 trades, so there's not enough amount for the next trade left.
|
||||||
LocalTrade.trades_open.append(trade)
|
LocalTrade.trades_open.append(trade)
|
||||||
LocalTrade.trades_open.append(trade)
|
LocalTrade.trades_open.append(trade)
|
||||||
|
backtesting.wallets.update()
|
||||||
trade = backtesting._enter_trade(pair, row=row)
|
trade = backtesting._enter_trade(pair, row=row)
|
||||||
assert trade is None
|
assert trade is None
|
||||||
LocalTrade.trades_open.pop()
|
LocalTrade.trades_open.pop()
|
||||||
@ -528,6 +529,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None:
|
|||||||
assert trade is not None
|
assert trade is not None
|
||||||
|
|
||||||
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
|
backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5
|
||||||
|
backtesting.wallets.update()
|
||||||
trade = backtesting._enter_trade(pair, row=row)
|
trade = backtesting._enter_trade(pair, row=row)
|
||||||
assert trade
|
assert trade
|
||||||
assert trade.stake_amount == 123.5
|
assert trade.stake_amount == 123.5
|
||||||
@ -635,7 +637,8 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None:
|
|||||||
assert res.sell_reason == SellType.ROI.value
|
assert res.sell_reason == SellType.ROI.value
|
||||||
# Sell at minute 3 (not available above!)
|
# Sell at minute 3 (not available above!)
|
||||||
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
|
assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc)
|
||||||
assert round(res.close_rate, 3) == round(209.0225, 3)
|
sell_order = res.select_order('sell', True)
|
||||||
|
assert sell_order is not None
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None:
|
||||||
@ -1020,6 +1023,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir):
|
|||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
@ -1128,6 +1133,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1135,6 +1142,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat
|
|||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@ -1237,6 +1246,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
|||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1244,6 +1255,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker,
|
|||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
@ -1305,6 +1318,8 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda
|
|||||||
'config': default_conf,
|
'config': default_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist',
|
||||||
|
@ -364,6 +364,8 @@ def test_hyperopt_format_results(hyperopt):
|
|||||||
'locks': [],
|
'locks': [],
|
||||||
'final_balance': 0.02,
|
'final_balance': 0.02,
|
||||||
'rejected_signals': 2,
|
'rejected_signals': 2,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'backtest_start_time': 1619718665,
|
'backtest_start_time': 1619718665,
|
||||||
'backtest_end_time': 1619718665,
|
'backtest_end_time': 1619718665,
|
||||||
}
|
}
|
||||||
@ -431,6 +433,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None:
|
|||||||
'config': hyperopt_conf,
|
'config': hyperopt_conf,
|
||||||
'locks': [],
|
'locks': [],
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'final_balance': 1000,
|
'final_balance': 1000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +82,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
|||||||
'locks': [],
|
'locks': [],
|
||||||
'final_balance': 1000.02,
|
'final_balance': 1000.02,
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||||
'run_id': '123',
|
'run_id': '123',
|
||||||
@ -131,6 +133,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir):
|
|||||||
'locks': [],
|
'locks': [],
|
||||||
'final_balance': 1000.02,
|
'final_balance': 1000.02,
|
||||||
'rejected_signals': 20,
|
'rejected_signals': 20,
|
||||||
|
'timedout_entry_orders': 0,
|
||||||
|
'timedout_exit_orders': 0,
|
||||||
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
'backtest_start_time': Arrow.utcnow().int_timestamp,
|
||||||
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
'backtest_end_time': Arrow.utcnow().int_timestamp,
|
||||||
'run_id': '124',
|
'run_id': '124',
|
||||||
|
@ -2042,6 +2042,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, li
|
|||||||
def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
|
||||||
fee, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
limit_buy_order_old['id'] = open_trade.open_order_id
|
||||||
limit_buy_cancel = deepcopy(limit_buy_order_old)
|
limit_buy_cancel = deepcopy(limit_buy_order_old)
|
||||||
limit_buy_cancel['status'] = 'canceled'
|
limit_buy_cancel['status'] = 'canceled'
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||||
@ -2126,6 +2127,8 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt,
|
|||||||
def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old,
|
def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old,
|
||||||
mocker, open_trade, caplog) -> None:
|
mocker, open_trade, caplog) -> None:
|
||||||
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1}
|
default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1}
|
||||||
|
limit_sell_order_old['id'] = open_trade.open_order_id
|
||||||
|
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -2174,7 +2177,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
|
|||||||
|
|
||||||
# 2nd canceled trade - Fail execute sell
|
# 2nd canceled trade - Fail execute sell
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
open_trade.open_order_id = 'order_id_2'
|
open_trade.open_order_id = limit_sell_order_old['id']
|
||||||
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
|
||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
|
||||||
side_effect=DependencyException)
|
side_effect=DependencyException)
|
||||||
@ -2185,7 +2188,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# 2nd canceled trade ...
|
# 2nd canceled trade ...
|
||||||
open_trade.open_order_id = 'order_id_2'
|
open_trade.open_order_id = limit_sell_order_old['id']
|
||||||
freqtrade.check_handle_timedout()
|
freqtrade.check_handle_timedout()
|
||||||
assert log_has_re('Emergencyselling trade.*', caplog)
|
assert log_has_re('Emergencyselling trade.*', caplog)
|
||||||
assert et_mock.call_count == 1
|
assert et_mock.call_count == 1
|
||||||
@ -2195,6 +2198,7 @@ def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_o
|
|||||||
open_trade) -> None:
|
open_trade) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
|
limit_sell_order_old['id'] = open_trade.open_order_id
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -2253,6 +2257,7 @@ def test_check_handle_cancelled_sell(default_conf_usdt, ticker_usdt, limit_sell_
|
|||||||
def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial,
|
def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial,
|
||||||
open_trade, mocker) -> None:
|
open_trade, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||||
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
||||||
limit_buy_canceled['status'] = 'canceled'
|
limit_buy_canceled['status'] = 'canceled'
|
||||||
|
|
||||||
@ -2283,6 +2288,7 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_
|
|||||||
limit_buy_order_old_partial, trades_for_order,
|
limit_buy_order_old_partial, trades_for_order,
|
||||||
limit_buy_order_old_partial_canceled, mocker) -> None:
|
limit_buy_order_old_partial_canceled, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0))
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -2322,6 +2328,8 @@ def test_check_handle_timedout_partial_except(default_conf_usdt, ticker_usdt, op
|
|||||||
fee, limit_buy_order_old_partial, trades_for_order,
|
fee, limit_buy_order_old_partial, trades_for_order,
|
||||||
limit_buy_order_old_partial_canceled, mocker) -> None:
|
limit_buy_order_old_partial_canceled, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id
|
||||||
|
limit_buy_order_old_partial['id'] = open_trade.open_order_id
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
Loading…
Reference in New Issue
Block a user