diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 76a2b1cc9..58b5539fe 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -2,6 +2,7 @@ This module contains the argument manager class """ import argparse +from functools import partial from pathlib import Path from typing import Any, Dict, List, Optional @@ -33,6 +34,8 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] +ARGS_LIST_PAIRS = ["exchange", "print_list", "base_currency", "quote_currency", "active_only"] + ARGS_CREATE_USERDIR = ["user_data_dir"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] @@ -43,7 +46,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_ ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "ticker_interval"] -NO_CONF_REQURIED = ["download-data", "list-timeframes", "plot-dataframe", "plot-profit"] +NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", + "plot-dataframe", "plot-profit"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] @@ -106,7 +110,8 @@ class Arguments: """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.utils import (start_create_userdir, start_download_data, - start_list_exchanges, start_list_timeframes) + start_list_exchanges, start_list_timeframes, + start_list_pairs) subparsers = self.parser.add_subparsers(dest='subparser') @@ -147,6 +152,22 @@ class Arguments: list_timeframes_cmd.set_defaults(func=start_list_timeframes) self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd) + # Add list-markets subcommand + list_markets_cmd = subparsers.add_parser( + 'list-markets', + help='Print markets on exchange.' + ) + list_markets_cmd.set_defaults(func=partial(start_list_pairs, pairs_only=False)) + self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd) + + # Add list-pairs subcommand + list_pairs_cmd = subparsers.add_parser( + 'list-pairs', + help='Print pairs on exchange.' + ) + list_pairs_cmd.set_defaults(func=partial(start_list_pairs, pairs_only=True)) + self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd) + # Add download-data subcommand download_data_cmd = subparsers.add_parser( 'download-data', diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 7e6a956ce..d3863481a 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -255,6 +255,26 @@ AVAILABLE_CLI_OPTIONS = { help='Print all exchanges known to the ccxt library.', action='store_true', ), + # List pairs / markets + "print_list": Arg( + '--print-list', + help='Print list of pairs or market symbols. By default data is ' + 'printed in the tabular format.', + action='store_true', + ), + "quote_currency": Arg( + '--quote-currency', + help='Select quote currency.', + ), + "base_currency": Arg( + '--base-currency', + help='Select base currency.', + ), + "active_only": Arg( + '--active-only', + help='Print only active pairs or markets.', + action='store_true', + ), # Script options "pairs": Arg( '-p', '--pairs', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index df7e5e2b4..a312d6c62 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -280,6 +280,25 @@ class Exchange: self._load_markets() return self._api.markets + def get_markets(self, base_currency: str = None, quote_currency: str = None, + pairs_only: bool = False, active_only: bool = False) -> Dict: + """ + Return exchange ccxt markets, filtered out by base currency and quote currency + if this was requested in parameters. + + TODO: consider moving it to the Dataprovider + """ + markets = self.markets + if base_currency: + markets = {k: v for k, v in markets.items() if v['base'] == base_currency} + if quote_currency: + markets = {k: v for k, v in markets.items() if v['quote'] == quote_currency} + if pairs_only: + markets = {k: v for k, v in markets.items() if '/' in v['symbol']} + if active_only: + markets = {k: v for k, v in markets.items() if v['active']} + return markets + def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: if pair_interval in self._klines: return self._klines[pair_interval].copy() if copy else self._klines[pair_interval] diff --git a/freqtrade/misc.py b/freqtrade/misc.py index c9fbda17e..387e8a42f 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -121,3 +121,7 @@ def round_dict(d, n): Rounds float values in the dict to n digits after the decimal point. """ return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()} + + +def plural(num, singular: str, plural: str = None) -> str: + return singular if (num == 1 or num == -1) else plural or singular + 's' diff --git a/freqtrade/utils.py b/freqtrade/utils.py index b3ff43aca..3f25778a4 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -4,12 +4,14 @@ from pathlib import Path from typing import Any, Dict, List import arrow +from tabulate import tabulate from freqtrade import OperationalException from freqtrade.configuration import Configuration, TimeRange from freqtrade.configuration.directory_operations import create_userdata_dir from freqtrade.data.history import refresh_backtest_ohlcv_data from freqtrade.exchange import available_exchanges, ccxt_exchanges +from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -117,3 +119,43 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: else: print(f"Timeframes available for the exchange `{config['exchange']['name']}`: " f"{', '.join(exchange.timeframes)}") + + +def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: + """ + Print pairs on the exchange + :param args: Cli args from Arguments() + :param pairs_only: if True print only pairs, otherwise print all instruments (markets) + :return: None + """ + config = setup_utils_configuration(args, RunMode.OTHER) + + # Init exchange + exchange = ExchangeResolver(config['exchange']['name'], config).exchange + + active_only = args.get('active_only', False) + base_currency = args.get('base_currency', '') + quote_currency = args.get('quote_currency', '') + + pairs = exchange.get_markets(base_currency=base_currency, + quote_currency=quote_currency, + pairs_only=pairs_only, + active_only=active_only) + + if args.get('print_list', False): + # print data as a list + print(f"Exchange {exchange.name} has {len(pairs)} " + + (plural(len(pairs), "pair" if pairs_only else "market")) + + (f" with {base_currency} as base currency" if base_currency else "") + + (" and" if base_currency and quote_currency else "") + + (f" with {quote_currency} as quote currency" if quote_currency else "") + + (f": {sorted(pairs.keys())}" if len(pairs) else "") + ".") + else: + # print data as a table + tabular_data = [] + for _, v in pairs.items(): + tabular_data.append([v['id'], v['symbol'], v['base'], v['quote'], + "Yes" if v['active'] else "No"]) + + headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] + print(tabulate(tabular_data, headers=headers, tablefmt='pipe'))