From 54bde6ac11a183be414a8faa37e588eafa269aaf Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:34:23 +0100 Subject: [PATCH 01/18] verify date on new candle before producing signal --- freqtrade/freqtradebot.py | 9 ++++++--- freqtrade/strategy/interface.py | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 914b8d9cd..02d8b5b56 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -390,14 +390,17 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") + dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) + latest = dataframe.iloc[-1] + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal( - pair, self.strategy.ticker_interval, - self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) + (buy, sell) = self.strategy.get_signal(pair, self.strategy.ticker_interval, dataframe) if buy and not sell: if not self.get_free_open_trades(): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d23af3f6e..765e23b0b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,15 +277,20 @@ class IStrategy(ABC): latest = dataframe.iloc[-1] - # Check if dataframe is out of date + # Check if dataframe has new candle signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) + if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: + logger.warning('Old candle for pair %s. Last tick is %s minutes old', + pair, (arrow.utcnow() - signal_date).total_seconds() // 60) + + # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, - (arrow.utcnow() - signal_date).seconds // 60 + (arrow.utcnow() - signal_date).total_seconds() // 60 ) return False, False From 4e45abbf139a08c79b2c1adee12cffbceda259c9 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:44:45 +0100 Subject: [PATCH 02/18] added return false, false --- freqtrade/strategy/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 765e23b0b..a26e8edbd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -283,6 +283,7 @@ class IStrategy(ABC): if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', pair, (arrow.utcnow() - signal_date).total_seconds() // 60) + return False, False # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) From d239e999043fe50b01b1e2a79b074bb3c9de707a Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:49:37 +0100 Subject: [PATCH 03/18] removed old code from create_trade --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 02d8b5b56..90f2c0c02 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -393,7 +393,6 @@ class FreqtradeBot: dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) latest = dataframe.iloc[-1] # Check if dataframe is out of date - signal_date = arrow.get(latest['date']) if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") From a85d17327bebfc7432a968464ac9b15f587f2813 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:54:27 +0100 Subject: [PATCH 04/18] fix --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 90f2c0c02..fd4daf7aa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -390,15 +390,13 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") - dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) - latest = dataframe.iloc[-1] # Check if dataframe is out of date - if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") return False # running get_signal on historical data fetched + dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) (buy, sell) = self.strategy.get_signal(pair, self.strategy.ticker_interval, dataframe) if buy and not sell: From a82cdf0add7f0fca9f0a3a4d32b5510aa4b5b461 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:04:51 +0100 Subject: [PATCH 05/18] fixed test --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a26e8edbd..47bdf781d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -282,7 +282,7 @@ class IStrategy(ABC): interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, (arrow.utcnow() - signal_date).total_seconds() // 60) + pair, int(arrow.utcnow() - signal_date).total_seconds() // 60) return False, False # Check if dataframe is out of date @@ -291,7 +291,7 @@ class IStrategy(ABC): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, - (arrow.utcnow() - signal_date).total_seconds() // 60 + int((arrow.utcnow() - signal_date).total_seconds() // 60) ) return False, False From d667acb3081132adc7207c4dad396e184ebec544 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:10:57 +0100 Subject: [PATCH 06/18] fixed typo --- freqtrade/strategy/interface.py | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 47bdf781d..781bd7a54 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -282,11 +282,11 @@ class IStrategy(ABC): interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, int(arrow.utcnow() - signal_date).total_seconds() // 60) + pair, int((arrow.utcnow() - signal_date).total_seconds() // 60) return False, False # Check if dataframe is out of date - offset = self.config.get('exchange', {}).get('outdated_offset', 5) + offset=self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', @@ -295,7 +295,7 @@ class IStrategy(ABC): ) return False, False - (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 + (buy, sell)=latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug( 'trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], @@ -306,8 +306,8 @@ class IStrategy(ABC): return buy, sell 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: + 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 has been reached, which can either be a stop-loss, ROI or sell-signal. @@ -317,12 +317,12 @@ class IStrategy(ABC): :return: True if trade should be sold, False otherwise """ # Set current rate to low for backtesting sell - current_rate = low or rate - current_profit = trade.calc_profit_ratio(current_rate) + current_rate=low or rate + current_profit=trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + stoplossflag=self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) @@ -332,9 +332,9 @@ class IStrategy(ABC): return stoplossflag # Set current rate to high for backtesting sell - current_rate = high or rate - current_profit = trade.calc_profit_ratio(current_rate) - config_ask_strategy = self.config.get('ask_strategy', {}) + current_rate=high or rate + current_profit=trade.calc_profit_ratio(current_rate) + config_ask_strategy=self.config.get('ask_strategy', {}) if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): # This one is noisy, commented out @@ -366,29 +366,29 @@ class IStrategy(ABC): def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, - force_stoploss: float, high: float = None) -> SellCheckTuple: + force_stoploss: float, high: float=None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit as ratio """ - stop_loss_value = force_stoploss if force_stoploss else self.stoploss + stop_loss_value=force_stoploss if force_stoploss else self.stoploss # 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.trailing_stop: # trailing stoploss handling - sl_offset = self.trailing_stop_positive_offset + sl_offset=self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. - high_profit = current_profit if not high else trade.calc_profit_ratio(high) + 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. if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): # Specific handling for trailing_stop_positive if self.trailing_stop_positive is not None and high_profit > sl_offset: - stop_loss_value = self.trailing_stop_positive + stop_loss_value=self.trailing_stop_positive logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} " f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") @@ -401,11 +401,11 @@ class IStrategy(ABC): (trade.stop_loss >= current_rate) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): - sell_type = SellType.STOP_LOSS + sell_type=SellType.STOP_LOSS # If initial stoploss is not the same as current one then it is trailing. if trade.initial_stop_loss != trade.stop_loss: - sell_type = SellType.TRAILING_STOP_LOSS + sell_type=SellType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " f"stoploss is {trade.stop_loss:.6f}, " @@ -425,10 +425,10 @@ class IStrategy(ABC): :return: minimal ROI entry value or None if none proper ROI entry was found. """ # Get highest entry in ROI dict where key <= trade-duration - roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) + roi_list=list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: return None, None - roi_entry = max(roi_list) + roi_entry=max(roi_list) return roi_entry, self.minimal_roi[roi_entry] def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: @@ -439,8 +439,8 @@ class IStrategy(ABC): :return: True if bot should sell at current rate """ # Check if time matches and current rate is above threshold - trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) - _, roi = self.min_roi_reached_entry(trade_dur) + trade_dur=int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + _, roi=self.min_roi_reached_entry(trade_dur) if roi is None: return False else: From 7754742459688e94b258ecf006883e57f0152b78 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:10:57 +0100 Subject: [PATCH 07/18] fix tests --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 47bdf781d..23c16d83e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -282,7 +282,7 @@ class IStrategy(ABC): interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, int(arrow.utcnow() - signal_date).total_seconds() // 60) + pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) return False, False # Check if dataframe is out of date From 2e679ee2ebee44fee6c445e568f03a5f58902aee Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:22:21 +0100 Subject: [PATCH 08/18] fixed log message --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 23c16d83e..cf205c709 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -281,7 +281,7 @@ class IStrategy(ABC): signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: - logger.warning('Old candle for pair %s. Last tick is %s minutes old', + logger.warning('Old candle for pair %s. Last candle is %s minutes old', pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) return False, False From d25cf1395b11f130877ef4864b12f480cf940ea9 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:22:21 +0100 Subject: [PATCH 09/18] fixed log message --- freqtrade/strategy/interface.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 23c16d83e..4d82c6c02 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,14 +277,6 @@ class IStrategy(ABC): latest = dataframe.iloc[-1] - # Check if dataframe has new candle - signal_date = arrow.get(latest['date']) - interval_minutes = timeframe_to_minutes(interval) - if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: - logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) - return False, False - # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): @@ -295,6 +287,14 @@ class IStrategy(ABC): ) return False, False + # Check if dataframe has new candle + signal_date = arrow.get(latest['date']) + interval_minutes = timeframe_to_minutes(interval) + if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: + logger.warning('Old candle for pair %s. Last candle is %s minutes old', + pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) + return False, False + (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug( 'trigger: %s (pair=%s) buy=%s sell=%s', From ba596af636b2a5a725cb0f13e4e71d81fa0157ef Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:26:57 +0100 Subject: [PATCH 10/18] final? --- freqtrade/strategy/interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4d82c6c02..a2a3c4f3a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -275,8 +275,6 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return False, False - latest = dataframe.iloc[-1] - # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): @@ -288,6 +286,7 @@ class IStrategy(ABC): return False, False # Check if dataframe has new candle + latest = dataframe.iloc[-1] signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: From c442913febf34cf610fa405bbb714667c45582a5 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:28:03 +0100 Subject: [PATCH 11/18] final --- freqtrade/strategy/interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a2a3c4f3a..081127370 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -275,6 +275,9 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return False, False + latest = dataframe.iloc[-1] + signal_date = arrow.get(latest['date']) + # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): @@ -286,8 +289,6 @@ class IStrategy(ABC): return False, False # Check if dataframe has new candle - latest = dataframe.iloc[-1] - signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last candle is %s minutes old', From 1395f658723dd5d4464026760adc00d701710907 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:29:22 +0100 Subject: [PATCH 12/18] meh --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 081127370..d840e2aea 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,6 +277,7 @@ class IStrategy(ABC): latest = dataframe.iloc[-1] signal_date = arrow.get(latest['date']) + interval_minutes = timeframe_to_minutes(interval) # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) @@ -289,7 +290,6 @@ class IStrategy(ABC): return False, False # Check if dataframe has new candle - interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last candle is %s minutes old', pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) From d752586b322b43afe652482ce0ddee0b46f0cac4 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:44:03 +0100 Subject: [PATCH 13/18] added test --- tests/strategy/test_interface.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 86d0738c6..d476b64b0 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -79,6 +79,21 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history assert log_has('Empty dataframe for pair xyz', caplog) +def test_get_signal_old_candle(default_conf, mocker, caplog, ticker_history): + caplog.set_level(logging.INFO) + # default_conf defines a 5m interval. we check interval of previous candle + # this is necessary as the last candle is removed (partial candles) by default + oldtime = arrow.utcnow().shift(minutes=-10) + ticks = DataFrame([{'buy': 1, 'date': oldtime}]) + mocker.patch.object( + _STRATEGY, '_analyze_ticker_internal', + return_value=DataFrame(ticks) + ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + ticker_history) + assert log_has('Old candle for pair xyz. Last candle is 10 minutes old', caplog) + + def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) # default_conf defines a 5m interval. we check interval * 2 + 5m @@ -198,14 +213,14 @@ def test_min_roi_reached3(default_conf, fee) -> None: strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = min_roi trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, - open_date=arrow.utcnow().shift(hours=-1).datetime, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='bittrex', - open_rate=1, + pair='ETH/BTC', + stake_amount=0.001, + amount=5, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, ) assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime) From bfa55f31c0aa51d6de3864a8dfe001d1aac6aa62 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 20 May 2020 17:45:27 +0300 Subject: [PATCH 14/18] Remove wrong comment --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dd419f541..32662ae09 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -402,7 +402,6 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") - # Check if dataframe is out of date if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") return False From c272944834b73f6705ab92d6eef1e956e3666cb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 11:09:09 +0200 Subject: [PATCH 15/18] Lock pair until a new candle arrives --- freqtrade/freqtradebot.py | 5 ++-- freqtrade/strategy/interface.py | 20 +++++++++------ tests/strategy/test_interface.py | 43 ++++++++++++++++++++------------ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2fcb9f3f9..eee60cc22 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -433,7 +433,9 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") - if self.strategy.is_pair_locked(pair): + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) + if self.strategy.is_pair_locked( + pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): logger.info(f"Pair {pair} is currently locked.") return False @@ -444,7 +446,6 @@ class FreqtradeBot: return False # running get_signal on historical data fetched - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index efc4b8430..4a3a78c8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -2,6 +2,7 @@ IStrategy interface This module defines the interface to apply for strategies """ +from freqtrade.exchange.exchange import timeframe_to_next_date import logging import warnings from abc import ABC, abstractmethod @@ -297,13 +298,22 @@ class IStrategy(ABC): if pair in self._pair_locked_until: del self._pair_locked_until[pair] - def is_pair_locked(self, pair: str) -> bool: + def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ Checks if a pair is currently locked """ if pair not in self._pair_locked_until: return False - return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + if not candle_date: + return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + else: + # Locking should happen until a new candle arrives + lock_time = timeframe_to_next_date(self.timeframe, candle_date) + # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) + res = self._pair_locked_until[pair] > lock_time + logger.debug(f"pair time = {lock_time} - pair_lock = {self._pair_locked_until[pair]} " + f"- res: {res}") + return res def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -438,12 +448,6 @@ class IStrategy(ABC): ) return False, False - # Check if dataframe has new candle - if (arrow.utcnow() - latest_date).total_seconds() // 60 >= timeframe_minutes: - logger.warning('Old candle for pair %s. Last candle is %s minutes old', - pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)) - return False, False - (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index bca7cc0d9..f1b5d0244 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import arrow @@ -8,12 +9,12 @@ import pytest from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.data.dataprovider import DataProvider from tests.conftest import log_has, log_has_re from .strats.default_strategy import DefaultStrategy @@ -87,21 +88,6 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) -def test_get_signal_old_candle(default_conf, mocker, caplog, ohlcv_history): - caplog.set_level(logging.INFO) - # default_conf defines a 5m interval. we check interval of previous candle - # this is necessary as the last candle is removed (partial candles) by default - oldtime = arrow.utcnow().shift(minutes=-10) - ticks = DataFrame([{'buy': 1, 'date': oldtime}]) - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame(ticks) - ) - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) - assert log_has('Old candle for pair xyz. Last candle is 10 minutes old', caplog) - - def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): # default_conf defines a 5m interval. we check interval * 2 + 5m # this is necessary as the last candle is removed (partial candles) by default @@ -402,6 +388,31 @@ def test_is_pair_locked(default_conf): strategy.unlock_pair(pair) assert not strategy.is_pair_locked(pair) + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + strategy.lock_pair(pair, lock_time) + # Lock is in the past ... + assert not strategy.is_pair_locked(pair) + # latest candle is from 14:20, lock goes to 14:30 + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + + # latest candle is from 14:25 (lock should be lifted) + # Since this is the "new candle" available at 14:30 + assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-4)) + + # Should not be locked after time expired + assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + # Change timeframe to 15m + strategy.timeframe = '15m' + # Candle from 14:14 - lock goes until 14:30 + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-16)) + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-15, seconds=-2)) + # Candle from 14:15 - lock goes until 14:30 + assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-15)) + def test_is_informative_pairs_callback(default_conf): default_conf.update({'strategy': 'TestStrategyLegacy'}) From 354a40624866645b191dde458c857588c17a211b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 11:44:32 +0200 Subject: [PATCH 16/18] Sort imports in interface.py --- freqtrade/strategy/interface.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4a3a78c8f..9673b0c68 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -2,7 +2,6 @@ IStrategy interface This module defines the interface to apply for strategies """ -from freqtrade.exchange.exchange import timeframe_to_next_date import logging import warnings from abc import ABC, abstractmethod @@ -15,8 +14,9 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import StrategyError, OperationalException +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -310,10 +310,7 @@ class IStrategy(ABC): # Locking should happen until a new candle arrives lock_time = timeframe_to_next_date(self.timeframe, candle_date) # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) - res = self._pair_locked_until[pair] > lock_time - logger.debug(f"pair time = {lock_time} - pair_lock = {self._pair_locked_until[pair]} " - f"- res: {res}") - return res + return self._pair_locked_until[pair] > lock_time def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ From fca11160e443470aea767bb4e7d43c021fd5d149 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 17:18:57 +0200 Subject: [PATCH 17/18] Improve docstring of is_pair_locked --- freqtrade/strategy/interface.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9673b0c68..69d9333e2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -301,6 +301,11 @@ class IStrategy(ABC): def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ 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. + :param: pair: "Pair to check" + :param candle_date: Date of the last candle. Optional, defaults to current date """ if pair not in self._pair_locked_until: return False From 3bb69bc1bd5b78498a9c2d236a7f32a9779dc322 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 17:31:00 +0200 Subject: [PATCH 18/18] Add returns statement to docstring --- freqtrade/strategy/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 69d9333e2..92d9f6c48 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -306,6 +306,7 @@ class IStrategy(ABC): of 2 seconds for a buy 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. """ if pair not in self._pair_locked_until: return False