From 7cf7982565b2a21dd4f3fb864fbb9a269cf6c64f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 13 Oct 2019 13:12:20 +0300 Subject: [PATCH 01/37] Add list-pairs and list-markets subcommands --- freqtrade/configuration/arguments.py | 25 +++++++++++++-- freqtrade/configuration/cli_options.py | 20 ++++++++++++ freqtrade/exchange/exchange.py | 19 ++++++++++++ freqtrade/misc.py | 4 +++ freqtrade/utils.py | 42 ++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 2 deletions(-) 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')) From 6e27c47dee53d71149c875b43f37fd2c95cf1788 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 14 Oct 2019 13:32:39 +0300 Subject: [PATCH 02/37] Handle properly exchanges with no active flag set for markets --- freqtrade/exchange/__init__.py | 2 ++ freqtrade/exchange/exchange.py | 26 ++++++++++++++++++-- freqtrade/utils.py | 45 +++++++++++++++++++--------------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 29971c897..470091181 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -10,5 +10,7 @@ from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date) +from freqtrade.exchange.exchange import (market_is_active, # noqa: F401 + market_is_pair) from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a312d6c62..185c9dc3d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -289,14 +289,17 @@ class Exchange: TODO: consider moving it to the Dataprovider """ markets = self.markets + if not markets: + raise OperationalException("Markets were not loaded.") + 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']} + markets = {k: v for k, v in markets.items() if market_is_pair(v)} if active_only: - markets = {k: v for k, v in markets.items() if v['active']} + markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: @@ -932,3 +935,22 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000, ROUND_UP) // 1000 return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) + + +def market_is_pair(market): + """ + Return True if the market is a pair. + Currently pairs are defined as markets containing '/' character in its symbol. + """ + return '/' in market.get('symbol', '') + + +def market_is_active(market): + """ + Return True if the market is active. + """ + # "It's active, if the active flag isn't explicitly set to false. If it's missing or + # true then it's true. If it's undefined, then it's most likely true, but not 100% )" + # See https://github.com/ccxt/ccxt/issues/4874, + # https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520 + return market.get('active', True) is not False diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 3f25778a4..67a981461 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -10,7 +10,7 @@ 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.exchange import (available_exchanges, ccxt_exchanges, market_is_active) from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -137,25 +137,30 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: 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) + try: + pairs = exchange.get_markets(base_currency=base_currency, + quote_currency=quote_currency, + pairs_only=pairs_only, + active_only=active_only) + except Exception as e: + raise OperationalException(f"Cannot get markets. Reason: {e}") from e - 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"]) + if args.get('print_list', False): + # print data as a list + print(f"Exchange {exchange.name} has {len(pairs)} " + + ("active " if active_only else "") + + (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 market_is_active(v) else "No"]) - headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] - print(tabulate(tabular_data, headers=headers, tablefmt='pipe')) + headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] + print(tabulate(tabular_data, headers=headers, tablefmt='pipe')) From 411173463799f7aac070cefad42080fe954d0d86 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 14 Oct 2019 13:48:33 +0300 Subject: [PATCH 03/37] Add 'Is pair' in the list-markets tabular output --- freqtrade/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 67a981461..6c36627f1 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -10,7 +10,8 @@ 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, market_is_active) +from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, + market_is_pair) from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -157,10 +158,12 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: (f": {sorted(pairs.keys())}" if len(pairs) else "") + ".") else: # print data as a table + headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] + if not pairs_only: + headers.append('Is pair') tabular_data = [] for _, v in pairs.items(): tabular_data.append([v['id'], v['symbol'], v['base'], v['quote'], - "Yes" if market_is_active(v) else "No"]) - - headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] + "Yes" if market_is_active(v) else "No", + "Yes" if market_is_pair(v) else "No"]) print(tabulate(tabular_data, headers=headers, tablefmt='pipe')) From ad89d199558314c017a9f39de27dbcd563122332 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 15 Oct 2019 21:07:01 +0300 Subject: [PATCH 04/37] Print list in the human-readable format --- freqtrade/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 6c36627f1..72476af45 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -119,7 +119,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: print('\n'.join(exchange.timeframes)) else: print(f"Timeframes available for the exchange `{config['exchange']['name']}`: " - f"{', '.join(exchange.timeframes)}") + f"{', '.join(exchange.timeframes)}.") def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: @@ -155,7 +155,7 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: (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 "") + ".") + (f": {', '.join(sorted(pairs.keys()))}" if len(pairs) else "") + ".") else: # print data as a table headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] From 89e0c76a3f403c748dde56bdfad1af869d5bc037 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 15 Oct 2019 22:31:23 +0300 Subject: [PATCH 05/37] Add --print-json and -1/--one-column options --- freqtrade/configuration/arguments.py | 3 ++- freqtrade/configuration/cli_options.py | 6 ++++++ freqtrade/utils.py | 5 +++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 58b5539fe..f240fc04a 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -34,7 +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_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", + "base_currency", "quote_currency", "active_only"] ARGS_CREATE_USERDIR = ["user_data_dir"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index d3863481a..93bdd9384 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -262,6 +262,12 @@ AVAILABLE_CLI_OPTIONS = { 'printed in the tabular format.', action='store_true', ), + "list_pairs_print_json": Arg( + '--print-json', + help='Print list of pairs or market symbols in JSON format.', + action='store_true', + default=False, + ), "quote_currency": Arg( '--quote-currency', help='Select quote currency.', diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 72476af45..95825ff4e 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Any, Dict, List import arrow +import rapidjson from tabulate import tabulate from freqtrade import OperationalException @@ -156,6 +157,10 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: (" and" if base_currency and quote_currency else "") + (f" with {quote_currency} as quote currency" if quote_currency else "") + (f": {', '.join(sorted(pairs.keys()))}" if len(pairs) else "") + ".") + elif args.get('print_one_column', False): + print('\n'.join(sorted(pairs.keys()))) + elif args.get('list_pairs_print_json', False): + print(rapidjson.dumps(sorted(pairs.keys()), default=str)) else: # print data as a table headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] From f348956e4c0778a8d06162ab85aa67706ab5fe6a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 16 Oct 2019 02:22:27 +0300 Subject: [PATCH 06/37] --print-csv added --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 5 +++ freqtrade/utils.py | 50 +++++++++++++++++--------- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index f240fc04a..aff2f3bb5 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -35,7 +35,7 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", - "base_currency", "quote_currency", "active_only"] + "print_csv", "base_currency", "quote_currency", "active_only"] ARGS_CREATE_USERDIR = ["user_data_dir"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 93bdd9384..61acb30ca 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -268,6 +268,11 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', default=False, ), + "print_csv": Arg( + '--print-csv', + help='Print exchange pair or market data in the csv format.', + action='store_true', + ), "quote_currency": Arg( '--quote-currency', help='Select quote currency.', diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 95825ff4e..52680de20 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -4,6 +4,7 @@ from pathlib import Path from typing import Any, Dict, List import arrow +import csv import rapidjson from tabulate import tabulate @@ -148,27 +149,42 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: raise OperationalException(f"Cannot get markets. Reason: {e}") from e else: + summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") + + ("active " if active_only else "") + + (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 "")) + + headers = ["Id", "Symbol", "Base", "Quote", "Active"] + if not pairs_only: + headers.append('Is pair') + if args.get('print_list', False): - # print data as a list - print(f"Exchange {exchange.name} has {len(pairs)} " + - ("active " if active_only else "") + - (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 "") + + # print data as a list, with human-readable summary + print(summary_str + (f": {', '.join(sorted(pairs.keys()))}" if len(pairs) else "") + ".") elif args.get('print_one_column', False): print('\n'.join(sorted(pairs.keys()))) elif args.get('list_pairs_print_json', False): print(rapidjson.dumps(sorted(pairs.keys()), default=str)) + elif args.get('print_csv', False): + if len(pairs): + writer = csv.DictWriter(sys.stdout, fieldnames=headers, extrasaction='ignore') + writer.writeheader() + for _, v in pairs.items(): + writer.writerow({'Id': v['id'], 'Symbol': v['symbol'], + 'Base': v['base'], 'Quote': v['quote'], + 'Active': market_is_active(v), + 'Is pair': market_is_pair(v)}) else: - # print data as a table - headers = ['Id', 'Symbol', 'Base', 'Quote', 'Active'] - if not pairs_only: - headers.append('Is pair') - tabular_data = [] - for _, v in pairs.items(): - tabular_data.append([v['id'], v['symbol'], v['base'], v['quote'], - "Yes" if market_is_active(v) else "No", - "Yes" if market_is_pair(v) else "No"]) - print(tabulate(tabular_data, headers=headers, tablefmt='pipe')) + print(summary_str + + (":" if len(pairs) else ".")) + if len(pairs): + # 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 market_is_active(v) else "No", + "Yes" if market_is_pair(v) else "No"]) + print(tabulate(tabular_data, headers=headers, tablefmt='pipe')) From 4c8411e8350439a8b24c8ac6ebc0d5ff5063121e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 16 Oct 2019 03:02:58 +0300 Subject: [PATCH 07/37] Cleanup in print tabular and print-csv parts --- freqtrade/utils.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 52680de20..3621787d8 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -156,9 +156,15 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: (" and" if base_currency and quote_currency else "") + (f" with {quote_currency} as quote currency" if quote_currency else "")) - headers = ["Id", "Symbol", "Base", "Quote", "Active"] - if not pairs_only: - headers.append('Is pair') + headers = ["Id", "Symbol", "Base", "Quote", "Active", + *(['Is pair'] if not pairs_only else [])] + + tabular_data = [] + for _, v in pairs.items(): + tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], + 'Base': v['base'], 'Quote': v['quote'], + 'Active': market_is_active(v), + **({'Is pair': market_is_pair(v)} if not pairs_only else {})}) if args.get('print_list', False): # print data as a list, with human-readable summary @@ -170,21 +176,12 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: print(rapidjson.dumps(sorted(pairs.keys()), default=str)) elif args.get('print_csv', False): if len(pairs): - writer = csv.DictWriter(sys.stdout, fieldnames=headers, extrasaction='ignore') + writer = csv.DictWriter(sys.stdout, fieldnames=headers) writer.writeheader() - for _, v in pairs.items(): - writer.writerow({'Id': v['id'], 'Symbol': v['symbol'], - 'Base': v['base'], 'Quote': v['quote'], - 'Active': market_is_active(v), - 'Is pair': market_is_pair(v)}) + writer.writerows(tabular_data) else: print(summary_str + (":" if len(pairs) else ".")) if len(pairs): # 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 market_is_active(v) else "No", - "Yes" if market_is_pair(v) else "No"]) - print(tabulate(tabular_data, headers=headers, tablefmt='pipe')) + print(tabulate(tabular_data, headers='keys', tablefmt='pipe')) From 7de16310457883e592a799e2a3f462558faa48b8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 16 Oct 2019 03:55:04 +0300 Subject: [PATCH 08/37] Print summary in the log for machine-readable formats --- freqtrade/utils.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 3621787d8..d2ea1f512 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -166,14 +166,21 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: 'Active': market_is_active(v), **({'Is pair': market_is_pair(v)} if not pairs_only else {})}) + if (args.get('print_one_column', False) or + args.get('list_pairs_print_json', False) or + args.get('print_csv', False)): + logger.info(f"{summary_str}.") + if args.get('print_list', False): # print data as a list, with human-readable summary print(summary_str + (f": {', '.join(sorted(pairs.keys()))}" if len(pairs) else "") + ".") elif args.get('print_one_column', False): - print('\n'.join(sorted(pairs.keys()))) + if len(pairs): + print('\n'.join(sorted(pairs.keys()))) elif args.get('list_pairs_print_json', False): - print(rapidjson.dumps(sorted(pairs.keys()), default=str)) + if len(pairs): + print(rapidjson.dumps(sorted(pairs.keys()), default=str)) elif args.get('print_csv', False): if len(pairs): writer = csv.DictWriter(sys.stdout, fieldnames=headers) From d72d3887265e9d2076256d961bc2b6ec0ba189ed Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 16 Oct 2019 10:55:09 +0300 Subject: [PATCH 09/37] Make flake happy --- freqtrade/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index d2ea1f512..966674f3b 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -124,7 +124,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: f"{', '.join(exchange.timeframes)}.") -def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: +def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # noqa: C901 """ Print pairs on the exchange :param args: Cli args from Arguments() From 92fda0f76c4a2cc2f83513a8e8e07f536ddc7375 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 02:09:19 +0300 Subject: [PATCH 10/37] Allow --base and --quote be lists of currencies --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 14 ++++++++------ freqtrade/exchange/exchange.py | 10 +++++----- freqtrade/utils.py | 18 +++++++++++------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index aff2f3bb5..937fe1a3c 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -35,7 +35,7 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", - "print_csv", "base_currency", "quote_currency", "active_only"] + "print_csv", "base_currencies", "quote_currencies", "active_only"] ARGS_CREATE_USERDIR = ["user_data_dir"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 61acb30ca..ab3f4f00a 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -273,13 +273,15 @@ AVAILABLE_CLI_OPTIONS = { help='Print exchange pair or market data in the csv format.', action='store_true', ), - "quote_currency": Arg( - '--quote-currency', - help='Select quote currency.', + "quote_currencies": Arg( + '--quote', + help='Specify quote currency(-ies). Space-separated list.', + nargs='+', ), - "base_currency": Arg( - '--base-currency', - help='Select base currency.', + "base_currencies": Arg( + '--base', + help='Specify base currency(-ies). Space-separated list.', + nargs='+', ), "active_only": Arg( '--active-only', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 185c9dc3d..95602082e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -280,7 +280,7 @@ class Exchange: self._load_markets() return self._api.markets - def get_markets(self, base_currency: str = None, quote_currency: 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: """ Return exchange ccxt markets, filtered out by base currency and quote currency @@ -292,10 +292,10 @@ class Exchange: if not markets: raise OperationalException("Markets were not loaded.") - 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 base_currencies: + markets = {k: v for k, v in markets.items() if v['base'] in base_currencies} + if quote_currencies: + markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} if pairs_only: markets = {k: v for k, v in markets.items() if market_is_pair(v)} if active_only: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 966674f3b..b927d73bf 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -137,12 +137,12 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # 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', '') + base_currencies = args.get('base_currencies', []) + quote_currencies = args.get('quote_currencies', []) try: - pairs = exchange.get_markets(base_currency=base_currency, - quote_currency=quote_currency, + pairs = exchange.get_markets(base_currencies=base_currencies, + quote_currencies=quote_currencies, pairs_only=pairs_only, active_only=active_only) except Exception as e: @@ -152,9 +152,13 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") + ("active " if active_only else "") + (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" with {', '.join(base_currencies)} as base " + f"{plural(len(base_currencies), 'currency', 'currencies')}" + if base_currencies else "") + + (" and" if base_currencies and quote_currencies else "") + + (f" with {', '.join(quote_currencies)} as quote " + f"{plural(len(quote_currencies), 'currency', 'currencies')}" + if quote_currencies else "")) headers = ["Id", "Symbol", "Base", "Quote", "Active", *(['Is pair'] if not pairs_only else [])] From a8ffd29e1872e8dff14ae087a636e9c23120da6d Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 02:42:07 +0300 Subject: [PATCH 11/37] Remove --active-only, introduce -a/--all instead --- freqtrade/configuration/arguments.py | 2 +- freqtrade/configuration/cli_options.py | 11 ++++++----- freqtrade/utils.py | 4 +++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 937fe1a3c..646cdb2b7 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -35,7 +35,7 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", - "print_csv", "base_currencies", "quote_currencies", "active_only"] + "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] ARGS_CREATE_USERDIR = ["user_data_dir"] diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index ab3f4f00a..926824cf4 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -256,6 +256,12 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), # List pairs / markets + "list_pairs_all": Arg( + '-a', '--all', + help='Print all pairs or market symbols. By default only active ' + 'ones are shown.', + action='store_true', + ), "print_list": Arg( '--print-list', help='Print list of pairs or market symbols. By default data is ' @@ -283,11 +289,6 @@ AVAILABLE_CLI_OPTIONS = { help='Specify base currency(-ies). Space-separated list.', nargs='+', ), - "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/utils.py b/freqtrade/utils.py index b927d73bf..1e9aca647 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -136,7 +136,9 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # # Init exchange exchange = ExchangeResolver(config['exchange']['name'], config).exchange - active_only = args.get('active_only', False) + # By default only active pairs/markets are to be shown + active_only = not args.get('list_pairs_all', False) + base_currencies = args.get('base_currencies', []) quote_currencies = args.get('quote_currencies', []) From 837d4d82b4ba326815d948f0a0b3af64cd05d634 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 03:06:51 +0300 Subject: [PATCH 12/37] Sort tabular and csv data by symbol as well --- freqtrade/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 1e9aca647..703cff35d 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -1,5 +1,6 @@ import logging import sys +from collections import OrderedDict from pathlib import Path from typing import Any, Dict, List @@ -147,6 +148,8 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # quote_currencies=quote_currencies, pairs_only=pairs_only, active_only=active_only) + # Sort the pairs/markets by symbol + pairs = OrderedDict(sorted(pairs.items())) except Exception as e: raise OperationalException(f"Cannot get markets. Reason: {e}") from e @@ -180,13 +183,13 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # if args.get('print_list', False): # print data as a list, with human-readable summary print(summary_str + - (f": {', '.join(sorted(pairs.keys()))}" if len(pairs) else "") + ".") + (f": {', '.join(pairs.keys())}" if len(pairs) else "") + ".") elif args.get('print_one_column', False): if len(pairs): - print('\n'.join(sorted(pairs.keys()))) + print('\n'.join(pairs.keys())) elif args.get('list_pairs_print_json', False): if len(pairs): - print(rapidjson.dumps(sorted(pairs.keys()), default=str)) + print(rapidjson.dumps(pairs.keys(), default=str)) elif args.get('print_csv', False): if len(pairs): writer = csv.DictWriter(sys.stdout, fieldnames=headers) From bf4e9a5dbbfbd01c2d24753415f4ddd431b9cb63 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 04:34:05 +0300 Subject: [PATCH 13/37] Code cleanup --- freqtrade/utils.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 703cff35d..90140f6ba 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -125,7 +125,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: f"{', '.join(exchange.timeframes)}.") -def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # noqa: C901 +def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: """ Print pairs on the exchange :param args: Cli args from Arguments() @@ -180,24 +180,23 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: # args.get('print_csv', False)): logger.info(f"{summary_str}.") - if args.get('print_list', False): - # print data as a list, with human-readable summary - print(summary_str + - (f": {', '.join(pairs.keys())}" if len(pairs) else "") + ".") - elif args.get('print_one_column', False): - if len(pairs): + if len(pairs): + if args.get('print_list', False): + # print data as a list, with human-readable summary + print(f"{summary_str}: {', '.join(pairs.keys())}.") + elif args.get('print_one_column', False): print('\n'.join(pairs.keys())) - elif args.get('list_pairs_print_json', False): - if len(pairs): - print(rapidjson.dumps(pairs.keys(), default=str)) - elif args.get('print_csv', False): - if len(pairs): + elif args.get('list_pairs_print_json', False): + print(rapidjson.dumps(list(pairs.keys()), default=str)) + elif args.get('print_csv', False): writer = csv.DictWriter(sys.stdout, fieldnames=headers) writer.writeheader() writer.writerows(tabular_data) - else: - print(summary_str + - (":" if len(pairs) else ".")) - if len(pairs): - # print data as a table + else: + # print data as a table, with the human-readable summary + print(f"{summary_str}:") print(tabulate(tabular_data, headers='keys', tablefmt='pipe')) + elif not (args.get('print_one_column', False) or + args.get('list_pairs_print_json', False) or + args.get('print_csv', False)): + print(f"{summary_str}.") From ff6a3465a78a05324be21e50b5bce12b118143db Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 17:18:26 +0300 Subject: [PATCH 14/37] Docs added --- docs/utils.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/docs/utils.md b/docs/utils.md index 93162aca2..f662e8d6c 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -54,3 +54,63 @@ Timeframes available for the exchange `binance`: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4 ``` $ for i in `freqtrade list-exchanges -1`; do freqtrade list-timeframes --exchange $i; done ``` + +## List pairs/list markets + +The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets available on exchange. + +Pairs are markets with the '/' character between the base currency part and the quote currency part. + +These subcommands have same usage and same set of available options: + +``` +usage: freqtrade list-pairs [-h] [--exchange EXCHANGE] [--print-list] + [--print-json] [-1] [--print-csv] + [--base BASE_CURRENCIES [BASE_CURRENCIES ...]] + [--quote QUOTE_CURRENCIES [QUOTE_CURRENCIES ...]] + [-a] + +optional arguments: + -h, --help show this help message and exit + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. + --print-list Print list of pairs or market symbols. By default data + is printed in the tabular format. + --print-json Print list of pairs or market symbols in JSON format. + -1, --one-column Print output in one column. + --print-csv Print exchange pair or market data in the csv format. + --base BASE_CURRENCY [BASE_CURRENCY ...] + Specify base currency(-ies). Space-separated list. + --quote QUOTE_CURRENCY [QUOTE_CURRENCY ...] + Specify quote currency(-ies). Space-separated list. + -a, --all Print all pairs or market symbols. By default only + active ones are shown. +``` + +By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded +on the exchange. The see the list of all pairs/markets (not only the active ones), use the `-a`/`-all` option. + +Pairs/markets are sorted by its symbol string in the printed output. + +### Examples + +* Print the list of active pairs with quote currency USD on exchange, specified in the default +configuration file (i.e. pairs on the "Bittrex" exchange) in JSON format: + +``` +$ freqtrade list-pairs --quote USD --print-json +``` + +* Print the list of all pairs on the exchange, specified in the `config_binance.json` configuration file +(i.e. on the "Binance" exchange) with base currencies BTC or ETH and quote currencies USDT or USD, as the +human-readable list with summary: + +``` +$ freqtrade -c config_binance.json list-pairs --all --base BTC ETH --quote USDT USD --print-list +``` + +* Print all markets on exchange "Kraken", in the tabular format: + +``` +$ freqtrade list-markets --exchange kraken --all +``` From bd08874f1ff1c5396c42d53ad43a9653411eadb0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 17:31:49 +0300 Subject: [PATCH 15/37] Fix options metavars shown in the helpstring --- freqtrade/configuration/cli_options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 926824cf4..cb6dece5c 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -283,11 +283,13 @@ AVAILABLE_CLI_OPTIONS = { '--quote', help='Specify quote currency(-ies). Space-separated list.', nargs='+', + metavar='QUOTE_CURRENCY', ), "base_currencies": Arg( '--base', help='Specify base currency(-ies). Space-separated list.', nargs='+', + metavar='BASE_CURRENCY', ), # Script options "pairs": Arg( From 1e61263a28b3d7ebb026869ace86f1f32bfb51b5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 17:49:04 +0300 Subject: [PATCH 16/37] More sofisticated market_is_pair(), taken from #1989 --- freqtrade/exchange/exchange.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 95602082e..164f21ac6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -937,12 +937,17 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def market_is_pair(market): +def market_is_pair(market, base_currency: str = None, quote_currency: str = None): """ - Return True if the market is a pair. - Currently pairs are defined as markets containing '/' character in its symbol. + Check if the market 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. """ - return '/' in market.get('symbol', '') + 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): From 66605a19095122d15de21048581fb07e39ddab00 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 17:52:33 +0300 Subject: [PATCH 17/37] Add tests for plural(), taken from #1989 --- tests/test_misc.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index 320ed208c..23231e2f0 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,7 +7,7 @@ from unittest.mock import MagicMock from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import pair_data_filename from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, - file_load_json, format_ms_time, shorten_date) + file_load_json, format_ms_time, plural, shorten_date) def test_shorten_date() -> None: @@ -69,3 +69,35 @@ def test_format_ms_time() -> None: # Date 2017-12-13 08:02:01 date_in_epoch_ms = 1513152121000 assert format_ms_time(date_in_epoch_ms) == res.astimezone(None).strftime('%Y-%m-%dT%H:%M:%S') + + +def test_plural() -> None: + assert plural(0, "page") == "pages" + assert plural(0.0, "page") == "pages" + assert plural(1, "page") == "page" + assert plural(1.0, "page") == "page" + assert plural(2, "page") == "pages" + assert plural(2.0, "page") == "pages" + assert plural(-1, "page") == "page" + assert plural(-1.0, "page") == "page" + assert plural(-2, "page") == "pages" + assert plural(-2.0, "page") == "pages" + assert plural(0.5, "page") == "pages" + assert plural(1.5, "page") == "pages" + assert plural(-0.5, "page") == "pages" + assert plural(-1.5, "page") == "pages" + + assert plural(0, "ox", "oxen") == "oxen" + assert plural(0.0, "ox", "oxen") == "oxen" + assert plural(1, "ox", "oxen") == "ox" + assert plural(1.0, "ox", "oxen") == "ox" + assert plural(2, "ox", "oxen") == "oxen" + assert plural(2.0, "ox", "oxen") == "oxen" + assert plural(-1, "ox", "oxen") == "ox" + assert plural(-1.0, "ox", "oxen") == "ox" + assert plural(-2, "ox", "oxen") == "oxen" + assert plural(-2.0, "ox", "oxen") == "oxen" + assert plural(0.5, "ox", "oxen") == "oxen" + assert plural(1.5, "ox", "oxen") == "oxen" + assert plural(-0.5, "ox", "oxen") == "oxen" + assert plural(-1.5, "ox", "oxen") == "oxen" From e8eb968a6f2b9fa673c259ce0b52055ca9bd64d8 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 18:19:50 +0300 Subject: [PATCH 18/37] Add tests for market_is_pair() --- tests/exchange/test_exchange.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bf6025322..d61748d1d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -18,7 +18,8 @@ from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, - timeframe_to_seconds) + timeframe_to_seconds, + market_is_pair) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1555,3 +1556,24 @@ def test_timeframe_to_next_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_next_date("5m") > date + + +@pytest.mark.parametrize("market,base_currency,quote_currency,expected_result", [ + ({'symbol': "BTC/USDT"}, None, None, True), + ({'symbol': "USDT/BTC"}, None, None, True), + ({'symbol': "BTCUSDT"}, None, None, False), + ({'symbol': "BTC/USDT"}, None, "USDT", True), + ({'symbol': "USDT/BTC"}, None, "USDT", False), + ({'symbol': "BTCUSDT"}, None, "USDT", False), + ({'symbol': "BTC/USDT"}, "BTC", None, True), + ({'symbol': "USDT/BTC"}, "BTC", None, False), + ({'symbol': "BTCUSDT"}, "BTC", None, False), + ({'symbol': "BTC/USDT"}, "BTC", "USDT", True), + ({'symbol': "BTC/USDT"}, "USDT", "BTC", False), + ({'symbol': "BTC/USDT"}, "BTC", "USD", False), + ({'symbol': "BTCUSDT"}, "BTC", "USDT", False), + ({'symbol': "BTC/"}, None, None, False), + ({'symbol': "/USDT"}, None, None, False), +]) +def test_market_is_pair(market, base_currency, quote_currency, expected_result) -> None: + assert market_is_pair(market, base_currency, quote_currency) == expected_result From b6e26c82eacae6f082c09d990fa2b345737cf035 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 18:44:25 +0300 Subject: [PATCH 19/37] Replace market_is_pair() by symbol_is_pair() --- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/exchange.py | 8 +++---- freqtrade/utils.py | 5 +++-- tests/exchange/test_exchange.py | 38 ++++++++++++++++----------------- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 470091181..7545bff6a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -11,6 +11,6 @@ from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 timeframe_to_next_date, timeframe_to_prev_date) from freqtrade.exchange.exchange import (market_is_active, # noqa: F401 - market_is_pair) + symbol_is_pair) from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401 diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 164f21ac6..193982b74 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -297,7 +297,7 @@ class Exchange: if quote_currencies: markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} if pairs_only: - markets = {k: v for k, v in markets.items() if market_is_pair(v)} + markets = {k: v for k, v in markets.items() if symbol_is_pair(v['symbol'])} if active_only: markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets @@ -937,14 +937,14 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def market_is_pair(market, base_currency: str = None, quote_currency: str = None): +def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency: str = None): """ - Check if the market is a pair, i.e. that its symbol consists of the base currency and the + 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('/') + 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)) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 90140f6ba..e00b0739e 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -14,7 +14,7 @@ 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, market_is_active, - market_is_pair) + symbol_is_pair) from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -173,7 +173,8 @@ def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], 'Base': v['base'], 'Quote': v['quote'], 'Active': market_is_active(v), - **({'Is pair': market_is_pair(v)} if not pairs_only else {})}) + **({'Is pair': symbol_is_pair(v['symbol'])} + if not pairs_only else {})}) if (args.get('print_one_column', False) or args.get('list_pairs_print_json', False) or diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d61748d1d..20449bd6b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -19,7 +19,7 @@ from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds, - market_is_pair) + symbol_is_pair) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1558,22 +1558,22 @@ def test_timeframe_to_next_date(): assert timeframe_to_next_date("5m") > date -@pytest.mark.parametrize("market,base_currency,quote_currency,expected_result", [ - ({'symbol': "BTC/USDT"}, None, None, True), - ({'symbol': "USDT/BTC"}, None, None, True), - ({'symbol': "BTCUSDT"}, None, None, False), - ({'symbol': "BTC/USDT"}, None, "USDT", True), - ({'symbol': "USDT/BTC"}, None, "USDT", False), - ({'symbol': "BTCUSDT"}, None, "USDT", False), - ({'symbol': "BTC/USDT"}, "BTC", None, True), - ({'symbol': "USDT/BTC"}, "BTC", None, False), - ({'symbol': "BTCUSDT"}, "BTC", None, False), - ({'symbol': "BTC/USDT"}, "BTC", "USDT", True), - ({'symbol': "BTC/USDT"}, "USDT", "BTC", False), - ({'symbol': "BTC/USDT"}, "BTC", "USD", False), - ({'symbol': "BTCUSDT"}, "BTC", "USDT", False), - ({'symbol': "BTC/"}, None, None, False), - ({'symbol': "/USDT"}, None, None, False), +@pytest.mark.parametrize("market_symbol,base_currency,quote_currency,expected_result", [ + ("BTC/USDT", None, None, True), + ("USDT/BTC", None, None, True), + ("BTCUSDT", None, None, False), + ("BTC/USDT", None, "USDT", True), + ("USDT/BTC", None, "USDT", False), + ("BTCUSDT", None, "USDT", False), + ("BTC/USDT", "BTC", None, True), + ("USDT/BTC", "BTC", None, False), + ("BTCUSDT", "BTC", None, False), + ("BTC/USDT", "BTC", "USDT", True), + ("BTC/USDT", "USDT", "BTC", False), + ("BTC/USDT", "BTC", "USD", False), + ("BTCUSDT", "BTC", "USDT", False), + ("BTC/", None, None, False), + ("/USDT", None, None, False), ]) -def test_market_is_pair(market, base_currency, quote_currency, expected_result) -> None: - assert market_is_pair(market, base_currency, quote_currency) == expected_result +def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_result) -> None: + assert symbol_is_pair(market_symbol, base_currency, quote_currency) == expected_result From 84ba431d10f88c92e0f815115a8e51692463fa84 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 19:05:50 +0300 Subject: [PATCH 20/37] Introduce a market with no 'active' field in conftest --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6a0a74b5b..44f99b033 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -318,7 +318,8 @@ def markets(): 'symbol': 'TKN/BTC', 'base': 'TKN', 'quote': 'BTC', - 'active': True, + # According to ccxt, markets without active item set are also active + # 'active': True, 'precision': { 'price': 8, 'amount': 8, From 033742b7083fe673abab179ee52b745936281ce7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 19:06:58 +0300 Subject: [PATCH 21/37] Fix pairlists to use market_is_active() instead of custom check --- freqtrade/pairlist/IPairList.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index a112c63b4..5afb0c4c2 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -8,6 +8,9 @@ import logging from abc import ABC, abstractmethod from typing import List +from freqtrade.exchange import market_is_active + + logger = logging.getLogger(__name__) @@ -77,7 +80,7 @@ class IPairList(ABC): continue # Check if market is active market = markets[pair] - if not market['active']: + if not market_is_active(market): logger.info(f"Ignoring {pair} from whitelist. Market is not active.") continue sanitized_whitelist.add(pair) From 750dc8bf566d891861ec1adf03afbe99a81e46f5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 19:24:39 +0300 Subject: [PATCH 22/37] Add tests for market_is_active() --- tests/exchange/test_exchange.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 20449bd6b..fee9041b9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -19,7 +19,8 @@ from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds, - symbol_is_pair) + symbol_is_pair, + market_is_active) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re @@ -1577,3 +1578,12 @@ def test_timeframe_to_next_date(): ]) def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_result) -> None: assert symbol_is_pair(market_symbol, base_currency, quote_currency) == expected_result + + +@pytest.mark.parametrize("market,expected_result", [ + ({'symbol': 'ETH/BTC', 'active': True}, True), + ({'symbol': 'ETH/BTC', 'active': False}, False), + ({'symbol': 'ETH/BTC', }, True), +]) +def test_market_is_active(market, expected_result) -> None: + assert market_is_active(market) == expected_result From 8564affdf031088a0bdc3ba6a0f0b7c102384346 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 22:45:20 +0300 Subject: [PATCH 23/37] Add tests for Exchange.get_markets() --- freqtrade/exchange/exchange.py | 1 + tests/conftest.py | 44 ++++++++++++++++++++++ tests/exchange/test_exchange.py | 67 +++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 193982b74..9e3406428 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,6 +22,7 @@ from freqtrade import (DependencyException, InvalidOrderException, from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.misc import deep_merge_dicts + logger = logging.getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index 44f99b033..f21d61ee3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -510,6 +510,50 @@ def markets(): } }, 'info': {}, + }, + 'LTC/USD': { + 'id': 'USD-LTC', + 'symbol': 'LTC/USD', + 'base': 'LTC', + 'quote': 'USD', + 'active': True, + 'precision': { + 'amount': 8, + 'price': 8 + }, + 'limits': { + 'amount': { + 'min': 0.06646786, + 'max': None + }, + 'price': { + 'min': 1e-08, + 'max': None + } + }, + 'info': {}, + }, + 'XLTCUSDT': { + 'id': 'xLTCUSDT', + 'symbol': 'XLTCUSDT', + 'base': 'LTC', + 'quote': 'USDT', + 'active': True, + 'precision': { + 'amount': 8, + 'price': 8 + }, + 'limits': { + 'amount': { + 'min': 0.06646786, + 'max': None + }, + 'price': { + 'min': 1e-08, + 'max': None + } + }, + 'info': {}, } } diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index fee9041b9..c5f41a784 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1488,6 +1488,73 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): ex.get_valid_pair_combination("NOPAIR", "ETH") +@pytest.mark.parametrize("base_currencies, quote_currencies, pairs_only, active_only," + "expected_keys", [ +# Testing markets (in conftest.py): +# 'BLK/BTC': 'active': True +# 'BTT/BTC': 'active': True +# 'ETH/BTC': 'active': True +# 'ETH/USDT': 'active': True +# 'LTC/BTC': 'active': False +# 'LTC/USD': 'active': True +# 'LTC/USDT': 'active': True +# 'NEO/BTC': 'active': False +# 'TKN/BTC': 'active' not set +# 'XLTCUSDT': 'active': True, not a pair +# 'XRP/BTC': 'active': False + # all markets + ([], [], False, False, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', + 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), + # active markets + ([], [], False, True, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC', 'XLTCUSDT']), + # all pairs + ([], [], True, False, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', + 'TKN/BTC', 'XRP/BTC']), + # active pairs + ([], [], True, True, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC']), + # all markets, base=ETH, LTC + (['ETH', 'LTC'], [], False, False, + ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # all markets, base=LTC + (['LTC'], [], False, False, + ['LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # all markets, quote=USDT + ([], ['USDT'], False, False, + ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']), + # all markets, quote=USDT, USD + ([], ['USDT', 'USD'], False, False, + ['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # all markets, base=LTC, quote=USDT + (['LTC'], ['USDT'], False, False, + ['LTC/USDT', 'XLTCUSDT']), + # all pairs, base=LTC, quote=USDT + (['LTC'], ['USDT'], True, False, + ['LTC/USDT']), + # all markets, base=LTC, quote=USDT, NONEXISTENT + (['LTC'], ['USDT', 'NONEXISTENT'], False, False, + ['LTC/USDT', 'XLTCUSDT']), + # all markets, base=LTC, quote=NONEXISTENT + (['LTC'], ['NONEXISTENT'], False, False, + []), +]) +def test_get_markets(default_conf, mocker, markets, + base_currencies, quote_currencies, pairs_only, active_only, + expected_keys): + mocker.patch.multiple('freqtrade.exchange.Exchange', + _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), + validate_pairs=MagicMock(), + validate_timeframes=MagicMock(), + markets=PropertyMock(return_value=markets)) + ex = Exchange(default_conf) + pairs = ex.get_markets(base_currencies, quote_currencies, pairs_only, active_only) + assert sorted(pairs.keys()) == sorted(expected_keys) + + def test_timeframe_to_minutes(): assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("10m") == 10 From 2ebddcf45c6ad0802e91955de7c3636c33980b7b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 17 Oct 2019 23:40:29 +0300 Subject: [PATCH 24/37] Make flake happy again --- tests/exchange/test_exchange.py | 129 ++++++++++++++++---------------- 1 file changed, 65 insertions(+), 64 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c5f41a784..a3135415c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -925,17 +925,17 @@ def test_get_balances_prod(default_conf, mocker, exchange_name): def test_get_tickers(default_conf, mocker, exchange_name): 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, - } - } + 'symbol': 'ETH/BTC', + 'bid': 0.5, + 'ask': 1, + 'last': 42, + }, 'BCH/BTC': { + 'symbol': 'BCH/BTC', + 'bid': 0.6, + 'ask': 0.5, + 'last': 41, + } + } api_mock.fetch_tickers = MagicMock(return_value=tick) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker @@ -1488,59 +1488,60 @@ def test_get_valid_pair_combination(default_conf, mocker, markets): ex.get_valid_pair_combination("NOPAIR", "ETH") -@pytest.mark.parametrize("base_currencies, quote_currencies, pairs_only, active_only," - "expected_keys", [ -# Testing markets (in conftest.py): -# 'BLK/BTC': 'active': True -# 'BTT/BTC': 'active': True -# 'ETH/BTC': 'active': True -# 'ETH/USDT': 'active': True -# 'LTC/BTC': 'active': False -# 'LTC/USD': 'active': True -# 'LTC/USDT': 'active': True -# 'NEO/BTC': 'active': False -# 'TKN/BTC': 'active' not set -# 'XLTCUSDT': 'active': True, not a pair -# 'XRP/BTC': 'active': False - # all markets - ([], [], False, False, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', - 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), - # active markets - ([], [], False, True, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC', 'XLTCUSDT']), - # all pairs - ([], [], True, False, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'NEO/BTC', - 'TKN/BTC', 'XRP/BTC']), - # active pairs - ([], [], True, True, - ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC']), - # all markets, base=ETH, LTC - (['ETH', 'LTC'], [], False, False, - ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), - # all markets, base=LTC - (['LTC'], [], False, False, - ['LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), - # all markets, quote=USDT - ([], ['USDT'], False, False, - ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']), - # all markets, quote=USDT, USD - ([], ['USDT', 'USD'], False, False, - ['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), - # all markets, base=LTC, quote=USDT - (['LTC'], ['USDT'], False, False, - ['LTC/USDT', 'XLTCUSDT']), - # all pairs, base=LTC, quote=USDT - (['LTC'], ['USDT'], True, False, - ['LTC/USDT']), - # all markets, base=LTC, quote=USDT, NONEXISTENT - (['LTC'], ['USDT', 'NONEXISTENT'], False, False, - ['LTC/USDT', 'XLTCUSDT']), - # all markets, base=LTC, quote=NONEXISTENT - (['LTC'], ['NONEXISTENT'], False, False, - []), -]) +@pytest.mark.parametrize( + "base_currencies, quote_currencies, pairs_only, active_only, expected_keys", [ + # Testing markets (in conftest.py): + # 'BLK/BTC': 'active': True + # 'BTT/BTC': 'active': True + # 'ETH/BTC': 'active': True + # 'ETH/USDT': 'active': True + # 'LTC/BTC': 'active': False + # 'LTC/USD': 'active': True + # 'LTC/USDT': 'active': True + # 'NEO/BTC': 'active': False + # 'TKN/BTC': 'active' not set + # 'XLTCUSDT': 'active': True, not a pair + # 'XRP/BTC': 'active': False + # all markets + ([], [], False, False, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', + 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XLTCUSDT', 'XRP/BTC']), + # active markets + ([], [], False, True, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', + 'TKN/BTC', 'XLTCUSDT']), + # all pairs + ([], [], True, False, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', + 'LTC/USDT', 'NEO/BTC', 'TKN/BTC', 'XRP/BTC']), + # active pairs + ([], [], True, True, + ['BLK/BTC', 'BTT/BTC', 'ETH/BTC', 'ETH/USDT', 'LTC/USD', 'LTC/USDT', 'TKN/BTC']), + # all markets, base=ETH, LTC + (['ETH', 'LTC'], [], False, False, + ['ETH/BTC', 'ETH/USDT', 'LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # all markets, base=LTC + (['LTC'], [], False, False, + ['LTC/BTC', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # all markets, quote=USDT + ([], ['USDT'], False, False, + ['ETH/USDT', 'LTC/USDT', 'XLTCUSDT']), + # all markets, quote=USDT, USD + ([], ['USDT', 'USD'], False, False, + ['ETH/USDT', 'LTC/USD', 'LTC/USDT', 'XLTCUSDT']), + # all markets, base=LTC, quote=USDT + (['LTC'], ['USDT'], False, False, + ['LTC/USDT', 'XLTCUSDT']), + # all pairs, base=LTC, quote=USDT + (['LTC'], ['USDT'], True, False, + ['LTC/USDT']), + # all markets, base=LTC, quote=USDT, NONEXISTENT + (['LTC'], ['USDT', 'NONEXISTENT'], False, False, + ['LTC/USDT', 'XLTCUSDT']), + # all markets, base=LTC, quote=NONEXISTENT + (['LTC'], ['NONEXISTENT'], False, False, + []), + ]) def test_get_markets(default_conf, mocker, markets, base_currencies, quote_currencies, pairs_only, active_only, expected_keys): From 369335b80c2c6a3720ad036d2c1d1bb9ce85a780 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 18 Oct 2019 00:48:40 +0300 Subject: [PATCH 25/37] Add tests for start_list_pairs() --- tests/test_utils.py | 159 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 55672c4c9..bc641af34 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,7 @@ from freqtrade import OperationalException from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, - start_list_timeframes) + start_list_pairs, start_list_timeframes) from tests.conftest import get_args, log_has, patch_exchange @@ -164,6 +164,163 @@ def test_list_timeframes(mocker, capsys): assert re.search(r"^1d$", captured.out, re.MULTILINE) +def test_list_markets(mocker, markets, capsys): + + api_mock = MagicMock() + api_mock.markets = markets + patch_exchange(mocker, api_mock=api_mock) + + # Test with no --config + args = [ + "list-markets", + ] + with pytest.raises(OperationalException, + match=r"This command requires a configured exchange.*"): + start_list_pairs(get_args(args), False) + + # Test with --config config.json.example + args = [ + '--config', 'config.json.example', + "list-markets", + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 8 active markets:\n", + captured.out) + +# # Test with --exchange +# args = [ +# "list-markets", +# "--exchange", "binance" +# ] +# start_list_pairs(get_args(args), False) +# captured = capsys.readouterr() +# assert re.match("Exchange Binance has 8 active markets:\n", +# captured.out) + + # Test with --all: all markets + args = [ + '--config', 'config.json.example', + "list-markets", "--all" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 11 markets:\n", + captured.out) + + # Test list-pairs subcommand: active pairs + args = [ + '--config', 'config.json.example', + "list-pairs", + ] + start_list_pairs(get_args(args), True) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 7 active pairs:\n", + captured.out) + + # Test list-pairs subcommand with --all: all pairs + args = [ + '--config', 'config.json.example', + "list-pairs", "--all" + ] + start_list_pairs(get_args(args), True) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 10 pairs:\n", + captured.out) + + # active markets, base=ETH, LTC + args = [ + '--config', 'config.json.example', + "list-markets", + "--base", "ETH", "LTC" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies:\n", + captured.out) + + # active markets, base=LTC + args = [ + '--config', 'config.json.example', + "list-markets", + "--base", "LTC" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 3 active markets with LTC as base currency:\n", + captured.out) + + # active markets, quote=USDT, USD + args = [ + '--config', 'config.json.example', + "list-markets", + "--quote", "USDT", "USD" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 4 active markets with USDT, USD as quote currencies:\n", + captured.out) + + # active markets, quote=USDT + args = [ + '--config', 'config.json.example', + "list-markets", + "--quote", "USDT" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 3 active markets with USDT as quote currency:\n", + captured.out) + + # active markets, base=LTC, quote=USDT + args = [ + '--config', 'config.json.example', + "list-markets", + "--base", "LTC", "--quote", "USDT" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 2 active markets with LTC as base currency and " + "with USDT as quote currency:\n", + captured.out) + + # active pairs, base=LTC, quote=USDT + args = [ + '--config', 'config.json.example', + "list-pairs", + "--base", "LTC", "--quote", "USDT" + ] + start_list_pairs(get_args(args), True) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 1 active pair with LTC as base currency and " + "with USDT as quote currency:\n", + captured.out) + + # active markets, base=LTC, quote=USDT, NONEXISTENT + args = [ + '--config', 'config.json.example', + "list-markets", + "--base", "LTC", "--quote", "USDT", "NONEXISTENT" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 2 active markets with LTC as base currency and " + "with USDT, NONEXISTENT as quote currencies:\n", + captured.out) + + # active markets, base=LTC, quote=NONEXISTENT + args = [ + '--config', 'config.json.example', + "list-markets", + "--base", "LTC", "--quote", "NONEXISTENT" + ] + start_list_pairs(get_args(args), False) + captured = capsys.readouterr() + assert re.match("Exchange Bittrex has 0 active markets with LTC as base currency and " + "with NONEXISTENT as quote currency.\n", + captured.out) + + def test_create_datadir_failed(caplog): args = [ From e957894852db8d817025da071fb06cf4c7a2366a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 18 Oct 2019 01:26:05 +0300 Subject: [PATCH 26/37] Rename start_list_pairs() -> start_list_markets() --- freqtrade/configuration/arguments.py | 6 +++--- freqtrade/utils.py | 4 ++-- tests/test_utils.py | 30 ++++++++++++++-------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 646cdb2b7..07813b104 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -112,7 +112,7 @@ 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_pairs) + start_list_markets) subparsers = self.parser.add_subparsers(dest='subparser') @@ -158,7 +158,7 @@ class Arguments: 'list-markets', help='Print markets on exchange.' ) - list_markets_cmd.set_defaults(func=partial(start_list_pairs, pairs_only=False)) + list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False)) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd) # Add list-pairs subcommand @@ -166,7 +166,7 @@ class Arguments: 'list-pairs', help='Print pairs on exchange.' ) - list_pairs_cmd.set_defaults(func=partial(start_list_pairs, pairs_only=True)) + list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True)) self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd) # Add download-data subcommand diff --git a/freqtrade/utils.py b/freqtrade/utils.py index e00b0739e..29b5c92f4 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -125,9 +125,9 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: f"{', '.join(exchange.timeframes)}.") -def start_list_pairs(args: Dict[str, Any], pairs_only: bool = False) -> None: +def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: """ - Print pairs on the exchange + Print pairs/markets on the exchange :param args: Cli args from Arguments() :param pairs_only: if True print only pairs, otherwise print all instruments (markets) :return: None diff --git a/tests/test_utils.py b/tests/test_utils.py index bc641af34..bcb49b7b5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -8,7 +8,7 @@ from freqtrade import OperationalException from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_list_exchanges, - start_list_pairs, start_list_timeframes) + start_list_markets, start_list_timeframes) from tests.conftest import get_args, log_has, patch_exchange @@ -176,14 +176,14 @@ def test_list_markets(mocker, markets, capsys): ] with pytest.raises(OperationalException, match=r"This command requires a configured exchange.*"): - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) # Test with --config config.json.example args = [ '--config', 'config.json.example', "list-markets", ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 8 active markets:\n", captured.out) @@ -193,7 +193,7 @@ def test_list_markets(mocker, markets, capsys): # "list-markets", # "--exchange", "binance" # ] -# start_list_pairs(get_args(args), False) +# start_list_markets(get_args(args), False) # captured = capsys.readouterr() # assert re.match("Exchange Binance has 8 active markets:\n", # captured.out) @@ -203,7 +203,7 @@ def test_list_markets(mocker, markets, capsys): '--config', 'config.json.example', "list-markets", "--all" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 11 markets:\n", captured.out) @@ -213,7 +213,7 @@ def test_list_markets(mocker, markets, capsys): '--config', 'config.json.example', "list-pairs", ] - start_list_pairs(get_args(args), True) + start_list_markets(get_args(args), True) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 7 active pairs:\n", captured.out) @@ -223,7 +223,7 @@ def test_list_markets(mocker, markets, capsys): '--config', 'config.json.example', "list-pairs", "--all" ] - start_list_pairs(get_args(args), True) + start_list_markets(get_args(args), True) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 10 pairs:\n", captured.out) @@ -234,7 +234,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--base", "ETH", "LTC" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies:\n", captured.out) @@ -245,7 +245,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--base", "LTC" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 3 active markets with LTC as base currency:\n", captured.out) @@ -256,7 +256,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--quote", "USDT", "USD" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 4 active markets with USDT, USD as quote currencies:\n", captured.out) @@ -267,7 +267,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--quote", "USDT" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 3 active markets with USDT as quote currency:\n", captured.out) @@ -278,7 +278,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--base", "LTC", "--quote", "USDT" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 2 active markets with LTC as base currency and " "with USDT as quote currency:\n", @@ -290,7 +290,7 @@ def test_list_markets(mocker, markets, capsys): "list-pairs", "--base", "LTC", "--quote", "USDT" ] - start_list_pairs(get_args(args), True) + start_list_markets(get_args(args), True) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 1 active pair with LTC as base currency and " "with USDT as quote currency:\n", @@ -302,7 +302,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--base", "LTC", "--quote", "USDT", "NONEXISTENT" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 2 active markets with LTC as base currency and " "with USDT, NONEXISTENT as quote currencies:\n", @@ -314,7 +314,7 @@ def test_list_markets(mocker, markets, capsys): "list-markets", "--base", "LTC", "--quote", "NONEXISTENT" ] - start_list_pairs(get_args(args), False) + start_list_markets(get_args(args), False) captured = capsys.readouterr() assert re.match("Exchange Bittrex has 0 active markets with LTC as base currency and " "with NONEXISTENT as quote currency.\n", From 5e731ec2789bc6d11ffe0988222cee47fb2ce6ea Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 18 Oct 2019 14:25:43 +0300 Subject: [PATCH 27/37] Add more tests --- tests/test_utils.py | 154 +++++++++++++++++++++++++++++++++----------- 1 file changed, 116 insertions(+), 38 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index bcb49b7b5..880a3762e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -182,11 +182,13 @@ def test_list_markets(mocker, markets, capsys): args = [ '--config', 'config.json.example', "list-markets", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 8 active markets:\n", - captured.out) + assert ("Exchange Bittrex has 8 active markets: " + "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC, XLTCUSDT.\n" + in captured.out) # # Test with --exchange # args = [ @@ -201,124 +203,200 @@ def test_list_markets(mocker, markets, capsys): # Test with --all: all markets args = [ '--config', 'config.json.example', - "list-markets", "--all" + "list-markets", "--all", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 11 markets:\n", - captured.out) + assert ("Exchange Bittrex has 11 markets: " + "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, LTC/USDT, NEO/BTC, " + "TKN/BTC, XLTCUSDT, XRP/BTC.\n" + in captured.out) # Test list-pairs subcommand: active pairs args = [ '--config', 'config.json.example', "list-pairs", + "--print-list", ] start_list_markets(get_args(args), True) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 7 active pairs:\n", - captured.out) + assert ("Exchange Bittrex has 7 active pairs: " + "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC.\n" + in captured.out) # Test list-pairs subcommand with --all: all pairs args = [ '--config', 'config.json.example', - "list-pairs", "--all" + "list-pairs", "--all", + "--print-list", ] start_list_markets(get_args(args), True) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 10 pairs:\n", - captured.out) + assert ("Exchange Bittrex has 10 pairs: " + "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/BTC, LTC/USD, LTC/USDT, NEO/BTC, " + "TKN/BTC, XRP/BTC.\n" + in captured.out) # active markets, base=ETH, LTC args = [ '--config', 'config.json.example', "list-markets", - "--base", "ETH", "LTC" + "--base", "ETH", "LTC", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies:\n", - captured.out) + assert ("Exchange Bittrex has 5 active markets with ETH, LTC as base currencies: " + "ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, XLTCUSDT.\n" + in captured.out) # active markets, base=LTC args = [ '--config', 'config.json.example', "list-markets", - "--base", "LTC" + "--base", "LTC", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 3 active markets with LTC as base currency:\n", - captured.out) + assert ("Exchange Bittrex has 3 active markets with LTC as base currency: " + "LTC/USD, LTC/USDT, XLTCUSDT.\n" + in captured.out) # active markets, quote=USDT, USD args = [ '--config', 'config.json.example', "list-markets", - "--quote", "USDT", "USD" + "--quote", "USDT", "USD", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 4 active markets with USDT, USD as quote currencies:\n", - captured.out) + assert ("Exchange Bittrex has 4 active markets with USDT, USD as quote currencies: " + "ETH/USDT, LTC/USD, LTC/USDT, XLTCUSDT.\n" + in captured.out) # active markets, quote=USDT args = [ '--config', 'config.json.example', "list-markets", - "--quote", "USDT" + "--quote", "USDT", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 3 active markets with USDT as quote currency:\n", - captured.out) + assert ("Exchange Bittrex has 3 active markets with USDT as quote currency: " + "ETH/USDT, LTC/USDT, XLTCUSDT.\n" + in captured.out) # active markets, base=LTC, quote=USDT args = [ '--config', 'config.json.example', "list-markets", - "--base", "LTC", "--quote", "USDT" + "--base", "LTC", "--quote", "USDT", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 2 active markets with LTC as base currency and " - "with USDT as quote currency:\n", - captured.out) + assert ("Exchange Bittrex has 2 active markets with LTC as base currency and " + "with USDT as quote currency: LTC/USDT, XLTCUSDT.\n" + in captured.out) # active pairs, base=LTC, quote=USDT args = [ '--config', 'config.json.example', "list-pairs", - "--base", "LTC", "--quote", "USDT" + "--base", "LTC", "--quote", "USDT", + "--print-list", ] start_list_markets(get_args(args), True) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 1 active pair with LTC as base currency and " - "with USDT as quote currency:\n", - captured.out) + assert ("Exchange Bittrex has 1 active pair with LTC as base currency and " + "with USDT as quote currency: LTC/USDT.\n" + in captured.out) # active markets, base=LTC, quote=USDT, NONEXISTENT args = [ '--config', 'config.json.example', "list-markets", - "--base", "LTC", "--quote", "USDT", "NONEXISTENT" + "--base", "LTC", "--quote", "USDT", "NONEXISTENT", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 2 active markets with LTC as base currency and " - "with USDT, NONEXISTENT as quote currencies:\n", - captured.out) + assert ("Exchange Bittrex has 2 active markets with LTC as base currency and " + "with USDT, NONEXISTENT as quote currencies: LTC/USDT, XLTCUSDT.\n" + in captured.out) # active markets, base=LTC, quote=NONEXISTENT args = [ '--config', 'config.json.example', "list-markets", - "--base", "LTC", "--quote", "NONEXISTENT" + "--base", "LTC", "--quote", "NONEXISTENT", + "--print-list", ] start_list_markets(get_args(args), False) captured = capsys.readouterr() - assert re.match("Exchange Bittrex has 0 active markets with LTC as base currency and " - "with NONEXISTENT as quote currency.\n", - captured.out) + assert ("Exchange Bittrex has 0 active markets with LTC as base currency and " + "with NONEXISTENT as quote currency.\n" + in captured.out) + + # Test tabular output + args = [ + '--config', 'config.json.example', + "list-markets", + ] + start_list_markets(get_args(args), False) + captured = capsys.readouterr() + assert ("Exchange Bittrex has 8 active markets:\n" + in captured.out) + + # Test tabular output, no markets found + args = [ + '--config', 'config.json.example', + "list-markets", + "--base", "LTC", "--quote", "NONEXISTENT", + ] + start_list_markets(get_args(args), False) + captured = capsys.readouterr() + assert ("Exchange Bittrex has 0 active markets with LTC as base currency and " + "with NONEXISTENT as quote currency.\n" + in captured.out) + + # Test --print-json + args = [ + '--config', 'config.json.example', + "list-markets", + "--print-json" + ] + start_list_markets(get_args(args), False) + captured = capsys.readouterr() + assert ('["BLK/BTC","BTT/BTC","ETH/BTC","ETH/USDT","LTC/USD","LTC/USDT","TKN/BTC","XLTCUSDT"]' + in captured.out) + + # Test --print-csv + args = [ + '--config', 'config.json.example', + "list-markets", + "--print-csv" + ] + start_list_markets(get_args(args), False) + captured = capsys.readouterr() + assert ("Id,Symbol,Base,Quote,Active,Is pair" in captured.out) + assert ("blkbtc,BLK/BTC,BLK,BTC,True,True" in captured.out) + assert ("BTTBTC,BTT/BTC,BTT,BTC,True,True" in captured.out) + + # Test --one-column + args = [ + '--config', 'config.json.example', + "list-markets", + "--one-column" + ] + start_list_markets(get_args(args), False) + captured = capsys.readouterr() + assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) + assert re.search(r"^BTT/BTC$", captured.out, re.MULTILINE) def test_create_datadir_failed(caplog): From d6b6ded8bd51a19b8b87287e43b83d0bfd4f4836 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 20 Oct 2019 22:28:54 +0300 Subject: [PATCH 28/37] Print empty line separator in case of human-readable formats (list and tabular) --- freqtrade/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 29b5c92f4..1f7650420 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -179,7 +179,13 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: if (args.get('print_one_column', False) or args.get('list_pairs_print_json', False) or args.get('print_csv', False)): + # Print summary string in the log in case of machine-readable + # regular formats. logger.info(f"{summary_str}.") + else: + # Print empty string separating leading logs and output in case of + # human-readable formats. + print() if len(pairs): if args.get('print_list', False): From 10ca249293fa81c7045a429b6c24561a07bab025 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 20 Oct 2019 22:43:00 +0300 Subject: [PATCH 29/37] Fix fluky test --- tests/test_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 880a3762e..3993787a6 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -174,9 +174,11 @@ def test_list_markets(mocker, markets, capsys): args = [ "list-markets", ] + pargs = get_args(args) + pargs['config'] = None with pytest.raises(OperationalException, match=r"This command requires a configured exchange.*"): - start_list_markets(get_args(args), False) + start_list_markets(pargs, False) # Test with --config config.json.example args = [ From 45b2d24b7941ec18f56897a84e068ccc838ee034 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 20 Oct 2019 23:00:17 +0300 Subject: [PATCH 30/37] Improve docs --- docs/utils.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index f662e8d6c..e6a4501bb 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -59,16 +59,25 @@ $ for i in `freqtrade list-exchanges -1`; do freqtrade list-timeframes --exchang The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets available on exchange. -Pairs are markets with the '/' character between the base currency part and the quote currency part. +Pairs are markets with the '/' character between the base currency part and the quote currency part in the market symbol. + +For example, in the 'ETH/BTC' pair 'ETH' is the base currency, while 'BTC' is the quote currency. + +For pairs traded by Freqtrade the pair quote currency should be equal to the value of the `stake_currency` configuration setting. These subcommands have same usage and same set of available options: ``` +usage: freqtrade list-markets [-h] [--exchange EXCHANGE] [--print-list] + [--print-json] [-1] [--print-csv] + [--base BASE_CURRENCY [BASE_CURRENCY ...]] + [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] + [-a] + usage: freqtrade list-pairs [-h] [--exchange EXCHANGE] [--print-list] [--print-json] [-1] [--print-csv] - [--base BASE_CURRENCIES [BASE_CURRENCIES ...]] - [--quote QUOTE_CURRENCIES [QUOTE_CURRENCIES ...]] - [-a] + [--base BASE_CURRENCY [BASE_CURRENCY ...]] + [--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a] optional arguments: -h, --help show this help message and exit From 5b680f2ece8b53e6dd09d21fc9516e0c14e891ad Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sun, 20 Oct 2019 23:26:25 +0300 Subject: [PATCH 31/37] minor: Condense paragraphs in the docs --- docs/utils.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index e6a4501bb..9d44e7282 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -60,9 +60,7 @@ $ for i in `freqtrade list-exchanges -1`; do freqtrade list-timeframes --exchang The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets available on exchange. Pairs are markets with the '/' character between the base currency part and the quote currency part in the market symbol. - For example, in the 'ETH/BTC' pair 'ETH' is the base currency, while 'BTC' is the quote currency. - For pairs traded by Freqtrade the pair quote currency should be equal to the value of the `stake_currency` configuration setting. These subcommands have same usage and same set of available options: From 8a0d90136c0425c517f0fc65a0d1bea69166c714 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sun, 20 Oct 2019 23:31:44 +0300 Subject: [PATCH 32/37] Improve unclear sentence in the docs --- docs/utils.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/utils.md b/docs/utils.md index 9d44e7282..57bb53cc7 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -61,7 +61,8 @@ The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets a Pairs are markets with the '/' character between the base currency part and the quote currency part in the market symbol. For example, in the 'ETH/BTC' pair 'ETH' is the base currency, while 'BTC' is the quote currency. -For pairs traded by Freqtrade the pair quote currency should be equal to the value of the `stake_currency` configuration setting. +For pairs traded by Freqtrade the pair quote currency is defined by the value of the `stake_currency` configuration setting, +but using these commands you can print info about any pair/market, with any quote currency. These subcommands have same usage and same set of available options: From ca4d0067e4b6cbfd302353bc9865fcd0f528797b Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 21 Oct 2019 02:15:37 +0300 Subject: [PATCH 33/37] Uncomment tests with --exchange --- tests/test_utils.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 35e18246c..f12b6670b 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -192,15 +192,17 @@ def test_list_markets(mocker, markets, capsys): "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC, XLTCUSDT.\n" in captured.out) -# # Test with --exchange -# args = [ -# "list-markets", -# "--exchange", "binance" -# ] -# start_list_markets(get_args(args), False) -# captured = capsys.readouterr() -# assert re.match("Exchange Binance has 8 active markets:\n", -# captured.out) + # Test with --exchange + args = [ + "list-markets", + "--exchange", "binance" + ] + pargs = get_args(args) + pargs['config'] = None + start_list_markets(pargs, False) + captured = capsys.readouterr() + assert re.match("Exchange Binance has 8 active markets:\n", + captured.out) # Test with --all: all markets args = [ From b116cc75c4ceaa6435a773469d3ba2918fde2678 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Oct 2019 07:07:25 +0200 Subject: [PATCH 34/37] Fix failing test --- tests/test_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index f12b6670b..0ad6a1369 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -192,6 +192,7 @@ def test_list_markets(mocker, markets, capsys): "BLK/BTC, BTT/BTC, ETH/BTC, ETH/USDT, LTC/USD, LTC/USDT, TKN/BTC, XLTCUSDT.\n" in captured.out) + patch_exchange(mocker, api_mock=api_mock, id="binance") # Test with --exchange args = [ "list-markets", @@ -201,9 +202,10 @@ def test_list_markets(mocker, markets, capsys): pargs['config'] = None start_list_markets(pargs, False) captured = capsys.readouterr() - assert re.match("Exchange Binance has 8 active markets:\n", + assert re.match("\nExchange Binance has 8 active markets:\n", captured.out) + patch_exchange(mocker, api_mock=api_mock, id="bittrex") # Test with --all: all markets args = [ '--config', 'config.json.example', From ff5ba64385e97a40b2b78406fd542c2368536fb7 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 21 Oct 2019 14:06:46 +0300 Subject: [PATCH 35/37] Improve docs --- docs/utils.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 57bb53cc7..9f5792660 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -61,8 +61,10 @@ The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets a Pairs are markets with the '/' character between the base currency part and the quote currency part in the market symbol. For example, in the 'ETH/BTC' pair 'ETH' is the base currency, while 'BTC' is the quote currency. -For pairs traded by Freqtrade the pair quote currency is defined by the value of the `stake_currency` configuration setting, -but using these commands you can print info about any pair/market, with any quote currency. + +For pairs traded by Freqtrade the pair quote currency is defined by the value of the `stake_currency` configuration setting. + +You can print info about any pair/market with these subcommands - and you can filter output by quote-currency using `--quote BTC`, or by base-currency using `--base ETH` options correspondingly. These subcommands have same usage and same set of available options: From 562e4e63de257d6260e9ec905261522fd7eaaea1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 22 Oct 2019 13:48:54 +0300 Subject: [PATCH 36/37] =?UTF-8?q?Set=20validate=3DFalse=20for=20exchang?= =?UTF-8?q?=C3=91e=20in=20start=5Flist=5Fmarkets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- freqtrade/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 3aadaa893..f4e9c8a3c 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -147,7 +147,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: config = setup_utils_configuration(args, RunMode.OTHER) # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config).exchange + exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange # By default only active pairs/markets are to be shown active_only = not args.get('list_pairs_all', False) From b26faa13bd0b43fff98d428b904f68e3a0d54e60 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 22 Oct 2019 13:51:36 +0300 Subject: [PATCH 37/37] Call validate_timeframe only when validate is True --- freqtrade/exchange/exchange.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8f1aa0c44..71f0737ef 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -217,20 +217,22 @@ class Exchange: logger.info('Using Exchange "%s"', self.name) - # Check if timeframe is available - self.validate_timeframes(config.get('ticker_interval')) - - # Converts the interval provided in minutes in config to seconds - self.markets_refresh_interval: int = exchange_config.get( - "markets_refresh_interval", 60) * 60 if validate: + # Check if timeframe is available + self.validate_timeframes(config.get('ticker_interval')) + # Initial markets load self._load_markets() + # Check if all pairs are available self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) + # Converts the interval provided in minutes in config to seconds + self.markets_refresh_interval: int = exchange_config.get( + "markets_refresh_interval", 60) * 60 + def __del__(self): """ Destructor - clean up async stuff