Merge pull request #2131 from freqtrade/lock_pairs

Lock pairs
This commit is contained in:
Matthias 2019-08-15 07:21:00 +02:00 committed by GitHub
commit 585536835a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 200 additions and 9 deletions

View File

@ -6,6 +6,8 @@ from freqtrade.exchange.exchange import (get_exchange_bad_reason, # 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

View File

@ -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
@ -790,13 +790,45 @@ 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_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.now(timezone.utc)
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) -> 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)
"""
prevdate = timeframe_to_prev_date(timeframe, date)
timeframe_secs = timeframe_to_seconds(timeframe)
# Add one interval to previous candle
new_timestamp = prevdate.timestamp() + timeframe_secs
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)

View File

@ -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
@ -283,6 +283,9 @@ class FreqtradeBot(object):
buycount = 0
# 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))
@ -674,6 +677,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
@ -884,6 +890,10 @@ class FreqtradeBot(object):
if order.get('status', 'unknown') == 'closed':
trade.update(order)
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):

View File

@ -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
@ -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,24 @@ 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.
: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
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:
"""
Parses the given ticker history and returns a populated DataFrame
@ -260,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 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
:param force_stoploss: Externally provided stoploss

View File

@ -14,7 +14,11 @@ 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_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
@ -1540,3 +1544,74 @@ 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
def test_timeframe_to_prev_date():
# 2019-08-12 13:22:08
date = datetime.fromtimestamp(1565616128, tz=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
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
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)),
]
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

View File

@ -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)

View File

@ -2598,6 +2598,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_trades()
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_trades()
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)