Support returning a string from custom_sell() and have it recorded as custom sell reason.

This commit is contained in:
Rokas Kupstys 2021-04-20 11:17:00 +03:00
parent a77337e424
commit 1aad128d85
3 changed files with 41 additions and 22 deletions

View File

@ -961,7 +961,7 @@ class FreqtradeBot(LoggingMixin):
if should_sell.sell_flag: if should_sell.sell_flag:
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') 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 True
return False return False
@ -1150,12 +1150,15 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException( raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") 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 Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param limit: limit rate for the sell order :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) :return: True if it succeeds (supported) False (not supported)
""" """
sell_type = 'sell' sell_type = 'sell'
@ -1213,7 +1216,7 @@ class FreqtradeBot(LoggingMixin):
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.sell_order_status = '' trade.sell_order_status = ''
trade.close_rate_requested = limit 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 # In case of market sell 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)

View File

@ -255,7 +255,7 @@ class Backtesting:
if sell.sell_flag: if sell.sell_flag:
trade.close_date = sell_row[DATE_IDX] 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) 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) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)

View File

@ -7,7 +7,7 @@ import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from enum import Enum from enum import Enum
from typing import Dict, List, NamedTuple, Optional, Tuple from typing import Dict, List, NamedTuple, Optional, Tuple, Union
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
@ -24,6 +24,7 @@ from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CUSTOM_SELL_MAX_LENGTH = 64
class SignalType(Enum): class SignalType(Enum):
@ -45,6 +46,7 @@ class SellType(Enum):
SELL_SIGNAL = "sell_signal" SELL_SIGNAL = "sell_signal"
FORCE_SELL = "force_sell" FORCE_SELL = "force_sell"
EMERGENCY_SELL = "emergency_sell" EMERGENCY_SELL = "emergency_sell"
CUSTOM_SELL = "custom_sell"
NONE = "" NONE = ""
def __str__(self): def __str__(self):
@ -58,6 +60,7 @@ class SellCheckTuple(NamedTuple):
""" """
sell_flag: bool sell_flag: bool
sell_type: SellType sell_type: SellType
sell_reason: Optional[str] = None
class IStrategy(ABC, HyperStrategyMixin): class IStrategy(ABC, HyperStrategyMixin):
@ -286,25 +289,28 @@ class IStrategy(ABC, HyperStrategyMixin):
return self.stoploss return self.stoploss
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, 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 Custom sell signal logic indicating that specified position should be sold. Returning a
from this method is equal to setting sell signal on a candle at specified time. string or True from this method is equal to setting sell signal on a candle at specified
This method is not called when sell signal is set. 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 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 example you could implement a stoploss relative to candle when trade was opened, or a custom
1:2 risk-reward ROI. 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 pair: Pair that's currently analyzed
:param trade: trade object. :param trade: trade object.
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy. :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 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. :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: def informative_pairs(self) -> ListPairsWithTimeframes:
""" """
@ -552,17 +558,27 @@ class IStrategy(ABC, HyperStrategyMixin):
and self.min_roi_reached(trade=trade, current_profit=current_profit, and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=date)) current_time=date))
sell_signal = SellType.NONE
custom_reason = None
if (ask_strategy.get('sell_profit_only', False) if (ask_strategy.get('sell_profit_only', False)
and current_profit <= ask_strategy.get('sell_profit_offset', 0)): and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal # sell_profit_only and profit doesn't reach the offset - ignore sell signal
sell_signal = False pass
elif ask_strategy.get('use_sell_signal', True): elif ask_strategy.get('use_sell_signal', True) and not buy:
sell = sell or strategy_safe_wrapper(self.custom_sell, default_retval=False)( if sell:
trade.pair, trade, date, current_rate, current_profit) sell_signal = SellType.SELL_SIGNAL
sell_signal = sell and not buy
# TODO: return here if sell-signal should be favored over ROI
else: else:
sell_signal = False 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
# Start evaluations # Start evaluations
# Sequence: # Sequence:
@ -574,10 +590,10 @@ class IStrategy(ABC, HyperStrategyMixin):
f"sell_type=SellType.ROI") f"sell_type=SellType.ROI")
return SellCheckTuple(sell_flag=True, 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, " logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
f"sell_type=SellType.SELL_SIGNAL") f"sell_type={sell_signal}, custom_reason={custom_reason}")
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) return SellCheckTuple(sell_flag=True, sell_type=sell_signal, sell_reason=custom_reason)
if stoplossflag.sell_flag: if stoplossflag.sell_flag: