Merge pull request #3396 from freqtrade/fix/broken_getpairs

Use dict for symbol_is_pair
This commit is contained in:
Matthias 2020-08-15 08:58:53 +02:00 committed by GitHub
commit 89b9a8cb1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 86 additions and 43 deletions

View File

@ -14,7 +14,7 @@ from freqtrade.configuration import setup_utils_configuration
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import (available_exchanges, ccxt_exchanges, from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
market_is_active, symbol_is_pair) market_is_active)
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -163,7 +163,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'],
'Base': v['base'], 'Quote': v['quote'], 'Base': v['base'], 'Quote': v['quote'],
'Active': market_is_active(v), 'Active': market_is_active(v),
**({'Is pair': symbol_is_pair(v['symbol'])} **({'Is pair': exchange.market_is_tradable(v)}
if not pairs_only else {})}) if not pairs_only else {})})
if (args.get('print_one_column', False) or if (args.get('print_one_column', False) or

View File

@ -12,8 +12,7 @@ from freqtrade.exchange.exchange import (timeframe_to_seconds,
timeframe_to_msecs, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_next_date,
timeframe_to_prev_date) timeframe_to_prev_date)
from freqtrade.exchange.exchange import (market_is_active, from freqtrade.exchange.exchange import (market_is_active)
symbol_is_pair)
from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.binance import Binance from freqtrade.exchange.binance import Binance
from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.bibox import Bibox

View File

@ -222,7 +222,7 @@ class Exchange:
if quote_currencies: if quote_currencies:
markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies}
if pairs_only: if pairs_only:
markets = {k: v for k, v in markets.items() if symbol_is_pair(v['symbol'])} markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)}
if active_only: if active_only:
markets = {k: v for k, v in markets.items() if market_is_active(v)} markets = {k: v for k, v in markets.items() if market_is_active(v)}
return markets return markets
@ -246,6 +246,19 @@ class Exchange:
""" """
return self.markets.get(pair, {}).get('base', '') return self.markets.get(pair, {}).get('base', '')
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
"""
Check if the market symbol is tradable by Freqtrade.
By default, checks if it's splittable by `/` and both sides correspond to base / quote
"""
symbol_parts = market['symbol'].split('/')
return (len(symbol_parts) == 2 and
len(symbol_parts[0]) > 0 and
len(symbol_parts[1]) > 0 and
symbol_parts[0] == market.get('base') and
symbol_parts[1] == market.get('quote')
)
def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame: def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame:
if pair_interval in self._klines: if pair_interval in self._klines:
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval] return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
@ -1258,20 +1271,6 @@ 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 symbol_is_pair(market_symbol: str, base_currency: str = None,
quote_currency: str = None) -> bool:
"""
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
it also checks that the symbol contains appropriate base and/or quote currency part before
and after the separating character correspondingly.
"""
symbol_parts = market_symbol.split('/')
return (len(symbol_parts) == 2 and
(symbol_parts[0] == base_currency if base_currency else len(symbol_parts[0]) > 0) and
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
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.

View File

@ -1,6 +1,6 @@
""" FTX exchange subclass """ """ FTX exchange subclass """
import logging import logging
from typing import Dict from typing import Any, Dict
import ccxt import ccxt
@ -20,6 +20,16 @@ class Ftx(Exchange):
"ohlcv_candle_limit": 1500, "ohlcv_candle_limit": 1500,
} }
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
"""
Check if the market symbol is tradable by Freqtrade.
Default checks + check if pair is spot pair (no futures trading yet).
"""
parent_check = super().market_is_tradable(market)
return (parent_check and
market.get('spot', False) is True)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)

View File

@ -1,6 +1,6 @@
""" Kraken exchange subclass """ """ Kraken exchange subclass """
import logging import logging
from typing import Dict from typing import Any, Dict
import ccxt import ccxt
@ -22,6 +22,16 @@ class Kraken(Exchange):
"trades_pagination_arg": "since", "trades_pagination_arg": "since",
} }
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
"""
Check if the market symbol is tradable by Freqtrade.
Default checks + check if pair is darkpool pair.
"""
parent_check = super().market_is_tradable(market)
return (parent_check and
market.get('darkpool', False) is False)
@retrier @retrier
def get_balances(self) -> dict: def get_balances(self) -> dict:
if self._config['dry_run']: if self._config['dry_run']:

View File

@ -162,6 +162,11 @@ class IPairList(ABC):
f"{self._exchange.name}. Removing it from whitelist..") f"{self._exchange.name}. Removing it from whitelist..")
continue continue
if not self._exchange.market_is_tradable(markets[pair]):
logger.warning(f"Pair {pair} is not tradable with Freqtrade."
"Removing it from whitelist..")
continue
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']: if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
logger.warning(f"Pair {pair} is not compatible with your stake currency " logger.warning(f"Pair {pair} is not compatible with your stake currency "
f"{self._config['stake_currency']}. Removing it from whitelist..") f"{self._config['stake_currency']}. Removing it from whitelist..")

View File

@ -11,11 +11,12 @@ import ccxt
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, from freqtrade.exceptions import (DDosProtection, DependencyException,
OperationalException, TemporaryError) InvalidOrderException, OperationalException,
TemporaryError)
from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange import Binance, Exchange, Kraken
from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff
from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, from freqtrade.exchange.exchange import (market_is_active,
timeframe_to_minutes, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_next_date,
@ -2218,25 +2219,42 @@ def test_timeframe_to_next_date():
assert timeframe_to_next_date("5m") > date assert timeframe_to_next_date("5m") > date
@pytest.mark.parametrize("market_symbol,base_currency,quote_currency,expected_result", [ @pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [
("BTC/USDT", None, None, True), ("BTC/USDT", 'BTC', 'USDT', "binance", {}, True),
("USDT/BTC", None, None, True), ("USDT/BTC", 'USDT', 'BTC', "binance", {}, True),
("BTCUSDT", None, None, False), ("USDT/BTC", 'BTC', 'USDT', "binance", {}, False), # Reversed currencies
("BTC/USDT", None, "USDT", True), ("BTCUSDT", 'BTC', 'USDT', "binance", {}, False), # No seperating /
("USDT/BTC", None, "USDT", False), ("BTCUSDT", None, "USDT", "binance", {}, False), #
("BTCUSDT", None, "USDT", False), ("USDT/BTC", "BTC", None, "binance", {}, False),
("BTC/USDT", "BTC", None, True), ("BTCUSDT", "BTC", None, "binance", {}, False),
("USDT/BTC", "BTC", None, False), ("BTC/USDT", "BTC", "USDT", "binance", {}, True),
("BTCUSDT", "BTC", None, False), ("BTC/USDT", "USDT", "BTC", "binance", {}, False), # reversed currencies
("BTC/USDT", "BTC", "USDT", True), ("BTC/USDT", "BTC", "USD", "binance", {}, False), # Wrong quote currency
("BTC/USDT", "USDT", "BTC", False), ("BTC/", "BTC", 'UNK', "binance", {}, False),
("BTC/USDT", "BTC", "USD", False), ("/USDT", 'UNK', 'USDT', "binance", {}, False),
("BTCUSDT", "BTC", "USDT", False), ("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": False}, True),
("BTC/", None, None, False), ("EUR/BTC", 'EUR', 'BTC', "kraken", {"darkpool": False}, True),
("/USDT", None, None, False), ("EUR/BTC", 'BTC', 'EUR', "kraken", {"darkpool": False}, False), # Reversed currencies
("BTC/EUR", 'BTC', 'USD', "kraken", {"darkpool": False}, False), # wrong quote currency
("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools
("BTC/EUR.d", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools
("BTC/USD", 'BTC', 'USD', "ftx", {'spot': True}, True),
("USD/BTC", 'USD', 'BTC', "ftx", {'spot': True}, True),
("BTC/USD", 'BTC', 'USDT', "ftx", {'spot': True}, False), # Wrong quote currency
("BTC/USD", 'USD', 'BTC', "ftx", {'spot': True}, False), # Reversed currencies
("BTC/USD", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets
("BTC-PERP", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets
]) ])
def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_result) -> None: def test_market_is_tradable(mocker, default_conf, market_symbol, base,
assert symbol_is_pair(market_symbol, base_currency, quote_currency) == expected_result quote, add_dict, exchange, expected_result) -> None:
ex = get_patched_exchange(mocker, default_conf, id=exchange)
market = {
'symbol': market_symbol,
'base': base,
'quote': quote,
**(add_dict),
}
assert ex.market_is_tradable(market) == expected_result
@pytest.mark.parametrize("market,expected_result", [ @pytest.mark.parametrize("market,expected_result", [

View File

@ -468,7 +468,9 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist):
# BCH/BTC not available # BCH/BTC not available
(['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"),
# BTT/BTC is inactive # BTT/BTC is inactive
(['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active"),
# XLTCUSDT is not a valid pair
(['ETH/BTC', 'TKN/BTC', 'XLTCUSDT'], "is not tradable with Freqtrade"),
]) ])
def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog,
log_message, tickers): log_message, tickers):