Merge pull request #4069 from freqtrade/refactor_ohlcv_download

Refactor pairlist ohlcv download to use async
This commit is contained in:
Matthias 2020-12-16 19:11:49 +01:00 committed by GitHub
commit 8441d0f60f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 156 additions and 96 deletions

View File

@ -44,7 +44,8 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
ARGS_TEST_PAIRLIST = ["config", "quote_currencies", "print_one_column", "list_pairs_print_json"] ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
"list_pairs_print_json"]
ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] ARGS_CREATE_USERDIR = ["user_data_dir", "reset"]

View File

@ -733,13 +733,17 @@ class Exchange:
logger.info("Downloaded data for %s with length %s.", pair, len(data)) logger.info("Downloaded data for %s with length %s.", pair, len(data))
return data return data
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes) -> List[Tuple[str, List]]: def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
since_ms: Optional[int] = None, cache: bool = True
) -> Dict[Tuple[str, str], DataFrame]:
""" """
Refresh in-memory OHLCV asynchronously and set `_klines` with the result Refresh in-memory OHLCV asynchronously and set `_klines` with the result
Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). Loops asynchronously over pair_list and downloads all pairs async (semi-parallel).
Only used in the dataprovider.refresh() method. Only used in the dataprovider.refresh() method.
:param pair_list: List of 2 element tuples containing pair, interval to refresh :param pair_list: List of 2 element tuples containing pair, interval to refresh
:return: TODO: return value is only used in the tests, get rid of it :param since_ms: time since when to download, in milliseconds
:param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists
:return: Dict of [{(pair, timeframe): Dataframe}]
""" """
logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list))
@ -749,7 +753,8 @@ class Exchange:
for pair, timeframe in set(pair_list): for pair, timeframe in set(pair_list):
if (not ((pair, timeframe) in self._klines) if (not ((pair, timeframe) in self._klines)
or self._now_is_time_to_refresh(pair, timeframe)): or self._now_is_time_to_refresh(pair, timeframe)):
input_coroutines.append(self._async_get_candle_history(pair, timeframe)) input_coroutines.append(self._async_get_candle_history(pair, timeframe,
since_ms=since_ms))
else: else:
logger.debug( logger.debug(
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...", "Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
@ -759,6 +764,7 @@ class Exchange:
results = asyncio.get_event_loop().run_until_complete( results = asyncio.get_event_loop().run_until_complete(
asyncio.gather(*input_coroutines, return_exceptions=True)) asyncio.gather(*input_coroutines, return_exceptions=True))
results_df = {}
# handle caching # handle caching
for res in results: for res in results:
if isinstance(res, Exception): if isinstance(res, Exception):
@ -770,11 +776,13 @@ class Exchange:
if ticks: if ticks:
self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000
# keeping parsed dataframe in cache # keeping parsed dataframe in cache
self._klines[(pair, timeframe)] = ohlcv_to_dataframe( ohlcv_df = ohlcv_to_dataframe(
ticks, timeframe, pair=pair, fill_missing=True, ticks, timeframe, pair=pair, fill_missing=True,
drop_incomplete=self._ohlcv_partial_candle) drop_incomplete=self._ohlcv_partial_candle)
results_df[(pair, timeframe)] = ohlcv_df
return results if cache:
self._klines[(pair, timeframe)] = ohlcv_df
return results_df
def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool:
# Timeframe in seconds # Timeframe in seconds

View File

@ -2,7 +2,7 @@
Minimum age (days listed) pair list filter Minimum age (days listed) pair list filter
""" """
import logging import logging
from typing import Any, Dict from typing import Any, Dict, List
import arrow import arrow
@ -49,35 +49,32 @@ class AgeFilter(IPairList):
return (f"{self.name} - Filtering pairs with age less than " return (f"{self.name} - Filtering pairs with age less than "
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
def _validate_pair(self, ticker: Dict) -> bool: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Validate age for the ticker :param pairlist: pairlist to filter or sort
:param ticker: ticker dict as returned from ccxt.load_markets() :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: True if the pair can stay, False if it should be removed :return: new allowlist
""" """
needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked]
# Check symbol in cache if not needed_pairs:
if ticker['symbol'] in self._symbolsChecked: return pairlist
return True
since_ms = int(arrow.utcnow() since_ms = int(arrow.utcnow()
.floor('day') .floor('day')
.shift(days=-self._min_days_listed) .shift(days=-self._min_days_listed - 1)
.float_timestamp) * 1000 .float_timestamp) * 1000
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False)
pairlist_new = []
if self._enabled:
for p, _ in needed_pairs:
daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'], age = len(candles[(p, '1d')]) if (p, '1d') in candles else 0
timeframe='1d', if age > self._min_days_listed:
since_ms=since_ms) pairlist_new.append(p)
self._symbolsChecked[p] = int(arrow.utcnow().float_timestamp) * 1000
if daily_candles is not None: else:
if len(daily_candles) > self._min_days_listed: self.log_once(f"Removed {p} from whitelist, because age "
# We have fetched at least the minimum required number of daily candles f"{age} is less than {self._min_days_listed} "
# Add to cache, store the time we last checked this symbol f"{plural(self._min_days_listed, 'day')}", logger.info)
self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 logger.info(f"Validated {len(pairlist_new)} pairs.")
return True return pairlist_new
else:
self.log_once(f"Removed {ticker['symbol']} from whitelist, because age "
f"{len(daily_candles)} is less than {self._min_days_listed} "
f"{plural(self._min_days_listed, 'day')}", logger.info)
return False
return False

View File

@ -60,13 +60,14 @@ class IPairList(LoggingMixin, ABC):
-> Please overwrite in subclasses -> Please overwrite in subclasses
""" """
def _validate_pair(self, ticker) -> bool: def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
""" """
Check one pair against Pairlist Handler's specific conditions. Check one pair against Pairlist Handler's specific conditions.
Either implement it in the Pairlist Handler or override the generic Either implement it in the Pairlist Handler or override the generic
filter_pairlist() method. filter_pairlist() method.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
@ -109,7 +110,7 @@ class IPairList(LoggingMixin, ABC):
# Copy list since we're modifying this list # Copy list since we're modifying this list
for p in deepcopy(pairlist): for p in deepcopy(pairlist):
# Filter out assets # Filter out assets
if not self._validate_pair(tickers[p]): if not self._validate_pair(p, tickers[p] if p in tickers else {}):
pairlist.remove(p) pairlist.remove(p)
return pairlist return pairlist

View File

@ -43,19 +43,20 @@ class PrecisionFilter(IPairList):
""" """
return f"{self.name} - Filtering untradable pairs." return f"{self.name} - Filtering untradable pairs."
def _validate_pair(self, ticker: dict) -> bool: def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
""" """
Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very
low value pairs. low value pairs.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, False if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
stop_price = ticker['ask'] * self._stoploss stop_price = ticker['ask'] * self._stoploss
# Adjust stop-prices to precision # Adjust stop-prices to precision
sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) sp = self._exchange.price_to_precision(pair, stop_price)
stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99)
logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}")
if sp <= stop_gap_price: if sp <= stop_gap_price:

View File

@ -57,31 +57,32 @@ class PriceFilter(IPairList):
return f"{self.name} - No price filters configured." return f"{self.name} - No price filters configured."
def _validate_pair(self, ticker) -> bool: def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
""" """
Check if if one price-step (pip) is > than a certain barrier. Check if if one price-step (pip) is > than a certain barrier.
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker['last'] is None or ticker['last'] == 0: if ticker['last'] is None or ticker['last'] == 0:
self.log_once(f"Removed {ticker['symbol']} from whitelist, because " self.log_once(f"Removed {pair} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).", "ticker['last'] is empty (Usually no trade in the last 24h).",
logger.info) logger.info)
return False return False
# Perform low_price_ratio check. # Perform low_price_ratio check.
if self._low_price_ratio != 0: if self._low_price_ratio != 0:
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) compare = self._exchange.price_get_one_pip(pair, ticker['last'])
changeperc = compare / ticker['last'] changeperc = compare / ticker['last']
if changeperc > self._low_price_ratio: if changeperc > self._low_price_ratio:
self.log_once(f"Removed {ticker['symbol']} from whitelist, " self.log_once(f"Removed {pair} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%", logger.info) f"because 1 unit is {changeperc * 100:.3f}%", logger.info)
return False return False
# Perform min_price check. # Perform min_price check.
if self._min_price != 0: if self._min_price != 0:
if ticker['last'] < self._min_price: if ticker['last'] < self._min_price:
self.log_once(f"Removed {ticker['symbol']} from whitelist, " self.log_once(f"Removed {pair} from whitelist, "
f"because last price < {self._min_price:.8f}", logger.info) f"because last price < {self._min_price:.8f}", logger.info)
return False return False

View File

@ -36,16 +36,17 @@ class SpreadFilter(IPairList):
return (f"{self.name} - Filtering pairs with ask/bid diff above " return (f"{self.name} - Filtering pairs with ask/bid diff above "
f"{self._max_spread_ratio * 100}%.") f"{self._max_spread_ratio * 100}%.")
def _validate_pair(self, ticker: dict) -> bool: def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
""" """
Validate spread for the ticker Validate spread for the ticker
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, False if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if 'bid' in ticker and 'ask' in ticker: if 'bid' in ticker and 'ask' in ticker:
spread = 1 - ticker['bid'] / ticker['ask'] spread = 1 - ticker['bid'] / ticker['ask']
if spread > self._max_spread_ratio: if spread > self._max_spread_ratio:
self.log_once(f"Removed {ticker['symbol']} from whitelist, because spread " self.log_once(f"Removed {pair} from whitelist, because spread "
f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%", f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%",
logger.info) logger.info)
return False return False

View File

@ -3,7 +3,7 @@ PairList manager class
""" """
import logging import logging
from copy import deepcopy from copy import deepcopy
from typing import Dict, List from typing import Any, Dict, List
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
@ -97,7 +97,7 @@ class PairListManager():
self._whitelist = pairlist self._whitelist = pairlist
def _prepare_whitelist(self, pairlist: List[str], tickers) -> List[str]: def _prepare_whitelist(self, pairlist: List[str], tickers: Dict[str, Any]) -> List[str]:
""" """
Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove
pairs that do not have ticker available pairs that do not have ticker available

View File

@ -2,10 +2,12 @@
Rate of change pairlist filter Rate of change pairlist filter
""" """
import logging import logging
from typing import Any, Dict from copy import deepcopy
from typing import Any, Dict, List, Optional
import arrow import arrow
from cachetools.ttl import TTLCache from cachetools.ttl import TTLCache
from pandas import DataFrame
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural from freqtrade.misc import plural
@ -42,7 +44,7 @@ class RangeStabilityFilter(IPairList):
If no Pairlist requires tickers, an empty List is passed If no Pairlist requires tickers, an empty List is passed
as tickers argument to filter_pairlist as tickers argument to filter_pairlist
""" """
return True return False
def short_desc(self) -> str: def short_desc(self) -> str:
""" """
@ -51,25 +53,43 @@ class RangeStabilityFilter(IPairList):
return (f"{self.name} - Filtering pairs with rate of change below " return (f"{self.name} - Filtering pairs with rate of change below "
f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.")
def _validate_pair(self, ticker: Dict) -> bool: def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
""" """
Validate trading range Validate trading range
:param ticker: ticker dict as returned from ccxt.load_markets() :param pairlist: pairlist to filter or sort
:return: True if the pair can stay, False if it should be removed :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new allowlist
"""
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
since_ms = int(arrow.utcnow()
.floor('day')
.shift(days=-self._days - 1)
.float_timestamp) * 1000
# Get all candles
candles = {}
if needed_pairs:
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms,
cache=False)
if self._enabled:
for p in deepcopy(pairlist):
daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None
if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p)
return pairlist
def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool:
"""
Validate trading range
:param pair: Pair that's currently validated
:param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed
""" """
pair = ticker['symbol']
# Check symbol in cache # Check symbol in cache
if pair in self._pair_cache: if pair in self._pair_cache:
return self._pair_cache[pair] return self._pair_cache[pair]
since_ms = int(arrow.utcnow()
.floor('day')
.shift(days=-self._days)
.float_timestamp) * 1000
daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair,
timeframe='1d',
since_ms=since_ms)
result = False result = False
if daily_candles is not None and not daily_candles.empty: if daily_candles is not None and not daily_candles.empty:
highest_high = daily_candles['high'].max() highest_high = daily_candles['high'].max()
@ -79,7 +99,7 @@ class RangeStabilityFilter(IPairList):
result = True result = True
else: else:
self.log_once(f"Removed {pair} from whitelist, because rate of change " self.log_once(f"Removed {pair} from whitelist, because rate of change "
f"over {plural(self._days, 'day')} is {pct_change:.3f}, " f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, "
f"which is below the threshold of {self._min_rate_of_change}.", f"which is below the threshold of {self._min_rate_of_change}.",
logger.info) logger.info)
result = False result = False

View File

@ -1084,7 +1084,7 @@ def ohlcv_history_list():
@pytest.fixture @pytest.fixture
def ohlcv_history(ohlcv_history_list): def ohlcv_history(ohlcv_history_list):
return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC", return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC",
fill_missing=True) fill_missing=True, drop_incomplete=False)
@pytest.fixture @pytest.fixture

View File

@ -1385,6 +1385,12 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]
# empty dicts # empty dicts
assert not exchange._klines assert not exchange._klines
exchange.refresh_latest_ohlcv(pairs, cache=False)
# No caching
assert not exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2
exchange._api_async.fetch_ohlcv.reset_mock()
exchange.refresh_latest_ohlcv(pairs) exchange.refresh_latest_ohlcv(pairs)
assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog)
@ -1499,11 +1505,9 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
assert exchange._klines assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
assert type(res) is list assert type(res) is dict
assert len(res) == 2 assert len(res) == 1
# Test that each is in list at least once as order is not guaranteed # Test that each is in list at least once as order is not guaranteed
assert type(res[0]) is tuple or type(res[1]) is tuple
assert type(res[0]) is TypeError or type(res[1]) is TypeError
assert log_has("Error loading ETH/BTC. Result was [[]].", caplog) assert log_has("Error loading ETH/BTC. Result was [[]].", caplog)
assert log_has("Async code raised an exception: TypeError", caplog) assert log_has("Async code raised an exception: TypeError", caplog)

View File

@ -353,11 +353,19 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
"BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']),
]) ])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
ohlcv_history_list, pairlists, base_currency, ohlcv_history, pairlists, base_currency,
whitelist_result, caplog) -> None: whitelist_result, caplog) -> None:
whitelist_conf['pairlists'] = pairlists whitelist_conf['pairlists'] = pairlists
whitelist_conf['stake_currency'] = base_currency whitelist_conf['stake_currency'] = base_currency
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
('XRP/BTC', '1d'): ohlcv_history,
('HOT/BTC', '1d'): ohlcv_history,
}
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
if whitelist_result == 'static_in_the_middle': if whitelist_result == 'static_in_the_middle':
@ -374,7 +382,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
) )
# Provide for PerformanceFilter's dependency # Provide for PerformanceFilter's dependency
@ -402,7 +410,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
for pairlist in pairlists: for pairlist in pairlists:
if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \
len(ohlcv_history_list) <= pairlist['min_days_listed']: len(ohlcv_history) <= pairlist['min_days_listed']:
assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' assert log_has_re(r'^Removed .* from whitelist, because age .* is less than '
r'.* day.*', caplog) r'.* day.*', caplog)
if pairlist['method'] == 'PrecisionFilter' and whitelist_result: if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
@ -575,8 +583,13 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick
get_patched_freqtradebot(mocker, default_conf) get_patched_freqtradebot(mocker, default_conf)
def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list): def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history):
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
('XRP/BTC', '1d'): ohlcv_history,
}
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets), markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
@ -584,18 +597,18 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
) )
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter)
assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
# Should not have increased since first call. # Should not have increased since first call.
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count
def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
@ -625,7 +638,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
(0.01, 5), (0.01, 5),
(0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist.
]) ])
def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history,
min_rate_of_change, expected_length): min_rate_of_change, expected_length):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'RangeStabilityFilter', 'lookback_days': 2, {'method': 'RangeStabilityFilter', 'lookback_days': 2,
@ -636,22 +649,30 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh
exchange_has=MagicMock(return_value=True), exchange_has=MagicMock(return_value=True),
get_tickers=tickers get_tickers=tickers
) )
ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history,
('LTC/BTC', '1d'): ohlcv_history,
('XRP/BTC', '1d'): ohlcv_history,
('HOT/BTC', '1d'): ohlcv_history,
('BLK/BTC', '1d'): ohlcv_history,
}
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
) )
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == expected_length assert len(freqtrade.pairlists.whitelist) == expected_length
assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == expected_length assert len(freqtrade.pairlists.whitelist) == expected_length
# Should not have increased since first call. # Should not have increased since first call.
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count
@pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [

View File

@ -870,7 +870,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets):
def test_api_pair_candles(botclient, ohlcv_history): def test_api_pair_candles(botclient, ohlcv_history):
ftbot, client = botclient ftbot, client = botclient
timeframe = '5m' timeframe = '5m'
amount = 2 amount = 3
# No pair # No pair
rc = client_get(client, rc = client_get(client,
@ -910,8 +910,8 @@ def test_api_pair_candles(botclient, ohlcv_history):
assert 'data_stop_ts' in rc.json assert 'data_stop_ts' in rc.json
assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00'
assert rc.json['data_start_ts'] == 1511686200000 assert rc.json['data_start_ts'] == 1511686200000
assert rc.json['data_stop'] == '2017-11-26 08:55:00+00:00' assert rc.json['data_stop'] == '2017-11-26 09:00:00+00:00'
assert rc.json['data_stop_ts'] == 1511686500000 assert rc.json['data_stop_ts'] == 1511686800000
assert isinstance(rc.json['columns'], list) assert isinstance(rc.json['columns'], list)
assert rc.json['columns'] == ['date', 'open', 'high', assert rc.json['columns'] == ['date', 'open', 'high',
'low', 'close', 'volume', 'sma', 'buy', 'sell', 'low', 'close', 'volume', 'sma', 'buy', 'sell',
@ -926,7 +926,10 @@ def test_api_pair_candles(botclient, ohlcv_history):
[['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869,
None, 0, 0, 1511686200000, None, None], None, 0, 0, 1511686200000, None, None],
['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05,
8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None] 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None],
['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05,
0.7039405, 8.885000000000002e-05, 0, 0, 1511686800000, None, None]
]) ])

View File

@ -128,27 +128,29 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
def test_assert_df(default_conf, mocker, ohlcv_history, caplog): def test_assert_df(default_conf, mocker, ohlcv_history, caplog):
df_len = len(ohlcv_history) - 1
# Ensure it's running when passed correctly # Ensure it's running when passed correctly
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date'])
with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."): with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1, _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1,
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date'])
with pytest.raises(StrategyError, with pytest.raises(StrategyError,
match=r"Dataframe returned from strategy.*last close price\."): match=r"Dataframe returned from strategy.*last close price\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date']) ohlcv_history.loc[df_len, 'close'] + 0.01,
ohlcv_history.loc[df_len, 'date'])
with pytest.raises(StrategyError, with pytest.raises(StrategyError,
match=r"Dataframe returned from strategy.*last date\."): match=r"Dataframe returned from strategy.*last date\."):
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date'])
_STRATEGY.disable_dataframe_checks = True _STRATEGY.disable_dataframe_checks = True
caplog.clear() caplog.clear()
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) ohlcv_history.loc[2, 'close'], ohlcv_history.loc[0, 'date'])
assert log_has_re(r"Dataframe returned from strategy.*last date\.", caplog) assert log_has_re(r"Dataframe returned from strategy.*last date\.", caplog)
# reset to avoid problems in other tests due to test leakage # reset to avoid problems in other tests due to test leakage
_STRATEGY.disable_dataframe_checks = False _STRATEGY.disable_dataframe_checks = False