Merge pull request #5825 from freqtrade/futures_pairlist

Futures pairlist
This commit is contained in:
Matthias 2021-11-16 19:46:27 +01:00 committed by GitHub
commit cbb5025711
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 216 additions and 82 deletions

View File

@ -48,7 +48,8 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] "print_csv", "base_currencies", "quote_currencies", "list_pairs_all",
"trading_mode"]
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column", ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
"list_pairs_print_json"] "list_pairs_print_json"]

View File

@ -179,7 +179,6 @@ AVAILABLE_CLI_OPTIONS = {
'--export', '--export',
help='Export backtest results (default: trades).', help='Export backtest results (default: trades).',
choices=constants.EXPORT_OPTIONS, choices=constants.EXPORT_OPTIONS,
), ),
"exportfilename": Arg( "exportfilename": Arg(
'--export-filename', '--export-filename',
@ -349,6 +348,11 @@ AVAILABLE_CLI_OPTIONS = {
nargs='+', nargs='+',
metavar='BASE_CURRENCY', metavar='BASE_CURRENCY',
), ),
"trading_mode": Arg(
'--trading-mode',
help='Select Trading mode',
choices=constants.TRADING_MODES,
),
# Script options # Script options
"pairs": Arg( "pairs": Arg(
'-p', '--pairs', '-p', '--pairs',

View File

@ -129,10 +129,9 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
quote_currencies = args.get('quote_currencies', []) quote_currencies = args.get('quote_currencies', [])
try: try:
# TODO-lev: Add leverage amount to get markets that support a certain leverage
pairs = exchange.get_markets(base_currencies=base_currencies, pairs = exchange.get_markets(base_currencies=base_currencies,
quote_currencies=quote_currencies, quote_currencies=quote_currencies,
pairs_only=pairs_only, tradable_only=pairs_only,
active_only=active_only) active_only=active_only)
# Sort the pairs/markets by symbol # Sort the pairs/markets by symbol
pairs = dict(sorted(pairs.items())) pairs = dict(sorted(pairs.items()))
@ -152,15 +151,19 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
if quote_currencies else "")) if quote_currencies else ""))
headers = ["Id", "Symbol", "Base", "Quote", "Active", headers = ["Id", "Symbol", "Base", "Quote", "Active",
*(['Is pair'] if not pairs_only else [])] "Spot", "Margin", "Future", "Leverage"]
tabular_data = [] tabular_data = [{
for _, v in pairs.items(): 'Id': v['id'],
tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], 'Symbol': v['symbol'],
'Base': v['base'], 'Quote': v['quote'], 'Base': v['base'],
'Active': market_is_active(v), 'Quote': v['quote'],
**({'Is pair': exchange.market_is_tradable(v)} 'Active': market_is_active(v),
if not pairs_only else {})}) 'Spot': 'Spot' if exchange.market_is_spot(v) else '',
'Margin': 'Margin' if exchange.market_is_margin(v) else '',
'Future': 'Future' if exchange.market_is_future(v) else '',
'Leverage': exchange.get_max_leverage(v['symbol'], 20)
} for _, v in pairs.items()]
if (args.get('print_one_column', False) or if (args.get('print_one_column', False) or
args.get('list_pairs_print_json', False) or args.get('list_pairs_print_json', False) or

View File

@ -431,6 +431,8 @@ class Configuration:
self._args_to_config(config, argname='new_pairs_days', self._args_to_config(config, argname='new_pairs_days',
logstring='Detected --new-pairs-days: {}') logstring='Detected --new-pairs-days: {}')
self._args_to_config(config, argname='trading_mode',
logstring='Detected --trading-mode: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None: def _process_runmode(self, config: Dict[str, Any]) -> None:

View File

@ -3,7 +3,7 @@ import json
import logging import logging
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
@ -119,6 +119,10 @@ class Binance(Exchange):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
def market_is_future(self, market: Dict[str, Any]) -> bool:
# TODO-lev: This should be unified in ccxt to "swap"...
return market.get('future', False) is True
@retrier @retrier
def fill_leverage_brackets(self): def fill_leverage_brackets(self):
""" """
@ -161,6 +165,8 @@ class Binance(Exchange):
:param pair: The base/quote currency pair being traded :param pair: The base/quote currency pair being traded
:nominal_value: The total value of the trade in quote currency (collateral + debt) :nominal_value: The total value of the trade in quote currency (collateral + debt)
""" """
if pair not in self._leverage_brackets:
return 1.0
pair_brackets = self._leverage_brackets[pair] pair_brackets = self._leverage_brackets[pair]
max_lev = 1.0 max_lev = 1.0
for [min_amount, margin_req] in pair_brackets: for [min_amount, margin_req] in pair_brackets:

View File

@ -291,7 +291,9 @@ class Exchange:
timeframe, self._ft_has.get('ohlcv_candle_limit'))) timeframe, self._ft_has.get('ohlcv_candle_limit')))
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]: spot_only: bool = False, margin_only: bool = False, futures_only: bool = False,
tradable_only: bool = True,
active_only: bool = False) -> Dict[str, Any]:
""" """
Return exchange ccxt markets, filtered out by base currency and quote currency Return exchange ccxt markets, filtered out by base currency and quote currency
if this was requested in parameters. if this was requested in parameters.
@ -306,8 +308,14 @@ class Exchange:
markets = {k: v for k, v in markets.items() if v['base'] in base_currencies} markets = {k: v for k, v in markets.items() if v['base'] in base_currencies}
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 tradable_only:
markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)} markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)}
if spot_only:
markets = {k: v for k, v in markets.items() if self.market_is_spot(v)}
if margin_only:
markets = {k: v for k, v in markets.items() if self.market_is_margin(v)}
if futures_only:
markets = {k: v for k, v in markets.items() if self.market_is_future(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
@ -331,18 +339,27 @@ class Exchange:
""" """
return self.markets.get(pair, {}).get('base', '') return self.markets.get(pair, {}).get('base', '')
def market_is_future(self, market: Dict[str, Any]) -> bool:
return market.get('swap', False) is True
def market_is_spot(self, market: Dict[str, Any]) -> bool:
return market.get('spot', False) is True
def market_is_margin(self, market: Dict[str, Any]) -> bool:
return market.get('margin', False) is True
def market_is_tradable(self, market: Dict[str, Any]) -> bool: def market_is_tradable(self, market: Dict[str, Any]) -> bool:
""" """
Check if the market symbol is tradable by Freqtrade. Check if the market symbol is tradable by Freqtrade.
By default, checks if it's splittable by `/` and both sides correspond to base / quote Ensures that Configured mode aligns to
""" """
symbol_parts = market['symbol'].split('/') return (
return (len(symbol_parts) == 2 and market.get('quote', None) is not None
len(symbol_parts[0]) > 0 and and market.get('base', None) is not None
len(symbol_parts[1]) > 0 and and (self.trading_mode == TradingMode.SPOT and self.market_is_spot(market))
symbol_parts[0] == market.get('base') and or (self.trading_mode == TradingMode.MARGIN and self.market_is_margin(market))
symbol_parts[1] == market.get('quote') or (self.trading_mode == TradingMode.FUTURES and self.market_is_future(market))
) )
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:

View File

@ -30,16 +30,6 @@ class Ftx(Exchange):
# (TradingMode.FUTURES, Collateral.CROSS) # (TradingMode.FUTURES, Collateral.CROSS)
] ]
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, side: str) -> bool: def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
""" """
Verify stop_loss against stoploss-order value (limit or price) Verify stop_loss against stoploss-order value (limit or price)
@ -169,3 +159,7 @@ class Ftx(Exchange):
if order['type'] == 'stop': if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id') return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['id'] return order['id']
def market_is_future(self, market: Dict[str, Any]) -> bool:
# TODO-lev: This should be unified in ccxt to "swap"...
return market.get('future', False) is True

View File

@ -2,7 +2,7 @@ numpy==1.21.3
pandas==1.3.4 pandas==1.3.4
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.60.11 ccxt==1.61.24
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==35.0.0 cryptography==35.0.0
aiohttp==3.7.4.post0 aiohttp==3.7.4.post0

View File

@ -43,7 +43,7 @@ setup(
], ],
install_requires=[ install_requires=[
# from requirements.txt # from requirements.txt
'ccxt>=1.60.11', 'ccxt>=1.61.24',
'SQLAlchemy', 'SQLAlchemy',
'python-telegram-bot>=13.4', 'python-telegram-bot>=13.4',
'arrow>=0.17.0', 'arrow>=0.17.0',

View File

@ -434,9 +434,9 @@ def test_list_markets(mocker, markets_static, capsys):
] ]
start_list_markets(get_args(args), False) start_list_markets(get_args(args), False)
captured = capsys.readouterr() captured = capsys.readouterr()
assert ("Id,Symbol,Base,Quote,Active,Is pair" in captured.out) assert ("Id,Symbol,Base,Quote,Active,Spot,Margin,Future,Leverage" in captured.out)
assert ("blkbtc,BLK/BTC,BLK,BTC,True,True" in captured.out) assert ("blkbtc,BLK/BTC,BLK,BTC,True,Spot" in captured.out)
assert ("USD-LTC,LTC/USD,LTC,USD,True,True" in captured.out) assert ("USD-LTC,LTC/USD,LTC,USD,True,Spot" in captured.out)
# Test --one-column # Test --one-column
args = [ args = [

View File

@ -575,6 +575,8 @@ def get_markets():
'base': 'ETH', 'base': 'ETH',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -604,6 +606,8 @@ def get_markets():
'quote': 'BTC', 'quote': 'BTC',
# According to ccxt, markets without active item set are also active # According to ccxt, markets without active item set are also active
# 'active': True, # 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -632,6 +636,8 @@ def get_markets():
'base': 'BLK', 'base': 'BLK',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -660,6 +666,8 @@ def get_markets():
'base': 'LTC', 'base': 'LTC',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -685,6 +693,8 @@ def get_markets():
'base': 'XRP', 'base': 'XRP',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -710,6 +720,8 @@ def get_markets():
'base': 'NEO', 'base': 'NEO',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -735,6 +747,8 @@ def get_markets():
'base': 'BTT', 'base': 'BTT',
'quote': 'BTC', 'quote': 'BTC',
'active': False, 'active': False,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'base': 8, 'base': 8,
'quote': 8, 'quote': 8,
@ -762,6 +776,11 @@ def get_markets():
'symbol': 'ETH/USDT', 'symbol': 'ETH/USDT',
'base': 'ETH', 'base': 'ETH',
'quote': 'USDT', 'quote': 'USDT',
'spot': True,
'future': True,
'swap': True,
'margin': True,
'type': 'spot',
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8
@ -785,6 +804,11 @@ def get_markets():
'base': 'LTC', 'base': 'LTC',
'quote': 'USDT', 'quote': 'USDT',
'active': False, 'active': False,
'spot': True,
'future': True,
'swap': True,
'margin': True,
'type': 'spot',
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8
@ -807,6 +831,8 @@ def get_markets():
'base': 'XRP', 'base': 'XRP',
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -832,6 +858,8 @@ def get_markets():
'base': 'NEO', 'base': 'NEO',
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -857,6 +885,8 @@ def get_markets():
'base': 'TKN', 'base': 'TKN',
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'price': 8, 'price': 8,
'amount': 8, 'amount': 8,
@ -882,6 +912,8 @@ def get_markets():
'base': 'LTC', 'base': 'LTC',
'quote': 'USD', 'quote': 'USD',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8
@ -904,6 +936,8 @@ def get_markets():
'base': 'LTC', 'base': 'LTC',
'quote': 'USDT', 'quote': 'USDT',
'active': True, 'active': True,
'spot': False,
'type': 'SomethingElse',
'precision': { 'precision': {
'amount': 8, 'amount': 8,
'price': 8 'price': 8
@ -926,6 +960,8 @@ def get_markets():
'base': 'LTC', 'base': 'LTC',
'quote': 'ETH', 'quote': 'ETH',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'base': 8, 'base': 8,
'quote': 8, 'quote': 8,
@ -976,6 +1012,8 @@ def shitcoinmarkets(markets_static):
'base': 'HOT', 'base': 'HOT',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'base': 8, 'base': 8,
'quote': 8, 'quote': 8,
@ -1004,6 +1042,8 @@ def shitcoinmarkets(markets_static):
'base': 'FUEL', 'base': 'FUEL',
'quote': 'BTC', 'quote': 'BTC',
'active': True, 'active': True,
'spot': True,
'type': 'spot',
'precision': { 'precision': {
'base': 8, 'base': 8,
'quote': 8, 'quote': 8,

View File

@ -5,6 +5,7 @@ However, these tests should give a good idea to determine if a new exchange is
suitable to run with freqtrade. suitable to run with freqtrade.
""" """
from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@ -26,6 +27,7 @@ EXCHANGES = {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
'hasQuoteVolume': True, 'hasQuoteVolume': True,
'timeframe': '5m', 'timeframe': '5m',
'futures': True,
}, },
'kraken': { 'kraken': {
'pair': 'BTC/USDT', 'pair': 'BTC/USDT',
@ -82,13 +84,20 @@ def exchange(request, exchange_conf):
@pytest.fixture(params=EXCHANGES, scope="class") @pytest.fixture(params=EXCHANGES, scope="class")
def exchange_futures(request, exchange_conf): def exchange_futures(request, exchange_conf, class_mocker):
if not EXCHANGES[request.param].get('futures') is True: if not EXCHANGES[request.param].get('futures') is True:
yield None, request.param yield None, request.param
else: else:
exchange_conf = deepcopy(exchange_conf)
exchange_conf['exchange']['name'] = request.param exchange_conf['exchange']['name'] = request.param
exchange_conf['trading_mode'] = 'futures' exchange_conf['trading_mode'] = 'futures'
exchange_conf['collateral'] = 'cross' exchange_conf['collateral'] = 'cross'
# TODO-lev This mock should no longer be necessary once futures are enabled.
class_mocker.patch(
'freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral')
class_mocker.patch(
'freqtrade.exchange.binance.Binance.fill_leverage_brackets')
exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True) exchange = ExchangeResolver.load_exchange(request.param, exchange_conf, validate=True)
yield exchange, request.param yield exchange, request.param
@ -103,6 +112,20 @@ class TestCCXTExchange():
markets = exchange.markets markets = exchange.markets
assert pair in markets assert pair in markets
assert isinstance(markets[pair], dict) assert isinstance(markets[pair], dict)
assert exchange.market_is_spot(markets[pair])
def test_load_markets_futures(self, exchange_futures):
exchange, exchangename = exchange_futures
if not exchange:
# exchange_futures only returns values for supported exchanges
return
pair = EXCHANGES[exchangename]['pair']
pair = EXCHANGES[exchangename].get('futures_pair', pair)
markets = exchange.markets
assert pair in markets
assert isinstance(markets[pair], dict)
assert exchange.market_is_future(markets[pair])
def test_ccxt_fetch_tickers(self, exchange): def test_ccxt_fetch_tickers(self, exchange):
exchange, exchangename = exchange exchange, exchangename = exchange

View File

@ -2760,7 +2760,8 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"base_currencies, quote_currencies, pairs_only, active_only, expected_keys", [ "base_currencies,quote_currencies,tradable_only,active_only,spot_only,"
"futures_only,expected_keys", [
# Testing markets (in conftest.py): # Testing markets (in conftest.py):
# 'BLK/BTC': 'active': True # 'BLK/BTC': 'active': True
# 'BTT/BTC': 'active': True # 'BTT/BTC': 'active': True
@ -2775,48 +2776,62 @@ def test_get_valid_pair_combination(default_conf, mocker, markets):
# 'XLTCUSDT': 'active': True, not a pair # 'XLTCUSDT': 'active': True, not a pair
# 'XRP/BTC': 'active': False # 'XRP/BTC': 'active': False
# all markets # all markets
([], [], False, False, ([], [], False, False, False, False,
['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']),
# all markets, only spot pairs
([], [], False, False, True, False,
['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']),
# active markets # active markets
([], [], False, True, ([], [], False, True, False, False,
['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC', ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC',
'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']),
# all pairs # all pairs
([], [], True, False, ([], [], True, False, False, False,
['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD',
'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']),
# active pairs # active pairs
([], [], True, True, ([], [], True, True, False, False,
['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC', ['BLK/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'NEO/BTC',
'TKN/BTC', 'XRP/BTC']), 'TKN/BTC', 'XRP/BTC']),
# all markets, base=ETH, LTC # all markets, base=ETH, LTC
(['ETH', 'LTC'], [], False, False, (['ETH', 'LTC'], [], False, False, False, False,
['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
# all markets, base=LTC # all markets, base=LTC
(['LTC'], [], False, False, (['LTC'], [], False, False, False, False,
['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), ['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
# spot markets, base=LTC
(['LTC'], [], False, False, True, False,
['LTC/BTC', 'LTC/ETH', 'LTC/USD', 'LTC/USDT']),
# all markets, quote=USDT # all markets, quote=USDT
([], ['USDT'], False, False, ([], ['USDT'], False, False, False, False,
['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']), ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']),
# Futures markets, quote=USDT
([], ['USDT'], False, False, False, True,
['ETH/USDT', 'LTC/USDT']),
# all markets, quote=USDT, USD # all markets, quote=USDT, USD
([], ['USDT', 'USD'], False, False, ([], ['USDT', 'USD'], False, False, False, False,
['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), ['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']),
# spot markets, quote=USDT, USD
([], ['USDT', 'USD'], False, False, True, False,
['ETH/USDT', 'LTC/USD', 'LTC/USDT']),
# all markets, base=LTC, quote=USDT # all markets, base=LTC, quote=USDT
(['LTC'], ['USDT'], False, False, (['LTC'], ['USDT'], False, False, False, False,
['LTC/USDT', 'XLTCUSDT']), ['LTC/USDT', 'XLTCUSDT']),
# all pairs, base=LTC, quote=USDT # all pairs, base=LTC, quote=USDT
(['LTC'], ['USDT'], True, False, (['LTC'], ['USDT'], True, False, False, False,
['LTC/USDT']), ['LTC/USDT']),
# all markets, base=LTC, quote=USDT, NONEXISTENT # all markets, base=LTC, quote=USDT, NONEXISTENT
(['LTC'], ['USDT', 'NONEXISTENT'], False, False, (['LTC'], ['USDT', 'NONEXISTENT'], False, False, False, False,
['LTC/USDT', 'XLTCUSDT']), ['LTC/USDT', 'XLTCUSDT']),
# all markets, base=LTC, quote=NONEXISTENT # all markets, base=LTC, quote=NONEXISTENT
(['LTC'], ['NONEXISTENT'], False, False, (['LTC'], ['NONEXISTENT'], False, False, False, False,
[]), []),
]) ])
def test_get_markets(default_conf, mocker, markets_static, def test_get_markets(default_conf, mocker, markets_static,
base_currencies, quote_currencies, pairs_only, active_only, base_currencies, quote_currencies, tradable_only, active_only,
spot_only, futures_only,
expected_keys): expected_keys):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
_init_ccxt=MagicMock(return_value=MagicMock()), _init_ccxt=MagicMock(return_value=MagicMock()),
@ -2825,7 +2840,12 @@ def test_get_markets(default_conf, mocker, markets_static,
validate_timeframes=MagicMock(), validate_timeframes=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, quote_currencies, pairs_only, active_only) pairs = ex.get_markets(base_currencies,
quote_currencies,
tradable_only=tradable_only,
spot_only=spot_only,
futures_only=futures_only,
active_only=active_only)
assert sorted(pairs.keys()) == sorted(expected_keys) assert sorted(pairs.keys()) == sorted(expected_keys)
@ -2928,39 +2948,63 @@ 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)
@pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [ @pytest.mark.parametrize(
("BTC/USDT", 'BTC', 'USDT', "binance", {}, True), "market_symbol,base,quote,exchange,spot,margin,futures,trademode,add_dict,expected_result",
("USDT/BTC", 'USDT', 'BTC', "binance", {}, True), [
("USDT/BTC", 'BTC', 'USDT', "binance", {}, False), # Reversed currencies ("BTC/USDT", 'BTC', 'USDT', "binance", True, False, False, 'spot', {}, True),
("BTCUSDT", 'BTC', 'USDT', "binance", {}, False), # No seperating / ("USDT/BTC", 'USDT', 'BTC', "binance", True, False, False, 'spot', {}, True),
("BTCUSDT", None, "USDT", "binance", {}, False), # # No seperating /
("USDT/BTC", "BTC", None, "binance", {}, False), ("BTCUSDT", 'BTC', 'USDT', "binance", True, False, False, 'spot', {}, True),
("BTCUSDT", "BTC", None, "binance", {}, False), ("BTCUSDT", None, "USDT", "binance", True, False, False, 'spot', {}, False),
("BTC/USDT", "BTC", "USDT", "binance", {}, True), ("USDT/BTC", "BTC", None, "binance", True, False, False, 'spot', {}, False),
("BTC/USDT", "USDT", "BTC", "binance", {}, False), # reversed currencies ("BTCUSDT", "BTC", None, "binance", True, False, False, 'spot', {}, False),
("BTC/USDT", "BTC", "USD", "binance", {}, False), # Wrong quote currency ("BTC/USDT", "BTC", "USDT", "binance", True, False, False, 'spot', {}, True),
("BTC/", "BTC", 'UNK', "binance", {}, False), # Futures mode, spot pair
("/USDT", 'UNK', 'USDT', "binance", {}, False), ("BTC/USDT", "BTC", "USDT", "binance", True, False, False, 'futures', {}, False),
("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": False}, True), ("BTC/USDT", "BTC", "USDT", "binance", True, False, False, 'margin', {}, False),
("EUR/BTC", 'EUR', 'BTC', "kraken", {"darkpool": False}, True), ("BTC/USDT", "BTC", "USDT", "binance", True, True, True, 'margin', {}, True),
("EUR/BTC", 'BTC', 'EUR', "kraken", {"darkpool": False}, False), # Reversed currencies ("BTC/USDT", "BTC", "USDT", "binance", False, True, False, 'margin', {}, True),
("BTC/EUR", 'BTC', 'USD', "kraken", {"darkpool": False}, False), # wrong quote currency # Futures mode, futures pair
("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools ("BTC/USDT", "BTC", "USDT", "binance", False, False, True, 'futures', {}, True),
("BTC/EUR.d", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools # Futures market
("BTC/USD", 'BTC', 'USD', "ftx", {'spot': True}, True), ("BTC/UNK", "BTC", 'UNK', "binance", False, False, True, 'spot', {}, False),
("USD/BTC", 'USD', 'BTC', "ftx", {'spot': True}, True), ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, False, 'spot', {"darkpool": False}, True),
("BTC/USD", 'BTC', 'USDT', "ftx", {'spot': True}, False), # Wrong quote currency ("EUR/BTC", 'EUR', 'BTC', "kraken", True, False, False, 'spot', {"darkpool": False}, True),
("BTC/USD", 'USD', 'BTC', "ftx", {'spot': True}, False), # Reversed currencies # no darkpools
("BTC/USD", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets ("BTC/EUR", 'BTC', 'EUR', "kraken", True, False, False, 'spot',
("BTC-PERP", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets {"darkpool": True}, False),
]) # no darkpools
def test_market_is_tradable(mocker, default_conf, market_symbol, base, ("BTC/EUR.d", 'BTC', 'EUR', "kraken", True, False, False, 'spot',
quote, add_dict, exchange, expected_result) -> None: {"darkpool": True}, False),
("BTC/USD", 'BTC', 'USD', "ftx", True, False, False, 'spot', {}, True),
("USD/BTC", 'USD', 'BTC', "ftx", True, False, False, 'spot', {}, True),
# Can only trade spot markets
("BTC/USD", 'BTC', 'USD', "ftx", False, False, True, 'spot', {}, False),
("BTC/USD", 'BTC', 'USD', "ftx", False, False, True, 'futures', {}, True),
# Can only trade spot markets
("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'spot', {}, False),
("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'margin', {}, False),
("BTC-PERP", 'BTC', 'USD', "ftx", False, False, True, 'futures', {}, True),
("BTC/USDT:USDT", 'BTC', 'USD', "okex", False, False, True, 'spot', {}, False),
("BTC/USDT:USDT", 'BTC', 'USD', "okex", False, False, True, 'margin', {}, False),
("BTC/USDT:USDT", 'BTC', 'USD', "okex", False, False, True, 'futures', {}, True),
])
def test_market_is_tradable(
mocker, default_conf, market_symbol, base,
quote, spot, margin, futures, trademode, add_dict, exchange, expected_result
) -> None:
default_conf['trading_mode'] = trademode
mocker.patch('freqtrade.exchange.exchange.Exchange.validate_trading_mode_and_collateral')
ex = get_patched_exchange(mocker, default_conf, id=exchange) ex = get_patched_exchange(mocker, default_conf, id=exchange)
market = { market = {
'symbol': market_symbol, 'symbol': market_symbol,
'base': base, 'base': base,
'quote': quote, 'quote': quote,
'spot': spot,
'future': futures,
'swap': futures,
'margin': margin,
**(add_dict), **(add_dict),
} }
assert ex.market_is_tradable(market) == expected_result assert ex.market_is_tradable(market) == expected_result