diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9cce8c105..7888e64bc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -28,7 +28,7 @@ from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State -from freqtrade.strategy.interface import IStrategy, SellType +from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -850,7 +850,8 @@ class FreqtradeBot(LoggingMixin): trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Selling the trade forcefully') - self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) + self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( + sell_flag=True, sell_type=SellType.EMERGENCY_SELL)) except ExchangeError: trade.stoploss_order_id = None @@ -961,7 +962,7 @@ class FreqtradeBot(LoggingMixin): if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) + self.execute_sell(trade, sell_rate, should_sell) return True return False @@ -1150,8 +1151,7 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, - custom_reason: Optional[str] = None) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance @@ -1162,7 +1162,7 @@ class FreqtradeBot(LoggingMixin): :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' - if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, @@ -1179,10 +1179,10 @@ class FreqtradeBot(LoggingMixin): logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") order_type = self.strategy.order_types[sell_type] - if sell_reason == SellType.EMERGENCY_SELL: + if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason == SellType.FORCE_SELL: + if sell_reason.sell_type == SellType.FORCE_SELL: # Force sells (default to the sell_type defined in the strategy, # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) @@ -1193,7 +1193,7 @@ class FreqtradeBot(LoggingMixin): 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, time_in_force=time_in_force, - sell_reason=sell_reason.value): + sell_reason=sell_reason.sell_type.value): logger.info(f"User requested abortion of selling {trade.pair}") return False @@ -1216,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = custom_reason or sell_reason.value + trade.sell_reason = sell_reason.sell_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': self.update_trade_state(trade, trade.open_order_id, order) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4a8907582..957f31b63 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -24,7 +24,7 @@ from freqtrade.persistence.models import PairLock from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.strategy.interface import SellType +from freqtrade.strategy.interface import SellCheckTuple, SellType logger = logging.getLogger(__name__) @@ -554,7 +554,8 @@ class RPC: if not fully_canceled: # Get current rate and execute sell current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.FORCE_SELL) + self._freqtrade.execute_sell(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 435ac3ed3..c73574729 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,7 +7,7 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from enum import Enum -from typing import Dict, List, NamedTuple, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -54,13 +54,18 @@ class SellType(Enum): return self.value -class SellCheckTuple(NamedTuple): +class SellCheckTuple(object): """ NamedTuple for Sell type + reason """ - sell_flag: bool + sell_flag: bool # TODO: Remove? sell_type: SellType - sell_reason: Optional[str] = None + sell_reason: Optional[str] + + def __init__(self, sell_flag: bool, sell_type: SellType, sell_reason: Optional[str] = None): + self.sell_flag = sell_flag + self.sell_type = sell_type + self.sell_reason = sell_reason or sell_type.value class IStrategy(ABC, HyperStrategyMixin): @@ -594,7 +599,8 @@ class IStrategy(ABC, HyperStrategyMixin): if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type={sell_signal}, custom_reason={custom_reason}") + f"sell_type=SellType.{sell_signal.name}" + + (f", custom_reason={custom_reason}" if custom_reason else "")) return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ad000515e..aecff95ee 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2606,14 +2606,16 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N fetch_ticker=ticker_sell_up ) # Prevented sell ... - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 assert rpc_mock.call_count == 1 @@ -2665,7 +2667,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2722,7 +2724,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe trade.stop_loss = 0.00001099 * 0.99 freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] @@ -2774,7 +2776,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade.stoploss_order_id = "abcd" freqtrade.execute_sell(trade=trade, limit=1234, - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) assert sellmock.call_count == 1 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2824,7 +2826,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.SELL_SIGNAL) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() assert trade @@ -2929,7 +2931,8 @@ def test_execute_sell_market_order(default_conf, ticker, fee, ) freqtrade.config['order_types']['sell'] = 'market' - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)) assert not trade.is_open assert trade.close_profit == 0.0620716 @@ -2983,8 +2986,9 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) + sell_reason = SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=SellType.ROI) + sell_reason=sell_reason) assert mock_insuf.call_count == 1 @@ -3226,7 +3230,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) + sell_reason=SellCheckTuple(sell_flag=True, sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair)