updated exit_positions, handle_trade, create_stoploss_order. Changed execute_sell, _check_and_execute_sell to execute_exit

This commit is contained in:
Sam Germain 2021-07-26 00:01:57 -06:00
parent 051bdc5fd0
commit 6bbe63eb62
12 changed files with 183 additions and 165 deletions

View File

@ -28,20 +28,24 @@ class Binance(Exchange):
maintenance_margin_formula = LiqFormula.BINANCE maintenance_margin_formula = LiqFormula.BINANCE
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. Returns True if adjustment is necessary.
:param side: "buy" or "sell"
""" """
# TODO-mg: Short support
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
""" """
creates a stoploss limit order. creates a stoploss limit order.
this stoploss-limit is binance-specific. this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet. It may work with a limited number of other exchanges, but this has not been tested yet.
:param side: "buy" or "sell"
""" """
# TODO-mg: Short support
# Limit price threshold: As limit price should always be below stop-price # Limit price threshold: As limit price should always be below stop-price
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
rate = stop_price * limit_price_pct rate = stop_price * limit_price_pct

View File

@ -784,7 +784,7 @@ class Exchange:
""" """
raise OperationalException(f"stoploss is not implemented for {self.name}.") raise OperationalException(f"stoploss is not implemented for {self.name}.")
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
""" """
creates a stoploss order. creates a stoploss order.
The precise ordertype is determined by the order_types dict or exchange default. The precise ordertype is determined by the order_types dict or exchange default.

View File

@ -34,21 +34,24 @@ class Ftx(Exchange):
return (parent_check and return (parent_check and
market.get('spot', False) is True) market.get('spot', False) is True)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. Returns True if adjustment is necessary.
""" """
# TODO-mg: Short support
return order['type'] == 'stop' and stop_loss > float(order['price']) return order['type'] == 'stop' and stop_loss > float(order['price'])
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
""" """
Creates a stoploss order. Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order. depending on order_types.stoploss configuration, uses 'market' or limit order.
Limit orders are defined by having orderPrice set, otherwise a market order is used. Limit orders are defined by having orderPrice set, otherwise a market order is used.
""" """
# TODO-mg: Short support
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
limit_rate = stop_price * limit_price_pct limit_rate = stop_price * limit_price_pct

View File

@ -71,20 +71,22 @@ class Kraken(Exchange):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. Returns True if adjustment is necessary.
""" """
# TODO-mg: Short support
return (order['type'] in ('stop-loss', 'stop-loss-limit') return (order['type'] in ('stop-loss', 'stop-loss-limit')
and stop_loss > float(order['price'])) and stop_loss > float(order['price']))
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict:
""" """
Creates a stoploss market order. Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken. Stoploss market orders is the only stoploss type supported by kraken.
""" """
# TODO-mg: Short support
params = self._params.copy() params = self._params.copy()
if order_types.get('stoploss', 'market') == 'limit': if order_types.get('stoploss', 'market') == 'limit':

View File

@ -715,7 +715,7 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a buy/short cancel occurred. Sends rpc notification when a buy/short cancel occurred.
""" """
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side)
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
msg = { msg = {
'trade_id': trade.id, 'trade_id': trade.id,
@ -795,33 +795,29 @@ class FreqtradeBot(LoggingMixin):
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
(enter_trade, exit_trade) = (False, False) (enter_trade, exit_trade) = (False, False)
exit_signal_type = "exit_short" if trade.is_short else "sell"
if trade.is_short: # TODO-mg: change to use_exit_signal, ignore_roi_if_enter_signal
# enter_signal = "short" signal_config = (self.config.get('use_sell_signal', True)
exit_signal = "exit_short" or self.config.get('ignore_roi_if_buy_signal', False))
signal_config = (self.config.get('use_exit_short_signal', True)
or self.config.get('ignore_roi_if_short_signal', False))
else:
# enter_signal = "buy"
exit_signal = "sell"
signal_config = (self.config.get('use_sell_signal', True)
or self.config.get('ignore_roi_if_buy_signal', False))
if (signal_config): if (signal_config):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe) self.strategy.timeframe)
# TODO-mg: Call get_short_signal for shorts
(enter_trade, exit_trade, _) = self.strategy.get_signal( (enter_trade, exit_trade, _) = self.strategy.get_signal(
trade.pair, self.strategy.timeframe, analyzed_df) trade.pair, self.strategy.timeframe, analyzed_df)
sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") logger.debug(f'checking {exit_signal_type}')
# TODO-mg: This will not works for shorts, logger.debug(f'checking {exit_signal_type}')
# TODO-mg: update exchange.get_exit_rate and _check_and_execute_sell exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side)
if self._check_and_execute_sell(trade, sell_rate, enter_trade, exit_trade):
if self._check_and_execute_exit(trade, exit_rate, enter_trade, exit_trade):
return True return True
# TODO-mg: end of non-working section
logger.debug(f'Found no {exit_signal} signal for %s.', trade) logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
return False return False
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
@ -834,7 +830,8 @@ class FreqtradeBot(LoggingMixin):
try: try:
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
stop_price=stop_price, stop_price=stop_price,
order_types=self.strategy.order_types) order_types=self.strategy.order_types,
side=trade.exit_side)
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
trade.orders.append(order_obj) trade.orders.append(order_obj)
@ -848,14 +845,9 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException as e: except InvalidOrderException as e:
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
if trade.is_short: logger.warning('Exiting the trade forcefully')
logger.warning('Exiting(Buying) the trade forcefully') self.execute_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
# self.execute_exit_short(trade, trade.stop_loss, sell_reason=SellCheckTuple( sell_type=SellType.EMERGENCY_SELL), side=trade.exit_side)
# sell_type=SellType.EMERGENCY_SELL))
else:
logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple(
sell_type=SellType.EMERGENCY_SELL))
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@ -947,7 +939,7 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :return: None
""" """
if self.exchange.stoploss_adjust(trade.stop_loss, order): if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side):
# we check if the update is necessary # we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
@ -967,20 +959,25 @@ class FreqtradeBot(LoggingMixin):
logger.warning(f"Could not create trailing stoploss order " logger.warning(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.") f"for pair {trade.pair}.")
def _check_and_execute_sell(self, trade: Trade, sell_rate: float, def _check_and_execute_exit(self, trade: Trade, exit_rate: float,
buy: bool, sell: bool) -> bool: buy: bool, sell: bool) -> bool:
""" """
Check and execute sell Check and execute sell
# TODO-mg: Update this for shorts # TODO-mg: Update this for shorts
""" """
should_sell = self.strategy.should_sell( exit = getattr(self.strategy, "should_exit_short") if trade.is_short else getattr(
trade, sell_rate, datetime.now(timezone.utc), buy, sell, self.strategy, "should_sell")
should_exit = exit(
trade, exit_rate, datetime.now(timezone.utc), buy, sell,
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
) )
if should_sell.sell_flag: if should_exit.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') # TODO-mg: Update to exit_type
self.execute_sell(trade, sell_rate, should_sell) logger.info(
f'Executing {trade.exit_side} for {trade.pair}. Reason: {should_exit.sell_type}')
self.execute_exit(trade, exit_rate, should_exit, side="sell")
return True return True
return False return False
@ -988,6 +985,7 @@ class FreqtradeBot(LoggingMixin):
""" """
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
""" """
# TODO-mg: Maybe update side in the next line
timeout = self.config.get('unfilledtimeout', {}).get(side) timeout = self.config.get('unfilledtimeout', {}).get(side)
ordertime = arrow.get(order['datetime']).datetime ordertime = arrow.get(order['datetime']).datetime
if timeout is not None: if timeout is not None:
@ -1020,20 +1018,20 @@ class FreqtradeBot(LoggingMixin):
order['status'] == 'open' or fully_cancelled) and ( order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out(trade.enter_side, order) or self._check_timed_out(trade.enter_side, order)
or strategy_safe_wrapper(self.strategy.check_buy_timeout, or strategy_safe_wrapper(self.strategy.check_buy_timeout, # TODO-mg: maybe change to check_enter_timeout
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order))):
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'] == trade.exit_side and (order['status'] == 'open' or fully_cancelled) and (
fully_cancelled fully_cancelled
or self._check_timed_out('sell', order) or self._check_timed_out(trade.exit_side, order)
or strategy_safe_wrapper(self.strategy.check_sell_timeout, or strategy_safe_wrapper(self.strategy.check_sell_timeout, # TODO-mg: maybe change to check_exit_timeout
default_retval=False)(pair=trade.pair, default_retval=False)(pair=trade.pair,
trade=trade, trade=trade,
order=order))): order=order))):
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
def cancel_all_open_orders(self) -> None: def cancel_all_open_orders(self) -> None:
""" """
@ -1048,11 +1046,11 @@ class FreqtradeBot(LoggingMixin):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
if order['side'] == 'buy': if order['side'] == trade.enter_side:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
elif order['side'] == 'sell': elif order['side'] == trade.exit_side:
self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
Trade.commit() Trade.commit()
def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool:
@ -1087,18 +1085,15 @@ class FreqtradeBot(LoggingMixin):
corder = order corder = order
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
# TODO-mg: get this side variable working, fails in tests side = trade.enter_side.capitalize()
# side = trade.enter_side[0].upper() + trade.enter_side[1:].lower() logger.info('%s order %s for %s.', side, reason, trade)
logger.info('Buy order %s for %s.', reason, trade)
# logger.info(f'{side} order %s for %s.', reason, trade)
# Using filled to determine the filled amount # Using filled to determine the filled amount
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
logger.info( logger.info(
'Buy order fully cancelled. Removing %s from database.', '%s order fully cancelled. Removing %s from database.',
# f'{side} order fully cancelled. Removing %s from database.', side, trade
trade
) )
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
trade.delete() trade.delete()
@ -1116,20 +1111,18 @@ class FreqtradeBot(LoggingMixin):
self.update_trade_state(trade, trade.open_order_id, corder) self.update_trade_state(trade, trade.open_order_id, corder)
trade.open_order_id = None trade.open_order_id = None
# TODO-mg change to buy or short order logger.info('Partial %s order timeout for %s.', trade.enter_side, trade)
logger.info('Partial buy order timeout for %s.', trade)
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
self.wallets.update() self.wallets.update()
# TODO-mg change to buy or short order # TODO-mg: Should short and exit_short be an order type?
# TODO-mg: Or should it be self.strategy.order_types['short'] or strategy.order_types['buy'] self._notify_open_cancel(trade, order_type=self.strategy.order_types[trade.enter_side],
self._notify_open_cancel(trade, order_type=self.strategy.order_types['buy'],
reason=reason) reason=reason)
return was_trade_fully_canceled return was_trade_fully_canceled
def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
""" """
Sell cancel - cancel order and update trade Sell/exit_short cancel - cancel order and update trade
:return: Reason for cancel :return: Reason for cancel
""" """
# if trade is not partially completed, just cancel the order # if trade is not partially completed, just cancel the order
@ -1141,12 +1134,13 @@ class FreqtradeBot(LoggingMixin):
trade.amount) trade.amount)
trade.update_order(co) trade.update_order(co)
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel sell order {trade.open_order_id}") logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return 'error cancelling order' return 'error cancelling order'
logger.info('Sell order %s for %s.', reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
logger.info('Sell order %s for %s.', reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
trade.update_order(order) trade.update_order(order)
trade.close_rate = None trade.close_rate = None
@ -1163,12 +1157,12 @@ class FreqtradeBot(LoggingMixin):
self.wallets.update() self.wallets.update()
self._notify_close_cancel( self._notify_close_cancel(
trade, trade,
order_type=self.strategy.order_types['sell'], order_type=self.strategy.order_types[trade.exit_side],
reason=reason reason=reason
) )
return reason return reason
def _safe_sell_amount(self, pair: str, amount: float) -> float: def _safe_exit_amount(self, pair: str, amount: float) -> float:
""" """
Get sellable amount. Get sellable amount.
Should be trade.amount - but will fall back to the available amount if necessary. Should be trade.amount - but will fall back to the available amount if necessary.
@ -1179,6 +1173,7 @@ class FreqtradeBot(LoggingMixin):
:return: amount to sell :return: amount to sell
:raise: DependencyException: if available balance is not within 2% of the available amount. :raise: DependencyException: if available balance is not within 2% of the available amount.
""" """
# TODO-mg Maybe update?
# Update wallets to ensure amounts tied up in a stoploss is now free! # Update wallets to ensure amounts tied up in a stoploss is now free!
self.wallets.update() self.wallets.update()
trade_base_currency = self.exchange.get_pair_base_currency(pair) trade_base_currency = self.exchange.get_pair_base_currency(pair)
@ -1191,9 +1186,15 @@ class FreqtradeBot(LoggingMixin):
return wallet_amount return wallet_amount
else: else:
raise DependencyException( raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: def execute_exit(
self,
trade: Trade,
limit: float,
sell_reason: SellCheckTuple,
side: str
) -> bool:
""" """
Executes a limit sell for the given trade and limit Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
@ -1201,14 +1202,14 @@ class FreqtradeBot(LoggingMixin):
:param sell_reason: Reason the sell was triggered :param sell_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported) :return: True if it succeeds (supported) False (not supported)
""" """
# TODO-mg: account for leverage and make this work for shorts
sell_type = 'sell' exit_type = 'sell' # TODO-mg: Update to exit
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss' exit_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode, # if stoploss is on exchange and we are on dry_run mode,
# we consider the sell price stop price # we consider the sell price stop price
if self.config['dry_run'] and sell_type == 'stoploss' \ if self.config['dry_run'] and exit_type == 'stoploss' \
and self.strategy.order_types['stoploss_on_exchange']: and self.strategy.order_types['stoploss_on_exchange']:
limit = trade.stop_loss limit = trade.stop_loss
@ -1221,8 +1222,8 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
order_type = self.strategy.order_types[sell_type] order_type = self.strategy.order_types[exit_type]
if sell_reason.sell_type == SellType.EMERGENCY_SELL: if sell_reason.sell_type == SellType.EMERGENCY_SELL: # TODO-mg update to exit_reason
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market") order_type = self.strategy.order_types.get("emergencysell", "market")
if sell_reason.sell_type == SellType.FORCE_SELL: if sell_reason.sell_type == SellType.FORCE_SELL:
@ -1230,42 +1231,44 @@ class FreqtradeBot(LoggingMixin):
# but we allow this value to be changed) # but we allow this value to be changed)
order_type = self.strategy.order_types.get("forcesell", order_type) order_type = self.strategy.order_types.get("forcesell", order_type)
amount = self._safe_sell_amount(trade.pair, trade.amount) amount = self._safe_exit_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell'] time_in_force = self.strategy.order_time_in_force['sell'] # TODO-mg update to exit
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=order_type, amount=amount, rate=limit, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, time_in_force=time_in_force, sell_reason=sell_reason.sell_reason,
current_time=datetime.now(timezone.utc)): current_time=datetime.now(timezone.utc)): # TODO-mg: Update to exit
logger.info(f"User requested abortion of selling {trade.pair}") logger.info(f"User requested abortion of exiting {trade.pair}")
return False return False
try: try:
# Execute sell and update trade record # Execute sell and update trade record
order = self.exchange.create_order(pair=trade.pair, order = self.exchange.create_order(
ordertype=order_type, side="sell", pair=trade.pair,
amount=amount, rate=limit, ordertype=order_type,
time_in_force=time_in_force amount=amount, rate=limit,
) time_in_force=time_in_force,
side=trade.exit_side
)
except InsufficientFundsError as e: except InsufficientFundsError as e:
logger.warning(f"Unable to place order {e}.") logger.warning(f"Unable to place order {e}.")
# Try to figure out what went wrong # Try to figure out what went wrong
self.handle_insufficient_funds(trade) self.handle_insufficient_funds(trade)
return False return False
order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.sell_order_status = '' trade.sell_order_status = '' # TODO-mg: Update to exit_order_status
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.sell_reason trade.sell_reason = sell_reason.sell_reason # TODO-mg: Update to exit_reason
# In case of market sell orders the order can be closed immediately # In case of market exit orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed': if order.get('status', 'unknown') == 'closed':
self.update_trade_state(trade, trade.open_order_id, order) self.update_trade_state(trade, trade.open_order_id, order)
Trade.commit() Trade.commit()
# Lock pair for one candle to prevent immediate re-buys # Lock pair for one candle to prevent immediate re-trading
self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc),
reason='Auto lock') reason='Auto lock')
@ -1281,7 +1284,7 @@ class FreqtradeBot(LoggingMixin):
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago. # Use cached rates here - it was updated seconds ago.
current_rate = self.exchange.get_rate( current_rate = self.exchange.get_rate(
trade.pair, refresh=False, side="sell") if not fill else None trade.pair, refresh=False, side=trade.exit_side) if not fill else None
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
@ -1300,7 +1303,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'sell_reason': trade.sell_reason, 'sell_reason': trade.sell_reason, # TODO-mg: trade to exit_reason
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(), 'close_date': trade.close_date or datetime.utcnow(),
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
@ -1319,14 +1322,14 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a sell cancel occurred. Sends rpc notification when a sell cancel occurred.
""" """
if trade.sell_order_status == reason: if trade.sell_order_status == reason: # TODO-mg: Update to exit_order_status
return return
else: else:
trade.sell_order_status = reason trade.sell_order_status = reason # TODO-mg: Update to exit_order_status
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side)
profit_ratio = trade.calc_profit_ratio(profit_rate) profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss" gain = "profit" if profit_ratio > 0 else "loss"
@ -1343,7 +1346,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'sell_reason': trade.sell_reason, 'sell_reason': trade.sell_reason, # TODO-mg: trade to exit_reason
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date, 'close_date': trade.close_date,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],

View File

@ -551,14 +551,14 @@ class RPC:
if order['side'] == 'sell': if order['side'] == 'sell':
# Cancel order - so it is placed anew with a fresh price. # Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL']) self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
if not fully_canceled: if not fully_canceled:
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="sell") trade.pair, refresh=False, side="sell")
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, sell_reason) self._freqtrade.execute_exit(trade, current_rate, sell_reason, side="sell")
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:

View File

@ -32,12 +32,13 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell",
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) order_types={'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types=order_types, side="sell")
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -54,17 +55,17 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
"stoploss", "create_order", retries=1, "stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={}) pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
def test_stoploss_order_dry_run_binance(default_conf, mocker): def test_stoploss_order_dry_run_binance(default_conf, mocker):
@ -77,12 +78,12 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell",
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) order_types={'stoploss_on_exchange_limit_ratio': 1.05})
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -100,8 +101,8 @@ def test_stoploss_adjust_binance(mocker, default_conf):
'price': 1500, 'price': 1500,
'info': {'stopPrice': 1500}, 'info': {'stopPrice': 1500},
} }
assert exchange.stoploss_adjust(1501, order) assert exchange.stoploss_adjust(1501, order, side="sell")
assert not exchange.stoploss_adjust(1499, order) assert not exchange.stoploss_adjust(1499, order, side="sell")
# Test with invalid order case # Test with invalid order case
order['type'] = 'stop_loss' order['type'] = 'stop_loss'
assert not exchange.stoploss_adjust(1501, order) assert not exchange.stoploss_adjust(1501, order, side="sell")

View File

@ -2530,7 +2530,7 @@ def test_get_fee(default_conf, mocker, exchange_name):
def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='bittrex') exchange = get_patched_exchange(mocker, default_conf, id='bittrex')
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
exchange.stoploss_adjust(1, {}) exchange.stoploss_adjust(1, {})

View File

@ -32,7 +32,7 @@ def test_stoploss_order_ftx(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell",
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) order_types={'stoploss_on_exchange_limit_ratio': 1.05})
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
@ -47,7 +47,7 @@ def test_stoploss_order_ftx(default_conf, mocker):
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -61,7 +61,7 @@ def test_stoploss_order_ftx(default_conf, mocker):
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={'stoploss': 'limit'}) order_types={'stoploss': 'limit'}, side="sell")
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -78,17 +78,17 @@ def test_stoploss_order_ftx(default_conf, mocker):
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
"stoploss", "create_order", retries=1, "stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={}) pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
def test_stoploss_order_dry_run_ftx(default_conf, mocker): def test_stoploss_order_dry_run_ftx(default_conf, mocker):
@ -101,7 +101,7 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker):
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -118,11 +118,11 @@ def test_stoploss_adjust_ftx(mocker, default_conf):
'type': STOPLOSS_ORDERTYPE, 'type': STOPLOSS_ORDERTYPE,
'price': 1500, 'price': 1500,
} }
assert exchange.stoploss_adjust(1501, order) assert exchange.stoploss_adjust(1501, order, side="sell")
assert not exchange.stoploss_adjust(1499, order) assert not exchange.stoploss_adjust(1499, order, side="sell")
# Test with invalid order case ... # Test with invalid order case ...
order['type'] = 'stop_loss_limit' order['type'] = 'stop_loss_limit'
assert not exchange.stoploss_adjust(1501, order) assert not exchange.stoploss_adjust(1501, order, side="sell")
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):

View File

@ -183,7 +183,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, side="sell",
order_types={'stoploss': ordertype, order_types={'stoploss': ordertype,
'stoploss_on_exchange_limit_ratio': 0.99 'stoploss_on_exchange_limit_ratio': 0.99
}) })
@ -208,17 +208,17 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype):
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"stoploss", "create_order", retries=1, "stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={}) pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
def test_stoploss_order_dry_run_kraken(default_conf, mocker): def test_stoploss_order_dry_run_kraken(default_conf, mocker):
@ -231,7 +231,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell")
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
@ -248,8 +248,8 @@ def test_stoploss_adjust_kraken(mocker, default_conf):
'type': STOPLOSS_ORDERTYPE, 'type': STOPLOSS_ORDERTYPE,
'price': 1500, 'price': 1500,
} }
assert exchange.stoploss_adjust(1501, order) assert exchange.stoploss_adjust(1501, order, side="sell")
assert not exchange.stoploss_adjust(1499, order) assert not exchange.stoploss_adjust(1499, order, side="sell")
# Test with invalid order case ... # Test with invalid order case ...
order['type'] = 'stop_loss_limit' order['type'] = 'stop_loss_limit'
assert not exchange.stoploss_adjust(1501, order) assert not exchange.stoploss_adjust(1501, order, side="sell")

View File

@ -254,7 +254,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
# stoploss shoud be hit # stoploss shoud be hit
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) assert log_has('Executing sell for NEO/BTC. Reason: stop_loss', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value assert trade.sell_reason == SellType.STOP_LOSS.value
@ -1158,7 +1158,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
assert trade.sell_reason == SellType.EMERGENCY_SELL.value assert trade.sell_reason == SellType.EMERGENCY_SELL.value
assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Unable to place a stoploss order on exchange. ", caplog)
assert log_has("Selling the trade forcefully", caplog) assert log_has("Exiting the trade forcefully", caplog)
# Should call a market sell # Should call a market sell
assert create_order_mock.call_count == 2 assert create_order_mock.call_count == 2
@ -1312,7 +1312,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
stoploss_order_mock.assert_called_once_with(amount=85.32423208, stoploss_order_mock.assert_called_once_with(amount=85.32423208,
pair='ETH/BTC', pair='ETH/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.95) stop_price=0.00002346 * 0.95,
side="sell")
# price fell below stoploss, so dry-run sells trade. # price fell below stoploss, so dry-run sells trade.
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
@ -1495,7 +1496,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
stoploss_order_mock.assert_called_once_with(amount=85.32423208, stoploss_order_mock.assert_called_once_with(amount=85.32423208,
pair='ETH/BTC', pair='ETH/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.96) stop_price=0.00002346 * 0.96,
side="sell")
# price fell below stoploss, so dry-run sells trade. # price fell below stoploss, so dry-run sells trade.
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
@ -1616,7 +1618,8 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, stoploss_order_mock.assert_called_once_with(amount=2132892.49146757,
pair='NEO/BTC', pair='NEO/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.99) stop_price=0.00002346 * 0.99,
side="sell")
def test_enter_positions(mocker, default_conf, caplog) -> None: def test_enter_positions(mocker, default_conf, caplog) -> None:
@ -1975,7 +1978,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
trade = Trade.query.first() trade = Trade.query.first()
trade.is_open = True trade.is_open = True
# FIX: sniffing logs, suggest handle_trade should not execute_sell # FIX: sniffing logs, suggest handle_trade should not execute_exit
# instead that responsibility should be moved out of handle_trade(), # instead that responsibility should be moved out of handle_trade(),
# we might just want to check if we are in a sell condition without # we might just want to check if we are in a sell condition without
# executing # executing
@ -2422,7 +2425,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
handle_cancel_enter=MagicMock(), handle_cancel_enter=MagicMock(),
handle_cancel_sell=MagicMock(), handle_cancel_exit=MagicMock(),
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
@ -2503,6 +2506,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf,
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/ETH' trade.pair = 'LTC/ETH'
trade.enter_side = "buy"
assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason)
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog) assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog)
@ -2531,6 +2535,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
trade = MagicMock() trade = MagicMock()
trade.pair = 'LTC/USDT' trade.pair = 'LTC/USDT'
trade.open_rate = 200 trade.open_rate = 200
trade.enter_side = "buy"
limit_buy_order['filled'] = 0.0 limit_buy_order['filled'] = 0.0
limit_buy_order['status'] = 'open' limit_buy_order['status'] = 'open'
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
@ -2543,7 +2548,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order,
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: def test_handle_cancel_exit_limit(mocker, default_conf, fee) -> None:
send_msg_mock = patch_RPCManager(mocker) send_msg_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
@ -2569,26 +2574,26 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None:
'amount': 1, 'amount': 1,
'status': "open"} 'status': "open"}
reason = CANCEL_REASON['TIMEOUT'] reason = CANCEL_REASON['TIMEOUT']
assert freqtrade.handle_cancel_sell(trade, order, reason) assert freqtrade.handle_cancel_exit(trade, order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
send_msg_mock.reset_mock() send_msg_mock.reset_mock()
order['amount'] = 2 order['amount'] = 2
assert freqtrade.handle_cancel_sell(trade, order, reason assert freqtrade.handle_cancel_exit(trade, order, reason
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Assert cancel_order was not called (callcount remains unchanged) # Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
assert freqtrade.handle_cancel_sell(trade, order, reason assert freqtrade.handle_cancel_exit(trade, order, reason
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Message should not be iterated again # Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch( mocker.patch(
@ -2601,10 +2606,10 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None:
order = {'remaining': 1, order = {'remaining': 1,
'amount': 1, 'amount': 1,
'status': "open"} 'status': "open"}
assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order' assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: def test_execute_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2632,7 +2637,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
# Prevented sell ... # Prevented sell ...
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.ROI)) sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert rpc_mock.call_count == 0 assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@ -2640,7 +2645,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
# Repatch with true # Repatch with true
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.ROI)) sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@ -2668,7 +2673,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
} == last_msg } == last_msg
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: def test_execute_exit_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
@ -2693,7 +2698,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
fetch_ticker=ticker_sell_down fetch_ticker=ticker_sell_down
) )
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_down()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
@ -2720,7 +2725,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
} == last_msg } == last_msg
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, def test_execute_exit_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
ticker_sell_down, mocker) -> None: ticker_sell_down, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -2751,7 +2756,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
# Setting trade stoploss to 0.01 # Setting trade stoploss to 0.01
trade.stop_loss = 0.00001099 * 0.99 trade.stop_loss = 0.00001099 * 0.99
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_down()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
@ -2779,7 +2784,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
} == last_msg } == last_msg
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None: def test_execute_exit_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
@ -2806,13 +2811,13 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c
freqtrade.config['dry_run'] = False freqtrade.config['dry_run'] = False
trade.stoploss_order_id = "abcd" trade.stoploss_order_id = "abcd"
freqtrade.execute_sell(trade=trade, limit=1234, freqtrade.execute_exit(trade=trade, limit=1234, side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
assert create_order_mock.call_count == 2 assert create_order_mock.call_count == 2
assert log_has('Could not cancel stoploss order abcd', caplog) assert log_has('Could not cancel stoploss order abcd', caplog)
def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, def test_execute_exit_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up,
mocker) -> None: mocker) -> None:
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
@ -2857,7 +2862,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
trade = Trade.query.first() trade = Trade.query.first()
@ -2866,7 +2871,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
assert rpc_mock.call_count == 3 assert rpc_mock.call_count == 3
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, def test_may_execute_exit_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
mocker) -> None: mocker) -> None:
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
@ -2938,7 +2943,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
def test_execute_sell_market_order(default_conf, ticker, fee, def test_execute_exit_market_order(default_conf, ticker, fee,
ticker_sell_up, mocker) -> None: ticker_sell_up, mocker) -> None:
rpc_mock = patch_RPCManager(mocker) rpc_mock = patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
@ -2965,7 +2970,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
) )
freqtrade.config['order_types']['sell'] = 'market' freqtrade.config['order_types']['sell'] = 'market'
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.ROI)) sell_reason=SellCheckTuple(sell_type=SellType.ROI))
assert not trade.is_open assert not trade.is_open
@ -2996,7 +3001,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
} == last_msg } == last_msg
def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, def test_execute_exit_insufficient_funds_error(default_conf, ticker, fee,
ticker_sell_up, mocker) -> None: ticker_sell_up, mocker) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds')
@ -3024,8 +3029,8 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee,
) )
sell_reason = SellCheckTuple(sell_type=SellType.ROI) sell_reason = SellCheckTuple(sell_type=SellType.ROI)
assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], assert not freqtrade.execute_exit(trade=trade, limit=ticker_sell_up()['bid'],
sell_reason=sell_reason) sell_reason=sell_reason, side="sell")
assert mock_insuf.call_count == 1 assert mock_insuf.call_count == 1
@ -3207,7 +3212,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
assert trade.amount != amnt assert trade.amount != amnt
def test__safe_sell_amount(default_conf, fee, caplog, mocker): def test__safe_exit_amount(default_conf, fee, caplog, mocker):
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
amount = 95.33 amount = 95.33
@ -3227,17 +3232,17 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker):
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
wallet_update.reset_mock() wallet_update.reset_mock()
assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
assert log_has_re(r'.*Falling back to wallet-amount.', caplog) assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert wallet_update.call_count == 1 assert wallet_update.call_count == 1
caplog.clear() caplog.clear()
wallet_update.reset_mock() wallet_update.reset_mock()
assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert wallet_update.call_count == 1 assert wallet_update.call_count == 1
def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
amount = 95.33 amount = 95.33
@ -3254,8 +3259,8 @@ def test__safe_sell_amount_error(default_conf, fee, caplog, mocker):
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
with pytest.raises(DependencyException, match=r"Not enough amount to sell."): with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."):
assert freqtrade._safe_sell_amount(trade.pair, trade.amount) assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
@ -3281,7 +3286,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
fetch_ticker=ticker_sell_down fetch_ticker=ticker_sell_down
) )
freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], freqtrade.execute_exit(trade=trade, limit=ticker_sell_down()['bid'], side="sell",
sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
trade.close(ticker_sell_down()['bid']) trade.close(ticker_sell_down()['bid'])
assert freqtrade.strategy.is_pair_locked(trade.pair) assert freqtrade.strategy.is_pair_locked(trade.pair)
@ -4221,7 +4226,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
side_effect=[ side_effect=[
ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order]) ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order])
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter') buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter')
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit')
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee) create_mock_trades(fee)

View File

@ -9,7 +9,7 @@ from freqtrade.strategy.interface import SellCheckTuple
from tests.conftest import get_patched_freqtradebot, patch_get_signal from tests.conftest import get_patched_freqtradebot, patch_get_signal
def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
limit_buy_order, mocker) -> None: limit_buy_order, mocker) -> None:
""" """
Tests workflow of selling stoploss_on_exchange. Tests workflow of selling stoploss_on_exchange.