commit
3d36d35e30
@ -79,6 +79,12 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
data_format_trades=config['dataformat_trades'],
|
data_format_trades=config['dataformat_trades'],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
if not exchange._ft_has.get('ohlcv_has_history', True):
|
||||||
|
raise OperationalException(
|
||||||
|
f"Historic klines not available for {exchange.name}. "
|
||||||
|
"Please use `--dl-trades` instead for this exchange "
|
||||||
|
"(will unfortunately take a long time)."
|
||||||
|
)
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
|
||||||
datadir=config['datadir'], timerange=timerange,
|
datadir=config['datadir'], timerange=timerange,
|
||||||
|
@ -64,6 +64,7 @@ class Exchange:
|
|||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_params": {},
|
"ohlcv_params": {},
|
||||||
"ohlcv_candle_limit": 500,
|
"ohlcv_candle_limit": 500,
|
||||||
|
"ohlcv_has_history": True, # Some exchanges (Kraken) don't provide history via ohlcv
|
||||||
"ohlcv_partial_candle": True,
|
"ohlcv_partial_candle": True,
|
||||||
"ohlcv_require_since": False,
|
"ohlcv_require_since": False,
|
||||||
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
# Check https://github.com/ccxt/ccxt/issues/10767 for removal of ohlcv_volume_currency
|
||||||
@ -308,12 +309,15 @@ class Exchange:
|
|||||||
if self.log_responses:
|
if self.log_responses:
|
||||||
logger.info(f"API {endpoint}: {response}")
|
logger.info(f"API {endpoint}: {response}")
|
||||||
|
|
||||||
def ohlcv_candle_limit(self, timeframe: str) -> int:
|
def ohlcv_candle_limit(
|
||||||
|
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||||
"""
|
"""
|
||||||
Exchange ohlcv candle limit
|
Exchange ohlcv candle limit
|
||||||
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limits
|
||||||
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
|
||||||
:param timeframe: Timeframe to check
|
:param timeframe: Timeframe to check
|
||||||
|
:param candle_type: Candle-type
|
||||||
|
:param since_ms: Starting timestamp
|
||||||
:return: Candle limit as integer
|
:return: Candle limit as integer
|
||||||
"""
|
"""
|
||||||
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
|
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
|
||||||
@ -615,19 +619,28 @@ class Exchange:
|
|||||||
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, self._config['candle_type_def'],
|
||||||
|
int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000)
|
||||||
|
if timeframe else None)
|
||||||
# Require one more candle - to account for the still open candle.
|
# Require one more candle - to account for the still open candle.
|
||||||
candle_count = startup_candles + 1
|
candle_count = startup_candles + 1
|
||||||
# Allow 5 calls to the exchange per pair
|
# Allow 5 calls to the exchange per pair
|
||||||
required_candle_call_count = int(
|
required_candle_call_count = int(
|
||||||
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
|
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
|
||||||
|
if self._ft_has['ohlcv_has_history']:
|
||||||
|
|
||||||
if required_candle_call_count > 5:
|
if required_candle_call_count > 5:
|
||||||
# Only allow 5 calls per pair to somewhat limit the impact
|
# Only allow 5 calls per pair to somewhat limit the impact
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f"This strategy requires {startup_candles} candles to start, which is more than 5x "
|
f"This strategy requires {startup_candles} candles to start, "
|
||||||
|
"which is more than 5x "
|
||||||
|
f"the amount of candles {self.name} provides for {timeframe}.")
|
||||||
|
elif required_candle_call_count > 1:
|
||||||
|
raise OperationalException(
|
||||||
|
f"This strategy requires {startup_candles} candles to start, which is more than "
|
||||||
f"the amount of candles {self.name} provides for {timeframe}.")
|
f"the amount of candles {self.name} provides for {timeframe}.")
|
||||||
|
|
||||||
if required_candle_call_count > 1:
|
if required_candle_call_count > 1:
|
||||||
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
|
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"This can result in slower operations for the bot. Please check "
|
||||||
@ -1703,7 +1716,8 @@ class Exchange:
|
|||||||
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
:param candle_type: Any of the enum CandleType (must match trading mode!)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
|
||||||
|
timeframe, candle_type, since_ms)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"one_call: %s msecs (%s)",
|
"one_call: %s msecs (%s)",
|
||||||
one_call,
|
one_call,
|
||||||
@ -1739,7 +1753,8 @@ class Exchange:
|
|||||||
if (not since_ms
|
if (not since_ms
|
||||||
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
|
and (self._ft_has["ohlcv_require_since"] or self.required_candle_call_count > 1)):
|
||||||
# Multiple calls for one pair - to get more history
|
# Multiple calls for one pair - to get more history
|
||||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(
|
||||||
|
timeframe, candle_type, since_ms)
|
||||||
move_to = one_call * self.required_candle_call_count
|
move_to = one_call * self.required_candle_call_count
|
||||||
now = timeframe_to_next_date(timeframe)
|
now = timeframe_to_next_date(timeframe)
|
||||||
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
||||||
@ -1857,7 +1872,9 @@ class Exchange:
|
|||||||
pair, timeframe, since_ms, s
|
pair, timeframe, since_ms, s
|
||||||
)
|
)
|
||||||
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
params = deepcopy(self._ft_has.get('ohlcv_params', {}))
|
||||||
candle_limit = self.ohlcv_candle_limit(timeframe)
|
candle_limit = self.ohlcv_candle_limit(
|
||||||
|
timeframe, candle_type=candle_type, since_ms=since_ms)
|
||||||
|
|
||||||
if candle_type != CandleType.SPOT:
|
if candle_type != CandleType.SPOT:
|
||||||
params.update({'price': candle_type})
|
params.update({'price': candle_type})
|
||||||
if candle_type != CandleType.FUNDING_RATE:
|
if candle_type != CandleType.FUNDING_RATE:
|
||||||
@ -2674,9 +2691,10 @@ def timeframe_to_msecs(timeframe: str) -> int:
|
|||||||
|
|
||||||
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
||||||
"""
|
"""
|
||||||
Use Timeframe and determine last possible candle.
|
Use Timeframe and determine the candle start date for this date.
|
||||||
|
Does not round when given a candle start date.
|
||||||
:param timeframe: timeframe in string format (e.g. "5m")
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
:param date: date to use. Defaults to utcnow()
|
:param date: date to use. Defaults to now(utc)
|
||||||
:returns: date of previous candle (with utc timezone)
|
:returns: date of previous candle (with utc timezone)
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
@ -2691,7 +2709,7 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
|||||||
"""
|
"""
|
||||||
Use Timeframe and determine next candle.
|
Use Timeframe and determine next candle.
|
||||||
:param timeframe: timeframe in string format (e.g. "5m")
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
:param date: date to use. Defaults to utcnow()
|
:param date: date to use. Defaults to now(utc)
|
||||||
:returns: date of next candle (with utc timezone)
|
:returns: date of next candle (with utc timezone)
|
||||||
"""
|
"""
|
||||||
if not date:
|
if not date:
|
||||||
@ -2701,6 +2719,23 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
|||||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def date_minus_candles(
|
||||||
|
timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
|
||||||
|
"""
|
||||||
|
subtract X candles from a date.
|
||||||
|
:param timeframe: timeframe in string format (e.g. "5m")
|
||||||
|
:param candle_count: Amount of candles to subtract.
|
||||||
|
:param date: date to use. Defaults to now(utc)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not date:
|
||||||
|
date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
tf_min = timeframe_to_minutes(timeframe)
|
||||||
|
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
|
||||||
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
def market_is_active(market: Dict) -> bool:
|
def market_is_active(market: Dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if the market is active.
|
Return True if the market is active.
|
||||||
|
@ -23,6 +23,7 @@ class Kraken(Exchange):
|
|||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
"ohlcv_candle_limit": 720,
|
"ohlcv_candle_limit": 720,
|
||||||
|
"ohlcv_has_history": False,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.constants import BuySell
|
from freqtrade.constants import BuySell
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
from freqtrade.exchange.exchange import date_minus_candles
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -20,7 +22,7 @@ class Okx(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"ohlcv_candle_limit": 100,
|
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
|
||||||
"mark_ohlcv_timeframe": "4h",
|
"mark_ohlcv_timeframe": "4h",
|
||||||
"funding_fee_timeframe": "8h",
|
"funding_fee_timeframe": "8h",
|
||||||
}
|
}
|
||||||
@ -37,6 +39,27 @@ class Okx(Exchange):
|
|||||||
|
|
||||||
net_only = True
|
net_only = True
|
||||||
|
|
||||||
|
def ohlcv_candle_limit(
|
||||||
|
self, timeframe: str, candle_type: CandleType, since_ms: Optional[int] = None) -> int:
|
||||||
|
"""
|
||||||
|
Exchange ohlcv candle limit
|
||||||
|
OKX has the following behaviour:
|
||||||
|
* 300 candles for uptodate data
|
||||||
|
* 100 candles for historic data
|
||||||
|
* 100 candles for additional candles (not futures or spot).
|
||||||
|
:param timeframe: Timeframe to check
|
||||||
|
:param candle_type: Candle-type
|
||||||
|
:param since_ms: Starting timestamp
|
||||||
|
:return: Candle limit as integer
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
candle_type in (CandleType.FUTURES, CandleType.SPOT) and
|
||||||
|
(not since_ms or since_ms > (date_minus_candles(timeframe, 300).timestamp() * 1000))
|
||||||
|
):
|
||||||
|
return 300
|
||||||
|
|
||||||
|
return super().ohlcv_candle_limit(timeframe, candle_type, since_ms)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def additional_exchange_init(self) -> None:
|
def additional_exchange_init(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
@ -32,18 +32,19 @@ class AgeFilter(IPairList):
|
|||||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||||
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
self._max_days_listed = pairlistconfig.get('max_days_listed', None)
|
||||||
|
|
||||||
|
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
||||||
if self._min_days_listed < 1:
|
if self._min_days_listed < 1:
|
||||||
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
|
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
|
||||||
if self._min_days_listed > exchange.ohlcv_candle_limit('1d'):
|
if self._min_days_listed > candle_limit:
|
||||||
raise OperationalException("AgeFilter requires min_days_listed to not exceed "
|
raise OperationalException("AgeFilter requires min_days_listed to not exceed "
|
||||||
"exchange max request size "
|
"exchange max request size "
|
||||||
f"({exchange.ohlcv_candle_limit('1d')})")
|
f"({candle_limit})")
|
||||||
if self._max_days_listed and self._max_days_listed <= self._min_days_listed:
|
if self._max_days_listed and self._max_days_listed <= self._min_days_listed:
|
||||||
raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted")
|
raise OperationalException("AgeFilter max_days_listed <= min_days_listed not permitted")
|
||||||
if self._max_days_listed and self._max_days_listed > exchange.ohlcv_candle_limit('1d'):
|
if self._max_days_listed and self._max_days_listed > candle_limit:
|
||||||
raise OperationalException("AgeFilter requires max_days_listed to not exceed "
|
raise OperationalException("AgeFilter requires max_days_listed to not exceed "
|
||||||
"exchange max request size "
|
"exchange max request size "
|
||||||
f"({exchange.ohlcv_candle_limit('1d')})")
|
f"({candle_limit})")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
@ -38,12 +38,12 @@ class VolatilityFilter(IPairList):
|
|||||||
|
|
||||||
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
||||||
|
|
||||||
|
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
||||||
if self._days < 1:
|
if self._days < 1:
|
||||||
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
|
raise OperationalException("VolatilityFilter requires lookback_days to be >= 1")
|
||||||
if self._days > exchange.ohlcv_candle_limit('1d'):
|
if self._days > candle_limit:
|
||||||
raise OperationalException("VolatilityFilter requires lookback_days to not "
|
raise OperationalException("VolatilityFilter requires lookback_days to not "
|
||||||
"exceed exchange max request size "
|
f"exceed exchange max request size ({candle_limit})")
|
||||||
f"({exchange.ohlcv_candle_limit('1d')})")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
@ -84,12 +84,13 @@ class VolumePairList(IPairList):
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'key {self._sort_key} not in {SORT_VALUES}')
|
f'key {self._sort_key} not in {SORT_VALUES}')
|
||||||
|
|
||||||
|
candle_limit = exchange.ohlcv_candle_limit(
|
||||||
|
self._lookback_timeframe, self._config['candle_type_def'])
|
||||||
if self._lookback_period < 0:
|
if self._lookback_period < 0:
|
||||||
raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
|
raise OperationalException("VolumeFilter requires lookback_period to be >= 0")
|
||||||
if self._lookback_period > exchange.ohlcv_candle_limit(self._lookback_timeframe):
|
if self._lookback_period > candle_limit:
|
||||||
raise OperationalException("VolumeFilter requires lookback_period to not "
|
raise OperationalException("VolumeFilter requires lookback_period to not "
|
||||||
"exceed exchange max request size "
|
f"exceed exchange max request size ({candle_limit})")
|
||||||
f"({exchange.ohlcv_candle_limit(self._lookback_timeframe)})")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
@ -33,12 +33,12 @@ class RangeStabilityFilter(IPairList):
|
|||||||
|
|
||||||
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
|
||||||
|
|
||||||
|
candle_limit = exchange.ohlcv_candle_limit('1d', self._config['candle_type_def'])
|
||||||
if self._days < 1:
|
if self._days < 1:
|
||||||
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
|
||||||
if self._days > exchange.ohlcv_candle_limit('1d'):
|
if self._days > candle_limit:
|
||||||
raise OperationalException("RangeStabilityFilter requires lookback_days to not "
|
raise OperationalException("RangeStabilityFilter requires lookback_days to not "
|
||||||
"exceed exchange max request size "
|
f"exceed exchange max request size ({candle_limit})")
|
||||||
f"({exchange.ohlcv_candle_limit('1d')})")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
|
@ -835,6 +835,23 @@ def test_download_data_trades(mocker, caplog):
|
|||||||
start_download_data(pargs)
|
start_download_data(pargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_data_data_invalid(mocker):
|
||||||
|
patch_exchange(mocker, id="kraken")
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange", "kraken",
|
||||||
|
"--pairs", "ETH/BTC", "XRP/BTC",
|
||||||
|
"--days", "20",
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
with pytest.raises(OperationalException, match=r"Historic klines not available for .*"):
|
||||||
|
start_download_data(pargs)
|
||||||
|
|
||||||
|
|
||||||
def test_start_convert_trades(mocker, caplog):
|
def test_start_convert_trades(mocker, caplog):
|
||||||
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv',
|
||||||
MagicMock(return_value=[]))
|
MagicMock(return_value=[]))
|
||||||
|
@ -13,6 +13,7 @@ import pytest
|
|||||||
|
|
||||||
from freqtrade.enums import CandleType
|
from freqtrade.enums import CandleType
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
|
||||||
|
from freqtrade.exchange.exchange import timeframe_to_msecs
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from tests.conftest import get_default_conf_usdt
|
from tests.conftest import get_default_conf_usdt
|
||||||
|
|
||||||
@ -219,7 +220,7 @@ class TestCCXTExchange():
|
|||||||
assert len(l2['asks']) == next_limit
|
assert len(l2['asks']) == next_limit
|
||||||
assert len(l2['asks']) == next_limit
|
assert len(l2['asks']) == next_limit
|
||||||
|
|
||||||
def test_fetch_ohlcv(self, exchange):
|
def test_ccxt_fetch_ohlcv(self, exchange):
|
||||||
exchange, exchangename = exchange
|
exchange, exchangename = exchange
|
||||||
pair = EXCHANGES[exchangename]['pair']
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
timeframe = EXCHANGES[exchangename]['timeframe']
|
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||||
@ -231,11 +232,44 @@ class TestCCXTExchange():
|
|||||||
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
|
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
|
||||||
# assert len(exchange.klines(pair_tf)) > 200
|
# assert len(exchange.klines(pair_tf)) > 200
|
||||||
# Assume 90% uptime ...
|
# Assume 90% uptime ...
|
||||||
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(timeframe) * 0.90
|
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(
|
||||||
|
timeframe, CandleType.SPOT) * 0.90
|
||||||
# Check if last-timeframe is within the last 2 intervals
|
# Check if last-timeframe is within the last 2 intervals
|
||||||
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
|
||||||
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
|
||||||
|
|
||||||
|
def test_ccxt__async_get_candle_history(self, exchange):
|
||||||
|
exchange, exchangename = exchange
|
||||||
|
# For some weired reason, this test returns random lengths for bittrex.
|
||||||
|
if not exchange._ft_has['ohlcv_has_history'] or exchangename == 'bittrex':
|
||||||
|
return
|
||||||
|
pair = EXCHANGES[exchangename]['pair']
|
||||||
|
timeframe = EXCHANGES[exchangename]['timeframe']
|
||||||
|
candle_type = CandleType.SPOT
|
||||||
|
timeframe_ms = timeframe_to_msecs(timeframe)
|
||||||
|
now = timeframe_to_prev_date(
|
||||||
|
timeframe, datetime.now(timezone.utc))
|
||||||
|
for offset in (360, 120, 30, 10, 5, 2):
|
||||||
|
since = now - timedelta(days=offset)
|
||||||
|
since_ms = int(since.timestamp() * 1000)
|
||||||
|
|
||||||
|
res = exchange.loop.run_until_complete(exchange._async_get_candle_history(
|
||||||
|
pair=pair,
|
||||||
|
timeframe=timeframe,
|
||||||
|
since_ms=since_ms,
|
||||||
|
candle_type=candle_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert res
|
||||||
|
assert res[0] == pair
|
||||||
|
assert res[1] == timeframe
|
||||||
|
assert res[2] == candle_type
|
||||||
|
candles = res[3]
|
||||||
|
candle_count = exchange.ohlcv_candle_limit(timeframe, candle_type, since_ms) * 0.9
|
||||||
|
candle_count1 = (now.timestamp() * 1000 - since_ms) // timeframe_ms
|
||||||
|
assert len(candles) >= min(candle_count, candle_count1)
|
||||||
|
assert candles[0][0] == since_ms or (since_ms + timeframe_ms)
|
||||||
|
|
||||||
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
def test_ccxt_fetch_funding_rate_history(self, exchange_futures):
|
||||||
exchange, exchangename = exchange_futures
|
exchange, exchangename = exchange_futures
|
||||||
if not exchange:
|
if not exchange:
|
||||||
|
@ -17,9 +17,9 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOr
|
|||||||
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
||||||
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT,
|
||||||
calculate_backoff, remove_credentials)
|
calculate_backoff, remove_credentials)
|
||||||
from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs,
|
from freqtrade.exchange.exchange import (date_minus_candles, market_is_active, timeframe_to_minutes,
|
||||||
timeframe_to_next_date, timeframe_to_prev_date,
|
timeframe_to_msecs, timeframe_to_next_date,
|
||||||
timeframe_to_seconds)
|
timeframe_to_prev_date, timeframe_to_seconds)
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
|
from tests.conftest import get_mock_coro, get_patched_exchange, log_has, log_has_re, num_log_has_re
|
||||||
|
|
||||||
@ -939,6 +939,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||||
|
# TODO: this test does not assert ...
|
||||||
del default_conf["timeframe"]
|
del default_conf["timeframe"]
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
id_mock = PropertyMock(return_value='test_exchange')
|
id_mock = PropertyMock(return_value='test_exchange')
|
||||||
@ -954,6 +955,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
|
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_required_startup_candles')
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
@ -1084,6 +1086,13 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
|
|||||||
with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'):
|
with pytest.raises(OperationalException, match=r'This strategy requires 6000.*'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
# Emulate kraken mode
|
||||||
|
ex._ft_has['ohlcv_has_history'] = False
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'This strategy requires 2500.*, '
|
||||||
|
r'which is more than the amount.*'):
|
||||||
|
ex.validate_required_startup_candles(2500, '5m')
|
||||||
|
|
||||||
|
|
||||||
def test_exchange_has(default_conf, mocker):
|
def test_exchange_has(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
@ -1875,7 +1884,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name, candle_
|
|||||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
|
|
||||||
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8
|
since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
|
||||||
ret = exchange.get_historic_ohlcv(
|
ret = exchange.get_historic_ohlcv(
|
||||||
pair,
|
pair,
|
||||||
"5m",
|
"5m",
|
||||||
@ -1941,7 +1950,7 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name, candle_ty
|
|||||||
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
# one_call calculation * 1.8 should do 2 calls
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
|
|
||||||
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8
|
since = 5 * 60 * exchange.ohlcv_candle_limit('5m', CandleType.SPOT) * 1.8
|
||||||
ret = exchange.get_historic_ohlcv_as_df(
|
ret = exchange.get_historic_ohlcv_as_df(
|
||||||
pair,
|
pair,
|
||||||
"5m",
|
"5m",
|
||||||
@ -1995,7 +2004,7 @@ async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_
|
|||||||
)
|
)
|
||||||
# Required candles
|
# Required candles
|
||||||
candles = (end_ts - start_ts) / 300_000
|
candles = (end_ts - start_ts) / 300_000
|
||||||
exp = candles // exchange.ohlcv_candle_limit('5m') + 1
|
exp = candles // exchange.ohlcv_candle_limit('5m', CandleType.SPOT) + 1
|
||||||
|
|
||||||
# Depending on the exchange, this should be called between 1 and 6 times.
|
# Depending on the exchange, this should be called between 1 and 6 times.
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == exp
|
assert exchange._api_async.fetch_ohlcv.call_count == exp
|
||||||
@ -3342,7 +3351,7 @@ def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
|
|||||||
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
|
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
|
||||||
# This should only run for bittrex
|
# This should only run for bittrex
|
||||||
assert exchange_name == 'bittrex'
|
assert exchange_name == 'bittrex'
|
||||||
assert exchange.ohlcv_candle_limit(timeframe) == expected
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_timeframe_to_minutes():
|
def test_timeframe_to_minutes():
|
||||||
@ -3424,6 +3433,17 @@ def test_timeframe_to_next_date():
|
|||||||
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
|
assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_date_minus_candles():
|
||||||
|
|
||||||
|
date = datetime(2019, 8, 12, 13, 25, 0, tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
assert date_minus_candles("5m", 3, date) == date - timedelta(minutes=15)
|
||||||
|
assert date_minus_candles("5m", 5, date) == date - timedelta(minutes=25)
|
||||||
|
assert date_minus_candles("1m", 6, date) == date - timedelta(minutes=6)
|
||||||
|
assert date_minus_candles("1h", 3, date) == date - timedelta(hours=3, minutes=25)
|
||||||
|
assert date_minus_candles("1h", 3) == timeframe_to_prev_date('1h') - timedelta(hours=3)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result",
|
"market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result",
|
||||||
[
|
[
|
||||||
|
@ -1,12 +1,42 @@
|
|||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.enums import MarginMode, TradingMode
|
from freqtrade.enums import MarginMode, TradingMode
|
||||||
|
from freqtrade.enums.candletype import CandleType
|
||||||
|
from freqtrade.exchange.exchange import timeframe_to_minutes
|
||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
def test_okx_ohlcv_candle_limit(default_conf, mocker):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='okx')
|
||||||
|
timeframes = ('1m', '5m', '1h')
|
||||||
|
start_time = int(datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp() * 1000)
|
||||||
|
|
||||||
|
for timeframe in timeframes:
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT) == 300
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES) == 300
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK) == 100
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE) == 100
|
||||||
|
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, start_time) == 100
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, start_time) == 100
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.MARK, start_time) == 100
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUNDING_RATE, start_time) == 100
|
||||||
|
one_call = int((datetime.now(timezone.utc) - timedelta(
|
||||||
|
minutes=290 * timeframe_to_minutes(timeframe))).timestamp() * 1000)
|
||||||
|
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 300
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 300
|
||||||
|
|
||||||
|
one_call = int((datetime.now(timezone.utc) - timedelta(
|
||||||
|
minutes=320 * timeframe_to_minutes(timeframe))).timestamp() * 1000)
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.SPOT, one_call) == 100
|
||||||
|
assert exchange.ohlcv_candle_limit(timeframe, CandleType.FUTURES, one_call) == 100
|
||||||
|
|
||||||
|
|
||||||
def test_get_maintenance_ratio_and_amt_okx(
|
def test_get_maintenance_ratio_and_amt_okx(
|
||||||
default_conf,
|
default_conf,
|
||||||
mocker,
|
mocker,
|
||||||
|
Loading…
Reference in New Issue
Block a user