diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 243043d31..b366059da 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -167,8 +167,15 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + df_analyzed = self.strategy.advise_exit( + dataframe=self.strategy.advise_enter( + dataframe=pair_data, + metadata={'pair': pair}, + is_short=False + ), + metadata={'pair': pair}, + is_short=False + )[headers].copy() trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index d2995d57a..ffba5ee90 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -7,6 +7,8 @@ class SignalType(Enum): """ BUY = "buy" SELL = "sell" + SHORT = "short" + EXIT_SHORT = "exit_short" class SignalTagType(Enum): @@ -14,3 +16,4 @@ class SignalTagType(Enum): Enum for signal columns """ BUY_TAG = "buy_tag" + SELL_TAG = "sell_tag" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3079e326d..550ceecd8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -231,8 +231,8 @@ class Backtesting: if has_buy_tag: pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist - df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() + df_analyzed = self.strategy.advise_exit( + self.strategy.advise_enter(pair_data, {'pair': pair}), {'pair': pair}).copy() # Trim startup period from analyzed dataframe df_analyzed = trim_dataframe(df_analyzed, self.timerange, startup_candles=self.required_startup) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 0db78aa39..4c07419b8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -110,7 +110,7 @@ class Hyperopt: self.backtesting.strategy.advise_indicators = ( # type: ignore self.custom_hyperopt.populate_indicators) # type: ignore if hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.backtesting.strategy.advise_buy = ( # type: ignore + self.backtesting.strategy.advise_enter = ( # type: ignore self.custom_hyperopt.populate_buy_trend) # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): self.backtesting.strategy.advise_sell = ( # type: ignore @@ -283,12 +283,13 @@ class Hyperopt: params_dict = self._get_params_dict(self.dimensions, raw_params) # Apply parameters + # TODO-lev: These don't take a side, how can I pass is_short=True/False to it if HyperoptTools.has_space(self.config, 'buy'): - self.backtesting.strategy.advise_buy = ( # type: ignore + self.backtesting.strategy.advise_enter = ( # type: ignore self.custom_hyperopt.buy_strategy_generator(params_dict)) if HyperoptTools.has_space(self.config, 'sell'): - self.backtesting.strategy.advise_sell = ( # type: ignore + self.backtesting.strategy.advise_exit = ( # type: ignore self.custom_hyperopt.sell_strategy_generator(params_dict)) if HyperoptTools.has_space(self.config, 'protection'): diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 8327a4d13..fd7d3dbf6 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -51,6 +51,7 @@ class HyperOptResolver(IResolver): if not hasattr(hyperopt, 'populate_sell_trend'): logger.info("Hyperopt class does not provide populate_sell_trend() method. " "Using populate_sell_trend from the strategy.") + # TODO-lev: Short equivelents? return hyperopt diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index e7c077e84..38a5b4850 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -202,9 +202,14 @@ class StrategyResolver(IResolver): 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) + strategy._short_fun_len = len(getfullargspec(strategy.populate_short_trend).args) + strategy._exit_short_fun_len = len( + getfullargspec(strategy.populate_exit_short_trend).args) if any(x == 2 for x in [strategy._populate_fun_len, strategy._buy_fun_len, - strategy._sell_fun_len]): + strategy._sell_fun_len, + strategy._short_fun_len, + strategy._exit_short_fun_len]): strategy.INTERFACE_VERSION = 1 return strategy diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index 2f72cb74c..7d76d52ed 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -44,5 +44,5 @@ class UvicornServer(uvicorn.Server): time.sleep(1e-3) def cleanup(self): - self.should_exit = True + self.should_sell = True self.thread.join() diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index dad282d7e..87d4241f1 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -22,6 +22,8 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) +# TODO-lev: This file + class BaseParameter(ABC): """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index bf5cc10af..26ad2fcd4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -62,6 +62,8 @@ class IStrategy(ABC, HyperStrategyMixin): _populate_fun_len: int = 0 _buy_fun_len: int = 0 _sell_fun_len: int = 0 + _short_fun_len: int = 0 + _exit_short_fun_len: int = 0 _ft_params_from_file: Dict = {} # associated minimal roi minimal_roi: Dict @@ -135,7 +137,7 @@ class IStrategy(ABC, HyperStrategyMixin): @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Populate indicators that will be used in the Buy and Sell strategy + Populate indicators that will be used in the Buy, Sell, Short, Exit_short strategy :param dataframe: DataFrame with data from the exchange :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies @@ -143,7 +145,7 @@ class IStrategy(ABC, HyperStrategyMixin): return dataframe @abstractmethod - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def populate_enter_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame @@ -153,7 +155,7 @@ class IStrategy(ABC, HyperStrategyMixin): return dataframe @abstractmethod - 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 @@ -164,9 +166,9 @@ class IStrategy(ABC, HyperStrategyMixin): def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ - Check buy timeout function callback. - This method can be used to override the buy-timeout. - It is called whenever a limit buy order has been created, + Check enter timeout function callback. + This method can be used to override the enter-timeout. + It is called whenever a limit buy/short order has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. @@ -176,16 +178,16 @@ class IStrategy(ABC, HyperStrategyMixin): :param trade: trade object. :param order: Order dictionary as returned from CCXT. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the buy-order is cancelled. + :return bool: When True is returned, then the buy/short-order is cancelled. """ return False def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ - Check sell timeout function callback. - This method can be used to override the sell-timeout. - It is called whenever a limit sell order has been created, - and is not yet fully filled. + Check exit timeout function callback. + This method can be used to override the exit-timeout. + It is called whenever a (long) limit sell order or (short) limit buy + has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. @@ -194,7 +196,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param trade: trade object. :param order: Order dictionary as returned from CCXT. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the sell-order is cancelled. + :return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled. """ return False @@ -210,7 +212,7 @@ class IStrategy(ABC, HyperStrategyMixin): def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a buy/short order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -218,7 +220,7 @@ class IStrategy(ABC, HyperStrategyMixin): When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be bought. + :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (quote) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders @@ -234,7 +236,7 @@ class IStrategy(ABC, HyperStrategyMixin): rate: float, time_in_force: str, sell_reason: str, current_time: datetime, **kwargs) -> bool: """ - Called right before placing a regular sell order. + Called right before placing a regular sell/exit_short order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -242,18 +244,18 @@ class IStrategy(ABC, HyperStrategyMixin): When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be sold. + :param pair: Pair for trade that's about to be exited. :param trade: trade object. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in quote currency. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. + :param sell_reason: Exit reason. Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', 'sell_signal', 'force_sell', 'emergency_sell'] :param current_time: datetime object, containing the current datetime :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the sell-order is placed on the exchange. + :return bool: When True, then the sell-order/exit_short-order is placed on the exchange. False aborts the process """ return True @@ -283,15 +285,15 @@ 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]]: """ - 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. + 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 sell signals that depend on trade parameters. For - example you could implement a sell relative to the candle when the trade was opened, + 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 sell reason max length is 64. Exceeding characters will be removed. + Custom exit reason max length is 64. Exceeding characters will be removed. :param pair: Pair that's currently analyzed :param trade: trade object. @@ -299,7 +301,7 @@ class IStrategy(ABC, HyperStrategyMixin): :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 sell, return a string with custom sell reason or True. Otherwise return + :return: To execute exit, return a string with custom sell reason or True. Otherwise return None or False. """ return None @@ -371,7 +373,7 @@ class IStrategy(ABC, HyperStrategyMixin): Checks if a pair is currently locked The 2nd, optional parameter ensures that locks are applied until the new candle arrives, and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap - of 2 seconds for a buy to happen on an old signal. + of 2 seconds for a buy/short to happen on an old signal. :param pair: "Pair to check" :param candle_date: Date of the last candle. Optional, defaults to current date :returns: locking state of the pair in question. @@ -387,15 +389,17 @@ class IStrategy(ABC, HyperStrategyMixin): def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given candle (OHLCV) data and returns a populated DataFrame - add several TA indicators and buy signal to it + add several TA indicators and buy/short signal to it :param dataframe: Dataframe containing data from exchange :param metadata: Metadata dictionary with additional data (e.g. 'pair') :return: DataFrame of candle (OHLCV) data with indicator data and signals added """ logger.debug("TA Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.advise_enter(dataframe, metadata, is_short=False) + dataframe = self.advise_exit(dataframe, metadata, is_short=False) + dataframe = self.advise_enter(dataframe, metadata, is_short=True) + dataframe = self.advise_exit(dataframe, metadata, is_short=True) return dataframe def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -422,7 +426,10 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 dataframe['sell'] = 0 + dataframe['short'] = 0 + dataframe['exit_short'] = 0 dataframe['buy_tag'] = None + dataframe['short_tag'] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -482,6 +489,7 @@ class IStrategy(ABC, HyperStrategyMixin): if dataframe is None: message = "No dataframe returned (return statement missing?)." elif 'buy' not in dataframe: + # TODO-lev: Something? message = "Buy column not set." elif df_len != len(dataframe): message = message_template.format("length") @@ -499,15 +507,18 @@ class IStrategy(ABC, HyperStrategyMixin): self, pair: str, timeframe: str, - dataframe: DataFrame + dataframe: DataFrame, + is_short: bool = False ) -> Tuple[bool, bool, Optional[str]]: """ - Calculates current signal based based on the buy / sell columns of the dataframe. - Used by Bot to get the signal to buy or sell + Calculates current signal based based on the buy/short or sell/exit_short + columns of the dataframe. + Used by Bot to get the signal to buy, sell, short, or exit_short :param pair: pair in format ANT/BTC :param timeframe: timeframe to use :param dataframe: Analyzed dataframe to get signal from. - :return: (Buy, Sell) A bool-tuple indicating buy/sell signal + :return: (Buy, Sell)/(Short, Exit_short) A bool-tuple indicating + (buy/sell)/(short/exit_short) signal """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') @@ -528,42 +539,49 @@ class IStrategy(ABC, HyperStrategyMixin): ) return False, False, None - buy = latest[SignalType.BUY.value] == 1 + (enter_type, enter_tag) = ( + (SignalType.SHORT, SignalTagType.SHORT_TAG) + if is_short else + (SignalType.BUY, SignalTagType.BUY_TAG) + ) + exit_type = SignalType.EXIT_SHORT if is_short else SignalType.SELL - sell = False - if SignalType.SELL.value in latest: - sell = latest[SignalType.SELL.value] == 1 + enter = latest[enter_type.value] == 1 - buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) + exit = False + if exit_type.value in latest: + exit = latest[exit_type.value] == 1 - logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', - latest['date'], pair, str(buy), str(sell)) + enter_tag_value = latest.get(enter_tag.value, None) + + logger.debug(f'trigger: %s (pair=%s) {enter_type.value}=%s {exit_type.value}=%s', + latest['date'], pair, str(enter), str(exit)) timeframe_seconds = timeframe_to_seconds(timeframe) if self.ignore_expired_candle(latest_date=latest_date, current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, - buy=buy): - return False, sell, buy_tag - return buy, sell, buy_tag + enter=enter): + return False, exit, enter_tag_value + return enter, exit, enter_tag_value def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, - timeframe_seconds: int, buy: bool): - if self.ignore_buying_expired_candle_after and buy: + timeframe_seconds: int, enter: bool): + if self.ignore_buying_expired_candle_after and enter: time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds)) return time_delta.total_seconds() > self.ignore_buying_expired_candle_after else: return False - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, - sell: bool, low: float = None, high: float = None, + def should_sell(self, trade: Trade, rate: float, date: datetime, enter: bool, + exit: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluates if one of the conditions required to trigger a sell - has been reached, which can either be a stop-loss, ROI or sell-signal. - :param low: Only used during backtesting to simulate stoploss - :param high: Only used during backtesting, to simulate ROI + This function evaluates if one of the conditions required to trigger a sell/exit_short + has been reached, which can either be a stop-loss, ROI or exit-signal. + :param low: Only used during backtesting to simulate (long)stoploss/(short)ROI + :param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI :param force_stoploss: Externally provided stoploss - :return: True if trade should be sold, False otherwise + :return: True if trade should be exited, False otherwise """ current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) @@ -578,8 +596,8 @@ class IStrategy(ABC, HyperStrategyMixin): current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) - # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. - roi_reached = (not (buy and self.ignore_roi_if_buy_signal) + # if enter signal and ignore_roi is set, we don't need to evaluate min_roi. + roi_reached = (not (enter and self.ignore_roi_if_buy_signal) and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) @@ -592,10 +610,11 @@ class IStrategy(ABC, HyperStrategyMixin): if (self.sell_profit_only and current_profit <= self.sell_profit_offset): # sell_profit_only and profit doesn't reach the offset - ignore sell signal pass - elif self.use_sell_signal and not buy: - if sell: + elif self.use_sell_signal and not enter: + if exit: 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)( pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, current_profit=current_profit) @@ -603,18 +622,18 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH: - logger.warning(f'Custom sell reason returned from custom_sell is too ' - f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} ' - f'characters.') + 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] else: custom_reason = None - # TODO: return here if sell-signal should be favored over ROI + # TODO: return here if exit-signal should be favored over ROI # Start evaluations # Sequence: # ROI (if not stoploss) - # Sell-signal + # Exit-signal # Stoploss if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI") @@ -632,7 +651,7 @@ class IStrategy(ABC, HyperStrategyMixin): return stoplossflag # This one is noisy, commented out... - # logger.debug(f"{trade.pair} - No sell signal.") + # logger.debug(f"{trade.pair} - No exit signal.") return SellCheckTuple(sell_type=SellType.NONE) def stop_loss_reached(self, current_rate: float, trade: Trade, @@ -641,7 +660,7 @@ class IStrategy(ABC, HyperStrategyMixin): high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, - decides to sell or not + decides to exit or not :param current_profit: current profit as ratio :param low: Low value of this candle, only set in backtesting :param high: High value of this candle, only set in backtesting @@ -651,7 +670,12 @@ class IStrategy(ABC, HyperStrategyMixin): # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - if self.use_custom_stoploss and trade.stop_loss < (low or current_rate): + dir_correct = ( + trade.stop_loss < (low or current_rate) and not trade.is_short or + trade.stop_loss > (low or current_rate) and trade.is_short + ) + + if self.use_custom_stoploss and dir_correct: stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, @@ -735,7 +759,7 @@ class IStrategy(ABC, HyperStrategyMixin): def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Populates indicators for given candle (OHLCV) data (for multiple pairs) - Does not run advise_buy or advise_sell! + Does not run advise_enter or advise_exit! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. Has positive effects on memory usage for whatever reason - also when @@ -746,7 +770,7 @@ class IStrategy(ABC, HyperStrategyMixin): def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Populate indicators that will be used in the Buy and Sell strategy + Populate indicators that will be used in the Buy, Sell, short, exit_short strategy This method should not be overridden. :param dataframe: Dataframe with data from the exchange :param metadata: Additional information, like the currently traded pair @@ -760,37 +784,60 @@ class IStrategy(ABC, HyperStrategyMixin): else: return self.populate_indicators(dataframe, metadata) - def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_enter( + self, + dataframe: DataFrame, + metadata: dict, + is_short: bool = False + ) -> DataFrame: """ - Based on TA indicators, populates the buy signal for the given dataframe + Based on TA indicators, populates the buy/short signal for the given dataframe This method should not be overridden. :param dataframe: DataFrame :param metadata: Additional information dictionary, with details like the currently traded pair :return: DataFrame with buy column """ - logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.") + (type, fun_len) = ( + ("short", self._short_fun_len) + if is_short else + ("buy", self._buy_fun_len) + ) - if self._buy_fun_len == 2: + logger.debug(f"Populating {type} signals for pair {metadata.get('pair')}.") + + if fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_buy_trend(dataframe) # type: ignore + return self.populate_enter_trend(dataframe) # type: ignore else: - return self.populate_buy_trend(dataframe, metadata) + return self.populate_enter_trend(dataframe, metadata) - def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_exit( + self, + dataframe: DataFrame, + metadata: dict, + is_short: bool = False + ) -> DataFrame: """ - Based on TA indicators, populates the sell signal for the given dataframe + Based on TA indicators, populates the sell/exit_short signal for the given dataframe This method should not be overridden. :param dataframe: DataFrame :param metadata: Additional information dictionary, with details like the currently traded pair :return: DataFrame with sell column """ - logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.") - if self._sell_fun_len == 2: + + (type, fun_len) = ( + ("exit_short", self._exit_short_fun_len) + if is_short else + ("sell", self._sell_fun_len) + ) + + logger.debug(f"Populating {type} signals for pair {metadata.get('pair')}.") + if fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_sell_trend(dataframe) # type: ignore + return self.populate_exit_trend(dataframe) # type: ignore else: - return self.populate_sell_trend(dataframe, metadata) + return self.populate_exit_trend(dataframe, metadata) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index e089ebf31..e7dbfbac7 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -58,7 +58,11 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, return dataframe -def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float: +def stoploss_from_open( + open_relative_stop: float, + current_profit: float, + for_short: bool = False +) -> float: """ Given the current profit, and a desired stop loss value relative to the open price, @@ -72,14 +76,17 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa :param open_relative_stop: Desired stop loss percentage relative to open price :param current_profit: The current profit percentage - :return: Positive stop loss value relative to current price + :return: Stop loss value relative to current price """ # formula is undefined for current_profit -1, return maximum value if current_profit == -1: return 1 - stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + stoploss = 1-((1+open_relative_stop)/(1+current_profit)) # TODO-lev: Is this right? # negative stoploss values indicate the requested stop price is higher than the current price - return max(stoploss, 0.0) + if for_short: + return min(stoploss, 0.0) + else: + return max(stoploss, 0.0) diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index ed1af7718..6707ec8d4 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -172,3 +172,125 @@ class SampleHyperOpt(IHyperOpt): return dataframe return populate_sell_trend + + @staticmethod + def short_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the short strategy parameters to be used by Hyperopt. + """ + def populate_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + if 'mfi-enabled' in params and params['mfi-enabled']: + conditions.append(dataframe['mfi'] > params['mfi-value']) + if 'fastd-enabled' in params and params['fastd-enabled']: + conditions.append(dataframe['fastd'] > params['fastd-value']) + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] < params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] > params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_upper': + conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_below( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_below( + dataframe['close'], dataframe['sar'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'short'] = 1 + + return dataframe + + return populate_short_trend + + @staticmethod + def short_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching short strategy parameters. + """ + return [ + Integer(75, 90, name='mfi-value'), + Integer(55, 85, name='fastd-value'), + Integer(50, 80, name='adx-value'), + Integer(60, 80, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_upper', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + + @staticmethod + def exit_short_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the exit_short strategy parameters to be used by Hyperopt. + """ + def populate_exit_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Exit_short strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + if 'exit-short-mfi-enabled' in params and params['exit-short-mfi-enabled']: + conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) + if 'exit-short-fastd-enabled' in params and params['exit-short-fastd-enabled']: + conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) + if 'exit-short-adx-enabled' in params and params['exit-short-adx-enabled']: + conditions.append(dataframe['adx'] > params['exit-short-adx-value']) + if 'exit-short-rsi-enabled' in params and params['exit-short-rsi-enabled']: + conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) + + # TRIGGERS + if 'exit-short-trigger' in params: + if params['exit-short-trigger'] == 'exit-short-bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['exit-short-trigger'] == 'exit-short-macd_cross_signal': + conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], dataframe['macd'] + )) + if params['exit-short-trigger'] == 'exit-short-sar_reversal': + conditions.append(qtpylib.crossed_below( + dataframe['sar'], dataframe['close'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'exit_short'] = 1 + + return dataframe + + return populate_exit_short_trend + + @staticmethod + def exit_short_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching exit short strategy parameters. + """ + return [ + Integer(1, 25, name='exit_short-mfi-value'), + Integer(1, 50, name='exit_short-fastd-value'), + Integer(1, 50, name='exit_short-adx-value'), + Integer(1, 40, name='exit_short-rsi-value'), + Categorical([True, False], name='exit_short-mfi-enabled'), + Categorical([True, False], name='exit_short-fastd-enabled'), + Categorical([True, False], name='exit_short-adx-enabled'), + Categorical([True, False], name='exit_short-rsi-enabled'), + Categorical(['exit_short-bb_lower', + 'exit_short-macd_cross_signal', + 'exit_short-sar_reversal'], name='exit_short-trigger') + ] diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index cc13b6ba3..cee343bb6 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -187,9 +187,132 @@ class AdvancedSampleHyperOpt(IHyperOpt): return populate_sell_trend + @staticmethod + def short_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the short strategy parameters to be used by Hyperopt. + """ + def populate_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + if 'mfi-enabled' in params and params['mfi-enabled']: + conditions.append(dataframe['mfi'] > params['mfi-value']) + if 'fastd-enabled' in params and params['fastd-enabled']: + conditions.append(dataframe['fastd'] > params['fastd-value']) + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] < params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] > params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_upper': + conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_below( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_below( + dataframe['close'], dataframe['sar'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'short'] = 1 + + return dataframe + + return populate_short_trend + + @staticmethod + def short_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching short strategy parameters. + """ + return [ + Integer(75, 90, name='mfi-value'), + Integer(55, 85, name='fastd-value'), + Integer(50, 80, name='adx-value'), + Integer(60, 80, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_upper', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + + @staticmethod + def exit_short_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the exit_short strategy parameters to be used by Hyperopt. + """ + def populate_exit_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Exit_short strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + if 'exit-short-mfi-enabled' in params and params['exit-short-mfi-enabled']: + conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) + if 'exit-short-fastd-enabled' in params and params['exit-short-fastd-enabled']: + conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) + if 'exit-short-adx-enabled' in params and params['exit-short-adx-enabled']: + conditions.append(dataframe['adx'] > params['exit-short-adx-value']) + if 'exit-short-rsi-enabled' in params and params['exit-short-rsi-enabled']: + conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) + + # TRIGGERS + if 'exit-short-trigger' in params: + if params['exit-short-trigger'] == 'exit-short-bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['exit-short-trigger'] == 'exit-short-macd_cross_signal': + conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], dataframe['macd'] + )) + if params['exit-short-trigger'] == 'exit-short-sar_reversal': + conditions.append(qtpylib.crossed_below( + dataframe['sar'], dataframe['close'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'exit_short'] = 1 + + return dataframe + + return populate_exit_short_trend + + @staticmethod + def exit_short_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching exit short strategy parameters. + """ + return [ + Integer(1, 25, name='exit_short-mfi-value'), + Integer(1, 50, name='exit_short-fastd-value'), + Integer(1, 50, name='exit_short-adx-value'), + Integer(1, 40, name='exit_short-rsi-value'), + Categorical([True, False], name='exit_short-mfi-enabled'), + Categorical([True, False], name='exit_short-fastd-enabled'), + Categorical([True, False], name='exit_short-adx-enabled'), + Categorical([True, False], name='exit_short-rsi-enabled'), + Categorical(['exit_short-bb_lower', + 'exit_short-macd_cross_signal', + 'exit_short-sar_reversal'], name='exit_short-trigger') + ] + @staticmethod def generate_roi_table(params: Dict) -> Dict[int, float]: """ + # TODO-lev? Generate the ROI table that will be used by Hyperopt This implementation generates the default legacy Freqtrade ROI tables. @@ -211,6 +334,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): @staticmethod def roi_space() -> List[Dimension]: """ + # TODO-lev? Values to search for each ROI steps Override it if you need some different ranges for the parameters in the @@ -231,6 +355,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): @staticmethod def stoploss_space() -> List[Dimension]: """ + # TODO-lev? Stoploss Value to search Override it if you need some different range for the parameter in the @@ -243,6 +368,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): @staticmethod def trailing_space() -> List[Dimension]: """ + # TODO-lev? Create a trailing stoploss space. You may override it in your custom Hyperopt class. diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 574819949..3e73d3134 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -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_buy_trend, populate_sell_trend, populate_short_trend, populate_exit_short_trend You should keep: - timeframe, minimal_roi, stoploss, trailing_* """ @@ -58,6 +58,8 @@ class SampleStrategy(IStrategy): # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) + short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) + exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) # Optimal timeframe for the strategy. timeframe = '5m' @@ -373,3 +375,40 @@ class SampleStrategy(IStrategy): ), 'sell'] = 1 return dataframe + + def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the short signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with short column + """ + dataframe.loc[ + ( + # Signal: RSI crosses above 70 + (qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) & + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'short'] = 1 + return dataframe + + def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the exit_short signal for the given dataframe + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with exit_short column + """ + dataframe.loc[ + ( + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) & + (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle + (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'exit_short'] = 1 + + return dataframe diff --git a/tests/optimize/hyperopts/default_hyperopt.py b/tests/optimize/hyperopts/default_hyperopt.py index 2e2bca3d0..cc8771d1b 100644 --- a/tests/optimize/hyperopts/default_hyperopt.py +++ b/tests/optimize/hyperopts/default_hyperopt.py @@ -105,6 +105,66 @@ class DefaultHyperOpt(IHyperOpt): Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') ] + @staticmethod + def short_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the short strategy parameters to be used by Hyperopt. + """ + def populate_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + if 'mfi-enabled' in params and params['mfi-enabled']: + conditions.append(dataframe['mfi'] > params['mfi-value']) + if 'fastd-enabled' in params and params['fastd-enabled']: + conditions.append(dataframe['fastd'] > params['fastd-value']) + if 'adx-enabled' in params and params['adx-enabled']: + conditions.append(dataframe['adx'] < params['adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + conditions.append(dataframe['rsi'] > params['rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'bb_upper': + conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['trigger'] == 'macd_cross_signal': + conditions.append(qtpylib.crossed_below( + dataframe['macd'], dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + conditions.append(qtpylib.crossed_below( + dataframe['close'], dataframe['sar'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'short'] = 1 + + return dataframe + + return populate_short_trend + + @staticmethod + def short_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching short strategy parameters. + """ + return [ + Integer(75, 90, name='mfi-value'), + Integer(55, 85, name='fastd-value'), + Integer(50, 80, name='adx-value'), + Integer(60, 80, name='rsi-value'), + Categorical([True, False], name='mfi-enabled'), + Categorical([True, False], name='fastd-enabled'), + Categorical([True, False], name='adx-enabled'), + Categorical([True, False], name='rsi-enabled'), + Categorical(['bb_upper', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + @staticmethod def sell_strategy_generator(params: Dict[str, Any]) -> Callable: """ @@ -148,6 +208,49 @@ class DefaultHyperOpt(IHyperOpt): return populate_sell_trend + @staticmethod + def exit_short_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the exit_short strategy parameters to be used by Hyperopt. + """ + def populate_exit_short_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Exit_short strategy Hyperopt will build and use. + """ + conditions = [] + + # GUARDS AND TRENDS + if 'exit-short-mfi-enabled' in params and params['exit-short-mfi-enabled']: + conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) + if 'exit-short-fastd-enabled' in params and params['exit-short-fastd-enabled']: + conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) + if 'exit-short-adx-enabled' in params and params['exit-short-adx-enabled']: + conditions.append(dataframe['adx'] > params['exit-short-adx-value']) + if 'exit-short-rsi-enabled' in params and params['exit-short-rsi-enabled']: + conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) + + # TRIGGERS + if 'exit-short-trigger' in params: + if params['exit-short-trigger'] == 'exit-short-bb_lower': + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['exit-short-trigger'] == 'exit-short-macd_cross_signal': + conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], dataframe['macd'] + )) + if params['exit-short-trigger'] == 'exit-short-sar_reversal': + conditions.append(qtpylib.crossed_below( + dataframe['sar'], dataframe['close'] + )) + + if conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, conditions), + 'exit_short'] = 1 + + return dataframe + + return populate_exit_short_trend + @staticmethod def sell_indicator_space() -> List[Dimension]: """ @@ -167,6 +270,25 @@ class DefaultHyperOpt(IHyperOpt): 'sell-sar_reversal'], name='sell-trigger') ] + @staticmethod + def exit_short_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching exit short strategy parameters. + """ + return [ + Integer(1, 25, name='exit_short-mfi-value'), + Integer(1, 50, name='exit_short-fastd-value'), + Integer(1, 50, name='exit_short-adx-value'), + Integer(1, 40, name='exit_short-rsi-value'), + Categorical([True, False], name='exit_short-mfi-enabled'), + Categorical([True, False], name='exit_short-fastd-enabled'), + Categorical([True, False], name='exit_short-adx-enabled'), + Categorical([True, False], name='exit_short-rsi-enabled'), + Categorical(['exit_short-bb_lower', + 'exit_short-macd_cross_signal', + 'exit_short-sar_reversal'], name='exit_short-trigger') + ] + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators. Should be a copy of same method from strategy. @@ -200,3 +322,37 @@ class DefaultHyperOpt(IHyperOpt): 'sell'] = 1 return dataframe + + def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators. Should be a copy of same method from strategy. + Must align to populate_indicators in this file. + Only used when --spaces does not include short space. + """ + dataframe.loc[ + ( + (dataframe['close'] > dataframe['bb_upperband']) & + (dataframe['mfi'] < 84) & + (dataframe['adx'] > 75) & + (dataframe['rsi'] < 79) + ), + 'buy'] = 1 + + return dataframe + + def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators. Should be a copy of same method from strategy. + Must align to populate_indicators in this file. + Only used when --spaces does not include exit_short space. + """ + dataframe.loc[ + ( + (qtpylib.crossed_below( + dataframe['macdsignal'], dataframe['macd'] + )) & + (dataframe['fastd'] < 46) + ), + 'sell'] = 1 + + return dataframe diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e5c037f3e..0205369ba 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -597,8 +597,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 - backtesting.strategy.advise_buy = lambda a, m: frame - backtesting.strategy.advise_sell = lambda a, m: frame + backtesting.strategy.advise_enter = lambda a, m: frame + backtesting.strategy.advise_exit = lambda a, m: frame backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss caplog.set_level(logging.DEBUG) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index deaaf9f2f..afbfcb1c2 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -290,8 +290,8 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert backtesting.config == default_conf assert backtesting.timeframe == '5m' assert callable(backtesting.strategy.ohlcvdata_to_dataframe) - assert callable(backtesting.strategy.advise_buy) - assert callable(backtesting.strategy.advise_sell) + assert callable(backtesting.strategy.advise_enter) + assert callable(backtesting.strategy.advise_exit) assert isinstance(backtesting.strategy.dp, DataProvider) get_fee.assert_called() assert backtesting.fee == 0.5 @@ -700,8 +700,8 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = fun # Override - backtesting.strategy.advise_sell = fun # Override + backtesting.strategy.advise_enter = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -716,8 +716,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = fun # Override - backtesting.strategy.advise_sell = fun # Override + backtesting.strategy.advise_enter = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -731,8 +731,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.required_startup = 0 backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = _trend_alternate # Override - backtesting.strategy.advise_sell = _trend_alternate # Override + backtesting.strategy.advise_enter = _trend_alternate # Override + backtesting.strategy.advise_exit = _trend_alternate # Override result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) @@ -777,8 +777,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = _trend_alternate_hold # Override - backtesting.strategy.advise_sell = _trend_alternate_hold # Override + backtesting.strategy.advise_enter = _trend_alternate_hold # Override + backtesting.strategy.advise_exit = _trend_alternate_hold # Override processed = backtesting.strategy.ohlcvdata_to_dataframe(data) min_date, max_date = get_timerange(processed) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d146e84f1..855a752ac 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -25,6 +25,9 @@ from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, from .hyperopts.default_hyperopt import DefaultHyperOpt +# TODO-lev: This file + + def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -363,8 +366,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: # Should be called for historical candle data assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_enter") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -822,8 +825,8 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_enter") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -903,8 +906,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_enter") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -957,8 +960,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_enter") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1517b6fcc..439a99e2f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -264,7 +264,7 @@ def test_api_UvicornServer(mocker): assert thread_mock.call_count == 1 s.cleanup() - assert s.should_exit is True + assert s.should_sell is True def test_api_UvicornServer_run(mocker): diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index 7171b93ae..3e5695a99 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -154,3 +154,48 @@ class DefaultStrategy(IStrategy): ), 'sell'] = 1 return dataframe + + def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the short signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with short column + """ + dataframe.loc[ + ( + (dataframe['rsi'] > 65) & + (dataframe['fastd'] > 65) & + (dataframe['adx'] < 70) & + (dataframe['plus_di'] < 0.5) # TODO-lev: What to do here + ) | + ( + (dataframe['adx'] < 35) & + (dataframe['plus_di'] < 0.5) # TODO-lev: What to do here + ), + 'short'] = 1 + + return dataframe + + def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the exit_short signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with exit_short column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_below(dataframe['rsi'], 30)) | + (qtpylib.crossed_below(dataframe['fastd'], 30)) + ) & + (dataframe['adx'] < 90) & + (dataframe['minus_di'] < 0) # TODO-lev: what to do here + ) | + ( + (dataframe['adx'] > 30) & + (dataframe['minus_di'] < 0.5) # TODO-lev: what to do here + ), + 'exit_short'] = 1 + return dataframe diff --git a/tests/strategy/strats/hyperoptable_strategy.py b/tests/strategy/strats/hyperoptable_strategy.py index 88bdd078e..8d428b33d 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -60,6 +60,15 @@ class HyperoptableStrategy(IStrategy): 'sell_minusdi': 0.4 } + short_params = { + 'short_rsi': 65, + } + + exit_short_params = { + 'exit_short_rsi': 26, + 'exit_short_minusdi': 0.6 + } + buy_rsi = IntParameter([0, 50], default=30, space='buy') buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') @@ -78,6 +87,12 @@ class HyperoptableStrategy(IStrategy): }) return prot + short_rsi = IntParameter([50, 100], default=70, space='sell') + short_plusdi = RealParameter(low=0, high=1, default=0.5, space='sell') + exit_short_rsi = IntParameter(low=0, high=50, default=30, space='buy') + exit_short_minusdi = DecimalParameter(low=0, high=1, default=0.4999, decimals=3, space='buy', + load=False) + def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -167,7 +182,7 @@ class HyperoptableStrategy(IStrategy): Based on TA indicators, populates the sell 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 sell column """ dataframe.loc[ ( @@ -184,3 +199,48 @@ class HyperoptableStrategy(IStrategy): ), 'sell'] = 1 return dataframe + + def populate_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the short signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with short column + """ + dataframe.loc[ + ( + (dataframe['rsi'] > self.short_rsi.value) & + (dataframe['fastd'] > 65) & + (dataframe['adx'] < 70) & + (dataframe['plus_di'] < self.short_plusdi.value) + ) | + ( + (dataframe['adx'] < 35) & + (dataframe['plus_di'] < self.short_plusdi.value) + ), + 'short'] = 1 + + return dataframe + + def populate_exit_short_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the exit_short signal for the given dataframe + :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with exit_short column + """ + dataframe.loc[ + ( + ( + (qtpylib.crossed_below(dataframe['rsi'], self.exit_short_rsi.value)) | + (qtpylib.crossed_below(dataframe['fastd'], 30)) + ) & + (dataframe['adx'] < 90) & + (dataframe['minus_di'] < 0) # TODO-lev: What should this be + ) | + ( + (dataframe['adx'] < 30) & + (dataframe['minus_di'] < self.exit_short_minusdi.value) + ), + 'exit_short'] = 1 + return dataframe diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 9ef00b110..a5531b42f 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -85,3 +85,34 @@ class TestStrategyLegacy(IStrategy): ), 'sell'] = 1 return dataframe + + def populate_short_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 30) & + (dataframe['tema'] > dataframe['tema'].shift(1)) & + (dataframe['volume'] > 0) + ), + 'buy'] = 1 + + return dataframe + + def populate_exit_short_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 70) & + (dataframe['tema'] < dataframe['tema'].shift(1)) & + (dataframe['volume'] > 0) + ), + 'sell'] = 1 + return dataframe diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 92ac9f63a..420cf8f46 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -14,6 +14,8 @@ def test_default_strategy_structure(): assert hasattr(DefaultStrategy, 'populate_indicators') assert hasattr(DefaultStrategy, 'populate_buy_trend') assert hasattr(DefaultStrategy, 'populate_sell_trend') + assert hasattr(DefaultStrategy, 'populate_short_trend') + assert hasattr(DefaultStrategy, 'populate_exit_short_trend') def test_default_strategy(result, fee): @@ -27,6 +29,10 @@ def test_default_strategy(result, fee): assert type(indicators) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame + # TODO-lev: I think these two should be commented out in the strategy by default + # TODO-lev: so they can be tested, but the tests can't really remain + assert type(strategy.populate_short_trend(indicators, metadata)) is DataFrame + assert type(strategy.populate_exit_short_trend(indicators, metadata)) is DataFrame trade = Trade( open_rate=19_000, @@ -37,10 +43,28 @@ def test_default_strategy(result, fee): assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.utcnow()) is True + is_short=False, current_time=datetime.utcnow()) is True + assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi', - current_time=datetime.utcnow()) is True + is_short=False, current_time=datetime.utcnow()) is True + # TODO-lev: Test for shorts? assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), current_rate=20_000, current_profit=0.05) == strategy.stoploss + + short_trade = Trade( + open_rate=21_000, + amount=0.1, + pair='ETH/BTC', + fee_open=fee.return_value + ) + + assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, + rate=20000, time_in_force='gtc', + is_short=True, current_time=datetime.utcnow()) is True + + assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=short_trade, order_type='limit', + amount=0.1, rate=20000, time_in_force='gtc', + sell_reason='roi', is_short=True, + current_time=datetime.utcnow()) is True diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 0ad6d6f32..1e47575dc 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -156,17 +156,21 @@ def test_ignore_expired_candle(default_conf): # Add 1 candle length as the "latest date" defines candle open. current_time = latest_date + timedelta(seconds=80 + 300) - assert strategy.ignore_expired_candle(latest_date=latest_date, - current_time=current_time, - timeframe_seconds=300, - buy=True) is True + assert strategy.ignore_expired_candle( + latest_date=latest_date, + current_time=current_time, + timeframe_seconds=300, + enter=True + ) is True current_time = latest_date + timedelta(seconds=30 + 300) - assert not strategy.ignore_expired_candle(latest_date=latest_date, - current_time=current_time, - timeframe_seconds=300, - buy=True) is True + assert not strategy.ignore_expired_candle( + latest_date=latest_date, + current_time=current_time, + timeframe_seconds=300, + enter=True + ) is True def test_assert_df_raise(mocker, caplog, ohlcv_history): @@ -478,20 +482,20 @@ def test_custom_sell(default_conf, fee, caplog) -> None: def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) - buy_mock = MagicMock(side_effect=lambda x, meta: x) - sell_mock = MagicMock(side_effect=lambda x, meta: x) + enter_mock = MagicMock(side_effect=lambda x, meta, is_short: x) + exit_mock = MagicMock(side_effect=lambda x, meta, is_short: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_enter=enter_mock, + advise_exit=exit_mock, ) strategy = DefaultStrategy({}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert enter_mock.call_count == 2 + assert enter_mock.call_count == 2 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -500,8 +504,8 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 2 - assert buy_mock.call_count == 2 - assert buy_mock.call_count == 2 + assert enter_mock.call_count == 4 + assert enter_mock.call_count == 4 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -509,13 +513,13 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) - buy_mock = MagicMock(side_effect=lambda x, meta: x) - sell_mock = MagicMock(side_effect=lambda x, meta: x) + enter_mock = MagicMock(side_effect=lambda x, meta, is_short: x) + exit_mock = MagicMock(side_effect=lambda x, meta, is_short: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_enter=enter_mock, + advise_exit=exit_mock, ) strategy = DefaultStrategy({}) @@ -528,8 +532,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert 'close' in ret.columns assert isinstance(ret, DataFrame) assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert enter_mock.call_count == 2 # Once for buy, once for short + assert enter_mock.call_count == 2 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() @@ -537,8 +541,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert enter_mock.call_count == 2 + assert enter_mock.call_count == 2 # only skipped analyze adds buy and sell columns, otherwise it's all mocked assert 'buy' in ret.columns assert 'sell' in ret.columns @@ -743,10 +747,10 @@ def test_auto_hyperopt_interface(default_conf): assert strategy.sell_minusdi.value == 0.5 all_params = strategy.detect_all_parameters() assert isinstance(all_params, dict) - assert len(all_params['buy']) == 2 - assert len(all_params['sell']) == 2 - # Number of Hyperoptable parameters - assert all_params['count'] == 6 + # TODO-lev: Should these be 4,4 and 10? + assert len(all_params['buy']) == 4 + assert len(all_params['sell']) == 4 + assert all_params['count'] == 10 strategy.__class__.sell_rsi = IntParameter([0, 10], default=5, space='buy') diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 115a2fbde..2cf77b172 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -117,12 +117,18 @@ def test_strategy(result, default_conf): df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = strategy.advise_buy(df_indicators, metadata=metadata) + dataframe = strategy.advise_enter(df_indicators, metadata=metadata, is_short=False) assert 'buy' in dataframe.columns - dataframe = strategy.advise_sell(df_indicators, metadata=metadata) + dataframe = strategy.advise_exit(df_indicators, metadata=metadata, is_short=False) assert 'sell' in dataframe.columns + dataframe = strategy.advise_enter(df_indicators, metadata=metadata, is_short=True) + assert 'short' in dataframe.columns + + dataframe = strategy.advise_exit(df_indicators, metadata=metadata, is_short=True) + assert 'exit_short' in dataframe.columns + def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) @@ -218,6 +224,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): def test_strategy_override_order_types(caplog, default_conf): caplog.set_level(logging.INFO) + # TODO-lev: Maybe change order_types = { 'buy': 'market', 'sell': 'limit', @@ -345,7 +352,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) + strategy.advise_enter(indicators, {'pair': 'ETH/BTC'}, is_short=False) # TODO-lev assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -354,7 +361,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) + strategy.advise_exit(indicators, {'pair': 'ETH_BTC'}, is_short=False) # TODO-lev assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -374,6 +381,8 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert strategy._populate_fun_len == 2 assert strategy._buy_fun_len == 2 assert strategy._sell_fun_len == 2 + # assert strategy._short_fun_len == 2 + # assert strategy._exit_short_fun_len == 2 assert strategy.INTERFACE_VERSION == 1 assert strategy.timeframe == '5m' assert strategy.ticker_interval == '5m' @@ -382,14 +391,22 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = strategy.advise_buy(result, metadata=metadata) + buydf = strategy.advise_enter(result, metadata=metadata, is_short=False) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = strategy.advise_sell(result, metadata=metadata) + selldf = strategy.advise_exit(result, metadata=metadata, is_short=False) assert isinstance(selldf, DataFrame) assert 'sell' in selldf + # shortdf = strategy.advise_enter(result, metadata=metadata, is_short=True) + # assert isinstance(shortdf, DataFrame) + # assert 'short' in shortdf.columns + + # exit_shortdf = strategy.advise_exit(result, metadata=metadata, is_short=True) + # assert isinstance(exit_shortdf, DataFrame) + # assert 'exit_short' in exit_shortdf + assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", caplog) @@ -403,16 +420,26 @@ def test_strategy_interface_versioning(result, monkeypatch, default_conf): assert strategy._populate_fun_len == 3 assert strategy._buy_fun_len == 3 assert strategy._sell_fun_len == 3 + assert strategy._short_fun_len == 3 + assert strategy._exit_short_fun_len == 3 assert strategy.INTERFACE_VERSION == 2 indicator_df = strategy.advise_indicators(result, metadata=metadata) assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = strategy.advise_buy(result, metadata=metadata) + buydf = strategy.advise_enter(result, metadata=metadata, is_short=False) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = strategy.advise_sell(result, metadata=metadata) + selldf = strategy.advise_exit(result, metadata=metadata, is_short=False) assert isinstance(selldf, DataFrame) assert 'sell' in selldf + + shortdf = strategy.advise_enter(result, metadata=metadata, is_short=True) + assert isinstance(shortdf, DataFrame) + assert 'short' in shortdf.columns + + exit_shortdf = strategy.advise_exit(result, metadata=metadata, is_short=True) + assert isinstance(exit_shortdf, DataFrame) + assert 'exit_short' in exit_shortdf