From af67bbde311b249910da29ed09fca1f5ec688639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 15:43:10 +0200 Subject: [PATCH 01/14] Test timeframe_to_x --- freqtrade/tests/exchange/test_exchange.py | 25 ++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e8a7201f1..245814a36 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,7 +14,9 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import API_RETRY_COUNT +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, + timeframe_to_msecs, + timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1540,3 +1542,24 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC" with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."): ex.get_valid_pair_combination("NOPAIR", "ETH") + + +def test_timeframe_to_minutes(): + assert timeframe_to_minutes("5m") == 5 + assert timeframe_to_minutes("10m") == 10 + assert timeframe_to_minutes("1h") == 60 + assert timeframe_to_minutes("1d") == 1440 + + +def test_timeframe_to_seconds(): + assert timeframe_to_seconds("5m") == 300 + assert timeframe_to_seconds("10m") == 600 + assert timeframe_to_seconds("1h") == 3600 + assert timeframe_to_seconds("1d") == 86400 + + +def test_timeframe_to_msecs(): + assert timeframe_to_msecs("5m") == 300000 + assert timeframe_to_msecs("10m") == 600000 + assert timeframe_to_msecs("1h") == 3600000 + assert timeframe_to_msecs("1d") == 86400000 From 933a553dd436eeadf1b7a65b2120a5fea0211779 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:07:19 +0200 Subject: [PATCH 02/14] Convert timeframe to next date --- freqtrade/exchange/exchange.py | 18 ++++++++++++--- freqtrade/tests/exchange/test_exchange.py | 28 ++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 657f382d8..3d2bf07c7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -6,7 +6,7 @@ import asyncio import inspect import logging from copy import deepcopy -from datetime import datetime +from datetime import datetime, timezone from math import ceil, floor from random import randint from typing import Any, Dict, List, Optional, Tuple @@ -781,13 +781,25 @@ def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_minutes(ticker_interval: str) -> int: """ - Same as above, but returns minutes. + Same as timeframe_to_seconds, but returns minutes. """ return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 def timeframe_to_msecs(ticker_interval: str) -> int: """ - Same as above, but returns milliseconds. + Same as timeframe_to_seconds, but returns milliseconds. """ return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 + + +def timeframe_to_next_date(timeframe: str, date: datetime = None): + """ + Use Timeframe and determine next candle. + """ + if not date: + date = datetime.utcnow() + timeframe_secs = timeframe_to_seconds(timeframe) + offset = date.timestamp() % timeframe_secs + new_timestamp = date.timestamp() + (timeframe_secs - offset) + return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 245814a36..23384aebe 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,7 +14,9 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, +from freqtrade.exchange.exchange import (API_RETRY_COUNT, + timeframe_to_next_date, + timeframe_to_minutes, timeframe_to_msecs, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver @@ -1563,3 +1565,27 @@ def test_timeframe_to_msecs(): assert timeframe_to_msecs("10m") == 600000 assert timeframe_to_msecs("1h") == 3600000 assert timeframe_to_msecs("1d") == 86400000 + + +def test_timeframe_to_next_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + # 5m -> 2019-08-12 13:25:00 + assert timeframe_to_next_date("5m", date) == datetime( + 2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc) + # 10m -> 2019-08-12 13:30:00 + assert timeframe_to_next_date("10m", date) == datetime( + 2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) + # 1h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("1h", date) == datetime( + 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) + # 2h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("2h", date) == datetime( + 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) + # 4h -> 2019-08-12 14:00:00 + assert timeframe_to_next_date("4h", date) == datetime( + 2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc) + # 1d -> 2019-08-13 00:00:00 + assert timeframe_to_next_date("1d", date) == datetime( + 2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc) From dd0ba183f88538f51aabc5db677d374a5ee12401 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:11:43 +0200 Subject: [PATCH 03/14] Add timeframe_to_prev_candle --- freqtrade/exchange/exchange.py | 16 ++++++++++++ freqtrade/tests/exchange/test_exchange.py | 30 ++++++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3d2bf07c7..abb03b86d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,6 +793,20 @@ def timeframe_to_msecs(ticker_interval: str) -> int: return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 +def timeframe_to_prev_date(timeframe: str, date: datetime = None): + """ + Use Timeframe and determine last possible candle. + """ + if not date: + date = datetime.utcnow() + timeframe_secs = timeframe_to_seconds(timeframe) + # Get offset based on timerame_secs + offset = date.timestamp() % timeframe_secs + # Subtract seconds passed since last offset + new_timestamp = date.timestamp() - offset + return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) + + def timeframe_to_next_date(timeframe: str, date: datetime = None): """ Use Timeframe and determine next candle. @@ -800,6 +814,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None): if not date: date = datetime.utcnow() timeframe_secs = timeframe_to_seconds(timeframe) + # Get offset to prev timeframe offset = date.timestamp() % timeframe_secs + # Add remaining seconds to next timeframe new_timestamp = date.timestamp() + (timeframe_secs - offset) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 23384aebe..e342af604 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -14,10 +14,10 @@ from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.exchange import (API_RETRY_COUNT, - timeframe_to_next_date, - timeframe_to_minutes, +from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1567,6 +1567,30 @@ def test_timeframe_to_msecs(): assert timeframe_to_msecs("1d") == 86400000 +def test_timeframe_to_prev_date(): + # 2019-08-12 13:22:08 + date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + + # 5m -> 2019-08-12 13:20:00 + assert timeframe_to_prev_date("5m", date) == datetime( + 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + # 10m -> 2019-08-12 13:20:00 + assert timeframe_to_prev_date("10m", date) == datetime( + 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + # 1h -> 2019-08-12 13:00:00 + assert timeframe_to_prev_date("1h", date) == datetime( + 2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc) + # 2h -> 2019-08-12 12:00:00 + assert timeframe_to_prev_date("2h", date) == datetime( + 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) + # 4h -> 2019-08-12 12:00:00 + assert timeframe_to_prev_date("4h", date) == datetime( + 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) + # 1d -> 2019-08-12 00:00:00 + assert timeframe_to_prev_date("1d", date) == datetime( + 2019, 8, 12, 0, 0, 0, tzinfo=timezone.utc) + + def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) From 1ce63b5b423e8dfa55be28ff9c614b84a6a8cef9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:17:06 +0200 Subject: [PATCH 04/14] Reformat tests to be easier readable --- freqtrade/exchange/exchange.py | 10 +++- freqtrade/tests/exchange/test_exchange.py | 68 +++++++++++------------ 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index abb03b86d..fe5561be2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -793,9 +793,12 @@ def timeframe_to_msecs(ticker_interval: str) -> int: return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 -def timeframe_to_prev_date(timeframe: str, date: datetime = None): +def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: """ Use Timeframe and determine last possible candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to utcnow() + :returns: date of previous candle (with utc timezone) """ if not date: date = datetime.utcnow() @@ -807,9 +810,12 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None): return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def timeframe_to_next_date(timeframe: str, date: datetime = None): +def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: """ Use Timeframe and determine next candle. + :param timeframe: timeframe in string format (e.g. "5m") + :param date: date to use. Defaults to utcnow() + :returns: date of next candle (with utc timezone) """ if not date: date = datetime.utcnow() diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index e342af604..23604f44f 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1571,45 +1571,41 @@ def test_timeframe_to_prev_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) - # 5m -> 2019-08-12 13:20:00 - assert timeframe_to_prev_date("5m", date) == datetime( - 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) - # 10m -> 2019-08-12 13:20:00 - assert timeframe_to_prev_date("10m", date) == datetime( - 2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) - # 1h -> 2019-08-12 13:00:00 - assert timeframe_to_prev_date("1h", date) == datetime( - 2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc) - # 2h -> 2019-08-12 12:00:00 - assert timeframe_to_prev_date("2h", date) == datetime( - 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) - # 4h -> 2019-08-12 12:00:00 - assert timeframe_to_prev_date("4h", date) == datetime( - 2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc) - # 1d -> 2019-08-12 00:00:00 - assert timeframe_to_prev_date("1d", date) == datetime( - 2019, 8, 12, 0, 0, 0, tzinfo=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:20:00 + ("5m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:20:00 + ("10m", datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 13:00:00 + ("1h", datetime(2019, 8, 12, 13, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 12:00:00 + ("2h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 12:00:00 + ("4h", datetime(2019, 8, 12, 12, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-12 00:00:00 + ("1d", datetime(2019, 8, 12, 00, 00, 0, tzinfo=timezone.utc)), + ] + for interval, result in tf_list: + assert timeframe_to_prev_date(interval, date) == result def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 date = datetime.fromtimestamp(1565616128, tz=timezone.utc) + tf_list = [ + # 5m -> 2019-08-12 13:25:00 + ("5m", datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)), + # 10m -> 2019-08-12 13:30:00 + ("10m", datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc)), + # 1h -> 2019-08-12 14:00:00 + ("1h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 2h -> 2019-08-12 14:00:00 + ("2h", datetime(2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc)), + # 4h -> 2019-08-12 14:00:00 + ("4h", datetime(2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc)), + # 1d -> 2019-08-13 00:00:00 + ("1d", datetime(2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc)), + ] - # 5m -> 2019-08-12 13:25:00 - assert timeframe_to_next_date("5m", date) == datetime( - 2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc) - # 10m -> 2019-08-12 13:30:00 - assert timeframe_to_next_date("10m", date) == datetime( - 2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) - # 1h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("1h", date) == datetime( - 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) - # 2h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("2h", date) == datetime( - 2019, 8, 12, 14, 00, 0, tzinfo=timezone.utc) - # 4h -> 2019-08-12 14:00:00 - assert timeframe_to_next_date("4h", date) == datetime( - 2019, 8, 12, 16, 00, 0, tzinfo=timezone.utc) - # 1d -> 2019-08-13 00:00:00 - assert timeframe_to_next_date("1d", date) == datetime( - 2019, 8, 13, 0, 0, 0, tzinfo=timezone.utc) + for interval, result in tf_list: + assert timeframe_to_next_date(interval, date) == result From c042d08bb73bbb00be88fb1afed5b2f147972035 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 16:29:09 +0200 Subject: [PATCH 05/14] Add lock_pairs to interface --- freqtrade/strategy/interface.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 37aa97bb1..363b0739a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -107,6 +107,7 @@ class IStrategy(ABC): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} + self._pair_locked_until: Dict[str, datetime] = {} @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -154,6 +155,13 @@ class IStrategy(ABC): """ return self.__class__.__name__ + def lock_pair(self, pair: str, until: datetime) -> None: + """ + Locks pair until a given timestamp happens. + Locked pairs are not analyzed, and are prevented from opening new trades. + """ + self._pair_locked_until['pair'] = until + def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given ticker history and returns a populated DataFrame From 200b6ea10faccbe0ce8f57aa221bf8301cfd8eda Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 19:50:22 +0200 Subject: [PATCH 06/14] Add is_pair_locked --- freqtrade/strategy/interface.py | 19 +++++++++++++++---- freqtrade/tests/strategy/test_interface.py | 16 ++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 363b0739a..e887da43b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -4,7 +4,7 @@ This module defines the interface to apply for strategies """ import logging from abc import ABC, abstractmethod -from datetime import datetime +from datetime import datetime, timezone from enum import Enum from typing import Dict, List, NamedTuple, Optional, Tuple import warnings @@ -159,8 +159,19 @@ class IStrategy(ABC): """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. + :param pair: Pair to lock + :param until: datetime in UTC until the pair should be blocked from opening new trades. + Needs to be timezone aware `datetime.now(timezone.utc)` """ - self._pair_locked_until['pair'] = until + self._pair_locked_until[pair] = until + + def is_pair_locked(self, pair: str) -> 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) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -268,8 +279,8 @@ class IStrategy(ABC): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluate if on the condition required to trigger a sell has been reached - if the threshold is reached and updates the trade record. + This function evaluate 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 :param force_stoploss: Externally provided stoploss diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 0eb7630a1..36c9ffcd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -286,3 +286,19 @@ def test__analyze_ticker_internal_skip_analyze(ticker_history, mocker, caplog) - assert ret['sell'].sum() == 0 assert not log_has('TA Analysis Launched', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog) + + +def test_is_pair_locked(default_conf): + strategy = DefaultStrategy(default_conf) + # dict should be empty + assert not strategy._pair_locked_until + + pair = 'ETH/BTC' + assert not strategy.is_pair_locked(pair) + strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert strategy.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not strategy.is_pair_locked(pair) From 2600cb7b641dd6ce0ab5da9aafb5ba3056ad8274 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:04:19 +0200 Subject: [PATCH 07/14] simplify timeframe_next_date calculation --- freqtrade/exchange/__init__.py | 4 +++- freqtrade/exchange/exchange.py | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 5c58320f6..828042911 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,6 +5,8 @@ from freqtrade.exchange.exchange import (is_exchange_bad, # noqa: F401 available_exchanges) from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_minutes, - timeframe_to_msecs) + timeframe_to_msecs, + timeframe_to_next_date, + timeframe_to_prev_date) from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fe5561be2..d0432a060 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -817,11 +817,9 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: :param date: date to use. Defaults to utcnow() :returns: date of next candle (with utc timezone) """ - if not date: - date = datetime.utcnow() + prevdate = timeframe_to_prev_date(timeframe, date) timeframe_secs = timeframe_to_seconds(timeframe) - # Get offset to prev timeframe - offset = date.timestamp() % timeframe_secs - # Add remaining seconds to next timeframe - new_timestamp = date.timestamp() + (timeframe_secs - offset) + + # Add one interval to previous candle + new_timestamp = prevdate.timestamp() + timeframe_secs return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) From 23a70932d299b00f6d3f44e5c95676adfca8b71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:36:45 +0200 Subject: [PATCH 08/14] Remove pointless tests (without config?? really?) --- freqtrade/tests/test_freqtradebot.py | 94 ---------------------------- 1 file changed, 94 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4e649250a..41ce661b9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2374,100 +2374,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, assert rpc_mock.call_count == 2 -def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, - ticker_sell_up, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Increase the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up - ) - freqtrade.config = {} - - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'profit', - 'limit': 1.172e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.172e-05, - 'profit_amount': 6.126e-05, - 'profit_percent': 0.0611052, - 'sell_reason': SellType.ROI.value - - } == last_msg - - -def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, - ticker_sell_down, markets, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - _load_markets=MagicMock(return_value={}), - get_ticker=ticker, - get_fee=fee, - markets=PropertyMock(return_value=markets) - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - # Create some test data - freqtrade.create_trade() - - trade = Trade.query.first() - assert trade - - # Decrease the price and sell it - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down - ) - - freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], - sell_reason=SellType.STOP_LOSS) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - assert { - 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', - 'pair': 'ETH/BTC', - 'gain': 'loss', - 'limit': 1.044e-05, - 'amount': 90.99181073703367, - 'order_type': 'limit', - 'open_rate': 1.099e-05, - 'current_rate': 1.044e-05, - 'profit_amount': -5.492e-05, - 'profit_percent': -0.05478342, - 'sell_reason': SellType.STOP_LOSS.value - } == last_msg - - def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) From ca739f71fb17b0c0437745ab257f67afc3b9823a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:37:11 +0200 Subject: [PATCH 09/14] Fix default argument handling for timeframe_to_nextdate --- freqtrade/exchange/exchange.py | 2 +- freqtrade/tests/exchange/test_exchange.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d0432a060..e36032c49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -801,7 +801,7 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime: :returns: date of previous candle (with utc timezone) """ if not date: - date = datetime.utcnow() + date = datetime.now(timezone.utc) timeframe_secs = timeframe_to_seconds(timeframe) # Get offset based on timerame_secs offset = date.timestamp() % timeframe_secs diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 23604f44f..3747a2ad3 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1588,6 +1588,9 @@ def test_timeframe_to_prev_date(): for interval, result in tf_list: assert timeframe_to_prev_date(interval, date) == result + date = datetime.now(tz=timezone.utc) + assert timeframe_to_prev_date("5m", date) < date + def test_timeframe_to_next_date(): # 2019-08-12 13:22:08 @@ -1609,3 +1612,6 @@ def test_timeframe_to_next_date(): for interval, result in tf_list: assert timeframe_to_next_date(interval, date) == result + + date = datetime.now(tz=timezone.utc) + assert timeframe_to_next_date("5m", date) > date From 59acd5ec7c0b5927f1386ab6a10ee948b9ad9612 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:39:34 +0200 Subject: [PATCH 10/14] Lock pair for the rest of the candle in case of sells --- freqtrade/freqtradebot.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 603b0631f..c14392752 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver @@ -284,6 +284,9 @@ class FreqtradeBot(object): # running get_signal on historical data fetched for _pair in whitelist: + if self.strategy.is_pair_locked(_pair): + logger.info(f"Pair {_pair} is currently locked.") + continue (buy, sell) = self.strategy.get_signal( _pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) @@ -885,6 +888,10 @@ class FreqtradeBot(object): trade.close_rate_requested = limit trade.sell_reason = sell_reason.value Trade.session.flush() + + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) + self._notify_sell(trade) def _notify_sell(self, trade: Trade): From 2961efdc1802bba064d9ead55df125fa2c0816a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Aug 2019 20:48:21 +0200 Subject: [PATCH 11/14] Initial test for locked pair --- freqtrade/tests/test_freqtradebot.py | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 41ce661b9..6f6a539eb 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2499,6 +2499,43 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke assert trade.sell_reason == SellType.SELL_SIGNAL.value +def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mocker, caplog) -> None: + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + _load_markets=MagicMock(return_value={}), + get_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.create_trade() + + trade = Trade.query.first() + assert trade + + # Decrease the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker_sell_down + ) + + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], + sell_reason=SellType.STOP_LOSS) + trade.close(ticker_sell_down()['bid']) + assert trade.pair in freqtrade.strategy._pair_locked_until + assert freqtrade.strategy.is_pair_locked(trade.pair) + + # reinit - should buy other pair. + caplog.clear() + freqtrade.create_trade() + + assert log_has(f"Pair {trade.pair} is currently locked.", caplog) + + def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 28e318b6466845900d52f73e5d07c0d25d05f7ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Aug 2019 08:47:11 +0200 Subject: [PATCH 12/14] Lock pairs for stoploss_on_exchange fills too --- freqtrade/freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c14392752..2d1c1f566 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -681,6 +681,9 @@ class FreqtradeBot(object): if stoploss_order and stoploss_order['status'] == 'closed': trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(stoploss_order) + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, + timeframe_to_next_date(self.config['ticker_interval'])) self._notify_sell(trade) return True From e35a3492299fd4961585a38b9a3dfedcf920a05b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Aug 2019 06:07:03 +0200 Subject: [PATCH 13/14] Fix spelling of interface.py docstring --- 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 e887da43b..99f5f26de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -279,7 +279,7 @@ class IStrategy(ABC): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluate if one of the conditions required to trigger a sell + 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 From f5e437d8c789dc9e383ae2dddfe1c249ffbc956c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Aug 2019 06:59:45 +0200 Subject: [PATCH 14/14] Change create_trade to create_trades for new test --- freqtrade/tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d6d547c29..24d070d2d 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -2611,7 +2611,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trade() + freqtrade.create_trades() trade = Trade.query.first() assert trade @@ -2630,7 +2630,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, markets, mock # reinit - should buy other pair. caplog.clear() - freqtrade.create_trade() + freqtrade.create_trades() assert log_has(f"Pair {trade.pair} is currently locked.", caplog)