Merge pull request #6523 from freqtrade/short_exit_trend

Update methods wording
This commit is contained in:
Matthias 2022-03-12 15:53:11 +01:00 committed by GitHub
commit b8cbc4dcb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 201 additions and 51 deletions

View File

@ -220,6 +220,31 @@ class StrategyResolver(IResolver):
) )
if strategy: if strategy:
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# Require new method
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
raise OperationalException("`populate_entry_trend` must be implemented.")
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
raise OperationalException("`populate_exit_trend` must be implemented.")
if check_override(strategy, IStrategy, 'custom_sell'):
raise OperationalException(
"Please migrate your implementation of `custom_sell` to `custom_exit`.")
else:
# TODO: Implementing one of the following methods should show a deprecation warning
# buy_trend and sell_trend, custom_sell
if (
not check_override(strategy, IStrategy, 'populate_buy_trend')
and not check_override(strategy, IStrategy, 'populate_entry_trend')
):
raise OperationalException(
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
if (
not check_override(strategy, IStrategy, 'populate_sell_trend')
and not check_override(strategy, IStrategy, 'populate_exit_trend')
):
raise OperationalException(
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
@ -236,3 +261,11 @@ class StrategyResolver(IResolver):
f"Impossible to load Strategy '{strategy_name}'. This class does not exist " f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
"or contains Python code errors." "or contains Python code errors."
) )
def check_override(object, parentclass, attribute):
"""
Checks if a object overrides the parent class attribute.
:returns: True if the object is overridden.
"""
return getattr(type(object), attribute) != getattr(parentclass, attribute)

View File

@ -29,7 +29,7 @@ from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CUSTOM_SELL_MAX_LENGTH = 64 CUSTOM_EXIT_MAX_LENGTH = 64
class SellCheckTuple: class SellCheckTuple:
@ -177,19 +177,27 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return dataframe return dataframe
@abstractmethod
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe DEPRECATED - please migrate to populate_entry_trend
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
return dataframe return dataframe
@abstractmethod def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with entry columns populated
"""
return self.populate_buy_trend(dataframe, metadata)
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
DEPRECATED - please migrate to populate_exit_trend
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
@ -197,6 +205,15 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return dataframe return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit columns populated
"""
return self.populate_sell_trend(dataframe, metadata)
def bot_loop_start(self, **kwargs) -> None: def bot_loop_start(self, **kwargs) -> None:
""" """
Called at the start of the bot iteration (one loop). Called at the start of the bot iteration (one loop).
@ -363,6 +380,7 @@ class IStrategy(ABC, HyperStrategyMixin):
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) -> Optional[Union[str, bool]]: current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
""" """
DEPRECATED - please use custom_exit instead.
Custom exit signal logic indicating that specified position should be sold. Returning a Custom exit signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting exit signal on a candle at specified string or True from this method is equal to setting exit signal on a candle at specified
time. This method is not called when exit signal is set. time. This method is not called when exit signal is set.
@ -384,6 +402,30 @@ class IStrategy(ABC, HyperStrategyMixin):
""" """
return None return None
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
"""
Custom exit signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting exit signal on a candle at specified
time. This method is not called when exit signal is set.
This method should be overridden to create exit signals that depend on trade parameters. For
example you could implement an exit relative to the candle when the trade was opened,
or a custom 1:2 risk-reward ROI.
Custom exit reason max length is 64. Exceeding characters will be removed.
: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: To execute exit, return a string with custom sell reason or True. Otherwise return
None or False.
"""
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float, proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], side: str, **kwargs) -> float: entry_tag: Optional[str], side: str, **kwargs) -> float:
@ -849,17 +891,17 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_signal = SellType.SELL_SIGNAL sell_signal = SellType.SELL_SIGNAL
else: else:
trade_type = "exit_short" if trade.is_short else "sell" trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
pair=trade.pair, trade=trade, current_time=current_time, pair=trade.pair, trade=trade, current_time=current_time,
current_rate=current_rate, current_profit=current_profit) current_rate=current_rate, current_profit=current_profit)
if custom_reason: if custom_reason:
sell_signal = SellType.CUSTOM_SELL sell_signal = SellType.CUSTOM_SELL
if isinstance(custom_reason, str): if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
logger.warning(f'Custom {trade_type} reason returned from ' logger.warning(f'Custom {trade_type} reason returned from '
f'custom_{trade_type} is too long and was trimmed' f'custom_exit is too long and was trimmed'
f'to {CUSTOM_SELL_MAX_LENGTH} characters.') f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH] custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
else: else:
custom_reason = None custom_reason = None
if sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL): if sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL):
@ -1072,7 +1114,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)
df = self.populate_buy_trend(dataframe) # type: ignore df = self.populate_buy_trend(dataframe) # type: ignore
else: else:
df = self.populate_buy_trend(dataframe, metadata) df = self.populate_entry_trend(dataframe, metadata)
if 'enter_long' not in df.columns: if 'enter_long' not in df.columns:
df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns') df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns')
@ -1094,7 +1136,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)
df = self.populate_sell_trend(dataframe) # type: ignore df = self.populate_sell_trend(dataframe) # type: ignore
else: else:
df = self.populate_sell_trend(dataframe, metadata) df = self.populate_exit_trend(dataframe, metadata)
if 'exit_long' not in df.columns: if 'exit_long' not in df.columns:
df = df.rename({'sell': 'exit_long'}, axis='columns') df = df.rename({'sell': 'exit_long'}, axis='columns')
return df return df

View File

@ -29,7 +29,7 @@ class {{ strategy }}(IStrategy):
You must keep: You must keep:
- the lib in the section "Do not remove these libs" - the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend - the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep: You should keep:
- timeframe, minimal_roi, stoploss, trailing_* - timeframe, minimal_roi, stoploss, trailing_*
""" """
@ -119,12 +119,12 @@ class {{ strategy }}(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with entry columns populated
""" """
dataframe.loc[ dataframe.loc[
( (
@ -144,12 +144,12 @@ class {{ strategy }}(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with exit columns populated
""" """
dataframe.loc[ dataframe.loc[
( (

View File

@ -30,7 +30,7 @@ class SampleShortStrategy(IStrategy):
You must keep: You must keep:
- the lib in the section "Do not remove these libs" - the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend - the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep: You should keep:
- timeframe, minimal_roi, stoploss, trailing_* - timeframe, minimal_roi, stoploss, trailing_*
""" """
@ -341,7 +341,7 @@ class SampleShortStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame populated with indicators
@ -361,7 +361,7 @@ class SampleShortStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame populated with indicators

View File

@ -29,7 +29,7 @@ class SampleStrategy(IStrategy):
You must keep: You must keep:
- the lib in the section "Do not remove these libs" - the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend - the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep: You should keep:
- timeframe, minimal_roi, stoploss, trailing_* - timeframe, minimal_roi, stoploss, trailing_*
""" """
@ -342,12 +342,12 @@ class SampleStrategy(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the buy signal for the given dataframe Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column :return: DataFrame with entry columns populated
""" """
dataframe.loc[ dataframe.loc[
( (
@ -371,12 +371,12 @@ class SampleStrategy(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame populated with indicators :param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair :param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column :return: DataFrame with exit columns populated
""" """
dataframe.loc[ dataframe.loc[
( (

View File

@ -92,7 +92,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
""" """
return self.stoploss return self.stoploss
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]': current_profit: float, **kwargs) -> 'Optional[Union[str, bool]]':
""" """
Custom sell signal logic indicating that specified position should be sold. Returning a Custom sell signal logic indicating that specified position should be sold. Returning a

View File

@ -0,0 +1,31 @@
"""
The strategies here are minimal strategies designed to fail loading in certain conditions.
They are not operational, and don't aim to be.
"""
from datetime import datetime
from pandas import DataFrame
from freqtrade.strategy.interface import IStrategy
class TestStrategyNoImplements(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_indicators(dataframe, metadata)
class TestStrategyNoImplementSell(TestStrategyNoImplements):
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_entry_trend(dataframe, metadata)
class TestStrategyImplementCustomSell(TestStrategyNoImplementSell):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return super().populate_exit_trend(dataframe, metadata)
def custom_sell(self, pair: str, trade, current_time: datetime,
current_rate: float, current_profit: float,
**kwargs):
return False

View File

@ -125,7 +125,7 @@ class StrategyTestV3(IStrategy):
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[ dataframe.loc[
( (
@ -147,7 +147,7 @@ class StrategyTestV3(IStrategy):
return dataframe return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[ dataframe.loc[
( (
( (

View File

@ -13,8 +13,8 @@ def test_strategy_test_v3_structure():
assert hasattr(StrategyTestV3, 'stoploss') assert hasattr(StrategyTestV3, 'stoploss')
assert hasattr(StrategyTestV3, 'timeframe') assert hasattr(StrategyTestV3, 'timeframe')
assert hasattr(StrategyTestV3, 'populate_indicators') assert hasattr(StrategyTestV3, 'populate_indicators')
assert hasattr(StrategyTestV3, 'populate_buy_trend') assert hasattr(StrategyTestV3, 'populate_entry_trend')
assert hasattr(StrategyTestV3, 'populate_sell_trend') assert hasattr(StrategyTestV3, 'populate_exit_trend')
@pytest.mark.parametrize('is_short,side', [ @pytest.mark.parametrize('is_short,side', [

View File

@ -477,7 +477,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
strategy.custom_stoploss = original_stopvalue strategy.custom_stoploss = original_stopvalue
def test_custom_sell(default_conf, fee, caplog) -> None: def test_custom_exit(default_conf, fee, caplog) -> None:
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade( trade = Trade(
@ -499,7 +499,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
assert res.sell_flag is False assert res.sell_flag is False
assert res.sell_type == SellType.NONE assert res.sell_type == SellType.NONE
strategy.custom_sell = MagicMock(return_value=True) strategy.custom_exit = MagicMock(return_value=True)
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
@ -507,7 +507,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_reason == 'custom_sell' assert res.sell_reason == 'custom_sell'
strategy.custom_sell = MagicMock(return_value='hello world') strategy.custom_exit = MagicMock(return_value='hello world')
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
@ -517,14 +517,14 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
assert res.sell_reason == 'hello world' assert res.sell_reason == 'hello world'
caplog.clear() caplog.clear()
strategy.custom_sell = MagicMock(return_value='h' * 100) strategy.custom_exit = MagicMock(return_value='h' * 100)
res = strategy.should_exit(trade, 1, now, res = strategy.should_exit(trade, 1, now,
enter=False, exit_=False, enter=False, exit_=False,
low=None, high=None) low=None, high=None)
assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True assert res.sell_flag is True
assert res.sell_reason == 'h' * 64 assert res.sell_reason == 'h' * 64
assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) assert log_has_re('Custom sell reason returned from custom_exit is too long.*', caplog)
@pytest.mark.parametrize('side', TRADE_SIDES) @pytest.mark.parametrize('side', TRADE_SIDES)

View File

@ -144,6 +144,16 @@ def test_strategy_can_short(caplog, default_conf):
assert isinstance(strat, IStrategy) assert isinstance(strat, IStrategy)
def test_strategy_implements_populate_entry(caplog, default_conf):
caplog.set_level(logging.INFO)
default_conf.update({
'strategy': "StrategyTestV2",
})
default_conf['trading_mode'] = 'futures'
with pytest.raises(OperationalException, match="`populate_entry_trend` must be implemented."):
StrategyResolver.load_strategy(default_conf)
def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
default_conf.update({ default_conf.update({
@ -381,6 +391,40 @@ def test_deprecate_populate_indicators(result, default_conf):
in str(w[-1].message) in str(w[-1].message)
@pytest.mark.filterwarnings("ignore:deprecated")
def test_missing_implements(default_conf):
default_location = Path(__file__).parent / "strats/broken_strats"
default_conf.update({'strategy': 'TestStrategyNoImplements',
'strategy_path': default_location})
with pytest.raises(OperationalException,
match=r"`populate_entry_trend` or `populate_buy_trend`.*"):
StrategyResolver.load_strategy(default_conf)
default_conf['strategy'] = 'TestStrategyNoImplementSell'
with pytest.raises(OperationalException,
match=r"`populate_exit_trend` or `populate_sell_trend`.*"):
StrategyResolver.load_strategy(default_conf)
# Futures mode is more strict ...
default_conf['trading_mode'] = 'futures'
with pytest.raises(OperationalException,
match=r"`populate_exit_trend` must be implemented.*"):
StrategyResolver.load_strategy(default_conf)
default_conf['strategy'] = 'TestStrategyNoImplements'
with pytest.raises(OperationalException,
match=r"`populate_entry_trend` must be implemented.*"):
StrategyResolver.load_strategy(default_conf)
default_conf['strategy'] = 'TestStrategyImplementCustomSell'
with pytest.raises(OperationalException,
match=r"Please migrate your implementation of `custom_sell`.*"):
StrategyResolver.load_strategy(default_conf)
@pytest.mark.filterwarnings("ignore:deprecated") @pytest.mark.filterwarnings("ignore:deprecated")
def test_call_deprecated_function(result, default_conf, caplog): def test_call_deprecated_function(result, default_conf, caplog):
default_location = Path(__file__).parent / "strats" default_location = Path(__file__).parent / "strats"