Merge branch 'feat/short' into fs_fix

This commit is contained in:
adriance 2022-03-13 12:13:33 +08:00
commit 56c4ea6619
11 changed files with 201 additions and 51 deletions

View File

@ -220,15 +220,40 @@ class StrategyResolver(IResolver):
)
if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any(x == 2 for x in [
strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len
]):
strategy.INTERFACE_VERSION = 1
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._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any(x == 2 for x in [
strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len
]):
strategy.INTERFACE_VERSION = 1
return strategy
@ -236,3 +261,11 @@ class StrategyResolver(IResolver):
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
"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__)
CUSTOM_SELL_MAX_LENGTH = 64
CUSTOM_EXIT_MAX_LENGTH = 64
class SellCheckTuple:
@ -177,19 +177,27 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return dataframe
@abstractmethod
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 metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
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:
"""
DEPRECATED - please migrate to populate_exit_trend
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
@ -197,6 +205,15 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
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:
"""
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,
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
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.
@ -384,6 +402,30 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
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,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
@ -849,17 +891,17 @@ class IStrategy(ABC, HyperStrategyMixin):
sell_signal = SellType.SELL_SIGNAL
else:
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,
current_rate=current_rate, current_profit=current_profit)
if custom_reason:
sell_signal = SellType.CUSTOM_SELL
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 '
f'custom_{trade_type} is too long and was trimmed'
f'to {CUSTOM_SELL_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
f'custom_exit is too long and was trimmed'
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
else:
custom_reason = None
if sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL):
@ -1072,7 +1114,7 @@ class IStrategy(ABC, HyperStrategyMixin):
"the current function headers!", DeprecationWarning)
df = self.populate_buy_trend(dataframe) # type: ignore
else:
df = self.populate_buy_trend(dataframe, metadata)
df = self.populate_entry_trend(dataframe, metadata)
if 'enter_long' not in df.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)
df = self.populate_sell_trend(dataframe) # type: ignore
else:
df = self.populate_sell_trend(dataframe, metadata)
df = self.populate_exit_trend(dataframe, metadata)
if 'exit_long' not in df.columns:
df = df.rename({'sell': 'exit_long'}, axis='columns')
return df

View File

@ -29,7 +29,7 @@ class {{ strategy }}(IStrategy):
You must keep:
- 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:
- timeframe, minimal_roi, stoploss, trailing_*
"""
@ -119,12 +119,12 @@ class {{ strategy }}(IStrategy):
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
:param dataframe: DataFrame populated with indicators
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 buy column
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
@ -144,12 +144,12 @@ class {{ strategy }}(IStrategy):
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
:param dataframe: DataFrame populated with indicators
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 buy column
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(

View File

@ -30,7 +30,7 @@ class SampleShortStrategy(IStrategy):
You must keep:
- 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:
- timeframe, minimal_roi, stoploss, trailing_*
"""
@ -341,7 +341,7 @@ class SampleShortStrategy(IStrategy):
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
:param dataframe: DataFrame populated with indicators
@ -361,7 +361,7 @@ class SampleShortStrategy(IStrategy):
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
:param dataframe: DataFrame populated with indicators

View File

@ -29,7 +29,7 @@ class SampleStrategy(IStrategy):
You must keep:
- 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:
- timeframe, minimal_roi, stoploss, trailing_*
"""
@ -342,12 +342,12 @@ class SampleStrategy(IStrategy):
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
:param dataframe: DataFrame populated with indicators
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 buy column
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
@ -371,12 +371,12 @@ class SampleStrategy(IStrategy):
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
:param dataframe: DataFrame populated with indicators
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 sell column
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(

View File

@ -92,7 +92,7 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime',
"""
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]]':
"""
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
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
@ -147,7 +147,7 @@ class StrategyTestV3(IStrategy):
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(

View File

@ -13,8 +13,8 @@ def test_strategy_test_v3_structure():
assert hasattr(StrategyTestV3, 'stoploss')
assert hasattr(StrategyTestV3, 'timeframe')
assert hasattr(StrategyTestV3, 'populate_indicators')
assert hasattr(StrategyTestV3, 'populate_buy_trend')
assert hasattr(StrategyTestV3, 'populate_sell_trend')
assert hasattr(StrategyTestV3, 'populate_entry_trend')
assert hasattr(StrategyTestV3, 'populate_exit_trend')
@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
def test_custom_sell(default_conf, fee, caplog) -> None:
def test_custom_exit(default_conf, fee, caplog) -> None:
strategy = StrategyResolver.load_strategy(default_conf)
trade = Trade(
@ -499,7 +499,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
assert res.sell_flag is False
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,
enter=False, exit_=False,
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_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,
enter=False, exit_=False,
@ -517,14 +517,14 @@ def test_custom_sell(default_conf, fee, caplog) -> None:
assert res.sell_reason == 'hello world'
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,
enter=False, exit_=False,
low=None, high=None)
assert res.sell_type == SellType.CUSTOM_SELL
assert res.sell_flag is True
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)

View File

@ -144,6 +144,16 @@ def test_strategy_can_short(caplog, default_conf):
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):
caplog.set_level(logging.INFO)
default_conf.update({
@ -381,6 +391,40 @@ def test_deprecate_populate_indicators(result, default_conf):
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")
def test_call_deprecated_function(result, default_conf, caplog):
default_location = Path(__file__).parent / "strats"