From 1aad128d85dd2e9962631a1ffd8d5a3fd0dacbd1 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 20 Apr 2021 11:17:00 +0300 Subject: [PATCH] Support returning a string from custom_sell() and have it recorded as custom sell reason. --- freqtrade/freqtradebot.py | 11 ++++--- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/interface.py | 50 ++++++++++++++++++++----------- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ceb822472..9cce8c105 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -961,7 +961,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) + self.execute_sell(trade, sell_rate, should_sell.sell_type, should_sell.sell_reason) return True return False @@ -1150,12 +1150,15 @@ 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) -> bool: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType, + custom_reason: Optional[str] = None) -> bool: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order - :param sellreason: Reason the sell was triggered + :param sell_reason: Reason the sell was triggered + :param custom_reason: A custom sell reason. Provided only if + sell_reason == SellType.CUSTOM_SELL, :return: True if it succeeds (supported) False (not supported) """ sell_type = 'sell' @@ -1213,7 +1216,7 @@ class FreqtradeBot(LoggingMixin): trade.open_order_id = order['id'] trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.value + trade.sell_reason = custom_reason or sell_reason.value # 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/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fb9826a23..57ac70cc1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -255,7 +255,7 @@ class Backtesting: if sell.sell_flag: trade.close_date = sell_row[DATE_IDX] - trade.sell_reason = sell.sell_type.value + trade.sell_reason = sell.sell_reason or sell.sell_type.value trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1a007da15..74e92f389 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 +from typing import Dict, List, NamedTuple, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) +CUSTOM_SELL_MAX_LENGTH = 64 class SignalType(Enum): @@ -45,6 +46,7 @@ class SellType(Enum): SELL_SIGNAL = "sell_signal" FORCE_SELL = "force_sell" EMERGENCY_SELL = "emergency_sell" + CUSTOM_SELL = "custom_sell" NONE = "" def __str__(self): @@ -58,6 +60,7 @@ class SellCheckTuple(NamedTuple): """ sell_flag: bool sell_type: SellType + sell_reason: Optional[str] = None class IStrategy(ABC, HyperStrategyMixin): @@ -286,25 +289,28 @@ class IStrategy(ABC, HyperStrategyMixin): return self.stoploss def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, - current_profit: float, **kwargs) -> bool: + current_profit: float, **kwargs) -> Optional[Union[str, bool]]: """ - Custom sell signal logic indicating that specified position should be sold. Returning True - from this method is equal to setting sell signal on a candle at specified time. - This method is not called when sell signal is set. + Custom sell signal logic indicating that specified position should be sold. Returning a + string or True from this method is equal to setting sell signal on a candle at specified + time. This method is not called when sell signal is set. This method should be overridden to create sell signals that depend on trade parameters. For example you could implement a stoploss relative to candle when trade was opened, or a custom 1:2 risk-reward ROI. + Custom sell reason max length is 64. Exceeding this limit will raise OperationalException. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: Whether trade should exit now. + :return: To execute sell, return a string with custom sell reason or True. Otherwise return + None or False. """ - return False + return None def informative_pairs(self) -> ListPairsWithTimeframes: """ @@ -552,17 +558,27 @@ class IStrategy(ABC, HyperStrategyMixin): and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) + sell_signal = SellType.NONE + custom_reason = None if (ask_strategy.get('sell_profit_only', False) and current_profit <= ask_strategy.get('sell_profit_offset', 0)): # sell_profit_only and profit doesn't reach the offset - ignore sell signal - sell_signal = False - elif ask_strategy.get('use_sell_signal', True): - sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( - trade.pair, trade, date, current_rate, current_profit) - sell_signal = sell and not buy + pass + elif ask_strategy.get('use_sell_signal', True) and not buy: + if sell: + sell_signal = SellType.SELL_SIGNAL + else: + custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( + trade.pair, trade, date, current_rate, current_profit) + if custom_reason: + sell_signal = SellType.CUSTOM_SELL + if isinstance(custom_reason, bool): + custom_reason = None + elif isinstance(custom_reason, str): + if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: + raise OperationalException('Custom sell reason returned ' + 'from custom_sell is too long.') # TODO: return here if sell-signal should be favored over ROI - else: - sell_signal = False # Start evaluations # Sequence: @@ -574,10 +590,10 @@ class IStrategy(ABC, HyperStrategyMixin): f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if sell_signal: + if sell_signal != SellType.NONE: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " - f"sell_type=SellType.SELL_SIGNAL") - return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + f"sell_type={sell_signal}, custom_reason={custom_reason}") + return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason) if stoplossflag.sell_flag: