Merge pull request #6550 from freqtrade/short_tickerproblems

Short tickerproblems
This commit is contained in:
Matthias 2022-03-19 15:43:40 +01:00 committed by GitHub
commit 7d8ca63752
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 246 additions and 42 deletions

View File

@ -12,6 +12,7 @@ from freqtrade.enums import CandleType, MarginMode, TradingMode
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.misc import deep_merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -22,7 +23,6 @@ class Binance(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop_loss_limit"}, "stoploss_order_types": {"limit": "stop_loss_limit"},
"stoploss_order_types_futures": {"limit": "stop"},
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce", "time_in_force_parameter": "timeInForce",
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
@ -31,6 +31,10 @@ class Binance(Exchange):
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "future" "ccxt_futures_name": "future"
} }
_ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop"},
"tickers_have_price": False,
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
@ -53,6 +57,15 @@ class Binance(Exchange):
(side == "buy" and stop_loss < float(order['info']['stopPrice'])) (side == "buy" and stop_loss < float(order['info']['stopPrice']))
) )
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES:
# Binance's future result has no bid/ask values.
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
bidsasks = self.fetch_bids_asks(symbols, cached)
tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False)
return tickers
@retrier @retrier
def _set_leverage( def _set_leverage(
self, self,

View File

@ -65,6 +65,8 @@ class Exchange:
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
# 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
"ohlcv_volume_currency": "base", # "base" or "quote" "ohlcv_volume_currency": "base", # "base" or "quote"
"tickers_have_quoteVolume": True,
"tickers_have_price": True,
"trades_pagination": "time", # Possible are "time" or "id" "trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
"l2_limit_range": None, "l2_limit_range": None,
@ -74,6 +76,7 @@ class Exchange:
"ccxt_futures_name": "swap", "ccxt_futures_name": "swap",
} }
_ft_has: Dict = {} _ft_has: Dict = {}
_ft_has_futures: Dict = {}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
@ -101,7 +104,7 @@ class Exchange:
self._last_markets_refresh: int = 0 self._last_markets_refresh: int = 0
# Cache for 10 minutes ... # Cache for 10 minutes ...
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=2, ttl=60 * 10)
# Cache values for 1800 to avoid frequent polling of the exchange for prices # Cache values for 1800 to avoid frequent polling of the exchange for prices
# Caching only applies to RPC methods, so prices for open trades are still # Caching only applies to RPC methods, so prices for open trades are still
# refreshed once every iteration. # refreshed once every iteration.
@ -121,8 +124,19 @@ class Exchange:
exchange_config = config['exchange'] exchange_config = config['exchange']
self.log_responses = exchange_config.get('log_responses', False) self.log_responses = exchange_config.get('log_responses', False)
# Leverage properties
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
self.margin_mode: Optional[MarginMode] = (
MarginMode(config.get('margin_mode'))
if config.get('margin_mode')
else None
)
self.liquidation_buffer = config.get('liquidation_buffer', 0.05)
# Deep merge ft_has with default ft_has options # Deep merge ft_has with default ft_has options
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
if self.trading_mode == TradingMode.FUTURES:
self._ft_has = deep_merge_dicts(self._ft_has_futures, self._ft_has)
if exchange_config.get('_ft_has_params'): if exchange_config.get('_ft_has_params'):
self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'), self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'),
self._ft_has) self._ft_has)
@ -134,15 +148,6 @@ class Exchange:
self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination = self._ft_has['trades_pagination']
self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
# Leverage properties
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
self.margin_mode: Optional[MarginMode] = (
MarginMode(config.get('margin_mode'))
if config.get('margin_mode')
else None
)
self.liquidation_buffer = config.get('liquidation_buffer', 0.05)
# Initialize ccxt objects # Initialize ccxt objects
ccxt_config = self._ccxt_config ccxt_config = self._ccxt_config
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
@ -176,6 +181,8 @@ class Exchange:
self.required_candle_call_count = self.validate_required_startup_candles( self.required_candle_call_count = self.validate_required_startup_candles(
config.get('startup_candle_count', 0), config.get('timeframe', '')) config.get('startup_candle_count', 0), config.get('timeframe', ''))
self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode) self.validate_trading_mode_and_margin_mode(self.trading_mode, self.margin_mode)
self.validate_pricing(config['ask_strategy'])
self.validate_pricing(config['bid_strategy'])
# 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(
@ -569,6 +576,14 @@ class Exchange:
f'On exchange stoploss is not supported for {self.name}.' f'On exchange stoploss is not supported for {self.name}.'
) )
def validate_pricing(self, pricing: Dict) -> None:
if pricing.get('use_order_book', False) and not self.exchange_has('fetchL2OrderBook'):
raise OperationalException(f'Orderbook not available for {self.name}.')
if (not pricing.get('use_order_book', False) and (
not self.exchange_has('fetchTicker')
or not self._ft_has['tickers_have_price'])):
raise OperationalException(f'Ticker pricing not available for {self.name}.')
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
""" """
Checks if order time in force configured in strategy/config are supported Checks if order time in force configured in strategy/config are supported
@ -1010,10 +1025,6 @@ class Exchange:
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]: def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"] available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
if self.trading_mode == TradingMode.FUTURES:
# Optionally use different order type for stop order
available_order_Types = self._ft_has.get('stoploss_order_types_futures',
self._ft_has["stoploss_order_types"])
if user_order_type in available_order_Types.keys(): if user_order_type in available_order_Types.keys():
ordertype = available_order_Types[user_order_type] ordertype = available_order_Types[user_order_type]
@ -1288,6 +1299,34 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier
def fetch_bids_asks(self, symbols: List[str] = None, cached: bool = False) -> Dict:
"""
:param cached: Allow cached result
:return: fetch_tickers result
"""
if not self.exchange_has('fetchBidsAsks'):
return {}
if cached:
tickers = self._fetch_tickers_cache.get('fetch_bids_asks')
if tickers:
return tickers
try:
tickers = self._api.fetch_bids_asks(symbols)
self._fetch_tickers_cache['fetch_bids_asks'] = tickers
return tickers
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching bids/asks in batch. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load bids/asks due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier @retrier
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict: def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
""" """
@ -1397,7 +1436,7 @@ class Exchange:
conf_strategy = self._config.get(strat_name, {}) conf_strategy = self._config.get(strat_name, {})
if conf_strategy.get('use_order_book', False) and ('use_order_book' in conf_strategy): if conf_strategy.get('use_order_book', False):
order_book_top = conf_strategy.get('order_book_top', 1) order_book_top = conf_strategy.get('order_book_top', 1)
order_book = self.fetch_l2_order_book(pair, order_book_top) order_book = self.fetch_l2_order_book(pair, order_book_top)

View File

@ -23,6 +23,9 @@ class Okx(Exchange):
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h", "funding_fee_timeframe": "8h",
} }
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [ _supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list

View File

@ -129,7 +129,7 @@ def format_ms_time(date: int) -> str:
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')
def deep_merge_dicts(source, destination): def deep_merge_dicts(source, destination, allow_null_overrides: bool = True):
""" """
Values from Source override destination, destination is returned (and modified!!) Values from Source override destination, destination is returned (and modified!!)
Sample: Sample:
@ -142,8 +142,8 @@ def deep_merge_dicts(source, destination):
if isinstance(value, dict): if isinstance(value, dict):
# get node or create one # get node or create one
node = destination.setdefault(key, {}) node = destination.setdefault(key, {})
deep_merge_dicts(value, node) deep_merge_dicts(value, node, allow_null_overrides)
else: elif value is not None or allow_null_overrides:
destination[key] = value destination[key] = value
return destination return destination

View File

@ -71,10 +71,13 @@ class VolumePairList(IPairList):
f'to at least {self._tf_in_sec} and restart the bot.' f'to at least {self._tf_in_sec} and restart the bot.'
) )
if not self._exchange.exchange_has('fetchTickers'): if (not self._use_range and not (
self._exchange.exchange_has('fetchTickers')
and self._exchange._ft_has["tickers_have_quoteVolume"])):
raise OperationalException( raise OperationalException(
'Exchange does not support dynamic whitelist. ' "Exchange does not support dynamic whitelist in this configuration. "
'Please edit your config and restart the bot.' "Please edit your config and either remove Volumepairlist, "
"or switch to using candles. and restart the bot."
) )
if not self._validate_keys(self._sort_key): if not self._validate_keys(self._sort_key):
@ -95,7 +98,7 @@ class VolumePairList(IPairList):
If no Pairlist requires tickers, an empty Dict is passed If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist as tickers argument to filter_pairlist
""" """
return True return not self._use_range
def _validate_keys(self, key): def _validate_keys(self, key):
return key in SORT_VALUES return key in SORT_VALUES
@ -126,13 +129,15 @@ class VolumePairList(IPairList):
tradable_only=True, active_only=True).keys()] tradable_only=True, active_only=True).keys()]
# No point in testing for blacklisted pairs... # No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info) _pairlist = self.verify_blacklist(_pairlist, logger.info)
if not self._use_range:
filtered_tickers = [ filtered_tickers = [
v for k, v in tickers.items() v for k, v in tickers.items()
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v[self._sort_key] is not None) and (self._use_range or v[self._sort_key] is not None)
and v['symbol'] in _pairlist)] and v['symbol'] in _pairlist)]
pairlist = [s['symbol'] for s in filtered_tickers] pairlist = [s['symbol'] for s in filtered_tickers]
else:
pairlist = _pairlist
pairlist = self.filter_pairlist(pairlist, tickers) pairlist = self.filter_pairlist(pairlist, tickers)
self._pair_cache['pairlist'] = pairlist.copy() self._pair_cache['pairlist'] = pairlist.copy()
@ -147,11 +152,11 @@ class VolumePairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached. :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist :return: new whitelist
""" """
# Use the incoming pairlist. if self._use_range:
filtered_tickers = [v for k, v in tickers.items() if k in pairlist] # Create bare minimum from tickers structure.
filtered_tickers: List[Dict[str, Any]] = [{'symbol': k} for k in pairlist]
# get lookback period in ms, for exchange ohlcv fetch # get lookback period in ms, for exchange ohlcv fetch
if self._use_range:
since_ms = int(arrow.utcnow() since_ms = int(arrow.utcnow()
.floor('minute') .floor('minute')
.shift(minutes=-(self._lookback_period * self._tf_in_min) .shift(minutes=-(self._lookback_period * self._tf_in_min)
@ -208,6 +213,9 @@ class VolumePairList(IPairList):
filtered_tickers[i]['quoteVolume'] = quoteVolume filtered_tickers[i]['quoteVolume'] = quoteVolume
else: else:
filtered_tickers[i]['quoteVolume'] = 0 filtered_tickers[i]['quoteVolume'] = 0
else:
# Tickers mode - filter based on incomming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if self._min_value > 0: if self._min_value > 0:
filtered_tickers = [ filtered_tickers = [

View File

@ -104,6 +104,7 @@ def patch_exchange(
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))

View File

@ -107,6 +107,8 @@ def exchange_conf():
config['exchange']['key'] = '' config['exchange']['key'] = ''
config['exchange']['secret'] = '' config['exchange']['secret'] = ''
config['dry_run'] = False config['dry_run'] = False
config['bid_strategy']['use_order_book'] = True
config['ask_strategy']['use_order_book'] = True
return config return config

View File

@ -167,6 +167,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
exchange = ExchangeResolver.load_exchange('zaif', default_conf) exchange = ExchangeResolver.load_exchange('zaif', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
@ -570,6 +571,7 @@ def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_markets') mocker.patch('freqtrade.exchange.Exchange._load_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
exchange = Exchange(default_conf) exchange = Exchange(default_conf)
exchange._api_async.load_markets = get_mock_coro(None) exchange._api_async.load_markets = get_mock_coro(None)
exchange._load_async_markets() exchange._load_async_markets()
@ -591,6 +593,7 @@ def test__load_markets(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to initialize markets.', caplog) assert log_has('Unable to initialize markets.', caplog)
@ -659,6 +662,7 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
Exchange(default_conf) Exchange(default_conf)
@ -731,6 +735,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
Exchange(default_conf) Exchange(default_conf)
@ -757,6 +762,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
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._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'):
@ -777,6 +783,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf) Exchange(default_conf)
@ -796,6 +803,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
Exchange(default_conf) Exchange(default_conf)
@ -812,6 +820,7 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
Exchange(default_conf) Exchange(default_conf)
assert type(api_mock).load_markets.call_count == 1 assert type(api_mock).load_markets.call_count == 1
@ -852,6 +861,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe):
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
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')
Exchange(default_conf) Exchange(default_conf)
@ -936,10 +946,49 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
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')
Exchange(default_conf) Exchange(default_conf)
def test_validate_order_types(default_conf, mocker): def test_validate_pricing(default_conf, mocker):
api_mock = MagicMock()
has = {
'fetchL2OrderBook': True,
'fetchTicker': True,
}
type(api_mock).has = PropertyMock(return_value=has)
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_margin_mode')
mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.name', 'Binance')
ExchangeResolver.load_exchange('binance', default_conf)
has.update({'fetchTicker': False})
with pytest.raises(OperationalException, match="Ticker pricing not available for .*"):
ExchangeResolver.load_exchange('binance', default_conf)
has.update({'fetchTicker': True})
default_conf['ask_strategy']['use_order_book'] = True
ExchangeResolver.load_exchange('binance', default_conf)
has.update({'fetchL2OrderBook': False})
with pytest.raises(OperationalException, match="Orderbook not available for .*"):
ExchangeResolver.load_exchange('binance', default_conf)
has.update({'fetchL2OrderBook': True})
# Binance has no tickers on futures
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED
with pytest.raises(OperationalException, match="Ticker pricing not available for .*"):
ExchangeResolver.load_exchange('binance', default_conf)
def test_validate_ordertypes(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True})
@ -948,6 +997,7 @@ def test_validate_order_types(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
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.name', 'Bittrex') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
default_conf['order_types'] = { default_conf['order_types'] = {
@ -988,6 +1038,7 @@ def test_validate_order_types_not_in_config(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
@ -1002,6 +1053,7 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_pricing')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
default_conf['startup_candle_count'] = 20 default_conf['startup_candle_count'] = 20
@ -1572,6 +1624,59 @@ def test_fetch_positions(default_conf, mocker, exchange_name):
"fetch_positions", "fetch_positions") "fetch_positions", "fetch_positions")
def test_fetch_bids_asks(default_conf, mocker):
api_mock = MagicMock()
tick = {'ETH/BTC': {
'symbol': 'ETH/BTC',
'bid': 0.5,
'ask': 1,
'last': 42,
}, 'BCH/BTC': {
'symbol': 'BCH/BTC',
'bid': 0.6,
'ask': 0.5,
'last': 41,
}
}
exchange_name = 'binance'
api_mock.fetch_bids_asks = MagicMock(return_value=tick)
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
# retrieve original ticker
bidsasks = exchange.fetch_bids_asks()
assert 'ETH/BTC' in bidsasks
assert 'BCH/BTC' in bidsasks
assert bidsasks['ETH/BTC']['bid'] == 0.5
assert bidsasks['ETH/BTC']['ask'] == 1
assert bidsasks['BCH/BTC']['bid'] == 0.6
assert bidsasks['BCH/BTC']['ask'] == 0.5
assert api_mock.fetch_bids_asks.call_count == 1
api_mock.fetch_bids_asks.reset_mock()
# Cached ticker should not call api again
tickers2 = exchange.fetch_bids_asks(cached=True)
assert tickers2 == bidsasks
assert api_mock.fetch_bids_asks.call_count == 0
tickers2 = exchange.fetch_bids_asks(cached=False)
assert api_mock.fetch_bids_asks.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
"fetch_bids_asks", "fetch_bids_asks")
with pytest.raises(OperationalException):
api_mock.fetch_bids_asks = MagicMock(side_effect=ccxt.NotSupported("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.fetch_bids_asks()
api_mock.fetch_bids_asks = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.fetch_bids_asks()
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
assert exchange.fetch_bids_asks() == {}
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_tickers(default_conf, mocker, exchange_name): def test_get_tickers(default_conf, mocker, exchange_name):
api_mock = MagicMock() api_mock = MagicMock()
@ -1588,6 +1693,7 @@ def test_get_tickers(default_conf, mocker, exchange_name):
} }
} }
api_mock.fetch_tickers = MagicMock(return_value=tick) api_mock.fetch_tickers = MagicMock(return_value=tick)
api_mock.fetch_bids_asks = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
# retrieve original ticker # retrieve original ticker
tickers = exchange.get_tickers() tickers = exchange.get_tickers()
@ -1599,6 +1705,7 @@ def test_get_tickers(default_conf, mocker, exchange_name):
assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5 assert tickers['BCH/BTC']['ask'] == 0.5
assert api_mock.fetch_tickers.call_count == 1 assert api_mock.fetch_tickers.call_count == 1
assert api_mock.fetch_bids_asks.call_count == 0
api_mock.fetch_tickers.reset_mock() api_mock.fetch_tickers.reset_mock()
@ -1606,8 +1713,10 @@ def test_get_tickers(default_conf, mocker, exchange_name):
tickers2 = exchange.get_tickers(cached=True) tickers2 = exchange.get_tickers(cached=True)
assert tickers2 == tickers assert tickers2 == tickers
assert api_mock.fetch_tickers.call_count == 0 assert api_mock.fetch_tickers.call_count == 0
assert api_mock.fetch_bids_asks.call_count == 0
tickers2 = exchange.get_tickers(cached=False) tickers2 = exchange.get_tickers(cached=False)
assert api_mock.fetch_tickers.call_count == 1 assert api_mock.fetch_tickers.call_count == 1
assert api_mock.fetch_bids_asks.call_count == 0
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
"get_tickers", "fetch_tickers") "get_tickers", "fetch_tickers")
@ -1621,6 +1730,17 @@ def test_get_tickers(default_conf, mocker, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_tickers() exchange.get_tickers()
api_mock.fetch_tickers.reset_mock()
api_mock.fetch_bids_asks.reset_mock()
default_conf['trading_mode'] = TradingMode.FUTURES
default_conf['margin_mode'] = MarginMode.ISOLATED
mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_tickers()
assert api_mock.fetch_tickers.call_count == 1
assert api_mock.fetch_bids_asks.call_count == (1 if exchange_name == 'binance' else 0)
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_fetch_ticker(default_conf, mocker, exchange_name): def test_fetch_ticker(default_conf, mocker, exchange_name):
@ -2977,7 +3097,8 @@ def test_merge_ft_has_dict(default_conf, mocker):
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
validate_stakecurrency=MagicMock() validate_stakecurrency=MagicMock(),
validate_pricing=MagicMock(),
) )
ex = Exchange(default_conf) ex = Exchange(default_conf)
assert ex._ft_has == Exchange._ft_has_default assert ex._ft_has == Exchange._ft_has_default
@ -3011,6 +3132,7 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets)) markets=PropertyMock(return_value=markets))
ex = Exchange(default_conf) ex = Exchange(default_conf)
@ -3102,6 +3224,7 @@ def test_get_markets(default_conf, mocker, markets_static,
_load_async_markets=MagicMock(), _load_async_markets=MagicMock(),
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
validate_timeframes=MagicMock(), validate_timeframes=MagicMock(),
validate_pricing=MagicMock(),
markets=PropertyMock(return_value=markets_static)) markets=PropertyMock(return_value=markets_static))
ex = Exchange(default_conf) ex = Exchange(default_conf)
pairs = ex.get_markets(base_currencies, pairs = ex.get_markets(base_currencies,

View File

@ -15,6 +15,7 @@ def test_validate_order_types_gateio(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
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.name', 'Bittrex') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
exch = ExchangeResolver.load_exchange('gateio', default_conf, True) exch = ExchangeResolver.load_exchange('gateio', default_conf, True)
assert isinstance(exch, Gateio) assert isinstance(exch, Gateio)

View File

@ -587,10 +587,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}],
"BTC", "binance", ['LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC', 'HOT/BTC']), "BTC", "binance", ['LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC', 'HOT/BTC']),
# expecting pairs from default tickers, because 1h candles are not available # expecting pairs as input, because 1h candles are not available
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1h", "lookback_period": 2, "refresh_period": 3600}], "lookback_timeframe": "1h", "lookback_period": 2, "refresh_period": 3600}],
"BTC", "binance", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'HOT/BTC', 'FUEL/BTC']), "BTC", "binance", ['ETH/BTC', 'LTC/BTC', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']),
# ftx data is already in Quote currency, therefore won't require conversion # ftx data is already in Quote currency, therefore won't require conversion
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume",
"lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}],

View File

@ -1,15 +1,16 @@
# pragma pylint: disable=missing-docstring,C0103 # pragma pylint: disable=missing-docstring,C0103
import datetime import datetime
from copy import deepcopy
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.misc import (decimals_per_coin, file_dump_json, file_load_json, format_ms_time, from freqtrade.misc import (decimals_per_coin, deep_merge_dicts, file_dump_json, file_load_json,
pair_to_filename, parse_db_uri_for_logging, plural, render_template, format_ms_time, pair_to_filename, parse_db_uri_for_logging, plural,
render_template_with_fallback, round_coin_value, safe_value_fallback, render_template, render_template_with_fallback, round_coin_value,
safe_value_fallback2, shorten_date) safe_value_fallback, safe_value_fallback2, shorten_date)
def test_decimals_per_coin(): def test_decimals_per_coin():
@ -203,3 +204,16 @@ def test_render_template_fallback(mocker):
def test_parse_db_uri_for_logging(conn_url, expected) -> None: def test_parse_db_uri_for_logging(conn_url, expected) -> None:
assert parse_db_uri_for_logging(conn_url) == expected assert parse_db_uri_for_logging(conn_url) == expected
def test_deep_merge_dicts():
a = {'first': {'rows': {'pass': 'dog', 'number': '1', 'test': None}}}
b = {'first': {'rows': {'fail': 'cat', 'number': '5', 'test': 'asdf'}}}
res = {'first': {'rows': {'pass': 'dog', 'fail': 'cat', 'number': '5', 'test': 'asdf'}}}
res2 = {'first': {'rows': {'pass': 'dog', 'fail': 'cat', 'number': '1', 'test': None}}}
assert deep_merge_dicts(b, deepcopy(a)) == res
assert deep_merge_dicts(a, deepcopy(b)) == res2
res2['first']['rows']['test'] = 'asdf'
assert deep_merge_dicts(a, deepcopy(b), allow_null_overrides=False) == res2