Merge pull request #5855 from freqtrade/multi_ohlcv_calls

Provide more historic data in trade mode
This commit is contained in:
Matthias 2021-11-08 19:30:40 +01:00 committed by GitHub
commit f2be820f73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 35 deletions

View File

@ -134,7 +134,7 @@ Additional technical libraries can be installed as necessary, or custom indicato
### Strategy startup period ### Strategy startup period
Most indicators have an instable startup period, in which they are either not available, or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be. Most indicators have an instable startup period, in which they are either not available (NaN), or the calculation is incorrect. This can lead to inconsistencies, since Freqtrade does not know how long this instable period should be.
To account for this, the strategy can be assigned the `startup_candle_count` attribute. To account for this, the strategy can be assigned the `startup_candle_count` attribute.
This should be set to the maximum number of candles that the strategy requires to calculate stable indicators. This should be set to the maximum number of candles that the strategy requires to calculate stable indicators.
@ -146,8 +146,14 @@ In this example strategy, this should be set to 100 (`startup_candle_count = 100
By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt. By letting the bot know how much history is needed, backtest trades can start at the specified timerange during backtesting and hyperopt.
!!! Warning "Using x calls to get OHLCV"
If you receive a warning like `WARNING - Using 3 calls to get OHLCV. This can result in slower operations for the bot. Please check if you really need 1500 candles for your strategy` - you should consider if you really need this much historic data for your signals.
Having this will cause Freqtrade to make multiple calls for the same pair, which will obviously be slower than one network request.
As a consequence, Freqtrade will take longer to refresh candles - and should therefore be avoided if possible.
This is capped to 5 total calls to avoid overloading the exchange, or make freqtrade too slow.
!!! Warning !!! Warning
`startup_candle_count` should be below `ohlcv_candle_limit` (which is 500 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations. `startup_candle_count` should be below `ohlcv_candle_limit * 5` (which is 500 * 5 for most exchanges) - since only this amount of candles will be available during Dry-Run/Live Trade operations.
#### Example #### Example

View File

@ -1,6 +1,6 @@
""" Binance exchange subclass """ """ Binance exchange subclass """
import logging import logging
from typing import Dict, List from typing import Dict, List, Tuple
import arrow import arrow
import ccxt import ccxt
@ -93,8 +93,9 @@ class Binance(Exchange):
raise OperationalException(e) from e raise OperationalException(e) from e
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, is_new_pair: bool since_ms: int, is_new_pair: bool = False,
) -> List: raise_: bool = False
) -> Tuple[str, str, List]:
""" """
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0" Does not work for other exchanges, which don't return the earliest data when called with "0"
@ -107,4 +108,5 @@ class Binance(Exchange):
logger.info(f"Candle-data for {pair} available starting with " logger.info(f"Candle-data for {pair} available starting with "
f"{arrow.get(since_ms // 1000).isoformat()}.") f"{arrow.get(since_ms // 1000).isoformat()}.")
return await super()._async_get_historic_ohlcv( return await super()._async_get_historic_ohlcv(
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair) pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
raise_=raise_)

View File

@ -7,7 +7,7 @@ import http
import inspect import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from math import ceil from math import ceil
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@ -155,8 +155,8 @@ class Exchange:
self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {})) self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0), self.required_candle_call_count = self.validate_required_startup_candles(
config.get('timeframe', '')) config.get('startup_candle_count', 0), config.get('timeframe', ''))
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get( self.markets_refresh_interval: int = exchange_config.get(
@ -471,16 +471,29 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Time in force policies are not supported for {self.name} yet.') f'Time in force policies are not supported for {self.name} yet.')
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None: def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
""" """
Checks if required startup_candles is more than ohlcv_candle_limit(). Checks if required startup_candles is more than ohlcv_candle_limit().
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default. Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
""" """
candle_limit = self.ohlcv_candle_limit(timeframe) candle_limit = self.ohlcv_candle_limit(timeframe)
if startup_candles + 5 > candle_limit: # Require one more candle - to account for the still open candle.
candle_count = startup_candles + 1
# Allow 5 calls to the exchange per pair
required_candle_call_count = int(
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
if required_candle_call_count > 5:
# Only allow 5 calls per pair to somewhat limit the impact
raise OperationalException( raise OperationalException(
f"This strategy requires {startup_candles} candles to start. " f"This strategy requires {startup_candles} candles to start, which is more than 5x "
f"{self.name} only provides {candle_limit - 5} for {timeframe}.") f"the amount of candles {self.name} provides for {timeframe}.")
if required_candle_call_count > 1:
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
f"This can result in slower operations for the bot. Please check "
f"if you really need {startup_candles} candles for your strategy")
return required_candle_call_count
def exchange_has(self, endpoint: str) -> bool: def exchange_has(self, endpoint: str) -> bool:
""" """
@ -1205,9 +1218,11 @@ class Exchange:
:param since_ms: Timestamp in milliseconds to get history from :param since_ms: Timestamp in milliseconds to get history from
:return: List with candle (OHLCV) data :return: List with candle (OHLCV) data
""" """
return asyncio.get_event_loop().run_until_complete( pair, timeframe, data = asyncio.get_event_loop().run_until_complete(
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms, is_new_pair=is_new_pair)) since_ms=since_ms, is_new_pair=is_new_pair))
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
return data
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
since_ms: int) -> DataFrame: since_ms: int) -> DataFrame:
@ -1223,8 +1238,9 @@ class Exchange:
drop_incomplete=self._ohlcv_partial_candle) drop_incomplete=self._ohlcv_partial_candle)
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, is_new_pair: bool since_ms: int, is_new_pair: bool = False,
) -> List: raise_: bool = False
) -> Tuple[str, str, List]:
""" """
Download historic ohlcv Download historic ohlcv
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading :param is_new_pair: used by binance subclass to allow "fast" new pair downloading
@ -1248,15 +1264,17 @@ class Exchange:
for res in results: for res in results:
if isinstance(res, Exception): if isinstance(res, Exception):
logger.warning("Async code raised an exception: %s", res.__class__.__name__) logger.warning("Async code raised an exception: %s", res.__class__.__name__)
if raise_:
raise
continue continue
# Deconstruct tuple if it's not an exception else:
p, _, new_data = res # Deconstruct tuple if it's not an exception
if p == pair: p, _, new_data = res
data.extend(new_data) if p == pair:
data.extend(new_data)
# Sort data again after extending the result - above calls return in "async order" # Sort data again after extending the result - above calls return in "async order"
data = sorted(data, key=lambda x: x[0]) data = sorted(data, key=lambda x: x[0])
logger.info(f"Downloaded data for {pair} with length {len(data)}.") return pair, timeframe, data
return data
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
since_ms: Optional[int] = None, cache: bool = True since_ms: Optional[int] = None, cache: bool = True
@ -1276,10 +1294,22 @@ class Exchange:
cached_pairs = [] cached_pairs = []
# Gather coroutines to run # Gather coroutines to run
for pair, timeframe in set(pair_list): for pair, timeframe in set(pair_list):
if (((pair, timeframe) not in self._klines) if ((pair, timeframe) not 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, if not since_ms and self.required_candle_call_count > 1:
since_ms=since_ms)) # Multiple calls for one pair - to get more history
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
move_to = one_call * self.required_candle_call_count
now = timeframe_to_next_date(timeframe)
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
if since_ms:
input_coroutines.append(self._async_get_historic_ohlcv(
pair, timeframe, since_ms=since_ms, raise_=True))
else:
# One call ... "regular" refresh
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 ...",

View File

@ -126,13 +126,16 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/BTC' pair = 'ETH/BTC'
res = await exchange._async_get_historic_ohlcv(pair, "5m", respair, restf, res = await exchange._async_get_historic_ohlcv(
1500000000000, is_new_pair=False) pair, "5m", 1500000000000, is_new_pair=False)
assert respair == pair
assert restf == '5m'
# Call with very old timestamp - causes tons of requests # Call with very old timestamp - causes tons of requests
assert exchange._api_async.fetch_ohlcv.call_count > 400 assert exchange._api_async.fetch_ohlcv.call_count > 400
# assert res == ohlcv # assert res == ohlcv
exchange._api_async.fetch_ohlcv.reset_mock() exchange._api_async.fetch_ohlcv.reset_mock()
res = await exchange._async_get_historic_ohlcv(pair, "5m", 1500000000000, is_new_pair=True) _, _, res = await exchange._async_get_historic_ohlcv(
pair, "5m", 1500000000000, is_new_pair=True)
# Called twice - one "init" call - and one to get the actual data. # Called twice - one "init" call - and one to get the actual data.
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2

View File

@ -941,9 +941,22 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
default_conf['startup_candle_count'] = 20 default_conf['startup_candle_count'] = 20
ex = Exchange(default_conf) ex = Exchange(default_conf)
assert ex assert ex
default_conf['startup_candle_count'] = 600 # assumption is that the exchange provides 500 candles per call.s
assert ex.validate_required_startup_candles(200, '5m') == 1
assert ex.validate_required_startup_candles(499, '5m') == 1
assert ex.validate_required_startup_candles(600, '5m') == 2
assert ex.validate_required_startup_candles(501, '5m') == 2
assert ex.validate_required_startup_candles(499, '5m') == 1
assert ex.validate_required_startup_candles(1000, '5m') == 3
assert ex.validate_required_startup_candles(2499, '5m') == 5
assert log_has_re(r'Using 5 calls to get OHLCV. This.*', caplog)
with pytest.raises(OperationalException, match=r'This strategy requires 600.*'): with pytest.raises(OperationalException, match=r'This strategy requires 2500.*'):
ex.validate_required_startup_candles(2500, '5m')
# Ensure the same also happens on init
default_conf['startup_candle_count'] = 6000
with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'):
Exchange(default_conf) Exchange(default_conf)
@ -1506,6 +1519,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
assert exchange._async_get_candle_history.call_count == 2 assert exchange._async_get_candle_history.call_count == 2
# Returns twice the above OHLCV data # Returns twice the above OHLCV data
assert len(ret) == 2 assert len(ret) == 2
assert log_has_re(r'Downloaded data for .* with length .*\.', caplog)
caplog.clear() caplog.clear()
@ -1587,12 +1601,13 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv)
pair = 'ETH/USDT' pair = 'ETH/USDT'
res = await exchange._async_get_historic_ohlcv(pair, "5m", respair, restf, res = await exchange._async_get_historic_ohlcv(
1500000000000, is_new_pair=False) pair, "5m", 1500000000000, is_new_pair=False)
assert respair == pair
assert restf == '5m'
# Call with very old timestamp - causes tons of requests # Call with very old timestamp - causes tons of requests
assert exchange._api_async.fetch_ohlcv.call_count > 200 assert exchange._api_async.fetch_ohlcv.call_count > 200
assert res[0] == ohlcv[0] assert res[0] == ohlcv[0]
assert log_has_re(r'Downloaded data for .* with length .*\.', caplog)
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
@ -1630,12 +1645,14 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 2
exchange._api_async.fetch_ohlcv.reset_mock() exchange._api_async.fetch_ohlcv.reset_mock()
exchange.required_candle_call_count = 2
res = exchange.refresh_latest_ohlcv(pairs) res = exchange.refresh_latest_ohlcv(pairs)
assert len(res) == len(pairs) assert len(res) == len(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)
assert exchange._klines assert exchange._klines
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 4
exchange._api_async.fetch_ohlcv.reset_mock()
for pair in pairs: for pair in pairs:
assert isinstance(exchange.klines(pair), DataFrame) assert isinstance(exchange.klines(pair), DataFrame)
assert len(exchange.klines(pair)) > 0 assert len(exchange.klines(pair)) > 0
@ -1651,7 +1668,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')]) res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m')])
assert len(res) == len(pairs) assert len(res) == len(pairs)
assert exchange._api_async.fetch_ohlcv.call_count == 2 assert exchange._api_async.fetch_ohlcv.call_count == 0
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
f"timeframe {pairs[0][1]} ...", f"timeframe {pairs[0][1]} ...",
caplog) caplog)