Update custom_sell to custom_exit
This commit is contained in:
parent
fe62a71f4c
commit
b044dd2c45
@ -222,21 +222,25 @@ class StrategyResolver(IResolver):
|
|||||||
if strategy:
|
if strategy:
|
||||||
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||||
# Require new method
|
# Require new method
|
||||||
if check_override(strategy, IStrategy, 'populate_entry_trend'):
|
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
|
||||||
raise OperationalException("`populate_entry_trend` must be implemented.")
|
raise OperationalException("`populate_entry_trend` must be implemented.")
|
||||||
if check_override(strategy, IStrategy, 'populate_exit_trend'):
|
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
|
||||||
raise OperationalException("`populate_exit_trend` must be implemented.")
|
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:
|
else:
|
||||||
# TODO: Implementing buy_trend and sell_trend should show a deprecation warning
|
# TODO: Implementing one of the following methods should show a deprecation warning
|
||||||
|
# buy_trend and sell_trend, custom_sell
|
||||||
if (
|
if (
|
||||||
check_override(strategy, IStrategy, 'populate_buy_trend')
|
not check_override(strategy, IStrategy, 'populate_buy_trend')
|
||||||
and check_override(strategy, IStrategy, 'populate_entry_trend')
|
and not check_override(strategy, IStrategy, 'populate_entry_trend')
|
||||||
):
|
):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
|
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
|
||||||
if (
|
if (
|
||||||
check_override(strategy, IStrategy, 'populate_sell_trend')
|
not check_override(strategy, IStrategy, 'populate_sell_trend')
|
||||||
and check_override(strategy, IStrategy, 'populate_exit_trend')
|
and not check_override(strategy, IStrategy, 'populate_exit_trend')
|
||||||
):
|
):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
|
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
|
||||||
@ -262,5 +266,6 @@ class StrategyResolver(IResolver):
|
|||||||
def check_override(object, parentclass, attribute):
|
def check_override(object, parentclass, attribute):
|
||||||
"""
|
"""
|
||||||
Checks if a object overrides the parent class 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)
|
return getattr(type(object), attribute) != getattr(parentclass, attribute)
|
||||||
|
@ -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:
|
||||||
@ -380,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.
|
||||||
@ -401,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:
|
||||||
@ -866,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):
|
||||||
|
@ -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
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
# The strategy which fails to load due to non-existent dependency
|
"""
|
||||||
|
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 pandas import DataFrame
|
||||||
|
|
||||||
@ -14,3 +19,13 @@ class TestStrategyNoImplements(IStrategy):
|
|||||||
class TestStrategyNoImplementSell(TestStrategyNoImplements):
|
class TestStrategyNoImplementSell(TestStrategyNoImplements):
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
return super().populate_entry_trend(dataframe, metadata)
|
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
|
||||||
|
@ -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)
|
||||||
|
@ -392,7 +392,7 @@ def test_deprecate_populate_indicators(result, default_conf):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:deprecated")
|
@pytest.mark.filterwarnings("ignore:deprecated")
|
||||||
def test_missing_implements(result, default_conf):
|
def test_missing_implements(default_conf):
|
||||||
default_location = Path(__file__).parent / "strats/broken_strats"
|
default_location = Path(__file__).parent / "strats/broken_strats"
|
||||||
default_conf.update({'strategy': 'TestStrategyNoImplements',
|
default_conf.update({'strategy': 'TestStrategyNoImplements',
|
||||||
'strategy_path': default_location})
|
'strategy_path': default_location})
|
||||||
@ -406,6 +406,7 @@ def test_missing_implements(result, default_conf):
|
|||||||
match=r"`populate_exit_trend` or `populate_sell_trend`.*"):
|
match=r"`populate_exit_trend` or `populate_sell_trend`.*"):
|
||||||
StrategyResolver.load_strategy(default_conf)
|
StrategyResolver.load_strategy(default_conf)
|
||||||
|
|
||||||
|
# Futures mode is more strict ...
|
||||||
default_conf['trading_mode'] = 'futures'
|
default_conf['trading_mode'] = 'futures'
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
@ -417,6 +418,12 @@ def test_missing_implements(result, default_conf):
|
|||||||
match=r"`populate_entry_trend` must be implemented.*"):
|
match=r"`populate_entry_trend` must be implemented.*"):
|
||||||
StrategyResolver.load_strategy(default_conf)
|
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):
|
||||||
|
Loading…
Reference in New Issue
Block a user