From c272944834b73f6705ab92d6eef1e956e3666cb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 11:09:09 +0200 Subject: [PATCH] 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'})