From d4a7d2d444354d7e0f71c5d0706ef750cdf326f3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 8 Aug 2021 03:38:34 -0600 Subject: [PATCH 01/46] Added short and exit_short to strategy --- freqtrade/edge/edge_positioning.py | 11 +- freqtrade/enums/signaltype.py | 3 + freqtrade/optimize/backtesting.py | 4 +- freqtrade/optimize/hyperopt.py | 7 +- freqtrade/resolvers/hyperopt_resolver.py | 1 + freqtrade/resolvers/strategy_resolver.py | 7 +- freqtrade/rpc/api_server/uvicorn_threaded.py | 2 +- freqtrade/strategy/hyper.py | 2 + freqtrade/strategy/interface.py | 203 +++++++++++------- freqtrade/strategy/strategy_helper.py | 15 +- freqtrade/templates/sample_hyperopt.py | 122 +++++++++++ .../templates/sample_hyperopt_advanced.py | 126 +++++++++++ freqtrade/templates/sample_strategy.py | 41 +++- tests/optimize/hyperopts/default_hyperopt.py | 156 ++++++++++++++ tests/optimize/test_backtest_detail.py | 4 +- tests/optimize/test_backtesting.py | 20 +- tests/optimize/test_hyperopt.py | 19 +- tests/rpc/test_rpc_apiserver.py | 2 +- tests/strategy/strats/default_strategy.py | 45 ++++ .../strategy/strats/hyperoptable_strategy.py | 62 +++++- tests/strategy/strats/legacy_strategy.py | 31 +++ tests/strategy/test_default_strategy.py | 28 ++- tests/strategy/test_interface.py | 60 +++--- tests/strategy/test_strategy_loading.py | 43 +++- 24 files changed, 862 insertions(+), 152 deletions(-) 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 From 092780df9d48de631bc09ea9d1b093c7f3e21ed0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 18 Aug 2021 04:19:17 -0600 Subject: [PATCH 02/46] condensed strategy methods down to 2 --- freqtrade/edge/edge_positioning.py | 10 +- freqtrade/enums/signaltype.py | 2 +- freqtrade/optimize/backtesting.py | 9 +- freqtrade/optimize/hyperopt.py | 13 +- freqtrade/resolvers/strategy_resolver.py | 13 +- freqtrade/rpc/api_server/uvicorn_threaded.py | 2 +- freqtrade/strategy/interface.py | 87 +++--- freqtrade/strategy/strategy_helper.py | 9 +- freqtrade/templates/sample_hyperopt.py | 237 ++++++---------- .../templates/sample_hyperopt_advanced.py | 233 ++++++--------- freqtrade/templates/sample_strategy.py | 41 +-- tests/optimize/hyperopts/default_hyperopt.py | 267 ++++++------------ tests/optimize/test_backtest_detail.py | 4 +- tests/optimize/test_backtesting.py | 20 +- tests/optimize/test_hyperopt.py | 40 ++- tests/rpc/test_rpc_apiserver.py | 2 +- tests/strategy/strats/default_strategy.py | 44 +-- .../strategy/strats/hyperoptable_strategy.py | 50 ++-- tests/strategy/strats/legacy_strategy.py | 30 -- tests/strategy/test_default_strategy.py | 27 +- tests/strategy/test_interface.py | 32 +-- tests/strategy/test_strategy_loading.py | 52 +--- 22 files changed, 451 insertions(+), 773 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index b366059da..9c1dd4d24 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -167,14 +167,12 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - df_analyzed = self.strategy.advise_exit( - dataframe=self.strategy.advise_enter( + df_analyzed = self.strategy.advise_sell( + dataframe=self.strategy.advise_buy( dataframe=pair_data, - metadata={'pair': pair}, - is_short=False + metadata={'pair': pair} ), - metadata={'pair': pair}, - is_short=False + metadata={'pair': pair} )[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 ffba5ee90..fcebd9f0e 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -16,4 +16,4 @@ class SignalTagType(Enum): Enum for signal columns """ BUY_TAG = "buy_tag" - SELL_TAG = "sell_tag" + SHORT_TAG = "short_tag" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 550ceecd8..cce3b6a0d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -231,8 +231,13 @@ class Backtesting: if has_buy_tag: pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist - df_analyzed = self.strategy.advise_exit( - self.strategy.advise_enter(pair_data, {'pair': pair}), {'pair': pair}).copy() + df_analyzed = self.strategy.advise_sell( + self.strategy.advise_buy( + 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 4c07419b8..5c627df35 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_enter = ( # type: ignore + self.backtesting.strategy.advise_buy = ( # 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,14 +283,15 @@ 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_enter = ( # type: ignore - self.custom_hyperopt.buy_strategy_generator(params_dict)) + self.backtesting.strategy.advise_buy = ( # type: ignore + self.custom_hyperopt.buy_strategy_generator(params_dict) + ) if HyperoptTools.has_space(self.config, 'sell'): - self.backtesting.strategy.advise_exit = ( # type: ignore - self.custom_hyperopt.sell_strategy_generator(params_dict)) + self.backtesting.strategy.advise_sell = ( # type: ignore + self.custom_hyperopt.sell_strategy_generator(params_dict) + ) if HyperoptTools.has_space(self.config, 'protection'): for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'): diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 38a5b4850..afb5916f1 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -202,14 +202,11 @@ 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._short_fun_len, - strategy._exit_short_fun_len]): + 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 diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index 7d76d52ed..2f72cb74c 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_sell = True + self.should_exit = True self.thread.join() diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 26ad2fcd4..b56a54d14 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -62,8 +62,6 @@ 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 @@ -145,7 +143,7 @@ class IStrategy(ABC, HyperStrategyMixin): return dataframe @abstractmethod - def populate_enter_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame @@ -155,7 +153,7 @@ class IStrategy(ABC, HyperStrategyMixin): return dataframe @abstractmethod - def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame @@ -166,7 +164,7 @@ class IStrategy(ABC, HyperStrategyMixin): def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ - Check enter timeout function callback. + Check buy 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. @@ -184,7 +182,7 @@ class IStrategy(ABC, HyperStrategyMixin): def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: """ - Check exit timeout function callback. + Check sell 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. @@ -396,10 +394,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug("TA Analysis Launched") dataframe = self.advise_indicators(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) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) return dataframe def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -426,7 +422,7 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 dataframe['sell'] = 0 - dataframe['short'] = 0 + dataframe['enter_short'] = 0 dataframe['exit_short'] = 0 dataframe['buy_tag'] = None dataframe['short_tag'] = None @@ -572,8 +568,8 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, trade: Trade, rate: float, date: datetime, enter: bool, - exit: bool, low: float = None, high: float = None, + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + sell: 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/exit_short @@ -597,7 +593,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_profit = trade.calc_profit_ratio(current_rate) # 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) + roi_reached = (not (buy and self.ignore_roi_if_buy_signal) and self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date)) @@ -610,8 +606,8 @@ 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 enter: - if exit: + elif self.use_sell_signal and not buy: + if sell: sell_signal = SellType.SELL_SIGNAL else: trade_type = "exit_short" if trade.is_short else "sell" @@ -759,7 +755,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_enter or advise_exit! + Does not run advise_buy or advise_sell! 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 @@ -784,12 +780,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: return self.populate_indicators(dataframe, metadata) - def advise_enter( - self, - dataframe: DataFrame, - metadata: dict, - is_short: bool = False - ) -> DataFrame: + def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy/short signal for the given dataframe This method should not be overridden. @@ -798,27 +789,17 @@ class IStrategy(ABC, HyperStrategyMixin): currently traded pair :return: DataFrame with buy column """ - (type, fun_len) = ( - ("short", self._short_fun_len) - if is_short else - ("buy", self._buy_fun_len) - ) - logger.debug(f"Populating {type} signals for pair {metadata.get('pair')}.") + logger.debug(f"Populating enter signals for pair {metadata.get('pair')}.") - if fun_len == 2: + if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_enter_trend(dataframe) # type: ignore + return self.populate_buy_trend(dataframe) # type: ignore else: - return self.populate_enter_trend(dataframe, metadata) + return self.populate_buy_trend(dataframe, metadata) - def advise_exit( - self, - dataframe: DataFrame, - metadata: dict, - is_short: bool = False - ) -> DataFrame: + def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell/exit_short signal for the given dataframe This method should not be overridden. @@ -828,16 +809,26 @@ class IStrategy(ABC, HyperStrategyMixin): :return: DataFrame with sell column """ - (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: + logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.") + if self._sell_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_exit_trend(dataframe) # type: ignore + return self.populate_sell_trend(dataframe) # type: ignore else: - return self.populate_exit_trend(dataframe, metadata) + return self.populate_sell_trend(dataframe, metadata) + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, + **kwargs) -> float: + """ + Customize leverage for each new trade. This method is not called when edge module is + enabled. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :return: A stake size, which is between min_stake and max_stake. + """ + return proposed_leverage diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index e7dbfbac7..9c4d2bf2d 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,5 +1,6 @@ import pandas as pd +from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -83,7 +84,13 @@ def stoploss_from_open( if current_profit == -1: return 1 - stoploss = 1-((1+open_relative_stop)/(1+current_profit)) # TODO-lev: Is this right? + if for_short is True: + # TODO-lev: How would this be calculated for short + raise OperationalException( + "Freqtrade hasn't figured out how to calculated stoploss on shorts") + # stoploss = 1-((1+open_relative_stop)/(1+current_profit)) + else: + stoploss = 1-((1+open_relative_stop)/(1+current_profit)) # negative stoploss values indicate the requested stop price is higher than the current price if for_short: diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 6707ec8d4..c39558108 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -46,7 +46,7 @@ class SampleHyperOpt(IHyperOpt): """ @staticmethod - def indicator_space() -> List[Dimension]: + def buy_indicator_space() -> List[Dimension]: """ Define your Hyperopt space for searching buy strategy parameters. """ @@ -55,11 +55,16 @@ class SampleHyperOpt(IHyperOpt): Integer(15, 45, name='fastd-value'), Integer(20, 50, name='adx-value'), Integer(20, 40, name='rsi-value'), + Integer(75, 90, name='short-mfi-value'), + Integer(55, 85, name='short-fastd-value'), + Integer(50, 80, name='short-adx-value'), + Integer(60, 80, name='short-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_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger'), + ] @staticmethod @@ -71,39 +76,61 @@ class SampleHyperOpt(IHyperOpt): """ Buy strategy Hyperopt will build and use. """ - conditions = [] + long_conditions = [] + short_conditions = [] # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: - conditions.append(dataframe['mfi'] < params['mfi-value']) + long_conditions.append(dataframe['mfi'] < params['mfi-value']) + short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) if 'fastd-enabled' in params and params['fastd-enabled']: - conditions.append(dataframe['fastd'] < params['fastd-value']) + long_conditions.append(dataframe['fastd'] < params['fastd-value']) + short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) + long_conditions.append(dataframe['adx'] > params['adx-value']) + short_conditions.append(dataframe['adx'] < params['short-adx-value']) if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) + long_conditions.append(dataframe['rsi'] < params['rsi-value']) + short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) # TRIGGERS if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'boll': + long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] + long_conditions.append(qtpylib.crossed_above( + dataframe['macd'], + dataframe['macdsignal'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['macd'], + dataframe['macdsignal'] )) if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] + long_conditions.append(qtpylib.crossed_above( + dataframe['close'], + dataframe['sar'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['close'], + dataframe['sar'] )) # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + long_conditions.append(dataframe['volume'] > 0) + short_conditions.append(dataframe['volume'] > 0) - if conditions: + if long_conditions: dataframe.loc[ - reduce(lambda x, y: x & y, conditions), + reduce(lambda x, y: x & y, long_conditions), 'buy'] = 1 + if short_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, short_conditions), + 'enter_short'] = 1 + return dataframe return populate_buy_trend @@ -118,13 +145,19 @@ class SampleHyperOpt(IHyperOpt): Integer(50, 100, name='sell-fastd-value'), Integer(50, 100, name='sell-adx-value'), Integer(60, 100, name='sell-rsi-value'), + 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='sell-mfi-enabled'), Categorical([True, False], name='sell-fastd-enabled'), Categorical([True, False], name='sell-adx-enabled'), Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', + Categorical(['sell-boll', 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') + 'sell-sar_reversal'], + name='sell-trigger' + ), ] @staticmethod @@ -136,161 +169,61 @@ class SampleHyperOpt(IHyperOpt): """ Sell strategy Hyperopt will build and use. """ - conditions = [] + exit_long_conditions = [] + exit_short_conditions = [] # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) # TRIGGERS if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-bb_upper': - conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['sell-trigger'] == 'sell-boll': + exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) + exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) if params['sell-trigger'] == 'sell-macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], + dataframe['macd'] + )) + exit_short_conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], + dataframe['macd'] )) if params['sell-trigger'] == 'sell-sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['sar'], dataframe['close'] + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['sar'], + dataframe['close'] + )) + exit_short_conditions.append(qtpylib.crossed_below( + dataframe['sar'], + dataframe['close'] )) # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + exit_long_conditions.append(dataframe['volume'] > 0) + exit_short_conditions.append(dataframe['volume'] > 0) - if conditions: + if exit_long_conditions: dataframe.loc[ - reduce(lambda x, y: x & y, conditions), + reduce(lambda x, y: x & y, exit_long_conditions), 'sell'] = 1 - 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: + if exit_short_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), + reduce(lambda x, y: x & y, exit_short_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') - ] + return populate_sell_trend diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index cee343bb6..feb617aae 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -70,11 +70,15 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(15, 45, name='fastd-value'), Integer(20, 50, name='adx-value'), Integer(20, 40, name='rsi-value'), + Integer(75, 90, name='short-mfi-value'), + Integer(55, 85, name='short-fastd-value'), + Integer(50, 80, name='short-adx-value'), + Integer(60, 80, name='short-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_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') + Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') ] @staticmethod @@ -86,38 +90,60 @@ class AdvancedSampleHyperOpt(IHyperOpt): """ Buy strategy Hyperopt will build and use """ - conditions = [] + long_conditions = [] + short_conditions = [] # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: - conditions.append(dataframe['mfi'] < params['mfi-value']) + long_conditions.append(dataframe['mfi'] < params['mfi-value']) + short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) if 'fastd-enabled' in params and params['fastd-enabled']: - conditions.append(dataframe['fastd'] < params['fastd-value']) + long_conditions.append(dataframe['fastd'] < params['fastd-value']) + short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) + long_conditions.append(dataframe['adx'] > params['adx-value']) + short_conditions.append(dataframe['adx'] < params['short-adx-value']) if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) + long_conditions.append(dataframe['rsi'] < params['rsi-value']) + short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) # TRIGGERS if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'boll': + long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] + long_conditions.append(qtpylib.crossed_above( + dataframe['macd'], + dataframe['macdsignal'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['macd'], + dataframe['macdsignal'] )) if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] + long_conditions.append(qtpylib.crossed_above( + dataframe['close'], + dataframe['sar'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['close'], + dataframe['sar'] )) # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + long_conditions.append(dataframe['volume'] > 0) + short_conditions.append(dataframe['volume'] > 0) - if conditions: + if long_conditions: dataframe.loc[ - reduce(lambda x, y: x & y, conditions), + reduce(lambda x, y: x & y, long_conditions), 'buy'] = 1 + if short_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, short_conditions), + 'enter_short'] = 1 + return dataframe return populate_buy_trend @@ -132,13 +158,18 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(50, 100, name='sell-fastd-value'), Integer(50, 100, name='sell-adx-value'), Integer(60, 100, name='sell-rsi-value'), + 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='sell-mfi-enabled'), Categorical([True, False], name='sell-fastd-enabled'), Categorical([True, False], name='sell-adx-enabled'), Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', + Categorical(['sell-boll', 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') + 'sell-sar_reversal'], + name='sell-trigger') ] @staticmethod @@ -151,163 +182,63 @@ class AdvancedSampleHyperOpt(IHyperOpt): Sell strategy Hyperopt will build and use """ # print(params) - conditions = [] + exit_long_conditions = [] + exit_short_conditions = [] # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) # TRIGGERS if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-bb_upper': - conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['sell-trigger'] == 'sell-boll': + exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) + exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) if params['sell-trigger'] == 'sell-macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], + dataframe['macd'] + )) + exit_long_conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], + dataframe['macd'] )) if params['sell-trigger'] == 'sell-sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['sar'], dataframe['close'] + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['sar'], + dataframe['close'] + )) + exit_long_conditions.append(qtpylib.crossed_below( + dataframe['sar'], + dataframe['close'] )) # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) + exit_long_conditions.append(dataframe['volume'] > 0) + exit_short_conditions.append(dataframe['volume'] > 0) - if conditions: + if exit_long_conditions: dataframe.loc[ - reduce(lambda x, y: x & y, conditions), + reduce(lambda x, y: x & y, exit_long_conditions), 'sell'] = 1 - 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: + if exit_short_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), + reduce(lambda x, y: x & y, exit_short_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') - ] + return populate_sell_trend @staticmethod def generate_roi_table(params: Dict) -> Dict[int, float]: diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 3e73d3134..b2d130059 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, populate_short_trend, populate_exit_short_trend + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend You should keep: - timeframe, minimal_roi, stoploss, trailing_* """ @@ -356,6 +356,16 @@ class SampleStrategy(IStrategy): ), 'buy'] = 1 + 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 + ), + 'enter_short'] = 1 + return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -374,38 +384,13 @@ class SampleStrategy(IStrategy): (dataframe['volume'] > 0) # Make sure Volume is not 0 ), '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 + # Guard: tema below BB middle + (dataframe['tema'] <= dataframe['bb_middleband']) & (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 ), diff --git a/tests/optimize/hyperopts/default_hyperopt.py b/tests/optimize/hyperopts/default_hyperopt.py index cc8771d1b..df39188e0 100644 --- a/tests/optimize/hyperopts/default_hyperopt.py +++ b/tests/optimize/hyperopts/default_hyperopt.py @@ -54,36 +54,57 @@ class DefaultHyperOpt(IHyperOpt): """ Buy strategy Hyperopt will build and use. """ - conditions = [] + long_conditions = [] + short_conditions = [] # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: - conditions.append(dataframe['mfi'] < params['mfi-value']) + long_conditions.append(dataframe['mfi'] < params['mfi-value']) + short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) if 'fastd-enabled' in params and params['fastd-enabled']: - conditions.append(dataframe['fastd'] < params['fastd-value']) + long_conditions.append(dataframe['fastd'] < params['fastd-value']) + short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) + long_conditions.append(dataframe['adx'] > params['adx-value']) + short_conditions.append(dataframe['adx'] < params['short-adx-value']) if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) + long_conditions.append(dataframe['rsi'] < params['rsi-value']) + short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) # TRIGGERS if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['trigger'] == 'boll': + long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] + long_conditions.append(qtpylib.crossed_above( + dataframe['macd'], + dataframe['macdsignal'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['macd'], + dataframe['macdsignal'] )) if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] + long_conditions.append(qtpylib.crossed_above( + dataframe['close'], + dataframe['sar'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['close'], + dataframe['sar'] )) - if conditions: + if long_conditions: dataframe.loc[ - reduce(lambda x, y: x & y, conditions), + reduce(lambda x, y: x & y, long_conditions), 'buy'] = 1 + if short_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, short_conditions), + 'enter_short'] = 1 + return dataframe return populate_buy_trend @@ -98,71 +119,15 @@ class DefaultHyperOpt(IHyperOpt): Integer(15, 45, name='fastd-value'), Integer(20, 50, name='adx-value'), Integer(20, 40, name='rsi-value'), + Integer(75, 90, name='short-mfi-value'), + Integer(55, 85, name='short-fastd-value'), + Integer(50, 80, name='short-adx-value'), + Integer(60, 80, name='short-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_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') + Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') ] @staticmethod @@ -174,83 +139,61 @@ class DefaultHyperOpt(IHyperOpt): """ Sell strategy Hyperopt will build and use. """ - conditions = [] + exit_long_conditions = [] + exit_short_conditions = [] # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) # TRIGGERS if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-bb_upper': - conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['sell-trigger'] == 'sell-boll': + exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) + exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) if params['sell-trigger'] == 'sell-macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], + dataframe['macd'] + )) + exit_short_conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], + dataframe['macd'] )) if params['sell-trigger'] == 'sell-sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['sar'], dataframe['close'] + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['sar'], + dataframe['close'] + )) + exit_short_conditions.append(qtpylib.crossed_below( + dataframe['sar'], + dataframe['close'] )) - if conditions: + if exit_long_conditions: dataframe.loc[ - reduce(lambda x, y: x & y, conditions), + reduce(lambda x, y: x & y, exit_long_conditions), 'sell'] = 1 + if exit_short_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, exit_short_conditions), + 'exit-short'] = 1 + return dataframe 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]: """ @@ -261,32 +204,18 @@ class DefaultHyperOpt(IHyperOpt): Integer(50, 100, name='sell-fastd-value'), Integer(50, 100, name='sell-adx-value'), Integer(60, 100, name='sell-rsi-value'), + 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='sell-mfi-enabled'), Categorical([True, False], name='sell-fastd-enabled'), Categorical([True, False], name='sell-adx-enabled'), Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-bb_upper', + Categorical(['sell-boll', 'sell-macd_cross_signal', - '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') + 'sell-sar_reversal'], + name='sell-trigger') ] def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -304,6 +233,15 @@ class DefaultHyperOpt(IHyperOpt): ), 'buy'] = 1 + dataframe.loc[ + ( + (dataframe['close'] > dataframe['bb_upperband']) & + (dataframe['mfi'] < 84) & + (dataframe['adx'] > 75) & + (dataframe['rsi'] < 79) + ), + 'enter_short'] = 1 + return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -321,31 +259,6 @@ 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( @@ -353,6 +266,6 @@ class DefaultHyperOpt(IHyperOpt): )) & (dataframe['fastd'] < 46) ), - 'sell'] = 1 + 'exit_short'] = 1 return dataframe diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 0205369ba..e5c037f3e 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_enter = lambda a, m: frame - backtesting.strategy.advise_exit = lambda a, m: frame + backtesting.strategy.advise_buy = lambda a, m: frame + backtesting.strategy.advise_sell = 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 afbfcb1c2..deaaf9f2f 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_enter) - assert callable(backtesting.strategy.advise_exit) + assert callable(backtesting.strategy.advise_buy) + assert callable(backtesting.strategy.advise_sell) 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_enter = fun # Override - backtesting.strategy.advise_exit = fun # Override + backtesting.strategy.advise_buy = fun # Override + backtesting.strategy.advise_sell = 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_enter = fun # Override - backtesting.strategy.advise_exit = fun # Override + backtesting.strategy.advise_buy = fun # Override + backtesting.strategy.advise_sell = 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_enter = _trend_alternate # Override - backtesting.strategy.advise_exit = _trend_alternate # Override + backtesting.strategy.advise_buy = _trend_alternate # Override + backtesting.strategy.advise_sell = _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_enter = _trend_alternate_hold # Override - backtesting.strategy.advise_exit = _trend_alternate_hold # Override + backtesting.strategy.advise_buy = _trend_alternate_hold # Override + backtesting.strategy.advise_sell = _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 855a752ac..333cea971 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -366,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_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_enter") + assert hasattr(hyperopt.backtesting.strategy, "advise_sell") + assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -451,6 +451,10 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None: 'fastd-value': 20, 'mfi-value': 20, 'rsi-value': 20, + 'short-adx-value': 80, + 'short-fastd-value': 80, + 'short-mfi-value': 80, + 'short-rsi-value': 80, 'adx-enabled': True, 'fastd-enabled': True, 'mfi-enabled': True, @@ -476,6 +480,10 @@ def test_sell_strategy_generator(hyperopt, testdatadir) -> None: 'sell-fastd-value': 75, 'sell-mfi-value': 80, 'sell-rsi-value': 20, + 'exit-short-adx-value': 80, + 'exit-short-fastd-value': 25, + 'exit-short-mfi-value': 20, + 'exit-short-rsi-value': 80, 'sell-adx-enabled': True, 'sell-fastd-enabled': True, 'sell-mfi-enabled': True, @@ -534,6 +542,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'fastd-value': 35, 'mfi-value': 0, 'rsi-value': 0, + 'short-adx-value': 100, + 'short-fastd-value': 65, + 'short-mfi-value': 100, + 'short-rsi-value': 100, 'adx-enabled': False, 'fastd-enabled': True, 'mfi-enabled': False, @@ -543,6 +555,10 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'sell-fastd-value': 75, 'sell-mfi-value': 0, 'sell-rsi-value': 0, + 'exit-short-adx-value': 100, + 'exit-short-fastd-value': 25, + 'exit-short-mfi-value': 100, + 'exit-short-rsi-value': 100, 'sell-adx-enabled': False, 'sell-fastd-enabled': True, 'sell-mfi-enabled': False, @@ -569,12 +585,16 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: ), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, + 'short-adx-value': 100, 'fastd-enabled': True, 'fastd-value': 35, + 'short-fastd-value': 65, 'mfi-enabled': False, 'mfi-value': 0, + 'short-mfi-value': 100, 'rsi-enabled': False, 'rsi-value': 0, + 'short-rsi-value': 100, 'trigger': 'macd_cross_signal'}, 'roi': {"0": 0.12000000000000001, "20.0": 0.02, @@ -583,12 +603,16 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'protection': {}, 'sell': {'sell-adx-enabled': False, 'sell-adx-value': 0, + 'exit-short-adx-value': 100, 'sell-fastd-enabled': True, 'sell-fastd-value': 75, + 'exit-short-fastd-value': 25, 'sell-mfi-enabled': False, 'sell-mfi-value': 0, + 'exit-short-mfi-value': 100, 'sell-rsi-enabled': False, 'sell-rsi-value': 0, + 'exit-short-rsi-value': 100, 'sell-trigger': 'macd_cross_signal'}, 'stoploss': {'stoploss': -0.4}, 'trailing': {'trailing_only_offset_is_reached': False, @@ -825,8 +849,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_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_enter") + assert hasattr(hyperopt.backtesting.strategy, "advise_sell") + assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -906,8 +930,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_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_enter") + assert hasattr(hyperopt.backtesting.strategy, "advise_sell") + assert hasattr(hyperopt.backtesting.strategy, "advise_buy") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -960,8 +984,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_exit") - assert hasattr(hyperopt.backtesting.strategy, "advise_enter") + assert hasattr(hyperopt.backtesting.strategy, "advise_sell") + assert hasattr(hyperopt.backtesting.strategy, "advise_buy") 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 439a99e2f..1517b6fcc 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_sell is True + assert s.should_exit 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 3e5695a99..be373e0ee 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -130,6 +130,19 @@ class DefaultStrategy(IStrategy): ), 'buy'] = 1 + 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 + ), + 'enter_short'] = 1 + return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -153,37 +166,7 @@ class DefaultStrategy(IStrategy): (dataframe['minus_di'] > 0.5) ), '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[ ( ( @@ -198,4 +181,5 @@ class DefaultStrategy(IStrategy): (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 8d428b33d..e45ba03f0 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -60,7 +60,7 @@ class HyperoptableStrategy(IStrategy): 'sell_minusdi': 0.4 } - short_params = { + enter_short_params = { 'short_rsi': 65, } @@ -87,8 +87,8 @@ 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') + enter_short_rsi = IntParameter([50, 100], default=70, space='sell') + enter_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) @@ -175,6 +175,19 @@ class HyperoptableStrategy(IStrategy): ), 'buy'] = 1 + dataframe.loc[ + ( + (dataframe['rsi'] > self.enter_short_rsi.value) & + (dataframe['fastd'] > 65) & + (dataframe['adx'] < 70) & + (dataframe['plus_di'] < self.enter_short_plusdi.value) + ) | + ( + (dataframe['adx'] < 35) & + (dataframe['plus_di'] < self.enter_short_plusdi.value) + ), + 'enter_short'] = 1 + return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -198,37 +211,7 @@ class HyperoptableStrategy(IStrategy): (dataframe['minus_di'] > self.sell_minusdi.value) ), '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[ ( ( @@ -243,4 +226,5 @@ class HyperoptableStrategy(IStrategy): (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 a5531b42f..20f24d6a3 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -84,35 +84,5 @@ class TestStrategyLegacy(IStrategy): (dataframe['volume'] > 0) ), '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 420cf8f46..42b1cc0a0 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -14,8 +14,6 @@ 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): @@ -29,10 +27,6 @@ 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, @@ -43,28 +37,11 @@ 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', - is_short=False, current_time=datetime.utcnow()) is True - + 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', - is_short=False, current_time=datetime.utcnow()) is True + 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 1e47575dc..7b7354bda 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -482,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) - enter_mock = MagicMock(side_effect=lambda x, meta, is_short: x) - exit_mock = MagicMock(side_effect=lambda x, meta, is_short: x) + buy_mock = MagicMock(side_effect=lambda x, meta: x) + sell_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_enter=enter_mock, - advise_exit=exit_mock, + advise_buy=buy_mock, + advise_sell=sell_mock, ) strategy = DefaultStrategy({}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 - assert enter_mock.call_count == 2 - assert enter_mock.call_count == 2 + assert buy_mock.call_count == 1 + assert buy_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -504,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 enter_mock.call_count == 4 - assert enter_mock.call_count == 4 + assert buy_mock.call_count == 2 + assert buy_mock.call_count == 2 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -513,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) - enter_mock = MagicMock(side_effect=lambda x, meta, is_short: x) - exit_mock = MagicMock(side_effect=lambda x, meta, is_short: x) + buy_mock = MagicMock(side_effect=lambda x, meta: x) + sell_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_enter=enter_mock, - advise_exit=exit_mock, + advise_buy=buy_mock, + advise_sell=sell_mock, ) strategy = DefaultStrategy({}) @@ -532,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 enter_mock.call_count == 2 # Once for buy, once for short - assert enter_mock.call_count == 2 + assert buy_mock.call_count == 1 + assert buy_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() @@ -541,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 enter_mock.call_count == 2 - assert enter_mock.call_count == 2 + assert buy_mock.call_count == 1 + assert buy_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked assert 'buy' in ret.columns assert 'sell' in ret.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2cf77b172..8f8a71097 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -117,16 +117,12 @@ def test_strategy(result, default_conf): df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = strategy.advise_enter(df_indicators, metadata=metadata, is_short=False) + dataframe = strategy.advise_buy(df_indicators, metadata=metadata) assert 'buy' in dataframe.columns + assert 'enter_short' in dataframe.columns - dataframe = strategy.advise_exit(df_indicators, metadata=metadata, is_short=False) + dataframe = strategy.advise_sell(df_indicators, metadata=metadata) 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 @@ -352,7 +348,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_enter(indicators, {'pair': 'ETH/BTC'}, is_short=False) # TODO-lev + strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -361,7 +357,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_exit(indicators, {'pair': 'ETH_BTC'}, is_short=False) # TODO-lev + strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -381,8 +377,6 @@ 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' @@ -391,22 +385,14 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = strategy.advise_enter(result, metadata=metadata, is_short=False) + buydf = strategy.advise_buy(result, metadata=metadata) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = strategy.advise_exit(result, metadata=metadata, is_short=False) + selldf = strategy.advise_sell(result, metadata=metadata) 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) @@ -420,26 +406,18 @@ 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_enter(result, metadata=metadata, is_short=False) - assert isinstance(buydf, DataFrame) - assert 'buy' in buydf.columns + enterdf = strategy.advise_buy(result, metadata=metadata) + assert isinstance(enterdf, DataFrame) + assert 'buy' in enterdf.columns + assert 'enter_short' in enterdf.columns - 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 + exitdf = strategy.advise_sell(result, metadata=metadata) + assert isinstance(exitdf, DataFrame) + assert 'sell' in exitdf + assert 'exit_short' in exitdf.columns From dc4090234de7b49ff908479161a89ba2809345a8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 18 Aug 2021 12:43:44 -0600 Subject: [PATCH 03/46] Added interface leverage method --- freqtrade/strategy/interface.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b56a54d14..3f886b5a6 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -816,19 +816,3 @@ class IStrategy(ABC, HyperStrategyMixin): return self.populate_sell_trend(dataframe) # type: ignore else: return self.populate_sell_trend(dataframe, metadata) - - def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, - **kwargs) -> float: - """ - Customize leverage for each new trade. This method is not called when edge module is - enabled. - - :param pair: Pair that's currently analyzed - :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. - :param proposed_leverage: A leverage proposed by the bot. - :param max_leverage: Max leverage allowed on this pair - :return: A stake size, which is between min_stake and max_stake. - """ - return proposed_leverage From 55c070f1bb3ed63871e74883c418c88717d1d168 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 18 Aug 2021 12:43:44 -0600 Subject: [PATCH 04/46] Added interface leverage method --- freqtrade/strategy/interface.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 3f886b5a6..21d0c70ae 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -816,3 +816,19 @@ class IStrategy(ABC, HyperStrategyMixin): return self.populate_sell_trend(dataframe) # type: ignore else: return self.populate_sell_trend(dataframe, metadata) + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, + **kwargs) -> float: + """ + Customize leverage for each new trade. This method is not called when edge module is + enabled. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 From 8644449c33b12f11d0f652ea309b8175481372bc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 21:38:15 -0600 Subject: [PATCH 05/46] Removed changes from tests/strategy/strats that hyperopted short parameters, because these are supposed to be legacy tests --- tests/strategy/strats/default_strategy.py | 29 ------------ .../strategy/strats/hyperoptable_strategy.py | 44 ------------------- tests/strategy/strats/legacy_strategy.py | 1 - tests/strategy/test_interface.py | 7 ++- tests/strategy/test_strategy_loading.py | 4 -- 5 files changed, 3 insertions(+), 82 deletions(-) diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index be373e0ee..7171b93ae 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -130,19 +130,6 @@ class DefaultStrategy(IStrategy): ), 'buy'] = 1 - 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 - ), - 'enter_short'] = 1 - return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -166,20 +153,4 @@ class DefaultStrategy(IStrategy): (dataframe['minus_di'] > 0.5) ), 'sell'] = 1 - - 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 e45ba03f0..1126bd6cf 100644 --- a/tests/strategy/strats/hyperoptable_strategy.py +++ b/tests/strategy/strats/hyperoptable_strategy.py @@ -60,15 +60,6 @@ class HyperoptableStrategy(IStrategy): 'sell_minusdi': 0.4 } - enter_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') @@ -87,12 +78,6 @@ class HyperoptableStrategy(IStrategy): }) return prot - enter_short_rsi = IntParameter([50, 100], default=70, space='sell') - enter_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. @@ -175,19 +160,6 @@ class HyperoptableStrategy(IStrategy): ), 'buy'] = 1 - dataframe.loc[ - ( - (dataframe['rsi'] > self.enter_short_rsi.value) & - (dataframe['fastd'] > 65) & - (dataframe['adx'] < 70) & - (dataframe['plus_di'] < self.enter_short_plusdi.value) - ) | - ( - (dataframe['adx'] < 35) & - (dataframe['plus_di'] < self.enter_short_plusdi.value) - ), - 'enter_short'] = 1 - return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -211,20 +183,4 @@ class HyperoptableStrategy(IStrategy): (dataframe['minus_di'] > self.sell_minusdi.value) ), 'sell'] = 1 - - 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 20f24d6a3..9ef00b110 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -84,5 +84,4 @@ class TestStrategyLegacy(IStrategy): (dataframe['volume'] > 0) ), 'sell'] = 1 - return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 958f4ebed..5aa18c7db 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -747,11 +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) - # TODO-lev: Should these be 4,4 and 10? - assert len(all_params['buy']) == 4 - assert len(all_params['sell']) == 4 + assert len(all_params['buy']) == 2 + assert len(all_params['sell']) == 2 # Number of Hyperoptable parameters - assert all_params['count'] == 10 + assert all_params['count'] == 6 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 73c7cb5f7..1c846ec13 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -119,11 +119,9 @@ def test_strategy(result, default_conf): dataframe = strategy.advise_buy(df_indicators, metadata=metadata) assert 'buy' in dataframe.columns - assert 'enter_short' in dataframe.columns dataframe = strategy.advise_sell(df_indicators, metadata=metadata) assert 'sell' in dataframe.columns - assert 'exit_short' in dataframe.columns def test_strategy_override_minimal_roi(caplog, default_conf): @@ -415,9 +413,7 @@ def test_strategy_interface_versioning(result, monkeypatch, default_conf): enterdf = strategy.advise_buy(result, metadata=metadata) assert isinstance(enterdf, DataFrame) assert 'buy' in enterdf.columns - assert 'enter_short' in enterdf.columns exitdf = strategy.advise_sell(result, metadata=metadata) assert isinstance(exitdf, DataFrame) assert 'sell' in exitdf - assert 'exit_short' in exitdf.columns From 9f6b6f04b4fa953a990ad575b511f33dc05699c1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 23:55:34 -0600 Subject: [PATCH 06/46] Added False to self.strategy.get_signal --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 179c99d2c..050818c13 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -423,7 +423,8 @@ class FreqtradeBot(LoggingMixin): (buy, sell, buy_tag) = self.strategy.get_signal( pair, self.strategy.timeframe, - analyzed_df + analyzed_df, + False ) if buy and not sell: From 0afeb269ad1beff0ca9fbc809e34cf390d3d001d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 23 Aug 2021 00:15:35 -0600 Subject: [PATCH 07/46] Removed unnecessary TODOs --- freqtrade/strategy/hyper.py | 2 -- tests/strategy/test_strategy_loading.py | 1 - 2 files changed, 3 deletions(-) diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 87d4241f1..dad282d7e 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -22,8 +22,6 @@ from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -# TODO-lev: This file - class BaseParameter(ABC): """ diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 1c846ec13..e76990ba9 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -218,7 +218,6 @@ 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', From 53b51ce8cfd4cd0bf318f130697b07f8bd62ee3c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 23 Aug 2021 00:17:20 -0600 Subject: [PATCH 08/46] Reverted freqtrade/templates/sample_strategy back to no shorting, and created a separate sample short strategy --- freqtrade/templates/sample_short_strategy.py | 379 +++++++++++++++++++ freqtrade/templates/sample_strategy.py | 24 -- 2 files changed, 379 insertions(+), 24 deletions(-) create mode 100644 freqtrade/templates/sample_short_strategy.py diff --git a/freqtrade/templates/sample_short_strategy.py b/freqtrade/templates/sample_short_strategy.py new file mode 100644 index 000000000..bdd0054e8 --- /dev/null +++ b/freqtrade/templates/sample_short_strategy.py @@ -0,0 +1,379 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# flake8: noqa: F401 +# isort: skip_file +# --- Do not remove these libs --- +import numpy as np # noqa +import pandas as pd # noqa +from pandas import DataFrame + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) + +# -------------------------------- +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib + + +# This class is a sample. Feel free to customize it. +class SampleStrategy(IStrategy): + """ + This is a sample strategy to inspire you. + More information in https://www.freqtrade.io/en/latest/strategy-customization/ + + You can: + :return: a Dataframe with all mandatory indicators for the strategies + - Rename the class name (Do not forget to update class_name) + - Add any methods you want to build your strategy + - Add any lib you need to build your strategy + + You must keep: + - the lib in the section "Do not remove these libs" + - the methods: populate_indicators, populate_buy_trend, populate_sell_trend + You should keep: + - timeframe, minimal_roi, stoploss, trailing_* + """ + # Strategy interface version - allow new iterations of the strategy interface. + # Check the documentation or the Sample strategy to get the latest version. + INTERFACE_VERSION = 2 + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi". + minimal_roi = { + "60": 0.01, + "30": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy. + # This attribute will be overridden if the config file contains "stoploss". + stoploss = -0.10 + + # Trailing stoploss + trailing_stop = False + # trailing_only_offset_is_reached = False + # trailing_stop_positive = 0.01 + # trailing_stop_positive_offset = 0.0 # Disabled / not configured + + # Hyperoptable parameters + 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' + + # Run "populate_indicators()" only for new candle. + process_only_new_candles = False + + # These values can be overridden in the "ask_strategy" section in the config. + use_sell_signal = True + sell_profit_only = False + ignore_roi_if_buy_signal = False + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 30 + + # Optional order type mapping. + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + # Optional order time in force. + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc' + } + + plot_config = { + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, + }, + 'subplots': { + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } + } + } + + def informative_pairs(self): + """ + Define additional, informative pair/interval combinations to be cached from the exchange. + These pair/interval combinations are non-tradeable, unless they are part + of the whitelist as well. + For more information, please consult the documentation + :return: List of tuples in the format (pair, interval) + Sample: return [("ETH/USDT", "5m"), + ("BTC/USDT", "15m"), + ] + """ + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :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 + """ + + # Momentum Indicators + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # # Plus Directional Indicator / Movement + # dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + # dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # # Minus Directional Indicator / Movement + # dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + # dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # # Aroon, Aroon Oscillator + # aroon = ta.AROON(dataframe) + # dataframe['aroonup'] = aroon['aroonup'] + # dataframe['aroondown'] = aroon['aroondown'] + # dataframe['aroonosc'] = ta.AROONOSC(dataframe) + + # # Awesome Oscillator + # dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # # Keltner Channel + # keltner = qtpylib.keltner_channel(dataframe) + # dataframe["kc_upperband"] = keltner["upper"] + # dataframe["kc_lowerband"] = keltner["lower"] + # dataframe["kc_middleband"] = keltner["mid"] + # dataframe["kc_percent"] = ( + # (dataframe["close"] - dataframe["kc_lowerband"]) / + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) + # ) + # dataframe["kc_width"] = ( + # (dataframe["kc_upperband"] - dataframe["kc_lowerband"]) / dataframe["kc_middleband"] + # ) + + # # Ultimate Oscillator + # dataframe['uo'] = ta.ULTOSC(dataframe) + + # # Commodity Channel Index: values [Oversold:-100, Overbought:100] + # dataframe['cci'] = ta.CCI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # # Inverse Fisher transform on RSI: values [-1.0, 1.0] (https://goo.gl/2JGGoy) + # rsi = 0.1 * (dataframe['rsi'] - 50) + # dataframe['fisher_rsi'] = (np.exp(2 * rsi) - 1) / (np.exp(2 * rsi) + 1) + + # # Inverse Fisher transform on RSI normalized: values [0.0, 100.0] (https://goo.gl/2JGGoy) + # dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # # Stochastic Slow + # stoch = ta.STOCH(dataframe) + # dataframe['slowd'] = stoch['slowd'] + # dataframe['slowk'] = stoch['slowk'] + + # Stochastic Fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. + # stoch_rsi = ta.STOCHRSI(dataframe) + # dataframe['fastd_rsi'] = stoch_rsi['fastd'] + # dataframe['fastk_rsi'] = stoch_rsi['fastk'] + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # # ROC + # dataframe['roc'] = ta.ROC(dataframe) + + # Overlap Studies + # ------------------------------------ + + # Bollinger Bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + dataframe["bb_percent"] = ( + (dataframe["close"] - dataframe["bb_lowerband"]) / + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) + ) + dataframe["bb_width"] = ( + (dataframe["bb_upperband"] - dataframe["bb_lowerband"]) / dataframe["bb_middleband"] + ) + + # Bollinger Bands - Weighted (EMA based instead of SMA) + # weighted_bollinger = qtpylib.weighted_bollinger_bands( + # qtpylib.typical_price(dataframe), window=20, stds=2 + # ) + # dataframe["wbb_upperband"] = weighted_bollinger["upper"] + # dataframe["wbb_lowerband"] = weighted_bollinger["lower"] + # dataframe["wbb_middleband"] = weighted_bollinger["mid"] + # dataframe["wbb_percent"] = ( + # (dataframe["close"] - dataframe["wbb_lowerband"]) / + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) + # ) + # dataframe["wbb_width"] = ( + # (dataframe["wbb_upperband"] - dataframe["wbb_lowerband"]) / + # dataframe["wbb_middleband"] + # ) + + # # EMA - Exponential Moving Average + # dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + # dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + # dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + # dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21) + # dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + # dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # # SMA - Simple Moving Average + # dataframe['sma3'] = ta.SMA(dataframe, timeperiod=3) + # dataframe['sma5'] = ta.SMA(dataframe, timeperiod=5) + # dataframe['sma10'] = ta.SMA(dataframe, timeperiod=10) + # dataframe['sma21'] = ta.SMA(dataframe, timeperiod=21) + # dataframe['sma50'] = ta.SMA(dataframe, timeperiod=50) + # dataframe['sma100'] = ta.SMA(dataframe, timeperiod=100) + + # Parabolic SAR + dataframe['sar'] = ta.SAR(dataframe) + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + # # Hammer: values [0, 100] + # dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # # Inverted Hammer: values [0, 100] + # dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # # Dragonfly Doji: values [0, 100] + # dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # # Piercing Line: values [0, 100] + # dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # # Morningstar: values [0, 100] + # dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # # Three White Soldiers: values [0, 100] + # dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + # # Hanging Man: values [0, 100] + # dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # # Shooting Star: values [0, 100] + # dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # # Gravestone Doji: values [0, 100] + # dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # # Dark Cloud Cover: values [0, 100] + # dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # # Evening Doji Star: values [0, 100] + # dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # # Evening Star: values [0, 100] + # dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + # # Three Line Strike: values [0, -100, 100] + # dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # # Spinning Top: values [0, -100, 100] + # dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # # Engulfing: values [0, -100, 100] + # dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # # Harami: values [0, -100, 100] + # dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # # Three Outside Up/Down: values [0, -100, 100] + # dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # # Three Inside Up/Down: values [0, -100, 100] + # dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + + # # Chart type + # # ------------------------------------ + # # Heikin Ashi Strategy + # heikinashi = qtpylib.heikinashi(dataframe) + # dataframe['ha_open'] = heikinashi['open'] + # dataframe['ha_close'] = heikinashi['close'] + # dataframe['ha_high'] = heikinashi['high'] + # dataframe['ha_low'] = heikinashi['low'] + + # Retrieve best bid and best ask from the orderbook + # ------------------------------------ + """ + # first check if dataprovider is available + if self.dp: + if self.dp.runmode.value in ('live', 'dry_run'): + ob = self.dp.orderbook(metadata['pair'], 1) + dataframe['best_bid'] = ob['bids'][0][0] + dataframe['best_ask'] = ob['asks'][0][0] + """ + + return dataframe + + def populate_buy_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 + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with buy 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 + ), + 'enter_short'] = 1 + + return dataframe + + def populate_sell_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 + :param metadata: Additional information, like the currently traded pair + :return: DataFrame with sell column + """ + + dataframe.loc[ + ( + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) & + # Guard: tema below BB middle + (dataframe['tema'] <= dataframe['bb_middleband']) & + (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/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b2d130059..574819949 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -58,8 +58,6 @@ 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' @@ -356,16 +354,6 @@ class SampleStrategy(IStrategy): ), 'buy'] = 1 - 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 - ), - 'enter_short'] = 1 - return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -384,16 +372,4 @@ class SampleStrategy(IStrategy): (dataframe['volume'] > 0) # Make sure Volume is not 0 ), 'sell'] = 1 - - dataframe.loc[ - ( - # Signal: RSI crosses above 30 - (qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) & - # Guard: tema below BB middle - (dataframe['tema'] <= dataframe['bb_middleband']) & - (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising - (dataframe['volume'] > 0) # Make sure Volume is not 0 - ), - 'exit_short'] = 1 - return dataframe From 61ad38500a903f82015c11bc9a2d7524f30d5eab Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 23 Aug 2021 00:18:15 -0600 Subject: [PATCH 09/46] Reverted freqtrade/templates/*hyperopt* files back to no shorting --- freqtrade/templates/sample_hyperopt.py | 48 --------------- .../templates/sample_hyperopt_advanced.py | 58 +------------------ 2 files changed, 2 insertions(+), 104 deletions(-) diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index ca72e3740..7ed726d7a 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -55,10 +55,6 @@ class SampleHyperOpt(IHyperOpt): Integer(15, 45, name='fastd-value'), Integer(20, 50, name='adx-value'), Integer(20, 40, name='rsi-value'), - Integer(75, 90, name='short-mfi-value'), - Integer(55, 85, name='short-fastd-value'), - Integer(50, 80, name='short-adx-value'), - Integer(60, 80, name='short-rsi-value'), Categorical([True, False], name='mfi-enabled'), Categorical([True, False], name='fastd-enabled'), Categorical([True, False], name='adx-enabled'), @@ -76,60 +72,40 @@ class SampleHyperOpt(IHyperOpt): Buy strategy Hyperopt will build and use. """ long_conditions = [] - short_conditions = [] # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: long_conditions.append(dataframe['mfi'] < params['mfi-value']) - short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) if 'fastd-enabled' in params and params['fastd-enabled']: long_conditions.append(dataframe['fastd'] < params['fastd-value']) - short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) if 'adx-enabled' in params and params['adx-enabled']: long_conditions.append(dataframe['adx'] > params['adx-value']) - short_conditions.append(dataframe['adx'] < params['short-adx-value']) if 'rsi-enabled' in params and params['rsi-enabled']: long_conditions.append(dataframe['rsi'] < params['rsi-value']) - short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) # TRIGGERS if 'trigger' in params: if params['trigger'] == 'boll': long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) if params['trigger'] == 'macd_cross_signal': long_conditions.append(qtpylib.crossed_above( dataframe['macd'], dataframe['macdsignal'] )) - short_conditions.append(qtpylib.crossed_below( - dataframe['macd'], - dataframe['macdsignal'] - )) if params['trigger'] == 'sar_reversal': long_conditions.append(qtpylib.crossed_above( dataframe['close'], dataframe['sar'] )) - short_conditions.append(qtpylib.crossed_below( - dataframe['close'], - dataframe['sar'] - )) # Check that volume is not 0 long_conditions.append(dataframe['volume'] > 0) - short_conditions.append(dataframe['volume'] > 0) if long_conditions: dataframe.loc[ reduce(lambda x, y: x & y, long_conditions), 'buy'] = 1 - if short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, short_conditions), - 'enter_short'] = 1 - return dataframe return populate_buy_trend @@ -144,10 +120,6 @@ class SampleHyperOpt(IHyperOpt): Integer(50, 100, name='sell-fastd-value'), Integer(50, 100, name='sell-adx-value'), Integer(60, 100, name='sell-rsi-value'), - 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='sell-mfi-enabled'), Categorical([True, False], name='sell-fastd-enabled'), Categorical([True, False], name='sell-adx-enabled'), @@ -169,60 +141,40 @@ class SampleHyperOpt(IHyperOpt): Sell strategy Hyperopt will build and use. """ exit_long_conditions = [] - exit_short_conditions = [] # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) if 'sell-adx-enabled' in params and params['sell-adx-enabled']: exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) - exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) # TRIGGERS if 'sell-trigger' in params: if params['sell-trigger'] == 'sell-boll': exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) if params['sell-trigger'] == 'sell-macd_cross_signal': exit_long_conditions.append(qtpylib.crossed_above( dataframe['macdsignal'], dataframe['macd'] )) - exit_short_conditions.append(qtpylib.crossed_below( - dataframe['macdsignal'], - dataframe['macd'] - )) if params['sell-trigger'] == 'sell-sar_reversal': exit_long_conditions.append(qtpylib.crossed_above( dataframe['sar'], dataframe['close'] )) - exit_short_conditions.append(qtpylib.crossed_below( - dataframe['sar'], - dataframe['close'] - )) # Check that volume is not 0 exit_long_conditions.append(dataframe['volume'] > 0) - exit_short_conditions.append(dataframe['volume'] > 0) if exit_long_conditions: dataframe.loc[ reduce(lambda x, y: x & y, exit_long_conditions), 'sell'] = 1 - if exit_short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_short_conditions), - 'exit_short'] = 1 - return dataframe return populate_sell_trend diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index feb617aae..733f1ef3e 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -70,10 +70,6 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(15, 45, name='fastd-value'), Integer(20, 50, name='adx-value'), Integer(20, 40, name='rsi-value'), - Integer(75, 90, name='short-mfi-value'), - Integer(55, 85, name='short-fastd-value'), - Integer(50, 80, name='short-adx-value'), - Integer(60, 80, name='short-rsi-value'), Categorical([True, False], name='mfi-enabled'), Categorical([True, False], name='fastd-enabled'), Categorical([True, False], name='adx-enabled'), @@ -91,59 +87,37 @@ class AdvancedSampleHyperOpt(IHyperOpt): Buy strategy Hyperopt will build and use """ long_conditions = [] - short_conditions = [] # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: long_conditions.append(dataframe['mfi'] < params['mfi-value']) - short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) if 'fastd-enabled' in params and params['fastd-enabled']: long_conditions.append(dataframe['fastd'] < params['fastd-value']) - short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) if 'adx-enabled' in params and params['adx-enabled']: long_conditions.append(dataframe['adx'] > params['adx-value']) - short_conditions.append(dataframe['adx'] < params['short-adx-value']) if 'rsi-enabled' in params and params['rsi-enabled']: long_conditions.append(dataframe['rsi'] < params['rsi-value']) - short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) # TRIGGERS if 'trigger' in params: if params['trigger'] == 'boll': long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) if params['trigger'] == 'macd_cross_signal': long_conditions.append(qtpylib.crossed_above( - dataframe['macd'], - dataframe['macdsignal'] - )) - short_conditions.append(qtpylib.crossed_below( - dataframe['macd'], - dataframe['macdsignal'] + dataframe['macd'], dataframe['macdsignal'] )) if params['trigger'] == 'sar_reversal': long_conditions.append(qtpylib.crossed_above( - dataframe['close'], - dataframe['sar'] - )) - short_conditions.append(qtpylib.crossed_below( - dataframe['close'], - dataframe['sar'] + dataframe['close'], dataframe['sar'] )) # Check that volume is not 0 long_conditions.append(dataframe['volume'] > 0) - short_conditions.append(dataframe['volume'] > 0) if long_conditions: dataframe.loc[ reduce(lambda x, y: x & y, long_conditions), 'buy'] = 1 - if short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, short_conditions), - 'enter_short'] = 1 - return dataframe return populate_buy_trend @@ -158,10 +132,6 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(50, 100, name='sell-fastd-value'), Integer(50, 100, name='sell-adx-value'), Integer(60, 100, name='sell-rsi-value'), - 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='sell-mfi-enabled'), Categorical([True, False], name='sell-fastd-enabled'), Categorical([True, False], name='sell-adx-enabled'), @@ -183,59 +153,39 @@ class AdvancedSampleHyperOpt(IHyperOpt): """ # print(params) exit_long_conditions = [] - exit_short_conditions = [] # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) if 'sell-adx-enabled' in params and params['sell-adx-enabled']: exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) - exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) # TRIGGERS if 'sell-trigger' in params: if params['sell-trigger'] == 'sell-boll': exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) if params['sell-trigger'] == 'sell-macd_cross_signal': exit_long_conditions.append(qtpylib.crossed_above( dataframe['macdsignal'], dataframe['macd'] )) - exit_long_conditions.append(qtpylib.crossed_below( - dataframe['macdsignal'], - dataframe['macd'] - )) if params['sell-trigger'] == 'sell-sar_reversal': exit_long_conditions.append(qtpylib.crossed_above( dataframe['sar'], dataframe['close'] )) - exit_long_conditions.append(qtpylib.crossed_below( - dataframe['sar'], - dataframe['close'] - )) # Check that volume is not 0 exit_long_conditions.append(dataframe['volume'] > 0) - exit_short_conditions.append(dataframe['volume'] > 0) if exit_long_conditions: dataframe.loc[ reduce(lambda x, y: x & y, exit_long_conditions), 'sell'] = 1 - if exit_short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_short_conditions), - 'exit_short'] = 1 - return dataframe return populate_sell_trend @@ -243,7 +193,6 @@ class AdvancedSampleHyperOpt(IHyperOpt): @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. @@ -265,7 +214,6 @@ 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 @@ -286,7 +234,6 @@ 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 @@ -299,7 +246,6 @@ 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. From 317a454c0e179ecc138060288dc437b4e25637f1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 23 Aug 2021 00:18:56 -0600 Subject: [PATCH 10/46] Removed shorting from tests/optimize/hyperopts/default_hyperopt.py and created another tests/optimize/hyperopts/short_hyperopt.py with long and shorting --- tests/optimize/hyperopts/default_hyperopt.py | 104 ++----- tests/optimize/hyperopts/short_hyperopt.py | 271 +++++++++++++++++++ tests/optimize/test_hyperopt.py | 16 -- 3 files changed, 291 insertions(+), 100 deletions(-) create mode 100644 tests/optimize/hyperopts/short_hyperopt.py diff --git a/tests/optimize/hyperopts/default_hyperopt.py b/tests/optimize/hyperopts/default_hyperopt.py index df39188e0..4147f475c 100644 --- a/tests/optimize/hyperopts/default_hyperopt.py +++ b/tests/optimize/hyperopts/default_hyperopt.py @@ -54,57 +54,38 @@ class DefaultHyperOpt(IHyperOpt): """ Buy strategy Hyperopt will build and use. """ - long_conditions = [] - short_conditions = [] + conditions = [] # GUARDS AND TRENDS if 'mfi-enabled' in params and params['mfi-enabled']: - long_conditions.append(dataframe['mfi'] < params['mfi-value']) - short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) + conditions.append(dataframe['mfi'] < params['mfi-value']) if 'fastd-enabled' in params and params['fastd-enabled']: - long_conditions.append(dataframe['fastd'] < params['fastd-value']) - short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) + conditions.append(dataframe['fastd'] < params['fastd-value']) if 'adx-enabled' in params and params['adx-enabled']: - long_conditions.append(dataframe['adx'] > params['adx-value']) - short_conditions.append(dataframe['adx'] < params['short-adx-value']) + conditions.append(dataframe['adx'] > params['adx-value']) if 'rsi-enabled' in params and params['rsi-enabled']: - long_conditions.append(dataframe['rsi'] < params['rsi-value']) - short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) + conditions.append(dataframe['rsi'] < params['rsi-value']) # TRIGGERS if 'trigger' in params: if params['trigger'] == 'boll': - long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) + conditions.append(dataframe['close'] < dataframe['bb_lowerband']) if params['trigger'] == 'macd_cross_signal': - long_conditions.append(qtpylib.crossed_above( - dataframe['macd'], - dataframe['macdsignal'] - )) - short_conditions.append(qtpylib.crossed_below( + conditions.append(qtpylib.crossed_above( dataframe['macd'], dataframe['macdsignal'] )) if params['trigger'] == 'sar_reversal': - long_conditions.append(qtpylib.crossed_above( - dataframe['close'], - dataframe['sar'] - )) - short_conditions.append(qtpylib.crossed_below( + conditions.append(qtpylib.crossed_above( dataframe['close'], dataframe['sar'] )) - if long_conditions: + if conditions: dataframe.loc[ - reduce(lambda x, y: x & y, long_conditions), + reduce(lambda x, y: x & y, conditions), 'buy'] = 1 - if short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, short_conditions), - 'enter_short'] = 1 - return dataframe return populate_buy_trend @@ -119,10 +100,6 @@ class DefaultHyperOpt(IHyperOpt): Integer(15, 45, name='fastd-value'), Integer(20, 50, name='adx-value'), Integer(20, 40, name='rsi-value'), - Integer(75, 90, name='short-mfi-value'), - Integer(55, 85, name='short-fastd-value'), - Integer(50, 80, name='short-adx-value'), - Integer(60, 80, name='short-rsi-value'), Categorical([True, False], name='mfi-enabled'), Categorical([True, False], name='fastd-enabled'), Categorical([True, False], name='adx-enabled'), @@ -139,57 +116,38 @@ class DefaultHyperOpt(IHyperOpt): """ Sell strategy Hyperopt will build and use. """ - exit_long_conditions = [] - exit_short_conditions = [] + conditions = [] # GUARDS AND TRENDS if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) + conditions.append(dataframe['mfi'] > params['sell-mfi-value']) if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) + conditions.append(dataframe['fastd'] > params['sell-fastd-value']) if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) - exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) + conditions.append(dataframe['adx'] < params['sell-adx-value']) if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) + conditions.append(dataframe['rsi'] > params['sell-rsi-value']) # TRIGGERS if 'sell-trigger' in params: if params['sell-trigger'] == 'sell-boll': - exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + conditions.append(dataframe['close'] > dataframe['bb_upperband']) if params['sell-trigger'] == 'sell-macd_cross_signal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], - dataframe['macd'] - )) - exit_short_conditions.append(qtpylib.crossed_below( + conditions.append(qtpylib.crossed_above( dataframe['macdsignal'], dataframe['macd'] )) if params['sell-trigger'] == 'sell-sar_reversal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['sar'], - dataframe['close'] - )) - exit_short_conditions.append(qtpylib.crossed_below( + conditions.append(qtpylib.crossed_above( dataframe['sar'], dataframe['close'] )) - if exit_long_conditions: + if conditions: dataframe.loc[ - reduce(lambda x, y: x & y, exit_long_conditions), + reduce(lambda x, y: x & y, conditions), 'sell'] = 1 - if exit_short_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_short_conditions), - 'exit-short'] = 1 - return dataframe return populate_sell_trend @@ -204,10 +162,6 @@ class DefaultHyperOpt(IHyperOpt): Integer(50, 100, name='sell-fastd-value'), Integer(50, 100, name='sell-adx-value'), Integer(60, 100, name='sell-rsi-value'), - 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='sell-mfi-enabled'), Categorical([True, False], name='sell-fastd-enabled'), Categorical([True, False], name='sell-adx-enabled'), @@ -233,15 +187,6 @@ class DefaultHyperOpt(IHyperOpt): ), 'buy'] = 1 - dataframe.loc[ - ( - (dataframe['close'] > dataframe['bb_upperband']) & - (dataframe['mfi'] < 84) & - (dataframe['adx'] > 75) & - (dataframe['rsi'] < 79) - ), - 'enter_short'] = 1 - return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -259,13 +204,4 @@ class DefaultHyperOpt(IHyperOpt): ), 'sell'] = 1 - dataframe.loc[ - ( - (qtpylib.crossed_below( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] < 46) - ), - 'exit_short'] = 1 - return dataframe diff --git a/tests/optimize/hyperopts/short_hyperopt.py b/tests/optimize/hyperopts/short_hyperopt.py new file mode 100644 index 000000000..df39188e0 --- /dev/null +++ b/tests/optimize/hyperopts/short_hyperopt.py @@ -0,0 +1,271 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from functools import reduce +from typing import Any, Callable, Dict, List + +import talib.abstract as ta +from pandas import DataFrame +from skopt.space import Categorical, Dimension, Integer + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.optimize.hyperopt_interface import IHyperOpt + + +class DefaultHyperOpt(IHyperOpt): + """ + Default hyperopt provided by the Freqtrade bot. + You can override it with your own Hyperopt + """ + @staticmethod + def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Add several indicators needed for buy and sell strategies defined below. + """ + # ADX + dataframe['adx'] = ta.ADX(dataframe) + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + # Stochastic Fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + # Minus-DI + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_upperband'] = bollinger['upper'] + # SAR + dataframe['sar'] = ta.SAR(dataframe) + + return dataframe + + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Buy strategy Hyperopt will build and use. + """ + long_conditions = [] + short_conditions = [] + + # GUARDS AND TRENDS + if 'mfi-enabled' in params and params['mfi-enabled']: + long_conditions.append(dataframe['mfi'] < params['mfi-value']) + short_conditions.append(dataframe['mfi'] > params['short-mfi-value']) + if 'fastd-enabled' in params and params['fastd-enabled']: + long_conditions.append(dataframe['fastd'] < params['fastd-value']) + short_conditions.append(dataframe['fastd'] > params['short-fastd-value']) + if 'adx-enabled' in params and params['adx-enabled']: + long_conditions.append(dataframe['adx'] > params['adx-value']) + short_conditions.append(dataframe['adx'] < params['short-adx-value']) + if 'rsi-enabled' in params and params['rsi-enabled']: + long_conditions.append(dataframe['rsi'] < params['rsi-value']) + short_conditions.append(dataframe['rsi'] > params['short-rsi-value']) + + # TRIGGERS + if 'trigger' in params: + if params['trigger'] == 'boll': + long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + short_conditions.append(dataframe['close'] > dataframe['bb_upperband']) + if params['trigger'] == 'macd_cross_signal': + long_conditions.append(qtpylib.crossed_above( + dataframe['macd'], + dataframe['macdsignal'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['macd'], + dataframe['macdsignal'] + )) + if params['trigger'] == 'sar_reversal': + long_conditions.append(qtpylib.crossed_above( + dataframe['close'], + dataframe['sar'] + )) + short_conditions.append(qtpylib.crossed_below( + dataframe['close'], + dataframe['sar'] + )) + + if long_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, long_conditions), + 'buy'] = 1 + + if short_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, short_conditions), + 'enter_short'] = 1 + + return dataframe + + return populate_buy_trend + + @staticmethod + def indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching buy strategy parameters. + """ + return [ + Integer(10, 25, name='mfi-value'), + Integer(15, 45, name='fastd-value'), + Integer(20, 50, name='adx-value'), + Integer(20, 40, name='rsi-value'), + Integer(75, 90, name='short-mfi-value'), + Integer(55, 85, name='short-fastd-value'), + Integer(50, 80, name='short-adx-value'), + Integer(60, 80, name='short-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(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') + ] + + @staticmethod + def sell_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the sell strategy parameters to be used by Hyperopt. + """ + def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Sell strategy Hyperopt will build and use. + """ + exit_long_conditions = [] + exit_short_conditions = [] + + # GUARDS AND TRENDS + if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: + exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) + exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value']) + if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: + exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) + exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value']) + if 'sell-adx-enabled' in params and params['sell-adx-enabled']: + exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) + exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value']) + if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: + exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) + exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value']) + + # TRIGGERS + if 'sell-trigger' in params: + if params['sell-trigger'] == 'sell-boll': + exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) + exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) + if params['sell-trigger'] == 'sell-macd_cross_signal': + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['macdsignal'], + dataframe['macd'] + )) + exit_short_conditions.append(qtpylib.crossed_below( + dataframe['macdsignal'], + dataframe['macd'] + )) + if params['sell-trigger'] == 'sell-sar_reversal': + exit_long_conditions.append(qtpylib.crossed_above( + dataframe['sar'], + dataframe['close'] + )) + exit_short_conditions.append(qtpylib.crossed_below( + dataframe['sar'], + dataframe['close'] + )) + + if exit_long_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, exit_long_conditions), + 'sell'] = 1 + + if exit_short_conditions: + dataframe.loc[ + reduce(lambda x, y: x & y, exit_short_conditions), + 'exit-short'] = 1 + + return dataframe + + return populate_sell_trend + + @staticmethod + def sell_indicator_space() -> List[Dimension]: + """ + Define your Hyperopt space for searching sell strategy parameters. + """ + return [ + Integer(75, 100, name='sell-mfi-value'), + Integer(50, 100, name='sell-fastd-value'), + Integer(50, 100, name='sell-adx-value'), + Integer(60, 100, name='sell-rsi-value'), + 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='sell-mfi-enabled'), + Categorical([True, False], name='sell-fastd-enabled'), + Categorical([True, False], name='sell-adx-enabled'), + Categorical([True, False], name='sell-rsi-enabled'), + Categorical(['sell-boll', + 'sell-macd_cross_signal', + 'sell-sar_reversal'], + name='sell-trigger') + ] + + def populate_buy_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 buy space. + """ + dataframe.loc[ + ( + (dataframe['close'] < dataframe['bb_lowerband']) & + (dataframe['mfi'] < 16) & + (dataframe['adx'] > 25) & + (dataframe['rsi'] < 21) + ), + 'buy'] = 1 + + dataframe.loc[ + ( + (dataframe['close'] > dataframe['bb_upperband']) & + (dataframe['mfi'] < 84) & + (dataframe['adx'] > 75) & + (dataframe['rsi'] < 79) + ), + 'enter_short'] = 1 + + return dataframe + + def populate_sell_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 sell space. + """ + dataframe.loc[ + ( + (qtpylib.crossed_above( + dataframe['macdsignal'], dataframe['macd'] + )) & + (dataframe['fastd'] > 54) + ), + 'sell'] = 1 + + dataframe.loc[ + ( + (qtpylib.crossed_below( + dataframe['macdsignal'], dataframe['macd'] + )) & + (dataframe['fastd'] < 46) + ), + 'exit_short'] = 1 + + return dataframe diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 333cea971..dab10fc89 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -542,10 +542,6 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'fastd-value': 35, 'mfi-value': 0, 'rsi-value': 0, - 'short-adx-value': 100, - 'short-fastd-value': 65, - 'short-mfi-value': 100, - 'short-rsi-value': 100, 'adx-enabled': False, 'fastd-enabled': True, 'mfi-enabled': False, @@ -555,10 +551,6 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'sell-fastd-value': 75, 'sell-mfi-value': 0, 'sell-rsi-value': 0, - 'exit-short-adx-value': 100, - 'exit-short-fastd-value': 25, - 'exit-short-mfi-value': 100, - 'exit-short-rsi-value': 100, 'sell-adx-enabled': False, 'sell-fastd-enabled': True, 'sell-mfi-enabled': False, @@ -585,16 +577,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: ), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, - 'short-adx-value': 100, 'fastd-enabled': True, 'fastd-value': 35, - 'short-fastd-value': 65, 'mfi-enabled': False, 'mfi-value': 0, - 'short-mfi-value': 100, 'rsi-enabled': False, 'rsi-value': 0, - 'short-rsi-value': 100, 'trigger': 'macd_cross_signal'}, 'roi': {"0": 0.12000000000000001, "20.0": 0.02, @@ -603,16 +591,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'protection': {}, 'sell': {'sell-adx-enabled': False, 'sell-adx-value': 0, - 'exit-short-adx-value': 100, 'sell-fastd-enabled': True, 'sell-fastd-value': 75, - 'exit-short-fastd-value': 25, 'sell-mfi-enabled': False, 'sell-mfi-value': 0, - 'exit-short-mfi-value': 100, 'sell-rsi-enabled': False, 'sell-rsi-value': 0, - 'exit-short-rsi-value': 100, 'sell-trigger': 'macd_cross_signal'}, 'stoploss': {'stoploss': -0.4}, 'trailing': {'trailing_only_offset_is_reached': False, From 07de5d11caccbf88dc58e5e00fcfbf3d09c71777 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 23 Aug 2021 00:25:08 -0600 Subject: [PATCH 11/46] Removed a bug causing errors from freqtradebot --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 050818c13..179c99d2c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -423,8 +423,7 @@ class FreqtradeBot(LoggingMixin): (buy, sell, buy_tag) = self.strategy.get_signal( pair, self.strategy.timeframe, - analyzed_df, - False + analyzed_df ) if buy and not sell: From 9add3bf8088765d9c621e834e86bebf3fd98fcbc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Aug 2021 21:12:46 +0200 Subject: [PATCH 12/46] Add enter_long compatibility layer --- freqtrade/enums/signaltype.py | 6 +++--- freqtrade/strategy/interface.py | 12 ++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index fcebd9f0e..ca4b8482e 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -5,9 +5,9 @@ class SignalType(Enum): """ Enum to distinguish between buy and sell signals """ - BUY = "buy" - SELL = "sell" - SHORT = "short" + BUY = "buy" # To be renamed to enter_long + SELL = "sell" # To be renamed to exit_long + SHORT = "short" # Should be "enter_short" EXIT_SHORT = "exit_short" diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 50677c064..a1e820808 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -207,6 +207,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ pass + # TODO-lev: add side def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, **kwargs) -> bool: """ @@ -304,6 +305,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ return None + # TODO-lev: add side def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, **kwargs) -> float: @@ -804,7 +806,11 @@ class IStrategy(ABC, HyperStrategyMixin): "the current function headers!", DeprecationWarning) return self.populate_buy_trend(dataframe) # type: ignore else: - return self.populate_buy_trend(dataframe, metadata) + df = self.populate_buy_trend(dataframe, metadata) + # TODO-lev: IF both buy and enter_long exist, this will fail. + df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') + + return df def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -822,7 +828,9 @@ class IStrategy(ABC, HyperStrategyMixin): "the current function headers!", DeprecationWarning) return self.populate_sell_trend(dataframe) # type: ignore else: - return self.populate_sell_trend(dataframe, metadata) + df = self.populate_sell_trend(dataframe, metadata) + # TODO-lev: IF both sell and exit_long exist, this will fail at a later point + return df.rename({'sell': 'exit_long'}, axis='columns') def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, From 3e8164bfcafe5ffa473c585a5888dd626d4a5ea7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Aug 2021 21:13:47 +0200 Subject: [PATCH 13/46] Use proper exchange name in backtesting --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8b3eb46ca..1883f9670 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,8 +65,8 @@ class Backtesting: remove_credentials(self.config) self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} - - self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) + self._exchange_name = self.config['exchange']['name'] + self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config) self.dataprovider = DataProvider(self.config, None) if self.config.get('strategy_list', None): @@ -388,7 +388,7 @@ class Backtesting: fee_close=self.fee, is_open=True, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, - exchange='backtesting', + exchange=self._exchange_name, ) return trade return None From 7373b39015ec109fea422dee7aa657c809eff20b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Aug 2021 21:15:56 +0200 Subject: [PATCH 14/46] Initial support for backtesting with short --- freqtrade/optimize/backtesting.py | 71 ++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1883f9670..5e972f297 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -37,13 +37,16 @@ logger = logging.getLogger(__name__) # Indexes for backtest tuples DATE_IDX = 0 -BUY_IDX = 1 -OPEN_IDX = 2 -CLOSE_IDX = 3 -SELL_IDX = 4 -LOW_IDX = 5 -HIGH_IDX = 6 -BUY_TAG_IDX = 7 +OPEN_IDX = 1 +HIGH_IDX = 2 +LOW_IDX = 3 +CLOSE_IDX = 4 +BUY_IDX = 5 +SELL_IDX = 6 +SHORT_IDX = 7 +ESHORT_IDX = 8 +BUY_TAG_IDX = 9 +SHORT_TAG_IDX = 10 class Backtesting: @@ -215,7 +218,8 @@ class Backtesting: """ # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top - headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] + headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -223,13 +227,21 @@ class Backtesting: for pair, pair_data in processed.items(): self.check_abort() self.progress.increment() - has_buy_tag = 'buy_tag' in pair_data - headers = headers + ['buy_tag'] if has_buy_tag else headers + has_buy_tag = 'long_tag' in pair_data + has_short_tag = 'short_tag' in pair_data + headers = headers + ['long_tag'] if has_buy_tag else headers + headers = headers + ['short_tag'] if has_short_tag else headers if not pair_data.empty: - pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist - pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist + # Cleanup from prior runs + pair_data.loc[:, 'buy'] = 0 # TODO: Should be renamed to enter_long + pair_data.loc[:, 'enter_short'] = 0 + pair_data.loc[:, 'sell'] = 0 # TODO: should be renamed to exit_long + pair_data.loc[:, 'exit_short'] = 0 + # pair_data.loc[:, 'sell'] = 0 if has_buy_tag: - pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist + pair_data.loc[:, 'long_tag'] = None # cleanup if buy_tag is exist + if has_short_tag: + pair_data.loc[:, 'short_tag'] = None # cleanup if short_tag is exist df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), @@ -240,10 +252,12 @@ class Backtesting: startup_candles=self.required_startup) # To avoid using data from future, we use buy/sell signals shifted # from the previous candle - df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) - df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) + df_analyzed.loc[:, 'enter_long'] = df_analyzed.loc[:, 'enter_long'].shift(1) + df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1) + df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1) + df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1) if has_buy_tag: - df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) + df_analyzed.loc[:, 'long_tag'] = df_analyzed.loc[:, 'long_tag'].shift(1) df_analyzed.drop(df_analyzed.head(1).index, inplace=True) @@ -322,7 +336,7 @@ class Backtesting: return sell_row[OPEN_IDX] def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - + # TODO: short exits sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore sell_row[DATE_IDX].to_pydatetime(), sell_row[BUY_IDX], sell_row[SELL_IDX], @@ -349,7 +363,7 @@ class Backtesting: return None - def _enter_trade(self, pair: str, row: List) -> Optional[LocalTrade]: + def _enter_trade(self, pair: str, row: List, direction: str) -> Optional[LocalTrade]: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: @@ -389,6 +403,7 @@ class Backtesting: is_open=True, buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, exchange=self._exchange_name, + is_short=(direction == 'short'), ) return trade return None @@ -422,6 +437,20 @@ class Backtesting: self.rejected_trades += 1 return False + def check_for_trade_entry(self, row) -> Optional[str]: + enter_long = row[BUY_IDX] == 1 + exit_long = row[SELL_IDX] == 1 + enter_short = row[SHORT_IDX] == 1 + exit_short = row[ESHORT_IDX] == 1 + + if enter_long == 1 and not any([exit_long, enter_short]): + # Long + return 'long' + if enter_short == 1 and not any([exit_short, enter_long]): + # Short + return 'short' + return None + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -482,15 +511,15 @@ class Backtesting: # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row + trade_dir = self.check_for_trade_entry(row) if ( (position_stacking or len(open_trades[pair]) == 0) and self.trade_slot_available(max_open_trades, open_trade_count_start) and tmp != end_date - and row[BUY_IDX] == 1 - and row[SELL_IDX] != 1 + and trade_dir is not None and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) ): - trade = self._enter_trade(pair, row) + trade = self._enter_trade(pair, row, trade_dir) if trade: # TODO: hacky workaround to avoid opening > max_open_trades # This emulates previous behaviour - not sure if this is correct From faf5cfa66d7e6a3228ad638d83ec30b0022e8bdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Aug 2021 21:35:01 +0200 Subject: [PATCH 15/46] Update some tests for updated backtest interface --- freqtrade/strategy/interface.py | 9 +++++---- tests/optimize/__init__.py | 8 ++++++-- tests/optimize/test_backtest_detail.py | 13 +++++++------ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a1e820808..f721acafb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -807,8 +807,8 @@ class IStrategy(ABC, HyperStrategyMixin): return self.populate_buy_trend(dataframe) # type: ignore else: df = self.populate_buy_trend(dataframe, metadata) - # TODO-lev: IF both buy and enter_long exist, this will fail. - df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') + if 'enter_long' not in df.columns: + df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') return df @@ -829,8 +829,9 @@ class IStrategy(ABC, HyperStrategyMixin): return self.populate_sell_trend(dataframe) # type: ignore else: df = self.populate_sell_trend(dataframe, metadata) - # TODO-lev: IF both sell and exit_long exist, this will fail at a later point - return df.rename({'sell': 'exit_long'}, axis='columns') + if 'exit_long' not in df.columns: + df = df.rename({'sell': 'exit_long'}, axis='columns') + return df def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index f29d8d585..dffe3209f 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -44,8 +44,12 @@ def _get_frame_time_from_offset(offset): def _build_backtest_dataframe(data): - columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell'] - columns = columns + ['buy_tag'] if len(data[0]) == 9 else columns + columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'enter_long', 'exit_long', + 'enter_short', 'exit_short'] + if len(data[0]) == 8: + # No short columns + data = [d + [0, 0] for d in data] + columns = columns + ['long_tag'] if len(data[0]) == 11 else columns frame = DataFrame.from_records(data, columns=columns) frame['date'] = frame['date'].apply(_get_frame_time_from_offset) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e5c037f3e..e14f82c33 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -519,12 +519,12 @@ tc32 = BTContainer(data=[ # Test 33: trailing_stop should be triggered immediately on trade open candle. # stop-loss: 1%, ROI: 10% (should not apply) tc33 = BTContainer(data=[ - # D O H L C V B S BT - [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 'buy_signal_01'], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0, None], # enter trade (signal on last candle) and stop - [2, 4900, 5250, 4500, 5100, 6172, 0, 0, None], - [3, 5100, 5100, 4650, 4750, 6172, 0, 0, None], - [4, 4750, 4950, 4350, 4750, 6172, 0, 0, None]], + # D O H L C V EL XL ES Xs BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0, 'buy_signal_01'], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0, 0, 0, None], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0, None], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0, None], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0, None]], stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, trailing_stop_positive=0.01, use_custom_stoploss=True, @@ -571,6 +571,7 @@ TESTS = [ tc31, tc32, tc33, + # TODO-lev: Add tests for short here ] From 11bd8e912e7fa577ce760c7c0500e76c0312f940 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 06:45:09 +0200 Subject: [PATCH 16/46] Fix some tests --- freqtrade/optimize/backtesting.py | 10 +++---- freqtrade/strategy/interface.py | 3 +- tests/optimize/__init__.py | 6 ++-- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 40 ++++++++++++++++--------- tests/strategy/test_strategy_loading.py | 10 +++++-- 6 files changed, 44 insertions(+), 27 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ee784200f..ee8e3b050 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -233,12 +233,12 @@ class Backtesting: if not pair_data.empty: # Cleanup from prior runs - pair_data.loc[:, 'buy'] = 0 # TODO: Should be renamed to enter_long + pair_data.loc[:, 'enter_long'] = 0 pair_data.loc[:, 'enter_short'] = 0 - pair_data.loc[:, 'sell'] = 0 # TODO: should be renamed to exit_long + pair_data.loc[:, 'exit_long'] = 0 pair_data.loc[:, 'exit_short'] = 0 - pair_data.loc[:, 'long_tag'] = None # cleanup if buy_tag is exist - pair_data.loc[:, 'short_tag'] = None # cleanup if short_tag is exist + pair_data.loc[:, 'long_tag'] = None + pair_data.loc[:, 'short_tag'] = None df_analyzed = self.strategy.advise_sell( self.strategy.advise_buy(pair_data, {'pair': pair}), @@ -255,8 +255,6 @@ class Backtesting: df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1) df_analyzed.loc[:, 'long_tag'] = df_analyzed.loc[:, 'long_tag'].shift(1) - df_analyzed.drop(df_analyzed.head(1).index, inplace=True) - # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index c6cf7c0dc..63217df68 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -871,7 +871,7 @@ class IStrategy(ABC, HyperStrategyMixin): return df def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, + proposed_leverage: float, max_leverage: float, side: str, **kwargs) -> float: """ Customize leverage for each new trade. This method is not called when edge module is @@ -882,6 +882,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param proposed_leverage: A leverage proposed by the bot. :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A leverage amount, which is between 1.0 and max_leverage. """ return 1.0 diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index c40d11456..2ba9485fd 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -56,6 +56,8 @@ def _build_backtest_dataframe(data): # Ensure floats are in place for column in ['open', 'high', 'low', 'close', 'volume']: frame[column] = frame[column].astype('float64') - if 'buy_tag' not in columns: - frame['buy_tag'] = None + if 'long_tag' not in columns: + frame['long_tag'] = None + if 'short_tag' not in columns: + frame['short_tag'] = None return frame diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e14f82c33..9b99648b1 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -521,7 +521,7 @@ tc32 = BTContainer(data=[ tc33 = BTContainer(data=[ # D O H L C V EL XL ES Xs BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 0, 0, 'buy_signal_01'], - [1, 5000, 5500, 5000, 4900, 6172, 0, 0, 0, 0, None], # enter trade (signal on last candle) and stop + [1, 5000, 5500, 5000, 4900, 6172, 0, 0, 0, 0, None], # enter trade and stop [2, 4900, 5250, 4500, 5100, 6172, 0, 0, 0, 0, None], [3, 5100, 5100, 4650, 4750, 6172, 0, 0, 0, 0, None], [4, 4750, 4950, 4350, 4750, 6172, 0, 0, 0, 0, None]], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 998b2d837..11ca4b0ab 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -123,12 +123,14 @@ def _trend(signals, buy_value, sell_value): n = len(signals['low']) buy = np.zeros(n) sell = np.zeros(n) - for i in range(0, len(signals['buy'])): + for i in range(0, len(signals['enter_long'])): if random.random() > 0.5: # Both buy and sell signals at same timeframe buy[i] = buy_value sell[i] = sell_value - signals['buy'] = buy - signals['sell'] = sell + signals['enter_long'] = buy + signals['exit_long'] = sell + signals['enter_short'] = 0 + signals['exit_short'] = 0 return signals @@ -143,8 +145,10 @@ def _trend_alternate(dataframe=None, metadata=None): buy[i] = 1 else: sell[i] = 1 - signals['buy'] = buy - signals['sell'] = sell + signals['enter_long'] = buy + signals['exit_long'] = sell + signals['enter_short'] = 0 + signals['exit_short'] = 0 return dataframe @@ -499,41 +503,47 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: 0.0012, # High '', # Buy Signal Name ] - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert isinstance(trade, LocalTrade) assert trade.stake_amount == 495 # Fake 2 trades, so there's not enough amount for the next trade left. LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade) - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None LocalTrade.trades_open.pop() - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is not None backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5 - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade assert trade.stake_amount == 123.5 # In case of error - use proposed stake backtesting.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade assert trade.stake_amount == 495 + assert trade.is_short is False + + trade = backtesting._enter_trade(pair, row=row, direction='short') + assert trade + assert trade.stake_amount == 495 + assert trade.is_short is True # Stake-amount too high! mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=600.0) - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None # Stake-amount throwing error mocker.patch("freqtrade.wallets.Wallets.get_trade_stake_amount", side_effect=DependencyException) - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert trade is None backtesting.cleanup() @@ -766,8 +776,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) multi = 20 else: multi = 18 - dataframe['buy'] = np.where(dataframe.index % multi == 0, 1, 0) - dataframe['sell'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) + dataframe['enter_long'] = np.where(dataframe.index % multi == 0, 1, 0) + dataframe['exit_long'] = np.where((dataframe.index + multi - 2) % multi == 0, 1, 0) + dataframe['enter_short'] = 0 + dataframe['exit_short'] = 0 return dataframe mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index e76990ba9..7e94b7ccc 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -394,7 +394,8 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): caplog) -def test_strategy_interface_versioning(result, monkeypatch, default_conf): +def test_strategy_interface_versioning(result, default_conf): + # Tests interface compatibility with Interface version 2. default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} @@ -411,8 +412,11 @@ def test_strategy_interface_versioning(result, monkeypatch, default_conf): enterdf = strategy.advise_buy(result, metadata=metadata) assert isinstance(enterdf, DataFrame) - assert 'buy' in enterdf.columns + + assert 'buy' not in enterdf.columns + assert 'enter_long' in enterdf.columns exitdf = strategy.advise_sell(result, metadata=metadata) assert isinstance(exitdf, DataFrame) - assert 'sell' in exitdf + assert 'sell' not in exitdf + assert 'exit_long' in exitdf From eb71ee847c11be74c7907534b08d742e3a9eed56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 06:54:55 +0200 Subject: [PATCH 17/46] Rename backtest index constants --- freqtrade/optimize/backtesting.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ee8e3b050..100cf6548 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -41,10 +41,10 @@ OPEN_IDX = 1 HIGH_IDX = 2 LOW_IDX = 3 CLOSE_IDX = 4 -BUY_IDX = 5 -SELL_IDX = 6 +LONG_IDX = 5 +ELONG_IDX = 6 # Exit long SHORT_IDX = 7 -ESHORT_IDX = 8 +ESHORT_IDX = 8 # Exit short BUY_TAG_IDX = 9 SHORT_TAG_IDX = 10 @@ -335,8 +335,8 @@ class Backtesting: # TODO: short exits sell_candle_time = sell_row[DATE_IDX].to_pydatetime() sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore - sell_candle_time, sell_row[BUY_IDX], - sell_row[SELL_IDX], + sell_candle_time, buy=sell_row[LONG_IDX], + sell=sell_row[ELONG_IDX], low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: @@ -435,8 +435,8 @@ class Backtesting: return False def check_for_trade_entry(self, row) -> Optional[str]: - enter_long = row[BUY_IDX] == 1 - exit_long = row[SELL_IDX] == 1 + enter_long = row[LONG_IDX] == 1 + exit_long = row[ELONG_IDX] == 1 enter_short = row[SHORT_IDX] == 1 exit_short = row[ESHORT_IDX] == 1 From b40f985b1372feeba9470b90154a6e1b90d7b214 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 19:55:00 +0200 Subject: [PATCH 18/46] Add short-exit logic to backtesting --- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/optimize/backtesting.py | 11 ++++++----- freqtrade/strategy/interface.py | 25 ++++++++++++++++--------- tests/strategy/test_interface.py | 20 ++++++++++++++++---- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ce09e715e..c620e1a84 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -856,14 +856,14 @@ class FreqtradeBot(LoggingMixin): """ Check and execute sell """ - should_sell = self.strategy.should_sell( + should_exit: SellCheckTuple = self.strategy.should_exit( trade, sell_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) - if should_sell.sell_flag: - logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_sell(trade, sell_rate, should_sell) + if should_exit.sell_flag: + logger.info(f'Executing Sell for {trade.pair}. Reason: {should_exit.sell_type}') + self.execute_sell(trade, sell_rate, should_exit) return True return False diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 100cf6548..c3cd5b114 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -332,12 +332,13 @@ class Backtesting: return sell_row[OPEN_IDX] def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: - # TODO: short exits sell_candle_time = sell_row[DATE_IDX].to_pydatetime() - sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore - sell_candle_time, buy=sell_row[LONG_IDX], - sell=sell_row[ELONG_IDX], - low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) + sell = self.strategy.should_exit( + trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore + enter_long=sell_row[LONG_IDX], enter_short=sell_row[SHORT_IDX], + exit_long=sell_row[ELONG_IDX], exit_short=sell_row[ESHORT_IDX], + low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] + ) if sell.sell_flag: trade.close_date = sell_candle_time diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 63217df68..1aa9d3867 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -614,8 +614,10 @@ class IStrategy(ABC, HyperStrategyMixin): 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_exit(self, trade: Trade, rate: float, date: datetime, *, + enter_long: bool, enter_short: bool, + exit_long: bool, exit_short: 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/exit_short @@ -625,6 +627,10 @@ class IStrategy(ABC, HyperStrategyMixin): :param force_stoploss: Externally provided stoploss :return: True if trade should be exited, False otherwise """ + + enter = enter_short if trade.is_short else enter_long + exit_ = exit_short if trade.is_short else exit_long + current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) @@ -639,7 +645,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_profit = trade.calc_profit_ratio(current_rate) # if enter 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) + 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)) @@ -652,8 +658,8 @@ 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" @@ -712,10 +718,10 @@ 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) - 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 - ) + dir_correct = (trade.stop_loss < (low or current_rate) + if not trade.is_short else + trade.stop_loss > (high or current_rate) + ) if self.use_custom_stoploss and dir_correct: stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None @@ -735,6 +741,7 @@ class IStrategy(ABC, HyperStrategyMixin): sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. + # TODO-lev: Check this function - high / low usage must be inversed for short trades! high_profit = current_profit if not high else trade.calc_profit_ratio(high) # Don't update stoploss if trailing_only_offset_is_reached is true. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index af603e611..bfdf88dbb 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -452,27 +452,39 @@ def test_custom_sell(default_conf, fee, caplog) -> None: ) now = arrow.utcnow().datetime - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter_long=False, enter_short=False, + exit_long=False, exit_short=False, + low=None, high=None) assert res.sell_flag is False assert res.sell_type == SellType.NONE strategy.custom_sell = MagicMock(return_value=True) - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter_long=False, enter_short=False, + exit_long=False, exit_short=False, + low=None, high=None) assert res.sell_flag is True assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_reason == 'custom_sell' strategy.custom_sell = MagicMock(return_value='hello world') - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter_long=False, enter_short=False, + exit_long=False, exit_short=False, + low=None, high=None) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'hello world' caplog.clear() strategy.custom_sell = MagicMock(return_value='h' * 100) - res = strategy.should_sell(trade, 1, now, False, False, None, None, 0) + res = strategy.should_exit(trade, 1, now, + enter_long=False, enter_short=False, + exit_long=False, exit_short=False, + low=None, high=None) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True assert res.sell_reason == 'h' * 64 From 46285cd77e5c0e4f0edd45ca90230ed4b6c91dc0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 20:07:39 +0200 Subject: [PATCH 19/46] Fix some namings in freqtradebot --- freqtrade/freqtradebot.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c620e1a84..75f8d93ec 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -420,24 +420,24 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell, buy_tag) = self.strategy.get_signal( + (enter, exit_, enter_tag) = self.strategy.get_signal( pair, self.strategy.timeframe, analyzed_df ) - if buy and not sell: + if enter and not exit_: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): if self._check_depth_of_market_buy(pair, bid_check_dom): - return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) + return self.execute_buy(pair, stake_amount, enter_tag=enter_tag) else: return False - return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) + return self.execute_buy(pair, stake_amount, enter_tag=enter_tag) else: return False @@ -466,7 +466,7 @@ class FreqtradeBot(LoggingMixin): return False def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: + forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -575,7 +575,8 @@ class FreqtradeBot(LoggingMixin): exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), - buy_tag=buy_tag, + # TODO-lev: compatibility layer for buy_tag (!) + buy_tag=enter_tag, timeframe=timeframe_to_minutes(self.config['timeframe']) ) trade.orders.append(order_obj) From 9a03cb96f5386c3c0f17061756cffb6933750075 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 20:24:51 +0200 Subject: [PATCH 20/46] Update get_signal --- freqtrade/enums/__init__.py | 2 +- freqtrade/enums/signaltype.py | 5 ++ freqtrade/freqtradebot.py | 14 ++--- freqtrade/strategy/interface.py | 101 ++++++++++++++++++++++++-------- 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 692a7fcb6..e9d166258 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -4,6 +4,6 @@ from freqtrade.enums.collateral import Collateral from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType -from freqtrade.enums.signaltype import SignalTagType, SignalType +from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType from freqtrade.enums.state import State from freqtrade.enums.tradingmode import TradingMode diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index ca4b8482e..28f0676dd 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -17,3 +17,8 @@ class SignalTagType(Enum): """ BUY_TAG = "buy_tag" SHORT_TAG = "short_tag" + + +class SignalDirection(Enum): + LONG = 'long' + SHORT = 'short' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 75f8d93ec..9d4e6b26f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -420,19 +420,19 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (enter, exit_, enter_tag) = self.strategy.get_signal( - pair, - self.strategy.timeframe, - analyzed_df - ) + (side, enter_tag) = self.strategy.get_enter_signal( + pair, self.strategy.timeframe, analyzed_df + ) - if enter and not exit_: + if side: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): + # TODO-lev: Does the below need to be adjusted for shorts? if self._check_depth_of_market_buy(pair, bid_check_dom): + # TODO-lev: pass in "enter" as side. return self.execute_buy(pair, stake_amount, enter_tag=enter_tag) else: return False @@ -707,7 +707,7 @@ class FreqtradeBot(LoggingMixin): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) - (buy, sell, _) = self.strategy.get_signal( + (buy, sell) = self.strategy.get_exit_signal( trade.pair, self.strategy.timeframe, analyzed_df diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1aa9d3867..a8e6d7f76 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,7 +13,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType, SignalTagType, SignalType +from freqtrade.enums import SellType, SignalTagType, SignalType, SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date @@ -538,22 +538,18 @@ class IStrategy(ABC, HyperStrategyMixin): else: raise StrategyError(message) - def get_signal( + def get_latest_candle( self, pair: str, timeframe: str, dataframe: DataFrame, - is_short: bool = False - ) -> Tuple[bool, bool, Optional[str]]: + ) -> Tuple[Optional[DataFrame], arrow.Arrow]: """ - 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 + Get the latest candle. Used only during real mode :param pair: pair in format ANT/BTC :param timeframe: timeframe to use :param dataframe: Analyzed dataframe to get signal from. - :return: (Buy, Sell)/(Short, Exit_short) A bool-tuple indicating - (buy/sell)/(short/exit_short) signal + :return: (None, None) or (Dataframe, latest_date) - corresponding to the last candle """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') @@ -572,34 +568,89 @@ class IStrategy(ABC, HyperStrategyMixin): 'Outdated history for pair %s. Last tick is %s minutes old', pair, int((arrow.utcnow() - latest_date).total_seconds() // 60) ) + return None, None + return latest, latest_date + + def get_exit_signal( + self, + pair: str, + timeframe: str, + dataframe: DataFrame, + is_short: bool = None + ) -> Tuple[bool, bool]: + """ + Calculates current exit signal based based on the buy/short or sell/exit_short + columns of the dataframe. + Used by Bot to get the signal to exit. + depending on is_short, looks at "short" or "long" columns. + :param pair: pair in format ANT/BTC + :param timeframe: timeframe to use + :param dataframe: Analyzed dataframe to get signal from. + :param is_short: Indicating existing trade direction. + :return: (enter, exit) A bool-tuple with enter / exit values. + """ + latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe) + if latest is None: return False, False, None - (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 + if is_short: + enter = latest[SignalType.SHORT] == 1 + exit_ = latest[SignalType.EXIT_SHORT] == 1 + else: + enter = latest[SignalType.BUY] == 1 + exit_ = latest[SignalType.SELL] == 1 - enter = latest[enter_type.value] == 1 + logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) " + f"enter={enter} exit={exit_}") - exit = False - if exit_type.value in latest: - exit = latest[exit_type.value] == 1 + return enter, exit_ - enter_tag_value = latest.get(enter_tag.value, None) + def get_enter_signal( + self, + pair: str, + timeframe: str, + dataframe: DataFrame, + ) -> Tuple[Optional[SignalDirection], Optional[str]]: + """ + Calculates current entry 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: (SignalDirection, entry_tag) + """ + latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe) + if latest is None: + return False, False, None + + enter_long = latest[SignalType.BUY] == 1 + exit_long = latest[SignalType.SELL] == 1 + enter_short = latest[SignalType.SHORT] == 1 + exit_short = latest[SignalType.EXIT_SHORT] == 1 + + enter_signal: Optional[SignalDirection] = None + enter_tag_value = None + if enter_long == 1 and not any([exit_long, enter_short]): + enter_signal = SignalDirection.LONG + enter_tag_value = latest.get(SignalTagType.BUY_TAG, None) + if enter_short == 1 and not any([exit_short, enter_long]): + enter_signal = SignalDirection.SHORT + enter_tag_value = latest.get(SignalTagType.SHORT_TAG, 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, - enter=enter + enter=enter_signal ): - return False, exit, enter_tag_value - return enter, exit, enter_tag_value + return False, enter_tag_value + + logger.debug(f"entry trigger: {latest['date']} (pair={pair}) " + f"enter={enter_long} enter_tag_value={enter_tag_value}") + return enter_signal, enter_tag_value def ignore_expired_candle( self, From f9f32a15bb6d9122030a72af58a84eb66f7a1019 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 20:30:42 +0200 Subject: [PATCH 21/46] Update plotting tests for new strategy interface --- freqtrade/plot/plotting.py | 9 +++++---- tests/test_plotting.py | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 509c03e90..43b61cf67 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -386,8 +386,9 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(candles, 1, 1) - if 'buy' in data.columns: - df_buy = data[data['buy'] == 1] + # TODO-lev: Needs short equivalent + if 'enter_long' in data.columns: + df_buy = data[data['enter_long'] == 1] if len(df_buy) > 0: buys = go.Scatter( x=df_buy.date, @@ -405,8 +406,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra else: logger.warning("No buy-signals found.") - if 'sell' in data.columns: - df_sell = data[data['sell'] == 1] + if 'exit_long' in data.columns: + df_sell = data[data['exit_long'] == 1] if len(df_sell) > 0: sells = go.Scatter( x=df_sell.date, diff --git a/tests/test_plotting.py b/tests/test_plotting.py index ecadc3f8b..773fe8a5d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -203,8 +203,8 @@ def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, t timerange = TimeRange(None, 'line', 0, -1000) data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) - data['buy'] = 0 - data['sell'] = 0 + data['enter_long'] = 0 + data['exit_long'] = 0 indicators1 = [] indicators2 = [] @@ -264,12 +264,12 @@ def test_generate_candlestick_graph_no_trades(default_conf, mocker, testdatadir) buy = find_trace_in_fig_data(figure.data, "buy") assert isinstance(buy, go.Scatter) # All buy-signals should be plotted - assert int(data.buy.sum()) == len(buy.x) + assert int(data['enter_long'].sum()) == len(buy.x) sell = find_trace_in_fig_data(figure.data, "sell") assert isinstance(sell, go.Scatter) # All buy-signals should be plotted - assert int(data.sell.sum()) == len(sell.x) + assert int(data['exit_long'].sum()) == len(sell.x) assert find_trace_in_fig_data(figure.data, "Bollinger Band") From f3b6a0a7973699755f4a276932bc0de06a09563d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 20:40:35 +0200 Subject: [PATCH 22/46] Fix some type errors --- freqtrade/freqtradebot.py | 18 +++++++++--------- freqtrade/strategy/interface.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9d4e6b26f..0ddee5292 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -700,22 +700,22 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) - (buy, sell) = (False, False) + (enter, exit_) = (False, False) if (self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) - (buy, sell) = self.strategy.get_exit_signal( + (enter, exit_) = self.strategy.get_exit_signal( trade.pair, self.strategy.timeframe, analyzed_df ) - logger.debug('checking sell') + # TODO-lev: side should depend on trade side. sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + if self._check_and_execute_exit(trade, sell_rate, enter, exit_): return True logger.debug('Found no sell signal for %s.', trade) @@ -852,18 +852,18 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, trade: Trade, sell_rate: float, - buy: bool, sell: bool) -> bool: + def _check_and_execute_exit(self, trade: Trade, sell_rate: float, + enter: bool, exit_: bool) -> bool: """ - Check and execute sell + Check and execute trade exit """ should_exit: SellCheckTuple = self.strategy.should_exit( - trade, sell_rate, datetime.now(timezone.utc), buy, sell, + trade, sell_rate, datetime.now(timezone.utc), enter, exit_, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) if should_exit.sell_flag: - logger.info(f'Executing Sell for {trade.pair}. Reason: {should_exit.sell_type}') + logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}') self.execute_sell(trade, sell_rate, should_exit) return True return False diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a8e6d7f76..000e2b2dd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -543,7 +543,7 @@ class IStrategy(ABC, HyperStrategyMixin): pair: str, timeframe: str, dataframe: DataFrame, - ) -> Tuple[Optional[DataFrame], arrow.Arrow]: + ) -> Tuple[Optional[DataFrame], Optional[arrow.Arrow]]: """ Get the latest candle. Used only during real mode :param pair: pair in format ANT/BTC @@ -553,7 +553,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') - return False, False, None + return None, None latest_date = dataframe['date'].max() latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] @@ -591,7 +591,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe) if latest is None: - return False, False, None + return False, False if is_short: enter = latest[SignalType.SHORT] == 1 @@ -621,8 +621,8 @@ class IStrategy(ABC, HyperStrategyMixin): :return: (SignalDirection, entry_tag) """ latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe) - if latest is None: - return False, False, None + if latest is None or latest_date is None: + return None, None enter_long = latest[SignalType.BUY] == 1 exit_long = latest[SignalType.SELL] == 1 @@ -630,7 +630,7 @@ class IStrategy(ABC, HyperStrategyMixin): exit_short = latest[SignalType.EXIT_SHORT] == 1 enter_signal: Optional[SignalDirection] = None - enter_tag_value = None + enter_tag_value: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG enter_tag_value = latest.get(SignalTagType.BUY_TAG, None) @@ -641,12 +641,12 @@ class IStrategy(ABC, HyperStrategyMixin): timeframe_seconds = timeframe_to_seconds(timeframe) if self.ignore_expired_candle( - latest_date=latest_date, + latest_date=latest_date.datetime, current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, - enter=enter_signal + enter=bool(enter_signal) ): - return False, enter_tag_value + return None, enter_tag_value logger.debug(f"entry trigger: {latest['date']} (pair={pair}) " f"enter={enter_long} enter_tag_value={enter_tag_value}") From 6524edbb4e17472b6893d9a669cd31825fafa9d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 20:47:54 +0200 Subject: [PATCH 23/46] Simplify should_exit interface --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 5 +++-- freqtrade/strategy/interface.py | 6 +----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0ddee5292..7c43b599d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -858,7 +858,7 @@ class FreqtradeBot(LoggingMixin): Check and execute trade exit """ should_exit: SellCheckTuple = self.strategy.should_exit( - trade, sell_rate, datetime.now(timezone.utc), enter, exit_, + trade, sell_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c3cd5b114..3bd7f178c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -333,10 +333,11 @@ class Backtesting: def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: sell_candle_time = sell_row[DATE_IDX].to_pydatetime() + enter = sell_row[LONG_IDX] if trade.is_short else sell_row[SHORT_IDX] + exit_ = sell_row[ELONG_IDX] if trade.is_short else sell_row[ESHORT_IDX] sell = self.strategy.should_exit( trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore - enter_long=sell_row[LONG_IDX], enter_short=sell_row[SHORT_IDX], - exit_long=sell_row[ELONG_IDX], exit_short=sell_row[ESHORT_IDX], + enter=enter, exit_=exit_, low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 000e2b2dd..f9919877c 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -666,8 +666,7 @@ class IStrategy(ABC, HyperStrategyMixin): return False def should_exit(self, trade: Trade, rate: float, date: datetime, *, - enter_long: bool, enter_short: bool, - exit_long: bool, exit_short: bool, + enter: bool, exit_: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ @@ -679,9 +678,6 @@ class IStrategy(ABC, HyperStrategyMixin): :return: True if trade should be exited, False otherwise """ - enter = enter_short if trade.is_short else enter_long - exit_ = exit_short if trade.is_short else exit_long - current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) From b951f59f89e8f9e98b3f5338328af9972700a2db Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Aug 2021 21:03:13 +0200 Subject: [PATCH 24/46] Fix patch_get_signal --- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 2 +- tests/conftest.py | 28 ++++++++++++++++++++++++++-- tests/test_integration.py | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7c43b599d..e6be897f2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -710,7 +710,7 @@ class FreqtradeBot(LoggingMixin): (enter, exit_) = self.strategy.get_exit_signal( trade.pair, self.strategy.timeframe, - analyzed_df + analyzed_df, is_short=trade.is_short ) # TODO-lev: side should depend on trade side. diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f9919877c..04740b845 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,7 +13,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType, SignalTagType, SignalType, SignalDirection +from freqtrade.enums import SellType, SignalDirection, SignalTagType, SignalType from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date diff --git a/tests/conftest.py b/tests/conftest.py index 2b75956c4..03859d05c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import reduce from pathlib import Path +from typing import Optional from unittest.mock import MagicMock, Mock, PropertyMock import arrow @@ -18,6 +19,7 @@ from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.enums import RunMode +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db @@ -182,13 +184,35 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None: +def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, + enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None: """ :param mocker: mocker to patch IStrategy class :param value: which value IStrategy.get_signal() must return + (buy, sell, buy_tag) :return: None """ - freqtrade.strategy.get_signal = lambda e, s, x: value + # returns (Signal-direction, signaname) + def patched_get_enter_signal(*args, **kwargs): + direction = None + if enter_long and not any([exit_long, enter_short]): + direction = SignalDirection.LONG + if enter_short and not any([exit_short, enter_long]): + direction = SignalDirection.SHORT + + return direction, enter_tag + + freqtrade.strategy.get_enter_signal = patched_get_enter_signal + + def patched_get_exit_signal(pair, timeframe, dataframe, is_short): + if is_short: + return enter_short, exit_short + else: + return enter_long, exit_long + + # returns (enter, exit) + freqtrade.strategy.get_exit_signal = patched_get_exit_signal + freqtrade.exchange.refresh_latest_ohlcv = lambda p: None diff --git a/tests/test_integration.py b/tests/test_integration.py index b12959a03..0f0d6f067 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -72,7 +72,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, create_stoploss_order=MagicMock(return_value=True), _notify_sell=MagicMock(), ) - mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000)) @@ -163,7 +163,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc SellCheckTuple(sell_type=SellType.NONE), SellCheckTuple(sell_type=SellType.NONE)] ) - mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) freqtrade = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtrade) From 6b93c71d15da8ae9d76f9597856c7e4f1b74fe13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Aug 2021 06:43:58 +0200 Subject: [PATCH 25/46] Small refactorings, use only enter_long columns --- freqtrade/enums/signaltype.py | 6 ++-- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/interface.py | 22 +++++++------- tests/conftest.py | 4 +-- tests/strategy/test_interface.py | 39 ++++++++++++------------- tests/strategy/test_strategy_loading.py | 6 ++-- 6 files changed, 39 insertions(+), 40 deletions(-) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index 28f0676dd..23316c15a 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -5,9 +5,9 @@ class SignalType(Enum): """ Enum to distinguish between buy and sell signals """ - BUY = "buy" # To be renamed to enter_long - SELL = "sell" # To be renamed to exit_long - SHORT = "short" # Should be "enter_short" + ENTER_LONG = "enter_long" + EXIT_LONG = "exit_long" + ENTER_SHORT = "enter_short" EXIT_SHORT = "exit_short" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e6be897f2..ab5ae383a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -420,7 +420,7 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (side, enter_tag) = self.strategy.get_enter_signal( + (side, enter_tag) = self.strategy.get_entry_signal( pair, self.strategy.timeframe, analyzed_df ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 04740b845..7daec6b8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -594,18 +594,18 @@ class IStrategy(ABC, HyperStrategyMixin): return False, False if is_short: - enter = latest[SignalType.SHORT] == 1 - exit_ = latest[SignalType.EXIT_SHORT] == 1 + enter = latest.get(SignalType.ENTER_SHORT, 0) == 1 + exit_ = latest.get(SignalType.EXIT_SHORT, 0) == 1 else: - enter = latest[SignalType.BUY] == 1 - exit_ = latest[SignalType.SELL] == 1 + enter = latest[SignalType.ENTER_LONG] == 1 + exit_ = latest.get(SignalType.EXIT_LONG, 0) == 1 logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) " f"enter={enter} exit={exit_}") return enter, exit_ - def get_enter_signal( + def get_entry_signal( self, pair: str, timeframe: str, @@ -624,19 +624,19 @@ class IStrategy(ABC, HyperStrategyMixin): if latest is None or latest_date is None: return None, None - enter_long = latest[SignalType.BUY] == 1 - exit_long = latest[SignalType.SELL] == 1 - enter_short = latest[SignalType.SHORT] == 1 - exit_short = latest[SignalType.EXIT_SHORT] == 1 + enter_long = latest[SignalType.ENTER_LONG.value] == 1 + exit_long = latest.get(SignalType.EXIT_LONG.value, 0) == 1 + enter_short = latest.get(SignalType.ENTER_SHORT.value, 0) == 1 + exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1 enter_signal: Optional[SignalDirection] = None enter_tag_value: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG - enter_tag_value = latest.get(SignalTagType.BUY_TAG, None) + enter_tag_value = latest.get(SignalTagType.BUY_TAG.value, None) if enter_short == 1 and not any([exit_short, enter_long]): enter_signal = SignalDirection.SHORT - enter_tag_value = latest.get(SignalTagType.SHORT_TAG, None) + enter_tag_value = latest.get(SignalTagType.SHORT_TAG.value, None) timeframe_seconds = timeframe_to_seconds(timeframe) diff --git a/tests/conftest.py b/tests/conftest.py index 03859d05c..c146fd9ce 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -193,7 +193,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, :return: None """ # returns (Signal-direction, signaname) - def patched_get_enter_signal(*args, **kwargs): + def patched_get_entry_signal(*args, **kwargs): direction = None if enter_long and not any([exit_long, enter_short]): direction = SignalDirection.LONG @@ -202,7 +202,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, return direction, enter_tag - freqtrade.strategy.get_enter_signal = patched_get_enter_signal + freqtrade.strategy.get_entry_signal = patched_get_entry_signal def patched_get_exit_signal(pair, timeframe, dataframe, is_short): if is_short: diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index bfdf88dbb..831a06991 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 +from freqtrade.enums.signaltype import SignalDirection import logging from datetime import datetime, timedelta, timezone from pathlib import Path @@ -30,7 +31,7 @@ _STRATEGY = DefaultStrategy(config={}) _STRATEGY.dp = DataProvider({}, None, None) -def test_returns_latest_signal(mocker, default_conf, ohlcv_history): +def test_returns_latest_signal(default_conf, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow() # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() @@ -67,18 +68,18 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): assert log_has('Empty dataframe for pair ETH/BTC', caplog) -def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False, None) == _STRATEGY.get_signal( +def test_get_signal_empty(default_conf, caplog): + assert (None, None) == _STRATEGY.get_latest_candle( 'foo', default_conf['timeframe'], DataFrame() ) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) + assert (None, None) == _STRATEGY.get_latest_candle('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - assert (False, False, None) == _STRATEGY.get_signal( + assert (None, None) == _STRATEGY.get_latest_candle( 'baz', default_conf['timeframe'], DataFrame([]) @@ -86,7 +87,7 @@ def test_get_signal_empty(default_conf, mocker, caplog): assert log_has('Empty candle (OHLCV) data for pair baz', caplog) -def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): +def test_get_signal_exception_valueerror(mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) mocker.patch.object( @@ -111,14 +112,14 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow().shift(minutes=-16) # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() - mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'buy'] = 1 + mocked_history['exit_long'] = 0 + mocked_history['enter_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False, None) == _STRATEGY.get_signal( + assert (None, None) == _STRATEGY.get_latest_candle( 'xyz', default_conf['timeframe'], mocked_history @@ -134,13 +135,13 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): mocked_history = ohlcv_history.copy() # Intentionally don't set sell column # mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'buy'] = 1 + mocked_history['enter_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (True, False, None) == _STRATEGY.get_signal( + assert (SignalDirection.LONG, None) == _STRATEGY.get_entry_signal( 'xyz', default_conf['timeframe'], mocked_history @@ -453,8 +454,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None: now = arrow.utcnow().datetime res = strategy.should_exit(trade, 1, now, - enter_long=False, enter_short=False, - exit_long=False, exit_short=False, + enter=False, exit_=False, low=None, high=None) assert res.sell_flag is False @@ -462,8 +462,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None: strategy.custom_sell = MagicMock(return_value=True) res = strategy.should_exit(trade, 1, now, - enter_long=False, enter_short=False, - exit_long=False, exit_short=False, + enter=False, exit_=False, low=None, high=None) assert res.sell_flag is True assert res.sell_type == SellType.CUSTOM_SELL @@ -472,8 +471,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None: strategy.custom_sell = MagicMock(return_value='hello world') res = strategy.should_exit(trade, 1, now, - enter_long=False, enter_short=False, - exit_long=False, exit_short=False, + enter=False, exit_=False, low=None, high=None) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True @@ -482,8 +480,7 @@ def test_custom_sell(default_conf, fee, caplog) -> None: caplog.clear() strategy.custom_sell = MagicMock(return_value='h' * 100) res = strategy.should_exit(trade, 1, now, - enter_long=False, enter_short=False, - exit_long=False, exit_short=False, + enter=False, exit_=False, low=None, high=None) assert res.sell_type == SellType.CUSTOM_SELL assert res.sell_flag is True diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 7e94b7ccc..7a15f8c0c 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -118,10 +118,12 @@ def test_strategy(result, default_conf): assert 'adx' in df_indicators dataframe = strategy.advise_buy(df_indicators, metadata=metadata) - assert 'buy' in dataframe.columns + assert 'buy' not in dataframe.columns + assert 'enter_long' in dataframe.columns dataframe = strategy.advise_sell(df_indicators, metadata=metadata) - assert 'sell' in dataframe.columns + assert 'sell' not in dataframe.columns + assert 'exit_long' in dataframe.columns def test_strategy_override_minimal_roi(caplog, default_conf): From cb4889398be8e3f2e9c3cd4afa80900313412faf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Aug 2021 07:03:48 +0200 Subject: [PATCH 26/46] Fix backtesting bug --- freqtrade/optimize/backtesting.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3bd7f178c..0ebb36b7c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -233,9 +233,12 @@ class Backtesting: if not pair_data.empty: # Cleanup from prior runs - pair_data.loc[:, 'enter_long'] = 0 + # TODO-lev: The below is not 100% compatible with the interface compatibility layer + if 'enter_long' in pair_data.columns: + pair_data.loc[:, 'enter_long'] = 0 pair_data.loc[:, 'enter_short'] = 0 - pair_data.loc[:, 'exit_long'] = 0 + if 'exit_long' in pair_data.columns: + pair_data.loc[:, 'exit_long'] = 0 pair_data.loc[:, 'exit_short'] = 0 pair_data.loc[:, 'long_tag'] = None pair_data.loc[:, 'short_tag'] = None From b61735937c70c94465a716409add7b463433d5d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Aug 2021 20:56:16 +0200 Subject: [PATCH 27/46] Replace Patch_get_signal with proper calls --- tests/test_freqtradebot.py | 47 +++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cbaf7c22c..7fa02d706 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -254,7 +254,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf # stoploss shoud be hit assert freqtrade.handle_trade(trade) is True - assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) + assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog) assert trade.sell_reason == SellType.STOP_LOSS.value @@ -536,7 +536,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: ) default_conf['stake_amount'] = 10 freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, enter_long=False) Trade.query = MagicMock() Trade.query.filter = MagicMock() @@ -757,9 +757,10 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: refresh_latest_ohlcv=refresh_mock, ) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) - mocker.patch( - 'freqtrade.strategy.interface.IStrategy.get_signal', - return_value=(False, False, '') + mocker.patch.multiple( + 'freqtrade.strategy.interface.IStrategy', + get_exit_signal=MagicMock(return_value=(False, False)), + get_entry_signal=MagicMock(return_value=(None, None)) ) mocker.patch('time.sleep', return_value=None) @@ -1915,7 +1916,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi assert trade.is_open is True freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.open_order_id == limit_sell_order['id'] @@ -1943,7 +1944,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, ) freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() @@ -1962,7 +1963,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Buy and Sell are not triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, enter_long=False) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1970,7 +1971,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1978,7 +1979,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) trades = Trade.query.all() assert freqtrade.handle_trade(trades[0]) is True @@ -2012,7 +2013,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) @@ -2041,10 +2042,10 @@ def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open trade = Trade.query.first() trade.is_open = True - patch_get_signal(freqtrade, value=(False, False, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=False) assert not freqtrade.handle_trade(trade) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -3154,7 +3155,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is False freqtrade.strategy.sell_profit_offset = 0.0 @@ -3192,7 +3193,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_bu trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -3226,7 +3227,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is False @@ -3261,7 +3262,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_ trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -3293,7 +3294,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ trade = Trade.query.first() amnt = trade.amount trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) assert freqtrade.handle_trade(trade) is True @@ -3415,11 +3416,11 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trade) is False # Test if buy-signal is absent (should sell due to roi = true) - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value @@ -3693,11 +3694,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b trade = Trade.query.first() trade.update(limit_buy_order) # Sell due to min_roi_reached - patch_get_signal(freqtrade, value=(True, True, None)) + patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trade) is True # Test if buy-signal is absent - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -4238,7 +4239,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o freqtrade.wallets.update() assert trade.is_open is True - patch_get_signal(freqtrade, value=(False, True, None)) + patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] From 2e50948699fb5c241e5711e3b2f7ed739036a5ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Sep 2021 20:23:51 +0200 Subject: [PATCH 28/46] Fix some tests --- freqtrade/freqtradebot.py | 4 +-- freqtrade/strategy/interface.py | 8 ++--- tests/optimize/test_backtesting.py | 46 ++++++++++++++---------- tests/strategy/test_interface.py | 56 ++++++++++++++++++++++-------- 4 files changed, 75 insertions(+), 39 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f9bb8e77d..8ba1dcecc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -434,11 +434,11 @@ class FreqtradeBot(LoggingMixin): if self._check_depth_of_market_buy(pair, bid_check_dom): # TODO-lev: pass in "enter" as side. - return self.execute_entry(pair, stake_amount, buy_tag=enter_tag) + return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) else: return False - return self.execute_entry(pair, stake_amount, buy_tag=enter_tag) + return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) else: return False diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5fc975ef7..e89811bd0 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -596,11 +596,11 @@ class IStrategy(ABC, HyperStrategyMixin): return False, False if is_short: - enter = latest.get(SignalType.ENTER_SHORT, 0) == 1 - exit_ = latest.get(SignalType.EXIT_SHORT, 0) == 1 + enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1 + exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1 else: - enter = latest[SignalType.ENTER_LONG] == 1 - exit_ = latest.get(SignalType.EXIT_LONG, 0) == 1 + enter = latest[SignalType.ENTER_LONG.value] == 1 + exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1 logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) " f"enter={enter} exit={exit_}") diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index bdb491441..3e3b16371 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -570,47 +570,54 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: pair = 'UNITTEST/BTC' row = [ pd.Timestamp(year=2020, month=1, day=1, hour=4, minute=55, tzinfo=timezone.utc), - 1, # Buy 200, # Open - 201, # Close - 0, # Sell - 195, # Low 201.5, # High - '', # Buy Signal Name + 195, # Low + 201, # Close + 1, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_hsort + '', # Long Signal Name + '', # Short Signal Name ] - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert isinstance(trade, LocalTrade) row_sell = [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), - 0, # Buy 200, # Open - 201, # Close - 0, # Sell - 195, # Low 210.5, # High - '', # Buy Signal Name + 195, # Low + 201, # Close + 0, # enter_long + 0, # exit_long + 0, # enter_short + 0, # exit_short + '', # long Signal Name + '', # Short Signal Name ] row_detail = pd.DataFrame( [ [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=0, tzinfo=timezone.utc), - 1, 200, 199, 0, 197, 200.1, '', + 200, 200.1, 197, 199, 1, 0, 0, 0, '', '', ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=1, tzinfo=timezone.utc), - 0, 199, 199.5, 0, 199, 199.7, '', + 199, 199.7, 199, 199.5, 0, 0, 0, 0, '', '' ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=2, tzinfo=timezone.utc), - 0, 199.5, 200.5, 0, 199, 200.8, '', + 199.5, 200.8, 199, 200.9, 0, 0, 0, 0, '', '' ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=3, tzinfo=timezone.utc), - 0, 200.5, 210.5, 0, 193, 210.5, '', # ROI sell (?) + 200.5, 210.5, 193, 210.5, 0, 0, 0, 0, '', '' # ROI sell (?) ], [ pd.Timestamp(year=2020, month=1, day=1, hour=5, minute=4, tzinfo=timezone.utc), - 0, 200, 199, 0, 193, 200.1, '', + 200, 200.1, 193, 199, 0, 0, 0, 0, '', '' ], - ], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"] + ], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short', 'long_tag', 'short_tag'] ) # No data available. @@ -620,11 +627,12 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: assert res.close_date_utc == datetime(2020, 1, 1, 5, 0, tzinfo=timezone.utc) # Enter new trade - trade = backtesting._enter_trade(pair, row=row) + trade = backtesting._enter_trade(pair, row=row, direction='long') assert isinstance(trade, LocalTrade) # Assign empty ... no result. backtesting.detail_data[pair] = pd.DataFrame( - [], columns=["date", "buy", "open", "close", "sell", "low", "high", "buy_tag"]) + [], columns=['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', + 'enter_short', 'exit_short', 'long_tag', 'short_tag']) res = backtesting._get_sell_trade_entry(trade, row) assert res is None diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 39f0b8009..6f2adad33 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -31,28 +31,56 @@ _STRATEGY = StrategyTestV2(config={}) _STRATEGY.dp = DataProvider({}, None, None) -def test_returns_latest_signal(default_conf, ohlcv_history): +def test_returns_latest_signal(ohlcv_history): ohlcv_history.loc[1, 'date'] = arrow.utcnow() # Take a copy to correctly modify the call mocked_history = ohlcv_history.copy() - mocked_history['sell'] = 0 - mocked_history['buy'] = 0 - mocked_history.loc[1, 'sell'] = 1 + mocked_history['enter_long'] = 0 + mocked_history['exit_long'] = 0 + mocked_history['enter_short'] = 0 + mocked_history['exit_short'] = 0 + mocked_history.loc[1, 'exit_long'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 1 + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, True) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 0 + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history + ) == (SignalDirection.LONG, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 0 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) - mocked_history.loc[1, 'sell'] = 0 - mocked_history.loc[1, 'buy'] = 1 + assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history) == (None, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 1 mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01') + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) + + mocked_history.loc[1, 'exit_long'] = 0 + mocked_history.loc[1, 'enter_long'] = 0 + mocked_history.loc[1, 'enter_short'] = 1 + mocked_history.loc[1, 'exit_short'] = 0 + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (True, False) + + mocked_history.loc[1, 'enter_short'] = 0 + mocked_history.loc[1, 'exit_short'] = 1 + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (None, None) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, True) def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): From 49350f2a8ee6c2c3293325929fd0ffdece01bf15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Sep 2021 08:36:22 +0200 Subject: [PATCH 29/46] Fix backtesting test --- tests/optimize/test_backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 3e3b16371..d2ccef9db 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -123,7 +123,7 @@ def _trend(signals, buy_value, sell_value): n = len(signals['low']) buy = np.zeros(n) sell = np.zeros(n) - for i in range(0, len(signals['enter_long'])): + for i in range(0, len(signals['date'])): if random.random() > 0.5: # Both buy and sell signals at same timeframe buy[i] = buy_value sell[i] = sell_value From 68b75af08e654e226c0993124875b85f3ca98336 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Sep 2021 08:59:18 +0200 Subject: [PATCH 30/46] Fix bug with inversed sell signals in backtesting --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ad6bdbf18..cf670f87d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -365,8 +365,8 @@ class Backtesting: def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: sell_candle_time = sell_row[DATE_IDX].to_pydatetime() - enter = sell_row[LONG_IDX] if trade.is_short else sell_row[SHORT_IDX] - exit_ = sell_row[ELONG_IDX] if trade.is_short else sell_row[ESHORT_IDX] + enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX] + exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX] sell = self.strategy.should_exit( trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore enter=enter, exit_=exit_, From b752516f65604e6e06bed0f2282c85777dfbc3cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Sep 2021 15:23:27 +0200 Subject: [PATCH 31/46] Edge should use new columns, too --- freqtrade/edge/edge_positioning.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 8fe87d674..b945dd1bd 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -159,7 +159,8 @@ class Edge: logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days)..') - headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] + # TODO-lev: Should edge support shorts? needs to be investigated further... + headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long'] trades: list = [] for pair, pair_data in preprocessed.items(): @@ -387,8 +388,8 @@ class Edge: return final def _find_trades_for_stoploss_range(self, df, pair, stoploss_range): - buy_column = df['buy'].values - sell_column = df['sell'].values + buy_column = df['enter_long'].values + sell_column = df['exit_long'].values date_column = df['date'].values ohlc_columns = df[['open', 'high', 'low', 'close']].values From a89c67787bf16af1a828540644a9e5b71322530b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 09:23:53 +0200 Subject: [PATCH 32/46] Replace some more occurances of 'buy' --- freqtrade/strategy/interface.py | 12 ++++++------ freqtrade/templates/base_strategy.py.j2 | 4 ++-- freqtrade/templates/sample_strategy.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d3f3a1110..ce193426b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -461,12 +461,12 @@ class IStrategy(ABC, HyperStrategyMixin): self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") - dataframe['buy'] = 0 - dataframe['sell'] = 0 - dataframe['enter_short'] = 0 - dataframe['exit_short'] = 0 - dataframe['buy_tag'] = None - dataframe['short_tag'] = None + dataframe[SignalType.ENTER_LONG.value] = 0 + dataframe[SignalType.EXIT_LONG.value] = 0 + dataframe[SignalType.ENTER_SHORT.value] = 0 + dataframe[SignalType.EXIT_SHORT.value] = 0 + dataframe[SignalTagType.BUY_TAG.value] = None + dataframe[SignalTagType.SHORT_TAG.value] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 06d7cbc5c..3feff75c6 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -122,7 +122,7 @@ class {{ strategy }}(IStrategy): {{ buy_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'buy'] = 1 + 'enter_long'] = 1 return dataframe @@ -138,6 +138,6 @@ class {{ strategy }}(IStrategy): {{ sell_trend | indent(16) }} (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'sell'] = 1 + 'exit_long'] = 1 return dataframe {{ additional_methods | indent(4) }} diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 574819949..80fa7cdae 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -352,7 +352,7 @@ class SampleStrategy(IStrategy): (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'buy'] = 1 + 'enter_long'] = 1 return dataframe @@ -371,5 +371,5 @@ class SampleStrategy(IStrategy): (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['volume'] > 0) # Make sure Volume is not 0 ), - 'sell'] = 1 + 'exit_long'] = 1 return dataframe From 778f0d9d0a832940ebc58e9c80213e87230e12ee Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 17:44:12 -0600 Subject: [PATCH 33/46] Merged feat/short into lev-strat --- docs/includes/pairlists.md | 14 + docs/leverage.md | 4 + docs/strategy-advanced.md | 6 + docs/strategy-customization.md | 161 +++ freqtrade/commands/hyperopt_commands.py | 2 +- freqtrade/edge/edge_positioning.py | 2 +- freqtrade/exchange/bibox.py | 5 +- freqtrade/exchange/binance.py | 145 +- .../exchange/binance_leverage_brackets.json | 1214 +++++++++++++++++ freqtrade/exchange/exchange.py | 172 ++- freqtrade/exchange/ftx.py | 50 +- freqtrade/exchange/kraken.py | 87 +- freqtrade/freqtradebot.py | 27 +- freqtrade/leverage/interest.py | 7 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/edge_cli.py | 2 + freqtrade/optimize/hyperopt_tools.py | 8 +- freqtrade/persistence/models.py | 10 +- .../plugins/pairlist/PerformanceFilter.py | 11 +- freqtrade/rpc/api_server/api_schemas.py | 6 + freqtrade/rpc/rpc.py | 21 +- freqtrade/rpc/telegram.py | 22 +- freqtrade/strategy/__init__.py | 4 +- freqtrade/strategy/informative_decorator.py | 128 ++ freqtrade/strategy/interface.py | 46 +- freqtrade/strategy/strategy_helper.py | 45 +- setup.sh | 2 +- tests/conftest.py | 53 +- tests/exchange/test_binance.py | 286 +++- tests/exchange/test_exchange.py | 229 +++- tests/exchange/test_ftx.py | 116 +- tests/exchange/test_kraken.py | 108 +- .../{test_leverage.py => test_interest.py} | 7 +- tests/plugins/test_pairlist.py | 28 +- tests/rpc/test_rpc_apiserver.py | 15 +- tests/rpc/test_rpc_telegram.py | 2 + .../strats/informative_decorator_strategy.py | 75 + tests/strategy/test_interface.py | 2 +- tests/strategy/test_strategy_helpers.py | 66 +- tests/strategy/test_strategy_loading.py | 6 +- tests/test_freqtradebot.py | 591 +++----- 41 files changed, 3173 insertions(+), 614 deletions(-) create mode 100644 freqtrade/exchange/binance_leverage_brackets.json create mode 100644 freqtrade/strategy/informative_decorator.py rename tests/leverage/{test_leverage.py => test_interest.py} (83%) create mode 100644 tests/strategy/strats/informative_decorator_strategy.py diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 69e12d5dc..b612a4ddf 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist: ```json "pairlists": [ + // ... { "method": "OffsetFilter", "offset": 10 @@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window). +Not defining this parameter (or setting it to 0) will use all-time performance. + +```json +"pairlists": [ + // ... + { + "method": "PerformanceFilter", + "minutes": 1440 // rolling 24h + } +], +``` + !!! Note `PerformanceFilter` does not support backtesting mode. diff --git a/docs/leverage.md b/docs/leverage.md index c4b975a0b..9448c64c3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) I (interest) = Opening fee + Rollover fee [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) + +# TODO-lev: Mention that says you can't run 2 bots on the same account with leverage, + +#TODO-lev: Create a huge risk disclaimer \ No newline at end of file diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4409af6ea..2b9517f3b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. +### Calculating stoploss percentage from absolute price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cfea60d22..725252b30 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -639,6 +639,167 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +!!! Note + Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. + This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade + is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in + `confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when + `current_profit < open_relative_stop`. + +### *stoploss_from_absolute()* + +In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price. + +??? Example "Returning a stoploss using absolute price from the custom stoploss function" + + If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open + + class AwesomeStrategy(IStrategy): + + use_custom_stoploss = True + + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) + return dataframe + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + candle = dataframe.iloc[-1].squeeze() + return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate) + + ``` + +### *@informative()* + +``` python +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ +``` + +In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, +not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. +When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) +for more information. + +??? Example "Fast and easy way to define informative pairs" + + Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, informative + + class AwesomeStrategy(IStrategy): + + # This method is not required. + # def informative_pairs(self): ... + + # Define informative upper timeframe for each pair. Decorators can be stacked on same + # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as + # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable + # instead of hardcoding actual stake currency. Available in populate_indicators and other + # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/ETH informative pair. You must specify quote currency if it is different from + # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting + # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom + # formatting. Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{column}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + return dataframe + + ``` + +!!! Note + Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs + manually as described [in the DataProvider section](#complete-data-provider-sample). + +!!! Note + Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. + + ``` python + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + stake = self.config['stake_currency'] + dataframe.loc[ + ( + (dataframe[f'btc_{stake}_rsi_1h'] < 35) + & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + ``` + + Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. + +!!! Warning "Duplicate method names" + Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) + will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators + created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! ## Additional data (Wallets) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index d2d30f399..ec1ff92cf 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and export_csv: HyperoptTools.export_csv_file( - config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv + config, epochs, export_csv ) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index b945dd1bd..bee96c746 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -119,7 +119,7 @@ class Edge: ) # Download informative pairs too res = defaultdict(list) - for p, t in self.strategy.informative_pairs(): + for p, t in self.strategy.gather_informative_pairs(): res[t].append(p) for timeframe, inf_pairs in res.items(): timerange_startup = deepcopy(self._timerange) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -20,4 +20,7 @@ class Bibox(Exchange): # fetchCurrencies API point requires authentication for Bibox, # so switch it off for Freqtrade load_markets() - _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8dced3894..35f427c34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,10 +1,13 @@ """ Binance exchange subclass """ +import json import logging -from typing import Dict, List +from pathlib import Path +from typing import Dict, List, Optional, Tuple import arrow import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -26,36 +29,74 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + ] + + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + if self.trading_mode == TradingMode.MARGIN: + return { + "options": { + "defaultType": "margin" + } + } + elif self.trading_mode == TradingMode.FUTURES: + return { + "options": { + "defaultType": "future" + } + } + else: + return {} + + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. + :param side: "buy" or "sell" """ - return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + + return order['type'] == 'stop_loss_limit' and ( + (side == "sell" and stop_loss > float(order['info']['stopPrice'])) or + (side == "buy" and stop_loss < float(order['info']['stopPrice'])) + ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. It may work with a limited number of other exchanges, but this has not been tested yet. + :param side: "buy" or "sell" """ # Limit price threshold: As limit price should always be below stop-price limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - rate = stop_price * limit_price_pct + if side == "sell": + # TODO: Name limit_rate in other exchange subclasses + rate = stop_price * limit_price_pct + else: + rate = stop_price * (2 - limit_price_pct) ordertype = "stop_loss_limit" stop_price = self.price_to_precision(pair, stop_price) + bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate) + # Ensure rate is less than stop price - if stop_price <= rate: + if bad_stop_price: raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') + 'In stoploss limit order, stop price should be better than limit price') if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -66,7 +107,8 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + self._lev_prep(pair, leverage) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) @@ -74,21 +116,96 @@ class Binance(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: # Errors: # `binance Order would trigger immediately.` raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + if self.trading_mode == TradingMode.FUTURES: + try: + if self._config['dry_run']: + leverage_brackets_path = ( + Path(__file__).parent / 'binance_leverage_brackets.json' + ) + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + else: + leverage_brackets = self._api.load_leverage_brackets() + + for pair, brackets in leverage_brackets.items(): + self._leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch leverage amounts due to' + f'{e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + pair_brackets = self._leverage_brackets[pair] + max_lev = 1.0 + for [min_amount, margin_req] in pair_brackets: + if nominal_value >= min_amount: + max_lev = 1/margin_req + return max_lev + + @ retrier + def _set_leverage( + self, + leverage: float, + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None + ): + """ + Set's the leverage before making a trade, in order to not + have the same leverage on every trade + """ + trading_mode = trading_mode or self.trading_mode + + if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: + return + + try: + self._api.set_leverage(symbol=pair, leverage=leverage) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/exchange/binance_leverage_brackets.json b/freqtrade/exchange/binance_leverage_brackets.json new file mode 100644 index 000000000..4450b015e --- /dev/null +++ b/freqtrade/exchange/binance_leverage_brackets.json @@ -0,0 +1,1214 @@ +{ + "1000SHIB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "1INCH/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AAVE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "ADA/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "ADA/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "AKRO/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALGO/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "ALICE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALPHA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ANKR/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATOM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "AUDIO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AVAX/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "AXS/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "BAKE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAND/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BCH/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BEL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BLZ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BNB/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "BNB/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTC/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "BTC/USDT": [ + [0.0, "0.004"], + [50000.0, "0.005"], + [250000.0, "0.01"], + [1000000.0, "0.025"], + [5000000.0, "0.05"], + [20000000.0, "0.1"], + [50000000.0, "0.125"], + [100000000.0, "0.15"], + [200000000.0, "0.25"], + [300000000.0, "0.5"] + ], + "BTCBUSD_210129": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCBUSD_210226": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCDOM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCSTUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "BTS/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "C98/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CELR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHZ/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COMP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COTI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CRV/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CTK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CVC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DASH/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DEFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DENT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DGB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DODO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DOGE/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "DOGE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "DOT/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "DOTECOUSDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DYDX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EGLD/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ENJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EOS/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETH/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "ETH/USDT": [ + [0.0, "0.005"], + [10000.0, "0.0065"], + [100000.0, "0.01"], + [500000.0, "0.02"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "ETHUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "FIL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "FLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "FTM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "FTT/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "GRT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "GTC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HBAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HNT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HOT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOST/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KAVA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KEEP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KNC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KSM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LENDUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINK/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LIT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LRC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LTC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LUNA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "MANA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MASK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MATIC/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "MKR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MTL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NKN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OCEAN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OGN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OMG/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "QTUM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RAY/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REEF/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RLC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RSR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RUNE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RVN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SAND/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SFP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SKL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SNX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SOL/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "SOL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.25"], + [10000000.0, "0.5"] + ], + "SRM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STMX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STORJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SUSHI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SXP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "THETA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "TLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TOMO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRX/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "UNFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "UNI/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "VET/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "WAVES/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XEM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XLM/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XMR/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XRP/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "XRP/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XTZ/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "YFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "YFII/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZIL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ] +} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2b9b08d70..4617fd4c2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,6 +22,7 @@ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, ListPairsWithTimeframes) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -48,9 +49,6 @@ class Exchange: _config: Dict = {} - # Parameters to add directly to ccxt sync/async initialization. - _ccxt_config: Dict = {} - # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -74,6 +72,10 @@ class Exchange: } _ft_has: Dict = {} + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + ] + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -83,6 +85,7 @@ class Exchange: self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None self._markets: Dict = {} + self._leverage_brackets: Dict = {} self._config.update(config) @@ -125,14 +128,25 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] + self.trading_mode: TradingMode = ( + TradingMode(config.get('trading_mode')) + if config.get('trading_mode') + else TradingMode.SPOT + ) + self.collateral: Optional[Collateral] = ( + Collateral(config.get('collateral')) + if config.get('collateral') + else None + ) + # Initialize ccxt objects - ccxt_config = self._ccxt_config.copy() + ccxt_config = self._ccxt_config ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) - ccxt_async_config = self._ccxt_config.copy() + ccxt_async_config = self._ccxt_config ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_async_config) ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), @@ -140,6 +154,9 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + if self.trading_mode != TradingMode.SPOT: + self.fill_leverage_brackets() + logger.info('Using Exchange "%s"', self.name) if validate: @@ -157,7 +174,7 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - + self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 @@ -190,6 +207,7 @@ class Exchange: 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), + # 'options': exchange_config.get('options', {}) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -210,6 +228,11 @@ class Exchange: return api + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {} + @property def name(self) -> str: """exchange Name (from ccxt)""" @@ -355,6 +378,7 @@ class Exchange: # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) self._last_markets_refresh = arrow.utcnow().int_timestamp + self.fill_leverage_brackets() except ccxt.BaseError: logger.exception("Could not reload markets.") @@ -370,7 +394,7 @@ class Exchange: raise OperationalException( 'Could not load markets, therefore cannot start. ' 'Please investigate the above error for more details.' - ) + ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( @@ -482,6 +506,25 @@ class Exchange: f"This strategy requires {startup_candles} candles to start. " f"{self.name} only provides {candle_limit} for {timeframe}.") + def validate_trading_mode_and_collateral( + self, + trading_mode: TradingMode, + collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT + ): + """ + Checks if freqtrade can perform trades using the configured + trading mode(Margin, Futures) and Collateral(Cross, Isolated) + Throws OperationalException: + If the trading_mode/collateral type are not supported by freqtrade on this exchange + """ + if trading_mode != TradingMode.SPOT and ( + (trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs + ): + collateral_value = collateral and collateral.value + raise OperationalException( + f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}" + ) + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. @@ -541,8 +584,8 @@ class Exchange: else: return 1 / pow(10, precision) - def get_min_pair_stake_amount(self, pair: str, price: float, - stoploss: float) -> Optional[float]: + def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, + leverage: Optional[float] = 1.0) -> Optional[float]: try: market = self.markets[pair] except KeyError: @@ -576,12 +619,24 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return max(min_stake_amounts) * amount_reserve_percent + return self._get_stake_amount_considering_leverage( + max(min_stake_amounts) * amount_reserve_percent, + leverage or 1.0 + ) + + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): + """ + Takes the minimum stake amount for a pair with no leverage and returns the minimum + stake amount when leverage is considered + :param stake_amount: The stake amount for a pair before leverage is considered + :param leverage: The amount of leverage being used on the current trade + """ + return stake_amount / leverage # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -598,7 +653,8 @@ class Exchange: 'timestamp': arrow.utcnow().int_timestamp * 1000, 'status': "closed" if ordertype == "market" else "open", 'fee': None, - 'info': {} + 'info': {}, + 'leverage': leverage } if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: dry_order["info"] = {"stopPrice": dry_order["price"]} @@ -608,7 +664,7 @@ class Exchange: average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ 'average': average, - 'cost': dry_order['amount'] * average, + 'cost': (dry_order['amount'] * average) / leverage }) dry_order = self.add_dry_order_fee(pair, dry_order) @@ -716,17 +772,26 @@ class Exchange: # Order handling - def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc') -> Dict: - - if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) - return dry_order + def _lev_prep(self, pair: str, leverage: float): + if self.trading_mode != TradingMode.SPOT: + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair) + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') params.update({param: time_in_force}) + return params + + def create_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: + # TODO-lev: remove default for leverage + if self._config['dry_run']: + dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) + return dry_order + + params = self._get_params(ordertype, leverage, time_in_force) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -735,6 +800,7 @@ class Exchange: or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None + self._lev_prep(pair, leverage) order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) @@ -758,14 +824,15 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ raise OperationalException(f"stoploss is not implemented for {self.name}.") - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss order. The precise ordertype is determined by the order_types dict or exchange default. @@ -1528,6 +1595,69 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) + def fill_leverage_brackets(self): + """ + # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + return + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + return 1.0 + + @retrier + def _set_leverage( + self, + leverage: float, + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None + ): + """ + Set's the leverage before making a trade, in order to not + have the same leverage on every trade + """ + # TODO-lev: Make a documentation page that says you can't run 2 bots + # TODO-lev: on the same account with leverage + if self._config['dry_run'] or not self.exchange_has("setLeverage"): + # Some exchanges only support one collateral type + return + + try: + self._api.set_leverage(symbol=pair, leverage=leverage) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): + ''' + Set's the margin mode on the exchange to cross or isolated for a specific pair + :param symbol: base/quote currency pair (e.g. "ADA/USDT") + ''' + if self._config['dry_run'] or not self.exchange_has("setMarginMode"): + # Some exchanges only support one collateral type + return + + try: + self._api.set_margin_mode(pair, collateral.value, params) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6cd549d60..62adea04c 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,9 +1,10 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -21,6 +22,12 @@ class Ftx(Exchange): "ohlcv_candle_limit": 1500, } + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. @@ -31,15 +38,19 @@ class Ftx(Exchange): return (parent_check and market.get('spot', False) is True) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return order['type'] == 'stop' and stop_loss > float(order['price']) + return order['type'] == 'stop' and ( + side == "sell" and stop_loss > float(order['price']) or + side == "buy" and stop_loss < float(order['price']) + ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. @@ -47,7 +58,10 @@ class Ftx(Exchange): Limit orders are defined by having orderPrice set, otherwise a market order is used. """ limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - limit_rate = stop_price * limit_price_pct + if side == "sell": + limit_rate = stop_price * limit_price_pct + else: + limit_rate = stop_price * (2 - limit_price_pct) ordertype = "stop" @@ -55,7 +69,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -67,7 +81,8 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + self._lev_prep(pair, leverage) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' @@ -75,19 +90,19 @@ class Ftx(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -152,3 +167,18 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def fill_leverage_brackets(self): + """ + FTX leverage is static across the account, and doesn't change from pair to pair, + so _leverage_brackets doesn't need to be set + """ + return + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx + :param pair: Here for super method, not used on FTX + :nominal_value: Here for super method, not used on FTX + """ + return 20.0 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 1b069aa6c..19d0a4967 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,9 +1,10 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -23,6 +24,12 @@ class Kraken(Exchange): "trades_pagination_arg": "since", } + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: No CCXT support + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. @@ -67,16 +74,19 @@ class Kraken(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return (order['type'] in ('stop-loss', 'stop-loss-limit') - and stop_loss > float(order['price'])) + return (order['type'] in ('stop-loss', 'stop-loss-limit') and ( + (side == "sell" and stop_loss > float(order['price'])) or + (side == "buy" and stop_loss < float(order['price'])) + )) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. @@ -86,7 +96,10 @@ class Kraken(Exchange): if order_types.get('stoploss', 'market') == 'limit': ordertype = "stop-loss-limit" limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - limit_rate = stop_price * limit_price_pct + if side == "sell": + limit_rate = stop_price * limit_price_pct + else: + limit_rate = stop_price * (2 - limit_price_pct) params['price2'] = self.price_to_precision(pair, limit_rate) else: ordertype = "stop-loss" @@ -95,13 +108,13 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=stop_price, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' @@ -109,18 +122,70 @@ class Kraken(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + leverages = {} + + for pair, market in self.markets.items(): + leverages[pair] = [1] + info = market['info'] + leverage_buy = info.get('leverage_buy', []) + leverage_sell = info.get('leverage_sell', []) + if len(leverage_buy) > 0 or len(leverage_sell) > 0: + if leverage_buy != leverage_sell: + logger.warning( + f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal" + "for {pair}. Please notify freqtrade because this has never happened before" + ) + if max(leverage_buy) <= max(leverage_sell): + leverages[pair] += [int(lev) for lev in leverage_buy] + else: + leverages[pair] += [int(lev) for lev in leverage_sell] + else: + leverages[pair] += [int(lev) for lev in leverage_buy] + self._leverage_brackets = leverages + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: Here for super class, not needed on Kraken + """ + return float(max(self._leverage_brackets[pair])) + + def _set_leverage( + self, + leverage: float, + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None + ): + """ + Kraken set's the leverage as an option in the order object, so we need to + add it to params + """ + return + + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: + params = super()._get_params(ordertype, leverage, time_in_force) + if leverage > 1.0: + params['leverage'] = leverage + return params diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 17135eecb..43a7571f7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -85,10 +85,10 @@ class FreqtradeBot(LoggingMixin): self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - # Attach Dataprovider to Strategy baseclass - IStrategy.dp = self.dataprovider - # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + # Attach Dataprovider to strategy instance + self.strategy.dp = self.dataprovider + # Attach Wallets to strategy instance + self.strategy.wallets = self.wallets # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ @@ -162,7 +162,7 @@ class FreqtradeBot(LoggingMixin): # Refreshing candles self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.informative_pairs()) + self.strategy.gather_informative_pairs()) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() @@ -735,9 +735,14 @@ class FreqtradeBot(LoggingMixin): :return: True if the order succeeded, and False in case of problems. """ try: - stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types) + stoploss_order = self.exchange.stoploss( + pair=trade.pair, + amount=trade.amount, + stop_price=stop_price, + order_types=self.strategy.order_types, + side=trade.exit_side, + leverage=trade.leverage + ) order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') trade.orders.append(order_obj) @@ -829,11 +834,11 @@ class FreqtradeBot(LoggingMixin): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) + self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) return False - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange @@ -841,7 +846,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - if self.exchange.stoploss_adjust(trade.stop_loss, order): + if self.exchange.stoploss_adjust(trade.stop_loss, order, side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..2878ad784 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3c0fbd086..b43222fb3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -157,7 +157,7 @@ class Backtesting: self.strategy: IStrategy = strategy strategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + strategy.wallets = self.wallets # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 417faa685..f211da750 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -8,6 +8,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency +from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -33,6 +34,7 @@ class EdgeCli: self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) + self.strategy.dp = DataProvider(config, None) validate_config_consistency(self.config) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..cfbc2757e 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np +import pandas as pd import rapidjson import tabulate from colorama import Fore, Style @@ -298,8 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str: - + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' if 'results_metrics.winsdrawslosses' not in trials.columns: @@ -435,8 +436,7 @@ class HyperoptTools(): return table @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: + def export_csv_file(config: dict, results: list, csv_file: str) -> None: """ Log result to csv-file """ diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 84e402ce5..fe97c4a70 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -1026,17 +1026,21 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: + def get_overall_performance(minutes=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ + filters = [Trade.is_open.is_(False)] + if minutes: + start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) + filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ + ).filter(*filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 46a289ae6..301ee57ab 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Dict, List +from typing import Any, Dict, List import pandas as pd @@ -15,6 +15,13 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._minutes = pairlistconfig.get('minutes', 0) + @property def needstickers(self) -> bool: """ @@ -40,7 +47,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance(self._minutes)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3adbebc16..46187f571 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -46,6 +46,12 @@ class Balances(BaseModel): value: float stake: str note: str + starting_capital: float + starting_capital_ratio: float + starting_capital_pct: float + starting_capital_fiat: float + starting_capital_fiat_ratio: float + starting_capital_fiat_pct: float class Count(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7facacf97..b50f90de8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -459,6 +459,9 @@ class RPC: raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) + starting_capital = self._freqtrade.wallets.get_starting_balance() + starting_cap_fiat = self._fiat_converter.convert_amount( + starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: @@ -494,15 +497,25 @@ class RPC: else: raise RPCException('All balances are zero.') - symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, stake_currency, - symbol) if self._fiat_converter else 0 + value = self._fiat_converter.convert_amount( + total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + + starting_capital_ratio = 0.0 + starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + return { 'currencies': output, 'total': total, - 'symbol': symbol, + 'symbol': fiat_display_currency, 'value': value, 'stake': stake_currency, + 'starting_capital': starting_capital, + 'starting_capital_ratio': starting_capital_ratio, + 'starting_capital_pct': round(starting_capital_ratio * 100, 2), + 'starting_capital_fiat': starting_cap_fiat, + 'starting_capital_fiat_ratio': starting_cap_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a988d2b60..19c58b63d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -603,12 +603,15 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: - output += ( - f"*Warning:* Simulated balances in Dry Mode.\n" - "This mode is still experimental!\n" - "Starting capital: " - f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" - ) + output += "*Warning:* Simulated balances in Dry Mode.\n" + + output += ("Starting capital: " + f"`{result['starting_capital']}` {self._config['stake_currency']}" + ) + output += (f" `{result['starting_capital_fiat']}` " + f"{self._config['fiat_display_currency']}.\n" + ) if result['starting_capital_fiat'] > 0 else '.\n' + total_dust_balance = 0 total_dust_currencies = 0 for curr in result['currencies']: @@ -641,9 +644,12 @@ class Telegram(RPCHandler): f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n") output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['stake']}: " + f"{round_coin_value(result['total'], result['stake'], False)}`" + f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`\n") + f"{round_coin_value(result['value'], result['symbol'], False)}`" + f" `({result['starting_capital_fiat_pct']}%)`\n") self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index be655fc33..2ea0ad2b4 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) +from freqtrade.strategy.informative_decorator import informative from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open +from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute, + stoploss_from_open) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py new file mode 100644 index 000000000..4c5f21108 --- /dev/null +++ b/freqtrade/strategy/informative_decorator.py @@ -0,0 +1,128 @@ +from typing import Any, Callable, NamedTuple, Optional, Union + +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException +from freqtrade.strategy.strategy_helper import merge_informative_pair + + +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[Any], str], None] + ffill: bool + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[Any], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: PopulateIndicators): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + config = strategy.config + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, '' + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if inf_data.asset: + fmt = '{base}_{quote}_' + fmt # Informatives of other pairs + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=inf_data.ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index ce193426b..34cf9f749 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin +from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: Optional[DataProvider] = None + dp: Optional[DataProvider] wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + # Gather informative pairs from @informative-decorated methods. + self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = [] + for attr_name in dir(self.__class__): + cls_method = getattr(self.__class__, attr_name) + if not callable(cls_method): + continue + informative_data_list = getattr(cls_method, '_ft_informative', None) + if not isinstance(informative_data_list, list): + # Type check is required because mocker would return a mock object that evaluates to + # True, confusing this code. + continue + strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) + for informative_data in informative_data_list: + if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: + raise OperationalException('Informative timeframe must be equal or higher than ' + 'strategy timeframe!') + self._ft_informative.append((informative_data, cls_method)) + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -379,6 +400,23 @@ class IStrategy(ABC, HyperStrategyMixin): # END - Intended to be overridden by strategy ### + def gather_informative_pairs(self) -> ListPairsWithTimeframes: + """ + Internal method which gathers all informative pairs (user or automatically defined). + """ + informative_pairs = self.informative_pairs() + for inf_data, _ in self._ft_informative: + if inf_data.asset: + pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) + informative_pairs.append(pair_tf) + else: + if not self.dp: + raise OperationalException('@informative decorator with unspecified asset ' + 'requires DataProvider instance.') + for pair in self.dp.current_whitelist(): + informative_pairs.append((pair, inf_data.timeframe)) + return list(set(informative_pairs)) + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -878,6 +916,12 @@ class IStrategy(ABC, HyperStrategyMixin): :return: a Dataframe with all mandatory indicators for the strategies """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") + + # call populate_indicators_Nm() which were tagged with @informative decorator. + for inf_data, populate_fn in self._ft_informative: + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, inf_data, populate_fn) + if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 9c4d2bf2d..126a9c6c5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -5,7 +5,9 @@ from freqtrade.exchange import timeframe_to_minutes def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True, + append_timeframe: bool = True, + date_column: str = 'date') -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -25,6 +27,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required + :param append_timeframe: Rename columns by appending timeframe. + :param date_column: A custom date column name. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -33,25 +37,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, minutes = timeframe_to_minutes(timeframe) if minutes == minutes_inf: # No need to forwardshift if the timeframes are identical - informative['date_merge'] = informative["date"] + informative['date_merge'] = informative[date_column] elif minutes < minutes_inf: # Subtract "small" timeframe so merging is not delayed by 1 small candle # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 informative['date_merge'] = ( - informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') + informative[date_column] + pd.to_timedelta(minutes_inf, 'm') - + pd.to_timedelta(minutes, 'm') ) else: raise ValueError("Tried to merge a faster timeframe to a slower timeframe." "This would create new rows, and can throw off your regular indicators.") # Rename columns to be unique - informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + date_merge = 'date_merge' + if append_timeframe: + date_merge = f'date_merge_{timeframe_inf}' + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=f'date_merge_{timeframe_inf}', how='left') - dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + right_on=date_merge, how='left') + dataframe = dataframe.drop(date_merge, axis=1) if ffill: dataframe = dataframe.ffill() @@ -97,3 +105,28 @@ def stoploss_from_open( return min(stoploss, 0.0) else: return max(stoploss, 0.0) + + +def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: + """ + Given current price and desired stop price, return a stop loss value that is relative to current + price. + + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + + :param stop_rate: Stop loss price. + :param current_rate: Current asset price. + :return: Positive stop loss value relative to current price + """ + + # formula is undefined for current_rate 0, return maximum value + if current_rate == 0: + return 1 + + stoploss = 1 - (stop_rate / current_rate) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/setup.sh b/setup.sh index 217500569..aee7c80b5 100755 --- a/setup.sh +++ b/setup.sh @@ -62,7 +62,7 @@ function updateenv() { then REQUIREMENTS_PLOT="-r requirements-plot.txt" fi - if [ "${SYS_ARCH}" == "armv7l" ]; then + if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then echo "Detected Raspberry, installing cython, skipping hyperopt installation." ${PYTHON} -m pip install --upgrade cython else diff --git a/tests/conftest.py b/tests/conftest.py index ad949f9e1..d54e3a9a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import RunMode +from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.enums.signaltype import SignalDirection from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot @@ -82,7 +82,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: +def patch_exchange( + mocker, + api_mock=None, + id='binance', + mock_markets=True, + mock_supported_modes=True +) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -91,10 +97,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + if mock_supported_modes: + mocker.patch( + f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', + PropertyMock(return_value=[ + (TradingMode.MARGIN, Collateral.CROSS), + (TradingMode.MARGIN, Collateral.ISOLATED), + (TradingMode.FUTURES, Collateral.CROSS), + (TradingMode.FUTURES, Collateral.ISOLATED) + ]) + ) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -102,8 +120,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='binance', - mock_markets=True) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets) + mock_markets=True, mock_supported_modes=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) @@ -465,7 +483,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2'], + 'leverage_sell': ['2'], + }, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -491,7 +512,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2', '3', '4', '5'], + 'leverage_sell': ['2', '3', '4', '5'], + }, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -516,7 +540,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2', '3'], + 'leverage_sell': ['2', '3'], + }, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -541,7 +568,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': [], + 'leverage_sell': [], + }, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -619,7 +649,10 @@ def get_markets(): 'max': None } }, - 'info': {}, + 'info': { + 'leverage_buy': [], + 'leverage_sell': [], + }, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -735,6 +768,8 @@ def get_markets(): 'max': None } }, + 'info': { + } }, } diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index dd85c3abe..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -1,21 +1,31 @@ from datetime import datetime, timezone from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import ccxt import pytest +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers -@pytest.mark.parametrize('limitratio,expected', [ - (None, 220 * 0.99), - (0.99, 220 * 0.99), - (0.98, 220 * 0.98), +@pytest.mark.parametrize('limitratio,expected,side', [ + (None, 220 * 0.99, "sell"), + (0.99, 220 * 0.99, "sell"), + (0.98, 220 * 0.98, "sell"), + (None, 220 * 1.01, "buy"), + (0.99, 220 * 1.01, "buy"), + (0.98, 220 * 1.02, "buy"), ]) -def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): +def test_stoploss_order_binance( + default_conf, + mocker, + limitratio, + expected, + side +): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'stop_loss_limit' @@ -33,19 +43,32 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types=order_types, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == order_type - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 # Price should be 1% below stopprice assert api_mock.create_order.call_args_list[0][1]['price'] == expected @@ -55,17 +78,31 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -78,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side="sell", + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -94,18 +144,202 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_binance(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='binance') order = { 'type': 'stop_loss_limit', 'price': 1500, 'info': {'stopPrice': 1500}, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case order['type'] = 'stop_loss' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(sl3, order, side=side) + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("BNB/BUSD", 0.0, 40.0), + ("BNB/USDT", 100.0, 153.84615384615384), + ("BTC/USDT", 170.30, 250.0), + ("BNB/BUSD", 999999.9, 10.0), + ("BNB/USDT", 5000000.0, 6.666666666666667), + ("BTC/USDT", 300000000.1, 2.0), +]) +def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._leverage_brackets = { + 'BNB/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BNB/USDT': [[0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_binance(default_conf, mocker): + api_mock = MagicMock() + api_mock.load_leverage_brackets = MagicMock(return_value={ + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], + + }) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + assert exchange._leverage_brackets == { + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], + } + + api_mock = MagicMock() + api_mock.load_leverage_brackets = MagicMock() + type(api_mock).has = PropertyMock(return_value={'loadLeverageBrackets': True}) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "fill_leverage_brackets", + "load_leverage_brackets" + ) + + +def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + leverage_brackets = { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ] + } + + for key, value in leverage_brackets.items(): + assert exchange._leverage_brackets[key] == value + + +def test__set_leverage_binance(mocker, default_conf): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "_set_leverage", + "set_leverage", + pair="XRP/USDT", + leverage=5.0, + trading_mode=TradingMode.FUTURES + ) @pytest.mark.asyncio @@ -138,3 +372,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + + +@pytest.mark.parametrize("trading_mode,collateral,config", [ + ("", "", {}), + ("margin", "cross", {"options": {"defaultType": "margin"}}), + ("futures", "isolated", {"options": {"defaultType": "future"}}), +]) +def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral + exchange = get_patched_exchange(mocker, default_conf, id="binance") + assert exchange._ccxt_config == config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 97bc33429..8b16a9f12 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,6 +11,7 @@ import ccxt import pytest from pandas import DataFrame +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken @@ -131,6 +132,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert ex._api.headers == {'hello': 'world'} + assert ex._ccxt_config == {} Exchange._headers = {} @@ -395,7 +397,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) + expected_result = 2 * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) + assert isclose(result, expected_result/3) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -407,7 +413,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) + expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) + assert isclose(result, expected_result/5) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -419,7 +429,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) + expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) + assert isclose(result, expected_result/10) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -431,14 +445,26 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) + expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) + assert isclose(result, expected_result/7.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * 1.5) + expected_result = max(8, 2 * 2) * 1.5 + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) + assert isclose(result, expected_result/8.0) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) - assert isclose(result, max(8, 2 * 2) * 1.5) + expected_result = max(8, 2 * 2) * 1.5 + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) + assert isclose(result, expected_result/12) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -456,10 +482,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round( - max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), - 8 - ) + expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) + assert round(result, 8) == round(expected_result, 8) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) + assert round(result, 8) == round(expected_result/3, 8) def test_set_sandbox(default_conf, mocker): @@ -970,7 +996,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_dry_run_order( - pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype='limit', + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -993,7 +1025,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + pair='LTC/USDT', + ordertype='limit', + side=side, + amount=1, + rate=startprice, + leverage=1.0 + ) assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1039,7 +1077,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) + pair='LTC/USDT', + ordertype='market', + side=side, + amount=amount, + rate=rate, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -1049,10 +1093,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert round(order["average"], 4) == round(endprice, 4) -@pytest.mark.parametrize("side", [ - ("buy"), - ("sell") -]) +@pytest.mark.parametrize("side", ["buy", "sell"]) @pytest.mark.parametrize("ordertype,rate,marketprice", [ ("market", None, None), ("market", 200, True), @@ -1074,9 +1115,17 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -1086,6 +1135,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][2] == side assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert exchange._set_leverage.call_count == 0 + assert exchange.set_margin_mode.call_count == 0 + + exchange.trading_mode = TradingMode.FUTURES + order = exchange.create_order( + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) + + assert exchange._set_leverage.call_count == 1 + assert exchange.set_margin_mode.call_count == 1 def test_buy_dry_run(default_conf, mocker): @@ -2624,10 +2688,17 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss_adjust(1, {}) + exchange.stoploss_adjust(1, {}, side="sell") def test_merge_ft_has_dict(default_conf, mocker): @@ -2972,7 +3043,123 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 5, 5), (4, 5, 2), (5, 5, 1), - ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected + + +@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) +@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ + (9.0, 3.0, 3.0), + (20.0, 5.0, 4.0), + (100.0, 100.0, 1.0) +]) +def test_get_stake_amount_considering_leverage( + exchange, + stake_amount, + leverage, + min_stake_with_lev, + mocker, + default_conf +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange) + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev + + +@pytest.mark.parametrize("exchange_name,trading_mode", [ + ("binance", TradingMode.FUTURES), + ("ftx", TradingMode.MARGIN), + ("ftx", TradingMode.FUTURES) +]) +def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "_set_leverage", + "set_leverage", + pair="XRP/USDT", + leverage=5.0, + trading_mode=trading_mode + ) + + +@pytest.mark.parametrize("collateral", [ + (Collateral.CROSS), + (Collateral.ISOLATED) +]) +def test_set_margin_mode(mocker, default_conf, collateral): + + api_mock = MagicMock() + api_mock.set_margin_mode = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) + default_conf['dry_run'] = False + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "set_margin_mode", + "set_margin_mode", + pair="XRP/USDT", + collateral=collateral + ) + + +@pytest.mark.parametrize("exchange_name, trading_mode, collateral, exception_thrown", [ + ("binance", TradingMode.SPOT, None, False), + ("binance", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.SPOT, None, False), + ("kraken", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("ftx", TradingMode.SPOT, None, False), + ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("bittrex", TradingMode.SPOT, None, False), + ("bittrex", TradingMode.MARGIN, Collateral.CROSS, True), + ("bittrex", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), + ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), + + # TODO-lev: Remove once implemented + ("binance", TradingMode.MARGIN, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), + ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), + ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), + ("ftx", TradingMode.FUTURES, Collateral.CROSS, True), + + # TODO-lev: Uncomment once implemented + # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), + # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), + # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), + # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False) +]) +def test_validate_trading_mode_and_collateral( + default_conf, + mocker, + exchange_name, + trading_mode, + collateral, + exception_thrown +): + exchange = get_patched_exchange( + mocker, default_conf, id=exchange_name, mock_supported_modes=False) + if (exception_thrown): + with pytest.raises(OperationalException): + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) + else: + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..ca6b24d64 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -14,7 +14,11 @@ from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' -def test_stoploss_order_ftx(default_conf, mocker): +@pytest.mark.parametrize('order_price,exchangelimitratio,side', [ + (217.8, 1.05, "sell"), + (222.2, 0.95, "buy"), +]) +def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitratio, side): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -32,12 +36,18 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}, + leverage=1.0 + ) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] @@ -47,51 +57,79 @@ def test_stoploss_order_ftx(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={'stoploss': 'limit'}, side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] - assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 + assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == order_price assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) -def test_stoploss_order_dry_run_ftx(default_conf, mocker): +@pytest.mark.parametrize('side', [("sell"), ("buy")]) +def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) @@ -101,7 +139,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -112,20 +157,24 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_ftx(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='ftx') order = { 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(sl3, order, side=side) -def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): +def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_order): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 @@ -158,6 +207,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): assert resp['type'] == 'stop' assert resp['status_stop'] == 'triggered' + api_mock.fetch_order = MagicMock(return_value=limit_buy_order) + + resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') + assert resp + assert api_mock.fetch_order.call_count == 1 + assert resp['id_stop'] == 'mocked_limit_buy' + assert resp['id'] == 'X' + assert resp['type'] == 'stop' + assert resp['status_stop'] == 'triggered' + with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') @@ -191,3 +250,20 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 20.0), + ("BTC/EUR", 100.0, 20.0), + ("ZEC/USD", 173.31, 20.0), +]) +def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_ftx(default_conf, mocker): + # FTX only has one account wide leverage, so there's no leverage brackets + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + exchange.fill_leverage_brackets() + assert exchange._leverage_brackets == {} diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index eb79dfc10..a8cd8d8ef 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -166,7 +166,11 @@ def test_get_balances_prod(default_conf, mocker): @pytest.mark.parametrize('ordertype', ['market', 'limit']) -def test_stoploss_order_kraken(default_conf, mocker, ordertype): +@pytest.mark.parametrize('side,adjustedprice', [ + ("sell", 217.8), + ("buy", 222.2), +]) +def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -183,10 +187,17 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': ordertype, - 'stoploss_on_exchange_limit_ratio': 0.99 - }) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + side=side, + order_types={ + 'stoploss': ordertype, + 'stoploss_on_exchange_limit_ratio': 0.99 + }, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -195,12 +206,14 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): if ordertype == 'limit': assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { - 'trading_agreement': 'agree', 'price2': 217.8} + 'trading_agreement': 'agree', + 'price2': adjustedprice + } else: assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { 'trading_agreement': 'agree'} - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 @@ -208,20 +221,36 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) -def test_stoploss_order_dry_run_kraken(default_conf, mocker): +@pytest.mark.parametrize('side', ['buy', 'sell']) +def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) @@ -231,7 +260,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -242,14 +278,54 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_kraken(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='kraken') order = { 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(sl3, order, side=side) + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 3.0), + ("BTC/EUR", 100.0, 5.0), + ("ZEC/USD", 173.31, 2.0), +]) +def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="kraken") + exchange._leverage_brackets = { + 'ADA/BTC': ['2', '3'], + 'BTC/EUR': ['2', '3', '4', '5'], + 'ZEC/USD': ['2'] + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_kraken(default_conf, mocker): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + exchange.fill_leverage_brackets() + + assert exchange._leverage_brackets == { + 'BLK/BTC': [1, 2, 3], + 'TKN/BTC': [1, 2, 3, 4, 5], + 'ETH/BTC': [1, 2], + 'LTC/BTC': [1], + 'XRP/BTC': [1], + 'NEO/BTC': [1], + 'BTT/BTC': [1], + 'ETH/USDT': [1], + 'LTC/USDT': [1], + 'LTC/USD': [1], + 'XLTCUSDT': [1], + 'LTC/ETH': [1] + } diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_interest.py similarity index 83% rename from tests/leverage/test_leverage.py rename to tests/leverage/test_interest.py index 7b7ca0f9b..c7e787bdb 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_interest.py @@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0) ('kraken', 0.00025, five_hours, 0.045), ('kraken', 0.00025, twentyfive_hours, 0.12), # FTX - # TODO-lev: - implement FTX tests - # ('ftx', Decimal(0.0005), ten_mins, 0.06), - # ('ftx', Decimal(0.0005), five_hours, 0.045), + ('ftx', 0.0005, ten_mins, 0.00125), + ('ftx', 0.00025, ten_mins, 0.000625), + ('ftx', 0.00025, five_hours, 0.003125), + ('ftx', 0.00025, twentyfive_hours, 0.015625), ]) def test_interest(exchange, interest_rate, hours, expected): borrowed = Decimal(60.0) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 34770c03d..1ce8d172c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -12,7 +12,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re +from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, + log_has, log_has_re) @pytest.fixture(scope="function") @@ -663,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: + whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + {"method": "PerformanceFilter", "minutes": 60} + ] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + exchange = get_patched_exchange(mocker, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf) + pm.refresh_pairlist() + + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + create_mock_trades(fee) + pm.refresh_pairlist() + assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] + + # Move to "outside" of lookback window, so original sorting is restored. + t.move_to("2021-09-01 07:00:00 +00:00") + pm.refresh_pairlist() + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2852486ed..7c98b2df7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -422,20 +422,22 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 -def test_api_balance(botclient, mocker, rpc_balance): +def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot, client = botclient ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) - assert "currencies" in rc.json() - assert len(rc.json()["currencies"]) == 5 - assert rc.json()['currencies'][0] == { + response = rc.json() + assert "currencies" in response + assert len(response["currencies"]) == 5 + assert response['currencies'][0] == { 'currency': 'BTC', 'free': 12.0, 'balance': 12.0, @@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance): 'est_stake': 12.0, 'stake': 'BTC', } + assert 'starting_capital' in response + assert 'starting_capital_fiat' in response + assert 'starting_capital_pct' in response + assert 'starting_capital_ratio' in response def test_api_count(botclient, mocker, ticker, fee, markets): @@ -1218,6 +1224,7 @@ def test_api_strategies(botclient): assert_response(rc) assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'InformativeDecoratorTest', 'StrategyTestV2', 'TestStrategyLegacyV1' ]} diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2013dad7d..21f1cd000 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'total': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'starting_capital': 1000, + 'starting_capital_fiat': 1000, }) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py new file mode 100644 index 000000000..a32ad79e8 --- /dev/null +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -0,0 +1,75 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from pandas import DataFrame + +from freqtrade.strategy import informative, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +class InformativeDecoratorTest(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + stoploss = -0.10 + timeframe = '5m' + startup_candle_count: int = 20 + + def informative_pairs(self): + return [('BTC/USDT', '5m')] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['buy'] = 0 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['sell'] = 0 + return dataframe + + # Decorator stacking test. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Simple informative test. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Quote currency different from stake currency test. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Formatting test. + @informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Custom formatter test + @informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable') + def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = 14 + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + + # Mixing manual informative pairs with decorators. + informative = self.dp.get_pair_dataframe('BTC/USDT', '5m') + informative['rsi'] = 14 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True) + + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a9cb7b6ed..61ad5b734 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -648,7 +648,7 @@ def test_is_informative_pairs_callback(default_conf): strategy = StrategyResolver.load_strategy(default_conf) # Should return empty # Uses fallback to base implementation - assert [] == strategy.informative_pairs() + assert [] == strategy.gather_informative_pairs() @pytest.mark.parametrize('error', [ diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 3b84fc254..a01b55050 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -4,7 +4,9 @@ import numpy as np import pandas as pd import pytest -from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes +from freqtrade.data.dataprovider import DataProvider +from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, + timeframe_to_minutes) def generate_test_data(timeframe: str, size: int): @@ -132,3 +134,65 @@ def test_stoploss_from_open(): assert stoploss == 0 else: assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) + + +def test_stoploss_from_absolute(): + assert stoploss_from_absolute(90, 100) == 1 - (90 / 100) + assert stoploss_from_absolute(100, 100) == 0 + assert stoploss_from_absolute(110, 100) == 0 + assert stoploss_from_absolute(100, 0) == 1 + assert stoploss_from_absolute(0, 100) == 1 + + +def test_informative_decorator(mocker, default_conf): + test_data_5m = generate_test_data('5m', 40) + test_data_30m = generate_test_data('30m', 40) + test_data_1h = generate_test_data('1h', 40) + data = { + ('XRP/USDT', '5m'): test_data_5m, + ('XRP/USDT', '30m'): test_data_30m, + ('XRP/USDT', '1h'): test_data_1h, + ('LTC/USDT', '5m'): test_data_5m, + ('LTC/USDT', '30m'): test_data_30m, + ('LTC/USDT', '1h'): test_data_1h, + ('BTC/USDT', '30m'): test_data_30m, + ('BTC/USDT', '5m'): test_data_5m, + ('BTC/USDT', '1h'): test_data_1h, + ('ETH/USDT', '1h'): test_data_1h, + ('ETH/USDT', '30m'): test_data_30m, + ('ETH/BTC', '1h'): test_data_1h, + } + from .strats.informative_decorator_strategy import InformativeDecoratorTest + default_conf['stake_currency'] = 'USDT' + strategy = InformativeDecoratorTest(config=default_conf) + strategy.dp = DataProvider({}, None, None) + mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' + ]) + + assert len(strategy._ft_informative) == 6 # Equal to number of decorators used + informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), + ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), + ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] + for inf_pair in informative_pairs: + assert inf_pair in strategy.gather_informative_pairs() + + def test_historic_ohlcv(pair, timeframe): + return data[(pair, timeframe or strategy.timeframe)].copy() + mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', + side_effect=test_historic_ohlcv) + + analyzed = strategy.advise_all_indicators( + {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) + expected_columns = [ + 'rsi_1h', 'rsi_30m', # Stacked informative decorators + 'btc_usdt_rsi_1h', # BTC 1h informative + 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting + 'rsi_from_callable', # Custom column formatter + 'eth_btc_rsi_1h', # Quote currency not matching stake currency + 'rsi', 'rsi_less', # Non-informative columns + 'rsi_5m', # Manual informative dataframe + ] + for _, dataframe in analyzed.items(): + for col in expected_columns: + assert col in dataframe.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index d6c1197ab..e7571b798 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 3 + assert len([x for x in strategies if x['class'] is not None]) == 4 assert len([x for x in strategies if x['class'] is None]) == 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 901eeff70..71926f9b7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: assert coo_mock.call_count == 1 -def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: +@pytest.mark.parametrize('runmode', [ + RunMode.DRY_RUN, + RunMode.LIVE +]) +def test_order_dict(default_conf, mocker, runmode, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) + if runmode == RunMode.LIVE: + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) assert freqtrade.strategy.order_types['stoploss_on_exchange'] caplog.clear() # is left untouched conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - } - freqtrade = FreqtradeBot(conf) - assert not freqtrade.strategy.order_types['stoploss_on_exchange'] - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - - -def test_order_dict_live(default_conf, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': True, - } - conf['bid_strategy']['price_side'] = 'ask' - - freqtrade = FreqtradeBot(conf) - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - assert freqtrade.strategy.order_types['stoploss_on_exchange'] - - caplog.clear() - # is left untouched - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -219,8 +192,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: - +@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ + # Override stoploss + (0.79, False), + # Override strategy stoploss + (0.85, True) +]) +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, + buy_price_mult, ignore_strat_sl, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -234,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.79, - 'ask': buy_price * 0.79, - 'last': buy_price * 0.79 + 'bid': buy_price * buy_price_mult, + 'ask': buy_price * buy_price_mult, + 'last': buy_price * buy_price_mult, }), get_fee=fee, ) @@ -253,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf ############################################# # stoploss shoud be hit - assert freqtrade.handle_trade(trade) is True - assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog) - assert trade.sell_reason == SellType.STOP_LOSS.value - - -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, - mocker, edge_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_edge(mocker) - edge_conf['max_open_trades'] = float('inf') - - # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 - # Thus, if price falls 15%, stoploss should not be triggered - # - # mocking the ticker: price is falling ... - buy_price = limit_buy_order['price'] - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.85, - 'ask': buy_price * 0.85, - 'last': buy_price * 0.85 - }), - get_fee=fee, - ) - ############################################# - - # Create a trade with "limit_buy_order" price - freqtrade = FreqtradeBot(edge_conf) - freqtrade.active_pair_whitelist = ['NEO/BTC'] - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - trade = Trade.query.first() - trade.update(limit_buy_order) - ############################################# - - # stoploss shoud not be hit - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is not ignore_strat_sl + if not ignore_strat_sl: + assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog) + assert trade.sell_reason == SellType.STOP_LOSS.value def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: @@ -376,8 +319,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ + (0.0005, True, True, 99), + (0.000000005, True, False, 99), + (0, False, True, 99), + (UNLIMITED_STAKE_AMOUNT, False, True, 0), +]) +def test_create_trade_minimal_amount( + default_conf, ticker, limit_buy_order_open, fee, mocker, + stake_amount, create, amount_enough, max_open_trades, caplog +) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value=limit_buy_order_open) @@ -387,78 +338,33 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, create_order=buy_mock, get_fee=fee, ) - default_conf['stake_amount'] = 0.0005 + default_conf['max_open_trades'] = max_open_trades freqtrade = FreqtradeBot(default_conf) + freqtrade.config['stake_amount'] = stake_amount patch_get_signal(freqtrade) - freqtrade.create_trade('ETH/BTC') - rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] - assert rate * amount <= default_conf['stake_amount'] - - -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0.000000005 - - patch_get_signal(freqtrade) - - assert freqtrade.create_trade('ETH/BTC') - assert log_has_re(r"Stake amount for pair .* is too small.*", caplog) - - -def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0 - - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - - -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value=limit_buy_order_open), - get_fee=fee, - ) - default_conf['max_open_trades'] = 0 - default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 + if create: + assert freqtrade.create_trade('ETH/BTC') + if amount_enough: + rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + assert rate * amount <= default_conf['stake_amount'] + else: + assert log_has_re( + r"Stake amount for pair .* is too small.*", + caplog + ) + else: + assert not freqtrade.create_trade('ETH/BTC') + if not max_open_trades: + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 +@pytest.mark.parametrize('whitelist,positions', [ + (["ETH/BTC"], 1), # No pairs left + ([], 0), # No pairs in whitelist +]) def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, - mocker, caplog) -> None: + whitelist, positions, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -467,36 +373,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) - - default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_whitelist'] = whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - assert n == 1 - assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) - - -def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value={'id': limit_buy_order['id']}), - get_fee=fee, - ) - default_conf['exchange']['pair_whitelist'] = [] - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - n = freqtrade.enter_positions() - assert n == 0 - assert log_has("Active pair whitelist is empty.", caplog) + assert n == positions + if positions: + assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) + n = freqtrade.enter_positions() + assert n == 0 + assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) + else: + assert n == 0 + assert log_has("Active pair whitelist is empty.", caplog) @pytest.mark.usefixtures("init_persistence") @@ -1253,6 +1143,7 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, @pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1344,10 +1235,14 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208, - pair='ETH/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.95) + stoploss_order_mock.assert_called_once_with( + amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.95, + side="sell", + leverage=1.0 + ) # price fell below stoploss, so dry-run sells trade. mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -1360,6 +1255,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1418,7 +1314,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order @@ -1428,7 +1324,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1437,6 +1333,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set + # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1527,10 +1424,14 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208, - pair='ETH/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.96) + stoploss_order_mock.assert_called_once_with( + amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.96, + side="sell", + leverage=1.0 + ) # price fell below stoploss, so dry-run sells trade. mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -1543,7 +1444,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: - + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1648,36 +1549,37 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, # stoploss should be set to 1% as trailing is on assert trade.stop_loss == 0.00002346 * 0.99 cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') - stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, - pair='NEO/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.99) + stoploss_order_mock.assert_called_once_with( + amount=2132892.49146757, + pair='NEO/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.99, + side="sell", + leverage=1.0 + ) -def test_enter_positions(mocker, default_conf, caplog) -> None: +@pytest.mark.parametrize('return_value,side_effect,log_message', [ + (False, None, 'Found no enter signals for whitelisted currencies. Trying again...'), + (None, DependencyException, 'Unable to create trade for ETH/BTC: ') +]) +def test_enter_positions(mocker, default_conf, return_value, side_effect, + log_message, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(return_value=False)) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog) - # create_trade should be called once for every pair in the whitelist. - assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - - -def test_enter_positions_exception(mocker, default_conf, caplog) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(side_effect=DependencyException) + MagicMock( + return_value=return_value, + side_effect=side_effect + ) ) n = freqtrade.enter_positions() assert n == 0 + assert log_has(log_message, caplog) + # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - assert log_has('Unable to create trade for ETH/BTC: ', caplog) def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1771,8 +1673,13 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) +@pytest.mark.parametrize('initial_amount,has_rounding_fee', [ + (90.99181073 + 1e-14, True), + (8.0, False) +]) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, - mocker): + mocker, initial_amount, has_rounding_fee, caplog): + trades_for_order[0]['amount'] = initial_amount mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1793,32 +1700,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] - - -def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee, - limit_buy_order, mocker, caplog): - trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) - patch_exchange(mocker) - amount = sum(x['amount'] for x in trades_for_order) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_order_id='123456', - is_open=True, - open_date=arrow.utcnow().datetime, - ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order) - assert trade.amount != amount - assert trade.amount == limit_buy_order['amount'] - assert log_has_re(r'Applying fee on amount for .*', caplog) + if has_rounding_fee: + assert log_has_re(r'Applying fee on amount for .*', caplog) def test_update_trade_state_exception(mocker, default_conf, @@ -3130,16 +3013,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, assert mock_insuf.call_count == 1 -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [ + # Enable profit + (True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value), + # Disable profit + (False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value), + # Enable loss + # * Shouldn't this be SellType.STOP_LOSS.value + (True, 0.00000172, 0.00000173, False, False, None), + # Disable loss + (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), +]) +def test_sell_profit_only( + default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 + 'bid': bid, + 'ask': ask, + 'last': bid }), create_order=MagicMock(side_effect=[ limit_buy_order_open, @@ -3149,128 +3044,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy ) default_conf.update({ 'use_sell_signal': True, - 'sell_profit_only': True, + 'sell_profit_only': profit_only, 'sell_profit_offset': 0.1, }) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - + if sell_type == SellType.SELL_SIGNAL.value: + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) + else: + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is handle_first - freqtrade.strategy.sell_profit_offset = 0.0 - assert freqtrade.handle_trade(trade) is True + if handle_second: + freqtrade.strategy.sell_profit_offset = 0.0 + assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00002172, - 'ask': 0.00002173, - 'last': 0.00002172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': True, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_type=SellType.NONE)) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is False - - -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, enter_long=False, exit_long=True) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert trade.sell_reason == sell_type def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, @@ -3308,11 +3104,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ assert trade.amount != amnt -def test__safe_exit_amount(default_conf, fee, caplog, mocker): +@pytest.mark.parametrize('amount_wallet,has_err', [ + (95.29, False), + (91.29, True) +]) +def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 - amount_wallet = 95.29 + amount_wallet = amount_wallet mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') trade = Trade( @@ -3326,37 +3126,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet - assert log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - caplog.clear() - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet - assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - - -def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): - patch_RPCManager(mocker) - patch_exchange(mocker) - amount = 95.33 - amount_wallet = 91.29 - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - open_order_id="123456", - fee_open=fee.return_value, - fee_close=fee.return_value, - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to exit."): - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + if has_err: + with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."): + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + else: + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet + assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 + caplog.clear() + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet + assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: @@ -4144,50 +3926,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: +@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ + (False, 0.045, 0.046, 2, None), + (True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]}) +]) +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown, + ask, last, order_book_top, order_book, caplog) -> None: """ - test if function get_rate will return the order book price - instead of the ask rate + test if function get_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) + ticker_mock = MagicMock(return_value={'ask': ask, 'last': last}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_l2_order_book=order_book_l2, + fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2, fetch_ticker=ticker_mock, - ) default_conf['exchange']['name'] = 'binance' default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 2 + default_conf['bid_strategy']['order_book_top'] = order_book_top default_conf['bid_strategy']['ask_last_balance'] = 0 default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 - assert ticker_mock.call_count == 0 - - -def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None: - patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}), - fetch_ticker=ticker_mock, - - ) - default_conf['exchange']['name'] = 'binance' - default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 1 - default_conf['bid_strategy']['ask_last_balance'] = 0 - default_conf['telegram']['enabled'] = False - - freqtrade = FreqtradeBot(default_conf) - # orderbook shall be used even if tickers would be lower. - with pytest.raises(PricingError): - freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") - assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) + if exception_thrown: + with pytest.raises(PricingError): + freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") + assert log_has_re( + r'Buy Price at location 1 from orderbook could not be determined.', caplog) + else: + assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 + assert ticker_mock.call_count == 0 def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: From 4b5cd891cdc68c6132da40ce7aeb426e3f5d641c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 07:11:53 +0200 Subject: [PATCH 34/46] Add V3 test strategy --- .../strats/informative_decorator_strategy.py | 2 +- tests/strategy/strats/legacy_strategy_v1.py | 2 +- tests/strategy/strats/strategy_test_v2.py | 2 +- tests/strategy/strats/strategy_test_v3.py | 159 ++++++++++++++++++ tests/strategy/test_interface.py | 8 +- tests/strategy/test_strategy_loading.py | 6 +- 6 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/strategy_test_v3.py diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index a32ad79e8..4dd2d84eb 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -3,7 +3,7 @@ from pandas import DataFrame from freqtrade.strategy import informative, merge_informative_pair -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class InformativeDecoratorTest(IStrategy): diff --git a/tests/strategy/strats/legacy_strategy_v1.py b/tests/strategy/strats/legacy_strategy_v1.py index ebfce632b..adb75c33e 100644 --- a/tests/strategy/strats/legacy_strategy_v1.py +++ b/tests/strategy/strats/legacy_strategy_v1.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 53e39526f..428ecc8c0 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class StrategyTestV2(IStrategy): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py new file mode 100644 index 000000000..347fa43bb --- /dev/null +++ b/tests/strategy/strats/strategy_test_v3.py @@ -0,0 +1,159 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, + RealParameter) + + +class StrategyTestV3(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 3 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal timeframe for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74, + 'sell_minusdi': 0.4 + } + + 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') + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) + protection_enabled = BooleanParameter(default=True) + protection_cooldown_lookback = IntParameter([0, 50], default=30) + + @property + def protections(self): + prot = [] + if self.protection_enabled.value: + prot.append({ + "method": "CooldownPeriod", + "stop_duration_candles": self.protection_cooldown_lookback.value + }) + return prot + + def informative_pairs(self): + + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (dataframe['rsi'] < self.buy_rsi.value) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ), + 'enter_trade'] = 1 + # TODO-lev: Add short logic + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > self.sell_minusdi.value) + ), + 'exit_trade'] = 1 + + # TODO-lev: Add short logic + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 61ad5b734..b5e5a9eaa 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -581,10 +581,10 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked - assert 'buy' in ret.columns - assert 'sell' in ret.columns - assert ret['buy'].sum() == 0 - assert ret['sell'].sum() == 0 + assert 'enter_long' in ret.columns + assert 'exit_long' in ret.columns + assert ret['enter_long'].sum() == 0 + assert ret['exit_long'].sum() == 0 assert not log_has('TA Analysis Launched', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index e7571b798..3e8392596 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 5 + assert len(strategies) == 6 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 4 + assert len([x for x in strategies if x['class'] is not None]) == 5 assert len([x for x in strategies if x['class'] is None]) == 1 From 7a5c7e70208659e69de75b10caff828f5a17eb6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 19:14:14 +0200 Subject: [PATCH 35/46] Update some tests to use StrategyV3 --- freqtrade/strategy/interface.py | 5 ++-- tests/rpc/test_rpc_apiserver.py | 3 ++- tests/strategy/test_interface.py | 16 +++++------ tests/strategy/test_strategy_loading.py | 36 ++++++++++++------------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 34cf9f749..139729910 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -563,9 +563,8 @@ class IStrategy(ABC, HyperStrategyMixin): message = "" 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 'enter_long' not in dataframe: + message = "enter_long/buy column not set." elif df_len != len(dataframe): message = message_template.format("length") elif df_close != dataframe["close"].iloc[-1]: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7c98b2df7..dc29c3027 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1226,7 +1226,8 @@ def test_api_strategies(botclient): 'HyperoptableStrategy', 'InformativeDecoratorTest', 'StrategyTestV2', - 'TestStrategyLegacyV1' + 'StrategyTestV3', + 'TestStrategyLegacyV1', ]} diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index b5e5a9eaa..c09d5209c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -23,11 +23,11 @@ from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re -from .strats.strategy_test_v2 import StrategyTestV2 +from .strats.strategy_test_v3 import StrategyTestV3 # Avoid to reinit the same object again and again -_STRATEGY = StrategyTestV2(config={}) +_STRATEGY = StrategyTestV3(config={}) _STRATEGY.dp = DataProvider({}, None, None) @@ -224,8 +224,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history): def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 - ohlcv_history.loc[:, 'buy'] = 0 - ohlcv_history.loc[:, 'sell'] = 0 + ohlcv_history.loc[:, 'enter_long'] = 0 + ohlcv_history.loc[:, 'exit_long'] = 0 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) @@ -248,8 +248,8 @@ def test_assert_df(ohlcv_history, caplog): _STRATEGY.assert_df(None, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) with pytest.raises(StrategyError, - match="Buy column not set"): - _STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), + match="enter_long/buy column not set."): + _STRATEGY.assert_df(ohlcv_history.drop('enter_long', axis=1), len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True @@ -528,7 +528,7 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: advise_sell=sell_mock, ) - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 @@ -559,7 +559,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> advise_sell=sell_mock, ) - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) strategy.dp = DataProvider({}, None, None) strategy.process_only_new_candles = True diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 3e8392596..2d4cf7c35 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -74,7 +74,7 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): - default_conf['strategy'] = 'StrategyTestV2' + default_conf['strategy'] = 'StrategyTestV3' extra_dir = Path.cwd() / 'some/path' with pytest.raises(OperationalException): StrategyResolver._load_strategy('StrategyTestV2', config=default_conf, @@ -99,7 +99,7 @@ def test_load_strategy_noname(default_conf): StrategyResolver.load_strategy(default_conf) -def test_strategy(result, default_conf): +def test_strategy_v2(result, default_conf): default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -129,7 +129,7 @@ def test_strategy(result, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'minimal_roi': { "20": 0.1, "0": 0.5 @@ -146,7 +146,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'stoploss': -0.5 }) strategy = StrategyResolver.load_strategy(default_conf) @@ -158,7 +158,7 @@ def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'trailing_stop': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -171,7 +171,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'trailing_stop_positive': -0.1, 'trailing_stop_positive_offset': -0.2 @@ -191,7 +191,7 @@ def test_strategy_override_timeframe(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'timeframe': 60, 'stake_currency': 'ETH' }) @@ -207,7 +207,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'process_only_new_candles': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -227,7 +227,7 @@ def test_strategy_override_order_types(caplog, default_conf): 'stoploss_on_exchange': True, } default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_types': order_types }) strategy = StrategyResolver.load_strategy(default_conf) @@ -241,12 +241,12 @@ def test_strategy_override_order_types(caplog, default_conf): " 'stoploss_on_exchange': True}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_types': {'buy': 'market'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV2'. " + match=r"Impossible to load Strategy 'StrategyTestV3'. " r"Order-types mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -260,7 +260,7 @@ def test_strategy_override_order_tif(caplog, default_conf): } default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_time_in_force': order_time_in_force }) strategy = StrategyResolver.load_strategy(default_conf) @@ -273,12 +273,12 @@ def test_strategy_override_order_tif(caplog, default_conf): " {'buy': 'fok', 'sell': 'gtc'}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_time_in_force': {'buy': 'fok'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV2'. " + match=r"Impossible to load Strategy 'StrategyTestV3'. " r"Order-time-in-force mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -286,7 +286,7 @@ def test_strategy_override_order_tif(caplog, default_conf): def test_strategy_override_use_sell_signal(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.use_sell_signal @@ -296,7 +296,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): assert default_conf['use_sell_signal'] default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'use_sell_signal': False, }) strategy = StrategyResolver.load_strategy(default_conf) @@ -309,7 +309,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', }) strategy = StrategyResolver.load_strategy(default_conf) assert not strategy.sell_profit_only @@ -319,7 +319,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): assert not default_conf['sell_profit_only'] default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'sell_profit_only': True, }) strategy = StrategyResolver.load_strategy(default_conf) From c791b95405118429972cf62652f3bbf13eb770c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 20:18:14 +0200 Subject: [PATCH 36/46] Use new TestStrategy (V3) by default in tests --- build_helpers/publish_docker_arm64.sh | 2 +- build_helpers/publish_docker_multi.sh | 2 +- tests/commands/test_commands.py | 10 ++--- tests/conftest.py | 4 +- tests/conftest_trades.py | 8 ++-- tests/data/test_btanalysis.py | 6 +-- tests/data/test_history.py | 9 +++-- tests/optimize/test_backtesting.py | 35 ++++++++-------- tests/optimize/test_edge_cli.py | 8 ++-- tests/optimize/test_hyperopt.py | 4 +- tests/optimize/test_hyperopt_tools.py | 22 +++++----- tests/optimize/test_optimize_reports.py | 3 +- tests/rpc/test_rpc_apiserver.py | 32 +++++++-------- tests/rpc/test_rpc_telegram.py | 8 ++-- .../strats/informative_decorator_strategy.py | 3 +- tests/strategy/strats/strategy_test_v3.py | 24 ++++++----- tests/strategy/test_default_strategy.py | 16 ++++---- tests/strategy/test_interface.py | 11 ----- tests/strategy/test_strategy_loading.py | 40 +++++++++---------- tests/test_arguments.py | 3 +- tests/test_configuration.py | 9 +++-- 21 files changed, 127 insertions(+), 132 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 1ad8074d4..70f99e54b 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 +docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index dd6ac841e..fd5f0ef93 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 +docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 135510b38..a1d89d7d3 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -19,8 +19,8 @@ from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_in from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has, + log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT @@ -774,7 +774,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" not in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out # Test regular output args = [ @@ -789,7 +789,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out # Test color output args = [ @@ -803,7 +803,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out assert "LOAD FAILED" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index d54e3a9a1..a9fd42a05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,8 @@ logging.getLogger('').setLevel(logging.INFO) # Do not mask numpy errors as warnings that no one read, raise the exсeption np.seterr(all='raise') +CURRENT_TEST_STRATEGY = 'StrategyTestV3' + def pytest_addoption(parser): parser.addoption('--longrun', action='store_true', dest="longrun", @@ -406,7 +408,7 @@ def get_default_conf(testdatadir): "user_data_dir": Path("user_data"), "verbosity": 3, "strategy_path": str(Path(__file__).parent / "strategy" / "strats"), - "strategy": "StrategyTestV2", + "strategy": CURRENT_TEST_STRATEGY, "disableparamexport": True, "internals": {}, "export": "none", diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 700cd3fa7..cf3c970f6 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -33,7 +33,7 @@ def mock_trade_1(fee): open_rate=0.123, exchange='binance', open_order_id='dry_run_buy_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') @@ -87,7 +87,7 @@ def mock_trade_2(fee): exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), @@ -146,7 +146,7 @@ def mock_trade_3(fee): close_profit_abs=0.000155, exchange='binance', is_open=False, - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, sell_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), @@ -189,7 +189,7 @@ def mock_trade_4(fee): open_rate=0.123, exchange='binance', open_order_id='prod_buy_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1dcd04a80..e7b8c5b2f 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -16,7 +16,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_ get_latest_hyperopt_file, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history -from tests.conftest import create_mock_trades +from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): for col in BT_DATA_COLUMNS: if col not in ['index', 'open_at_end']: assert col in trades.columns - trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='StrategyTestV2') + trades = load_trades_from_db(db_url=default_conf['db_url'], strategy=CURRENT_TEST_STRATEGY) assert len(trades) == 4 trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') assert len(trades) == 0 @@ -186,7 +186,7 @@ def test_load_trades(default_conf, mocker): db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), no_trades=False, - strategy="StrategyTestV2", + strategy=CURRENT_TEST_STRATEGY, ) assert db_mock.call_count == 1 diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 575a590e7..73ceabbbf 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -26,7 +26,8 @@ from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHa from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange +from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re, + patch_exchange) # Change this if modifying UNITTEST/BTC testdatafile @@ -380,7 +381,7 @@ def test_file_dump_json_tofile(testdatadir) -> None: def test_get_timerange(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) data = strategy.advise_all_indicators( @@ -398,7 +399,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) data = strategy.advise_all_indicators( @@ -422,7 +423,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange('index', 'index', 200, 250) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index d2ccef9db..0d31846d5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -159,7 +159,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--export', 'none' ] @@ -194,7 +194,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--timeframe', '1m', '--enable-position-stacking', @@ -244,7 +244,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '2' ] @@ -255,7 +255,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '0.5' ] @@ -273,7 +273,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] pargs = get_args(args) start_backtesting(pargs) @@ -306,7 +306,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: patch_exchange(mocker) del default_conf['timeframe'] - default_conf['strategy_list'] = ['StrategyTestV2', + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'SampleStrategy'] mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) @@ -344,7 +344,6 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: assert len(processed['UNITTEST/BTC']) == 102 # Load strategy to compare the result between Backtesting function and strategy are the same - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) processed2 = strategy.advise_all_indicators(data) @@ -486,7 +485,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) # Multiple strategies - default_conf['strategy_list'] = ['StrategyTestV2', 'TestStrategyLegacyV1'] + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1'] with pytest.raises(OperationalException, match='PrecisionFilter not allowed for backtesting multiple strategies.'): Backtesting(default_conf) @@ -803,7 +802,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): - # Override the default buy trend function in our StrategyTestV2 + # Override the default buy trend function in our StrategyTest def fun(dataframe=None, pair=None): buy_value = 1 sell_value = 1 @@ -819,7 +818,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): def test_backtest_only_sell(mocker, default_conf, testdatadir): - # Override the default buy trend function in our StrategyTestV2 + # Override the default buy trend function in our StrategyTest def fun(dataframe=None, pair=None): buy_value = 0 sell_value = 1 @@ -948,7 +947,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', str(testdatadir), '--timeframe', '1m', '--timerange', '1510694220-1510700340', @@ -1019,7 +1018,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): '--enable-position-stacking', '--disable-max-market-positions', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1', ] args = get_args(args) @@ -1042,7 +1041,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy TestStrategyLegacyV1', ] @@ -1123,7 +1122,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '--enable-position-stacking', '--disable-max-market-positions', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1', ] args = get_args(args) @@ -1140,7 +1139,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy TestStrategyLegacyV1', ] @@ -1228,7 +1227,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, '--timeframe', '5m', '--timeframe-detail', '1m', '--strategy-list', - 'StrategyTestV2' + CURRENT_TEST_STRATEGY ] args = get_args(args) start_backtesting(args) @@ -1242,7 +1241,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'up to 2019-10-13 11:10:00 (2 days).', 'Backtesting with data from 2019-10-11 01:40:00 ' 'up to 2019-10-13 11:10:00 (2 days).', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', ] for line in exists: diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 18d5f1c76..e091c9c53 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.enums import RunMode from freqtrade.optimize.edge_cli import EdgeCli -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -16,7 +16,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] config = setup_optimize_configuration(get_args(args), RunMode.EDGE) @@ -46,7 +46,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--timeframe', '1m', '--timerange', ':100', @@ -80,7 +80,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] pargs = get_args(args) start_edge(pargs) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 27496a1fc..a83277dc6 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -18,7 +18,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy.hyper import IntParameter -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -125,7 +125,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None args = [ 'hyperopt', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '0.5' ] diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 9c2b2e8fc..5a46f238b 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -10,7 +10,7 @@ import rapidjson from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer -from tests.conftest import log_has +from tests.conftest import CURRENT_TEST_STRATEGY, log_has # Functions for recurrent object patching @@ -167,9 +167,9 @@ def test__pprint_dict(): def test_get_strategy_filename(default_conf): - x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV2') + x = HyperoptTools.get_strategy_filename(default_conf, CURRENT_TEST_STRATEGY) assert isinstance(x, Path) - assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v2.py' + assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py' x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy') assert x is None @@ -177,7 +177,7 @@ def test_get_strategy_filename(default_conf): def test_export_params(tmpdir): - filename = Path(tmpdir) / "StrategyTestV2.json" + filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -205,12 +205,12 @@ def test_export_params(tmpdir): } } - HyperoptTools.export_params(params, "StrategyTestV2", filename) + HyperoptTools.export_params(params, CURRENT_TEST_STRATEGY, filename) assert filename.is_file() content = rapidjson.load(filename.open('r')) - assert content['strategy_name'] == 'StrategyTestV2' + assert content['strategy_name'] == CURRENT_TEST_STRATEGY assert 'params' in content assert "buy" in content["params"] assert "sell" in content["params"] @@ -223,7 +223,7 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker): default_conf['disableparamexport'] = False export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") - filename = Path(tmpdir) / "StrategyTestV2.json" + filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -252,17 +252,17 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker): FTHYPT_FILEVERSION: 2, } - HyperoptTools.try_export_params(default_conf, "StrategyTestV222", params) + HyperoptTools.try_export_params(default_conf, "StrategyTestVXXX", params) assert log_has("Strategy not found, not exporting parameter file.", caplog) assert export_mock.call_count == 0 caplog.clear() - HyperoptTools.try_export_params(default_conf, "StrategyTestV2", params) + HyperoptTools.try_export_params(default_conf, CURRENT_TEST_STRATEGY, params) assert export_mock.call_count == 1 - assert export_mock.call_args_list[0][0][1] == 'StrategyTestV2' - assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v2.json' + assert export_mock.call_args_list[0][0][1] == CURRENT_TEST_STRATEGY + assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v3.json' def test_params_print(capsys): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 83caefd2d..b8cf0c682 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -21,6 +21,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver +from tests.conftest import CURRENT_TEST_STRATEGY from tests.data.test_history import _backup_file, _clean_test_file @@ -52,7 +53,7 @@ def test_text_table_bt_results(): def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) StrategyResolver.load_strategy(default_conf) results = {'DefStrat': { diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index dc29c3027..afce87b88 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,8 +24,8 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer -from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, - log_has_re, patch_get_signal) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro, + get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) BASE_URI = "/api/v1" @@ -885,7 +885,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'open_trade_value': 15.1668225, 'sell_reason': None, 'sell_order_status': None, - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', @@ -990,7 +990,7 @@ def test_api_forcebuy(botclient, mocker, fee): close_rate=0.265441, id=22, timeframe=5, - strategy="StrategyTestV2" + strategy=CURRENT_TEST_STRATEGY )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) @@ -1040,7 +1040,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'open_trade_value': 0.24605460, 'sell_reason': None, 'sell_order_status': None, - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', @@ -1107,7 +1107,7 @@ def test_api_pair_candles(botclient, ohlcv_history): f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) assert 'strategy' in rc.json() - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert 'columns' in rc.json() assert 'data_start_ts' in rc.json() assert 'data_start' in rc.json() @@ -1145,19 +1145,19 @@ def test_api_pair_history(botclient, ohlcv_history): # No pair rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No Timeframe rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No timerange rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&strategy=StrategyTestV2") + f"&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No strategy @@ -1169,14 +1169,14 @@ def test_api_pair_history(botclient, ohlcv_history): # Working rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 200) assert rc.json()['length'] == 289 assert len(rc.json()['data']) == rc.json()['length'] assert 'columns' in rc.json() assert 'data' in rc.json() assert rc.json()['pair'] == 'UNITTEST/BTC' - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' assert rc.json()['data_start_ts'] == 1515628800000 assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' @@ -1185,7 +1185,7 @@ def test_api_pair_history(botclient, ohlcv_history): # No data found rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&timerange=20200111-20200112&strategy=StrategyTestV2") + f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 502) assert rc.json()['error'] == ("Error querying /api/v1/pair_history: " "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") @@ -1234,12 +1234,12 @@ def test_api_strategies(botclient): def test_api_strategy(botclient): ftbot, client = botclient - rc = client_get(client, f"{BASE_URI}/strategy/StrategyTestV2") + rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}") assert_response(rc) - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY - data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v2.py").read_text() + data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text() assert rc.json()['code'] == data rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") @@ -1296,7 +1296,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog): # start backtesting data = { - "strategy": "StrategyTestV2", + "strategy": CURRENT_TEST_STRATEGY, "timeframe": "5m", "timerange": "20180110-20180111", "max_open_trades": 3, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21f1cd000..23ccadca0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, - patch_exchange, patch_get_signal, patch_whitelist) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot, + log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -1238,7 +1238,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] - assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] + assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() @@ -1247,7 +1247,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] - assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] + assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index 4dd2d84eb..68f8651c2 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -2,8 +2,7 @@ from pandas import DataFrame -from freqtrade.strategy import informative, merge_informative_pair -from freqtrade.strategy import IStrategy +from freqtrade.strategy import IStrategy, informative, merge_informative_pair class InformativeDecoratorTest(IStrategy): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 347fa43bb..db294d4e9 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -68,15 +68,17 @@ class StrategyTestV3(IStrategy): protection_enabled = BooleanParameter(default=True) protection_cooldown_lookback = IntParameter([0, 50], default=30) - @property - def protections(self): - prot = [] - if self.protection_enabled.value: - prot.append({ - "method": "CooldownPeriod", - "stop_duration_candles": self.protection_cooldown_lookback.value - }) - return prot + # TODO-lev: Can we make this work with protection tests? + # TODO-lev: (Would replace HyperoptableStrategy implicitly ... ) + # @property + # def protections(self): + # prot = [] + # if self.protection_enabled.value: + # prot.append({ + # "method": "CooldownPeriod", + # "stop_duration_candles": self.protection_cooldown_lookback.value + # }) + # return prot def informative_pairs(self): @@ -134,7 +136,7 @@ class StrategyTestV3(IStrategy): (dataframe['adx'] > 65) & (dataframe['plus_di'] > self.buy_plusdi.value) ), - 'enter_trade'] = 1 + 'enter_long'] = 1 # TODO-lev: Add short logic return dataframe @@ -153,7 +155,7 @@ class StrategyTestV3(IStrategy): (dataframe['adx'] > 70) & (dataframe['minus_di'] > self.sell_minusdi.value) ), - 'exit_trade'] = 1 + 'exit_long'] = 1 # TODO-lev: Add short logic return dataframe diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 06688619b..02597b672 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -4,20 +4,20 @@ from pandas import DataFrame from freqtrade.persistence.models import Trade -from .strats.strategy_test_v2 import StrategyTestV2 +from .strats.strategy_test_v3 import StrategyTestV3 def test_strategy_test_v2_structure(): - assert hasattr(StrategyTestV2, 'minimal_roi') - assert hasattr(StrategyTestV2, 'stoploss') - assert hasattr(StrategyTestV2, 'timeframe') - assert hasattr(StrategyTestV2, 'populate_indicators') - assert hasattr(StrategyTestV2, 'populate_buy_trend') - assert hasattr(StrategyTestV2, 'populate_sell_trend') + assert hasattr(StrategyTestV3, 'minimal_roi') + 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') def test_strategy_test_v2(result, fee): - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) metadata = {'pair': 'ETH/BTC'} assert type(strategy.minimal_roi) is dict diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index c09d5209c..65e7da9db 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -177,7 +177,6 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): def test_ignore_expired_candle(default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.ignore_buying_expired_candle_after = 60 @@ -262,7 +261,6 @@ def test_assert_df(ohlcv_history, caplog): def test_advise_all_indicators(default_conf, testdatadir) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange.parse_timerange('1510694220-1510700340') @@ -273,7 +271,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') timerange = TimeRange.parse_timerange('1510694220-1510700340') @@ -291,7 +288,6 @@ def test_min_roi_reached(default_conf, fee) -> None: min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1}, {0: 0.1, 20: 0.05, 55: 0.01}] for roi in min_roi_list: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = roi trade = Trade( @@ -330,7 +326,6 @@ def test_min_roi_reached2(default_conf, fee) -> None: }, ] for roi in min_roi_list: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = roi trade = Trade( @@ -365,7 +360,6 @@ def test_min_roi_reached3(default_conf, fee) -> None: 30: 0.05, 55: 0.30, } - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = min_roi trade = Trade( @@ -418,8 +412,6 @@ def test_min_roi_reached3(default_conf, fee) -> None: def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, profit2, adjusted2, expected2, custom_stop) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) - strategy = StrategyResolver.load_strategy(default_conf) trade = Trade( pair='ETH/BTC', @@ -466,8 +458,6 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili def test_custom_sell(default_conf, fee, caplog) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) - strategy = StrategyResolver.load_strategy(default_conf) trade = Trade( pair='ETH/BTC', @@ -591,7 +581,6 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> @pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) PairLocks.timeframe = default_conf['timeframe'] PairLocks.use_db = True strategy = StrategyResolver.load_strategy(default_conf) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2d4cf7c35..b3e79cd27 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -10,7 +10,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import IStrategy -from tests.conftest import log_has, log_has_re +from tests.conftest import CURRENT_TEST_STRATEGY, log_has, log_has_re def test_search_strategy(): @@ -18,7 +18,7 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_name='StrategyTestV2', + object_name=CURRENT_TEST_STRATEGY, add_source=True, ) assert issubclass(s, IStrategy) @@ -77,7 +77,7 @@ def test_load_strategy_invalid_directory(result, caplog, default_conf): default_conf['strategy'] = 'StrategyTestV3' extra_dir = Path.cwd() / 'some/path' with pytest.raises(OperationalException): - StrategyResolver._load_strategy('StrategyTestV2', config=default_conf, + StrategyResolver._load_strategy(CURRENT_TEST_STRATEGY, config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) @@ -129,7 +129,7 @@ def test_strategy_v2(result, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'minimal_roi': { "20": 0.1, "0": 0.5 @@ -146,7 +146,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'stoploss': -0.5 }) strategy = StrategyResolver.load_strategy(default_conf) @@ -158,7 +158,7 @@ def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'trailing_stop': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -171,7 +171,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'trailing_stop_positive': -0.1, 'trailing_stop_positive_offset': -0.2 @@ -191,7 +191,7 @@ def test_strategy_override_timeframe(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'timeframe': 60, 'stake_currency': 'ETH' }) @@ -207,7 +207,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'process_only_new_candles': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -227,7 +227,7 @@ def test_strategy_override_order_types(caplog, default_conf): 'stoploss_on_exchange': True, } default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_types': order_types }) strategy = StrategyResolver.load_strategy(default_conf) @@ -241,12 +241,12 @@ def test_strategy_override_order_types(caplog, default_conf): " 'stoploss_on_exchange': True}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_types': {'buy': 'market'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV3'. " + match=r"Impossible to load Strategy '" + CURRENT_TEST_STRATEGY + "'. " r"Order-types mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -260,7 +260,7 @@ def test_strategy_override_order_tif(caplog, default_conf): } default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_time_in_force': order_time_in_force }) strategy = StrategyResolver.load_strategy(default_conf) @@ -273,20 +273,20 @@ def test_strategy_override_order_tif(caplog, default_conf): " {'buy': 'fok', 'sell': 'gtc'}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_time_in_force': {'buy': 'fok'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV3'. " - r"Order-time-in-force mapping is incomplete."): + match=f"Impossible to load Strategy '{CURRENT_TEST_STRATEGY}'. " + "Order-time-in-force mapping is incomplete."): StrategyResolver.load_strategy(default_conf) def test_strategy_override_use_sell_signal(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.use_sell_signal @@ -296,7 +296,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): assert default_conf['use_sell_signal'] default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'use_sell_signal': False, }) strategy = StrategyResolver.load_strategy(default_conf) @@ -309,7 +309,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, }) strategy = StrategyResolver.load_strategy(default_conf) assert not strategy.sell_profit_only @@ -319,7 +319,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): assert not default_conf['sell_profit_only'] default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'sell_profit_only': True, }) strategy = StrategyResolver.load_strategy(default_conf) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index fca5c6ab9..c2ddaf0ff 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -7,6 +7,7 @@ import pytest from freqtrade.commands import Arguments from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive +from tests.conftest import CURRENT_TEST_STRATEGY # Parse common command-line-arguments. Used for all tools @@ -123,7 +124,7 @@ def test_parse_args_backtesting_custom() -> None: '-c', 'test_conf.json', '--ticker-interval', '1m', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'SampleStrategy' ] call_args = Arguments(args).get_parsed_arg() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1ce45e4d5..e25cd800d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -23,7 +23,8 @@ from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_ from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre -from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file +from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re, + patched_configuration_load_config_file) @pytest.fixture(scope="function") @@ -403,7 +404,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> arglist = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] args = Arguments(arglist).get_parsed_arg() @@ -440,7 +441,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non arglist = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--userdir', "/tmp/freqtrade", '--ticker-interval', '1m', @@ -497,7 +498,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non '--ticker-interval', '1m', '--export', 'trades', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategy' ] From 5928ba9c883a6b47bfd425eb497c2ba70f1cfa9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:14:52 +0200 Subject: [PATCH 37/46] Test and document leverage strategy callback --- docs/strategy-advanced.md | 28 +++++++++++++++++ freqtrade/strategy/interface.py | 37 +++++++++++------------ tests/conftest.py | 1 + tests/strategy/strats/strategy_test_v3.py | 9 ++++++ tests/strategy/test_interface.py | 28 ++++++++++++++++- 5 files changed, 83 insertions(+), 20 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2b9517f3b..13dec60ca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -642,6 +642,34 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an !!! Tip Returning `0` or `None` will prevent trades from being placed. +## Leverage Callback + +When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage). + +Assuming a capital of 500USDT, a trade with leverage=3 would result in a position with 500 x 3 = 1500 USDT. + +Values that are above `max_leverage` will be adjusted to `max_leverage`. +For markets / exchanges that don't support leverage, this method is ignored. + +``` python +class AwesomeStrategy(IStrategy): + def leverage(self, pair: str, current_time: 'datetime', current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + """ + Customize leverage for each new trade. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 +``` + --- ## Derived strategies diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 139729910..d852c7269 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -370,8 +370,7 @@ class IStrategy(ABC, HyperStrategyMixin): proposed_stake: float, min_stake: float, max_stake: float, **kwargs) -> float: """ - Customize stake size for each new trade. This method is not called when edge module is - enabled. + Customize stake size for each new trade. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -383,6 +382,23 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_stake + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + """ + Customize leverage for each new trade. This method is not called when edge module is + enabled. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -971,20 +987,3 @@ class IStrategy(ABC, HyperStrategyMixin): if 'exit_long' not in df.columns: df = df.rename({'sell': 'exit_long'}, axis='columns') return df - - def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, side: str, - **kwargs) -> float: - """ - Customize leverage for each new trade. This method is not called when edge module is - enabled. - - :param pair: Pair that's currently analyzed - :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. - :param proposed_leverage: A leverage proposed by the bot. - :param max_leverage: Max leverage allowed on this pair - :param side: 'long' or 'short' - indicating the direction of the proposed trade - :return: A leverage amount, which is between 1.0 and max_leverage. - """ - return 1.0 diff --git a/tests/conftest.py b/tests/conftest.py index a9fd42a05..b35ff17d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,7 @@ logging.getLogger('').setLevel(logging.INFO) np.seterr(all='raise') CURRENT_TEST_STRATEGY = 'StrategyTestV3' +TRADE_SIDES = ('long', 'short') def pytest_addoption(parser): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index db294d4e9..18c4ec93f 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from datetime import datetime import talib.abstract as ta from pandas import DataFrame @@ -159,3 +160,11 @@ class StrategyTestV3(IStrategy): # TODO-lev: Add short logic return dataframe + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + # Return 3.0 in all cases. + # Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly. + + return 3.0 diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 65e7da9db..ad393d6a4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -21,7 +21,7 @@ from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, Categoric DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from tests.conftest import log_has, log_has_re +from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re from .strats.strategy_test_v3 import StrategyTestV3 @@ -506,6 +506,32 @@ def test_custom_sell(default_conf, fee, caplog) -> None: assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) +@pytest.mark.parametrize('side', TRADE_SIDES) +def test_leverage_callback(default_conf, side) -> None: + default_conf['strategy'] = 'StrategyTestV2' + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.leverage( + pair='XRP/USDT', + current_time=datetime.now(timezone.utc), + current_rate=2.2, + proposed_leverage=1.0, + max_leverage=5.0, + side=side, + ) == 1 + + default_conf['strategy'] = CURRENT_TEST_STRATEGY + strategy = StrategyResolver.load_strategy(default_conf) + assert strategy.leverage( + pair='XRP/USDT', + current_time=datetime.now(timezone.utc), + current_rate=2.2, + proposed_leverage=1.0, + max_leverage=5.0, + side=side, + ) == 3 + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 4c6b1cd55bb4a4a73f97d053f638712c2bddf78b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:36:03 +0200 Subject: [PATCH 38/46] Add very simple short logic to test-strategy --- freqtrade/optimize/backtesting.py | 8 ++++++-- tests/strategy/strats/strategy_test_v3.py | 13 ++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b43222fb3..429ba7251 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -139,6 +139,10 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) + # TODO-lev: This should come from the configuration setting or better a + # TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange + self._can_short = False + self.progress = BTProgress() self.abort = False @@ -499,8 +503,8 @@ class Backtesting: def check_for_trade_entry(self, row) -> Optional[str]: enter_long = row[LONG_IDX] == 1 exit_long = row[ELONG_IDX] == 1 - enter_short = row[SHORT_IDX] == 1 - exit_short = row[ESHORT_IDX] == 1 + enter_short = self._can_short and row[SHORT_IDX] == 1 + exit_short = self._can_short and row[ESHORT_IDX] == 1 if enter_long == 1 and not any([exit_long, enter_short]): # Long diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 18c4ec93f..115211a7c 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from datetime import datetime + import talib.abstract as ta from pandas import DataFrame @@ -138,7 +139,11 @@ class StrategyTestV3(IStrategy): (dataframe['plus_di'] > self.buy_plusdi.value) ), 'enter_long'] = 1 - # TODO-lev: Add short logic + dataframe.loc[ + ( + qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value) + ), + 'enter_short'] = 1 return dataframe @@ -158,6 +163,12 @@ class StrategyTestV3(IStrategy): ), 'exit_long'] = 1 + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value) + ), + 'exit_short'] = 1 + # TODO-lev: Add short logic return dataframe From 0e13d57e5792d10dea887d64b9552de5094c7e6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:42:31 +0200 Subject: [PATCH 39/46] Update advise_* methods to entry/exit --- freqtrade/edge/edge_positioning.py | 4 ++-- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/strategy/interface.py | 10 ++++---- tests/optimize/test_backtest_detail.py | 4 ++-- tests/optimize/test_backtesting.py | 20 ++++++++-------- tests/optimize/test_hyperopt.py | 16 ++++++------- tests/strategy/test_interface.py | 32 ++++++++++++------------- tests/strategy/test_strategy_loading.py | 16 ++++++------- 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index bee96c746..e08b3df2f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -168,8 +168,8 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - df_analyzed = self.strategy.advise_sell( - dataframe=self.strategy.advise_buy( + df_analyzed = self.strategy.advise_exit( + dataframe=self.strategy.advise_entry( dataframe=pair_data, metadata={'pair': pair} ), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 429ba7251..4094cf0aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,8 +274,8 @@ class Backtesting: pair_data.loc[:, 'long_tag'] = None pair_data.loc[:, 'short_tag'] = None - df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), + df_analyzed = self.strategy.advise_exit( + self.strategy.advise_entry(pair_data, {'pair': pair}), {'pair': pair} ).copy() # Trim startup period from analyzed dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d852c7269..0d651ccbb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -489,8 +489,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ 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_entry(dataframe, metadata) + dataframe = self.advise_exit(dataframe, metadata) return dataframe def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -912,7 +912,7 @@ class IStrategy(ABC, HyperStrategyMixin): def advise_all_indicators(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_entry 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. Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. @@ -944,7 +944,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: return self.populate_indicators(dataframe, metadata) - def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the entry order signal for the given dataframe This method should not be overridden. @@ -967,7 +967,7 @@ class IStrategy(ABC, HyperStrategyMixin): return df - def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the exit order signal for the given dataframe This method should not be overridden. diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 9b99648b1..554122bd5 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -598,8 +598,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_entry = 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 0d31846d5..662ca0193 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -295,8 +295,8 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert backtesting.config == default_conf assert backtesting.timeframe == '5m' assert callable(backtesting.strategy.advise_all_indicators) - assert callable(backtesting.strategy.advise_buy) - assert callable(backtesting.strategy.advise_sell) + assert callable(backtesting.strategy.advise_entry) + assert callable(backtesting.strategy.advise_exit) assert isinstance(backtesting.strategy.dp, DataProvider) get_fee.assert_called() assert backtesting.fee == 0.5 @@ -811,8 +811,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_entry = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -827,8 +827,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_entry = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -842,8 +842,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_entry = _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) @@ -896,8 +896,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_entry = _trend_alternate_hold # Override + backtesting.strategy.advise_exit = _trend_alternate_hold # Override processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a83277dc6..57d10d048 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -318,8 +318,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_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -698,8 +698,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_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -772,8 +772,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_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -821,8 +821,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_entry") 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/strategy/test_interface.py b/tests/strategy/test_interface.py index ad393d6a4..4b39adaf7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -535,20 +535,20 @@ def test_leverage_callback(default_conf, side) -> 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) + entry_mock = MagicMock(side_effect=lambda x, meta: x) + exit_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_entry=entry_mock, + advise_exit=exit_mock, ) strategy = StrategyTestV3({}) 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 entry_mock.call_count == 1 + assert entry_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -557,8 +557,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 entry_mock.call_count == 2 + assert entry_mock.call_count == 2 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -566,13 +566,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) + entry_mock = MagicMock(side_effect=lambda x, meta: x) + exit_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_entry=entry_mock, + advise_exit=exit_mock, ) strategy = StrategyTestV3({}) @@ -585,8 +585,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 entry_mock.call_count == 1 + assert entry_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() @@ -594,8 +594,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 entry_mock.call_count == 1 + assert entry_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked assert 'enter_long' in ret.columns assert 'exit_long' in ret.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index b3e79cd27..4e29e1ebc 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -117,11 +117,11 @@ def test_strategy_v2(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_entry(df_indicators, metadata=metadata) assert 'buy' not in dataframe.columns assert 'enter_long' in dataframe.columns - dataframe = strategy.advise_sell(df_indicators, metadata=metadata) + dataframe = strategy.advise_exit(df_indicators, metadata=metadata) assert 'sell' not in dataframe.columns assert 'exit_long' in dataframe.columns @@ -347,7 +347,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_entry(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -356,7 +356,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'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -384,11 +384,11 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - enterdf = strategy.advise_buy(result, metadata=metadata) + enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) assert 'buy' in enterdf.columns - exitdf = strategy.advise_sell(result, metadata=metadata) + exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) assert 'sell' in exitdf @@ -411,13 +411,13 @@ def test_strategy_interface_versioning(result, default_conf): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - enterdf = strategy.advise_buy(result, metadata=metadata) + enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) assert 'buy' not in enterdf.columns assert 'enter_long' in enterdf.columns - exitdf = strategy.advise_sell(result, metadata=metadata) + exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) assert 'sell' not in exitdf assert 'exit_long' in exitdf From a0ef89d9101093a090e603d854fe1f53ea69d081 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:48:05 +0200 Subject: [PATCH 40/46] Also support column-transition for V1 strategies --- freqtrade/strategy/interface.py | 16 ++++++++-------- tests/strategy/test_strategy_loading.py | 12 +++++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0d651ccbb..abaf7d224 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -959,13 +959,13 @@ class IStrategy(ABC, HyperStrategyMixin): if self._buy_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 + df = self.populate_buy_trend(dataframe) # type: ignore else: df = self.populate_buy_trend(dataframe, metadata) - if 'enter_long' not in df.columns: - df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') + if 'enter_long' not in df.columns: + df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') - return df + return df def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -981,9 +981,9 @@ class IStrategy(ABC, HyperStrategyMixin): if self._sell_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 + df = self.populate_sell_trend(dataframe) # type: ignore else: df = self.populate_sell_trend(dataframe, metadata) - if 'exit_long' not in df.columns: - df = df.rename({'sell': 'exit_long'}, axis='columns') - return df + if 'exit_long' not in df.columns: + df = df.rename({'sell': 'exit_long'}, axis='columns') + return df diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 4e29e1ebc..e18a3710b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -99,8 +99,10 @@ def test_load_strategy_noname(default_conf): StrategyResolver.load_strategy(default_conf) -def test_strategy_v2(result, default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) +@pytest.mark.filterwarnings("ignore:deprecated") +@pytest.mark.parametrize('strategy_name', ['StrategyTestV2', 'TestStrategyLegacyV1']) +def test_strategy_pre_v3(result, default_conf, strategy_name): + default_conf.update({'strategy': strategy_name}) strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} @@ -364,7 +366,7 @@ def test_deprecate_populate_indicators(result, default_conf): @pytest.mark.filterwarnings("ignore:deprecated") -def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): +def test_call_deprecated_function(result, default_conf, caplog): default_location = Path(__file__).parent / "strats" del default_conf['timeframe'] default_conf.update({'strategy': 'TestStrategyLegacyV1', @@ -386,11 +388,11 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) - assert 'buy' in enterdf.columns + assert 'enter_long' in enterdf.columns exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) - assert 'sell' in exitdf + assert 'exit_long' in exitdf assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", caplog) From 2a678bdbb4494cb143b8a2b0dee4e7aebcaa06f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Sep 2021 19:31:06 +0200 Subject: [PATCH 41/46] Update buy_tag column to long_tag --- freqtrade/data/btanalysis.py | 1 + freqtrade/enums/signaltype.py | 2 +- freqtrade/optimize/backtesting.py | 7 ++++--- freqtrade/strategy/interface.py | 4 ++-- tests/optimize/test_hyperopt_tools.py | 2 +- tests/strategy/test_interface.py | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 7d97661c4..e8d878838 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -31,6 +31,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] +# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?) def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index b1b86fc47..1f2b6d331 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -15,7 +15,7 @@ class SignalTagType(Enum): """ Enum for signal columns """ - BUY_TAG = "buy_tag" + LONG_TAG = "long_tag" SHORT_TAG = "short_tag" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4094cf0aa..63d307908 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -45,7 +45,7 @@ LONG_IDX = 5 ELONG_IDX = 6 # Exit long SHORT_IDX = 7 ESHORT_IDX = 8 # Exit short -BUY_TAG_IDX = 9 +ENTER_TAG_IDX = 9 SHORT_TAG_IDX = 10 @@ -454,7 +454,8 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade - has_buy_tag = len(row) >= BUY_TAG_IDX + 1 + # TODO-lev: SHORT_TAG ... + has_buy_tag = len(row) >= ENTER_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -464,7 +465,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, - buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, + buy_tag=row[ENTER_TAG_IDX] if has_buy_tag else None, exchange=self._exchange_name, is_short=(direction == 'short'), ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index abaf7d224..4e8881295 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -519,7 +519,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe[SignalType.EXIT_LONG.value] = 0 dataframe[SignalType.ENTER_SHORT.value] = 0 dataframe[SignalType.EXIT_SHORT.value] = 0 - dataframe[SignalTagType.BUY_TAG.value] = None + dataframe[SignalTagType.LONG_TAG.value] = None dataframe[SignalTagType.SHORT_TAG.value] = None # Other Defs in strategy that want to be called every loop here @@ -690,7 +690,7 @@ class IStrategy(ABC, HyperStrategyMixin): enter_tag_value: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG - enter_tag_value = latest.get(SignalTagType.BUY_TAG.value, None) + enter_tag_value = latest.get(SignalTagType.LONG_TAG.value, None) if enter_short == 1 and not any([exit_short, enter_long]): enter_signal = SignalDirection.SHORT enter_tag_value = latest.get(SignalTagType.SHORT_TAG.value, None) diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 5a46f238b..17e8248c3 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -167,7 +167,7 @@ def test__pprint_dict(): def test_get_strategy_filename(default_conf): - x = HyperoptTools.get_strategy_filename(default_conf, CURRENT_TEST_STRATEGY) + x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV3') assert isinstance(x, Path) assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py' diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4b39adaf7..1ec5eef5a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -59,7 +59,7 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'enter_long'] = 1 - mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' + mocked_history.loc[1, 'long_tag'] = 'buy_signal_01' assert _STRATEGY.get_entry_signal( 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01') From 4fd00db630e27de686ad79f71096929385e5edfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 15:20:59 +0200 Subject: [PATCH 42/46] Use "combined" enter_tag column --- freqtrade/enums/signaltype.py | 3 +-- freqtrade/optimize/backtesting.py | 13 +++++-------- freqtrade/strategy/interface.py | 9 ++++----- tests/optimize/__init__.py | 10 ++++------ tests/optimize/test_backtest_detail.py | 4 ++-- tests/strategy/test_interface.py | 6 ++++-- 6 files changed, 20 insertions(+), 25 deletions(-) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index 1f2b6d331..fc585318c 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -15,8 +15,7 @@ class SignalTagType(Enum): """ Enum for signal columns """ - LONG_TAG = "long_tag" - SHORT_TAG = "short_tag" + ENTER_TAG = "enter_tag" class SignalDirection(Enum): diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 63d307908..4a20d9738 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -46,7 +46,6 @@ ELONG_IDX = 6 # Exit long SHORT_IDX = 7 ESHORT_IDX = 8 # Exit short ENTER_TAG_IDX = 9 -SHORT_TAG_IDX = 10 class Backtesting: @@ -253,7 +252,7 @@ class Backtesting: # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', - 'enter_short', 'exit_short', 'long_tag', 'short_tag'] + 'enter_short', 'exit_short', 'enter_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -271,8 +270,7 @@ class Backtesting: if 'exit_long' in pair_data.columns: pair_data.loc[:, 'exit_long'] = 0 pair_data.loc[:, 'exit_short'] = 0 - pair_data.loc[:, 'long_tag'] = None - pair_data.loc[:, 'short_tag'] = None + pair_data.loc[:, 'enter_tag'] = None df_analyzed = self.strategy.advise_exit( self.strategy.advise_entry(pair_data, {'pair': pair}), @@ -287,7 +285,7 @@ class Backtesting: df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1) df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1) df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1) - df_analyzed.loc[:, 'long_tag'] = df_analyzed.loc[:, 'long_tag'].shift(1) + df_analyzed.loc[:, 'enter_tag'] = df_analyzed.loc[:, 'enter_tag'].shift(1) # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) @@ -454,8 +452,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade - # TODO-lev: SHORT_TAG ... - has_buy_tag = len(row) >= ENTER_TAG_IDX + 1 + has_enter_tag = len(row) >= ENTER_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -465,7 +462,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, - buy_tag=row[ENTER_TAG_IDX] if has_buy_tag else None, + buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None, exchange=self._exchange_name, is_short=(direction == 'short'), ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4e8881295..e50795078 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -519,8 +519,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe[SignalType.EXIT_LONG.value] = 0 dataframe[SignalType.ENTER_SHORT.value] = 0 dataframe[SignalType.EXIT_SHORT.value] = 0 - dataframe[SignalTagType.LONG_TAG.value] = None - dataframe[SignalTagType.SHORT_TAG.value] = None + dataframe[SignalTagType.ENTER_TAG.value] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -690,10 +689,10 @@ class IStrategy(ABC, HyperStrategyMixin): enter_tag_value: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG - enter_tag_value = latest.get(SignalTagType.LONG_TAG.value, None) + enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) if enter_short == 1 and not any([exit_short, enter_long]): enter_signal = SignalDirection.SHORT - enter_tag_value = latest.get(SignalTagType.SHORT_TAG.value, None) + enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) timeframe_seconds = timeframe_to_seconds(timeframe) @@ -963,7 +962,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: df = self.populate_buy_trend(dataframe, metadata) if 'enter_long' not in df.columns: - df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') + df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns') return df diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 2ba9485fd..10518758c 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -18,7 +18,7 @@ class BTrade(NamedTuple): sell_reason: SellType open_tick: int close_tick: int - buy_tag: Optional[str] = None + enter_tag: Optional[str] = None class BTContainer(NamedTuple): @@ -49,15 +49,13 @@ def _build_backtest_dataframe(data): if len(data[0]) == 8: # No short columns data = [d + [0, 0] for d in data] - columns = columns + ['long_tag'] if len(data[0]) == 11 else columns + columns = columns + ['enter_tag'] if len(data[0]) == 11 else columns frame = DataFrame.from_records(data, columns=columns) frame['date'] = frame['date'].apply(_get_frame_time_from_offset) # Ensure floats are in place for column in ['open', 'high', 'low', 'close', 'volume']: frame[column] = frame[column].astype('float64') - if 'long_tag' not in columns: - frame['long_tag'] = None - if 'short_tag' not in columns: - frame['short_tag'] = None + if 'enter_tag' not in columns: + frame['enter_tag'] = None return frame diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 554122bd5..227d778af 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -532,7 +532,7 @@ tc33 = BTContainer(data=[ sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1, - buy_tag='buy_signal_01' + enter_tag='buy_signal_01' )] ) @@ -621,6 +621,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason.value - assert res.buy_tag == trade.buy_tag + assert res.buy_tag == trade.enter_tag assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 1ec5eef5a..a9334c616 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -59,7 +59,7 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'enter_long'] = 1 - mocked_history.loc[1, 'long_tag'] = 'buy_signal_01' + mocked_history.loc[1, 'enter_tag'] = 'buy_signal_01' assert _STRATEGY.get_entry_signal( 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01') @@ -70,8 +70,10 @@ def test_returns_latest_signal(ohlcv_history): mocked_history.loc[1, 'enter_long'] = 0 mocked_history.loc[1, 'enter_short'] = 1 mocked_history.loc[1, 'exit_short'] = 0 + mocked_history.loc[1, 'enter_tag'] = 'sell_signal_01' + assert _STRATEGY.get_entry_signal( - 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, None) + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, 'sell_signal_01') assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (True, False) From 4d49f1a0c7627f8e8a96adaf4d90c9dac3fc0eb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 15:39:34 +0200 Subject: [PATCH 43/46] Reset columns by dropping instead of resetting --- freqtrade/optimize/backtesting.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4a20d9738..c82ee4afc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -263,14 +263,7 @@ class Backtesting: if not pair_data.empty: # Cleanup from prior runs - # TODO-lev: The below is not 100% compatible with the interface compatibility layer - if 'enter_long' in pair_data.columns: - pair_data.loc[:, 'enter_long'] = 0 - pair_data.loc[:, 'enter_short'] = 0 - if 'exit_long' in pair_data.columns: - pair_data.loc[:, 'exit_long'] = 0 - pair_data.loc[:, 'exit_short'] = 0 - pair_data.loc[:, 'enter_tag'] = None + pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore') df_analyzed = self.strategy.advise_exit( self.strategy.advise_entry(pair_data, {'pair': pair}), @@ -281,11 +274,11 @@ class Backtesting: startup_candles=self.required_startup) # To avoid using data from future, we use buy/sell signals shifted # from the previous candle - df_analyzed.loc[:, 'enter_long'] = df_analyzed.loc[:, 'enter_long'].shift(1) - df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1) - df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1) - df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1) - df_analyzed.loc[:, 'enter_tag'] = df_analyzed.loc[:, 'enter_tag'].shift(1) + for col in headers[5:]: + if col in df_analyzed.columns: + df_analyzed.loc[:, col] = df_analyzed.loc[:, col].shift(1) + else: + df_analyzed.loc[:, col] = 0 if col != 'enter_tag' else None # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) From 84e013de2d5484c943e49b8e9e73d2272736a038 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 19:32:24 +0200 Subject: [PATCH 44/46] Update confirm_trade_entry to support "side" parameter --- docs/strategy-advanced.md | 8 +++++--- freqtrade/freqtradebot.py | 5 ++++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/strategy/interface.py | 5 +++-- .../templates/subtemplates/strategy_methods_advanced.j2 | 8 +++++--- tests/strategy/test_default_strategy.py | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 13dec60ca..731930020 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -539,9 +539,10 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: + time_in_force: str, current_time: datetime, + side: str, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -549,12 +550,13 @@ class AwesomeStrategy(IStrategy): 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 :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :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 placed on the exchange. False aborts the process diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43a7571f7..51c8b3ad9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -519,9 +519,12 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get('forcebuy', order_type) # TODO-lev: Will this work for shorting? + # TODO-lev: Add non-hardcoded "side" parameter if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc), + side='long' + ): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c82ee4afc..09248ae09 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -440,7 +440,8 @@ class Backtesting: # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], - time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime(), + side=direction): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e50795078..2dfd62185 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -230,9 +230,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ pass - # TODO-lev: add side def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: + time_in_force: str, current_time: datetime, + side: str, **kwargs) -> bool: """ Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or @@ -248,6 +248,7 @@ class IStrategy(ABC, HyperStrategyMixin): :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 current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :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 placed on the exchange. False aborts the process diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 2df23f365..1edf77f10 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -80,9 +80,10 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: 'datetime', **kwargs) -> bool: + time_in_force: str, current_time: datetime, + side: str, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -90,12 +91,13 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f 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 :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :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 placed on the exchange. False aborts the process diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 02597b672..a995491f2 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -37,7 +37,7 @@ def test_strategy_test_v2(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 + current_time=datetime.utcnow(), side='long') 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 From a926f54a25cb91fdb5ab566178dec90a34d40d57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 19:35:54 +0200 Subject: [PATCH 45/46] Add "side" parameter to custom_stake_amount --- docs/strategy-advanced.md | 2 +- freqtrade/freqtradebot.py | 4 +++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/strategy/interface.py | 4 ++-- .../templates/subtemplates/strategy_methods_advanced.j2 | 8 ++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 731930020..dc1e2831a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -619,7 +619,7 @@ It is possible to manage your risk by reducing or increasing stake amount when p class AwesomeStrategy(IStrategy): def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 51c8b3ad9..5e0508287 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -502,7 +502,9 @@ class FreqtradeBot(LoggingMixin): default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) + min_stake=min_stake_amount, max_stake=max_stake_amount, side='long') + # TODO-lev: Add non-hardcoded "side" parameter + stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 09248ae09..4890c20aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -429,7 +429,8 @@ class Backtesting: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], - proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) + proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount, + side=direction) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2dfd62185..a22a0b6b8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -366,10 +366,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ return None - # TODO-lev: add side def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: """ Customize stake size for each new trade. @@ -379,6 +378,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 1edf77f10..1f064f88e 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,12 +12,11 @@ def bot_loop_start(self, **kwargs) -> None: """ pass -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, - **kwargs) -> float: + side: str, **kwargs) -> float: """ - Customize stake size for each new trade. This method is not called when edge module is - enabled. + Customize stake size for each new trade. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -25,6 +24,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake From 6fb0d14f80e3308d61bf1b2be878381637931122 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Sep 2021 07:07:49 +0200 Subject: [PATCH 46/46] changed naming for signal variable --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5e0508287..32edd8588 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -422,11 +422,11 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (side, enter_tag) = self.strategy.get_entry_signal( + (signal, enter_tag) = self.strategy.get_entry_signal( pair, self.strategy.timeframe, analyzed_df ) - if side: + if signal: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})