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:
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)

View File

@ -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)

View File

@ -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
# TODO: return here if sell-signal should be favored over ROI
pass
elif ask_strategy.get('use_sell_signal', True) and not buy:
if sell:
sell_signal = SellType.SELL_SIGNAL
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
# 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: