Support returning a string from custom_sell() and have it recorded as custom sell reason.
This commit is contained in:
parent
a77337e424
commit
1aad128d85
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
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
|
# TODO: return here if sell-signal should be favored over ROI
|
||||||
else:
|
|
||||||
sell_signal = False
|
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user