commit
585536835a
@ -6,6 +6,8 @@ from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
|||||||
available_exchanges)
|
available_exchanges)
|
||||||
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
|
||||||
timeframe_to_minutes,
|
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.kraken import Kraken # noqa: F401
|
||||||
from freqtrade.exchange.binance import Binance # noqa: F401
|
from freqtrade.exchange.binance import Binance # noqa: F401
|
||||||
|
@ -6,7 +6,7 @@ import asyncio
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from math import ceil, floor
|
from math import ceil, floor
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
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:
|
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
|
return ccxt.Exchange.parse_timeframe(ticker_interval) // 60
|
||||||
|
|
||||||
|
|
||||||
def timeframe_to_msecs(ticker_interval: str) -> int:
|
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
|
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.converter import order_book_to_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
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.persistence import Trade
|
||||||
from freqtrade.rpc import RPCManager, RPCMessageType
|
from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver
|
||||||
@ -283,6 +283,9 @@ class FreqtradeBot(object):
|
|||||||
buycount = 0
|
buycount = 0
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
for _pair in whitelist:
|
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(
|
(buy, sell) = self.strategy.get_signal(
|
||||||
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
|
_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':
|
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
trade.update(stoploss_order)
|
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)
|
self._notify_sell(trade)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -884,6 +890,10 @@ class FreqtradeBot(object):
|
|||||||
if order.get('status', 'unknown') == 'closed':
|
if order.get('status', 'unknown') == 'closed':
|
||||||
trade.update(order)
|
trade.update(order)
|
||||||
Trade.session.flush()
|
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)
|
self._notify_sell(trade)
|
||||||
|
|
||||||
def _notify_sell(self, trade: Trade):
|
def _notify_sell(self, trade: Trade):
|
||||||
|
@ -4,7 +4,7 @@ This module defines the interface to apply for strategies
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||||
import warnings
|
import warnings
|
||||||
@ -107,6 +107,7 @@ class IStrategy(ABC):
|
|||||||
self.config = config
|
self.config = config
|
||||||
# Dict to determine if analysis is necessary
|
# Dict to determine if analysis is necessary
|
||||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
|
self._pair_locked_until: Dict[str, datetime] = {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
@ -154,6 +155,24 @@ class IStrategy(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
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:
|
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Parses the given ticker history and returns a populated 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,
|
sell: bool, low: float = None, high: float = None,
|
||||||
force_stoploss: float = 0) -> SellCheckTuple:
|
force_stoploss: float = 0) -> SellCheckTuple:
|
||||||
"""
|
"""
|
||||||
This function evaluate if on the condition required to trigger a sell has been reached
|
This function evaluates if one of the conditions required to trigger a sell
|
||||||
if the threshold is reached and updates the trade record.
|
has been reached, which can either be a stop-loss, ROI or sell-signal.
|
||||||
:param low: Only used during backtesting to simulate stoploss
|
:param low: Only used during backtesting to simulate stoploss
|
||||||
:param high: Only used during backtesting, to simulate ROI
|
:param high: Only used during backtesting, to simulate ROI
|
||||||
:param force_stoploss: Externally provided stoploss
|
:param force_stoploss: Externally provided stoploss
|
||||||
|
@ -14,7 +14,11 @@ from pandas import DataFrame
|
|||||||
from freqtrade import (DependencyException, InvalidOrderException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Binance, Exchange, Kraken
|
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.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re
|
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"
|
assert ex.get_valid_pair_combination("BTC", "ETH") == "ETH/BTC"
|
||||||
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
|
with pytest.raises(DependencyException, match=r"Could not combine.* to get a valid pair."):
|
||||||
ex.get_valid_pair_combination("NOPAIR", "ETH")
|
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 ret['sell'].sum() == 0
|
||||||
assert not log_has('TA Analysis Launched', caplog)
|
assert not log_has('TA Analysis Launched', caplog)
|
||||||
assert log_has('Skipping TA Analysis for already analyzed candle', 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
|
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:
|
def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
Loading…
Reference in New Issue
Block a user