commit
585536835a
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user