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,