From f3f6e9d365e06f3e82c533da460c7cf2f85540a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Oct 2019 10:33:22 +0200 Subject: [PATCH 01/50] Allow skipping of exchange validation --- freqtrade/exchange/exchange.py | 16 ++++++++-------- freqtrade/resolvers/exchange_resolver.py | 7 ++++--- freqtrade/utils.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index df7e5e2b4..8edbeaf50 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -155,7 +155,7 @@ class Exchange: } _ft_has: Dict = {} - def __init__(self, config: dict) -> None: + def __init__(self, config: dict, validate: bool = True) -> None: """ Initializes this module with the given config, it does basic validation whether the specified exchange and pairs are valid. @@ -209,13 +209,13 @@ class Exchange: # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 - # 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', {})) + if validate: + # 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', {})) def __del__(self): """ diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 6fb12a65f..929ba197a 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -17,14 +17,15 @@ class ExchangeResolver(IResolver): __slots__ = ['exchange'] - def __init__(self, exchange_name: str, config: dict) -> None: + def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None: """ Load the custom class from config parameter :param config: configuration dictionary """ exchange_name = exchange_name.title() try: - self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) + self.exchange = self._load_exchange(exchange_name, kwargs={'config': config, + 'validate': validate}) except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") @@ -43,7 +44,7 @@ class ExchangeResolver(IResolver): try: ex_class = getattr(exchanges, exchange_name) - exchange = ex_class(kwargs['config']) + exchange = ex_class(**kwargs) if exchange: logger.info(f"Using resolved exchange '{exchange_name}'...") return exchange diff --git a/freqtrade/utils.py b/freqtrade/utils.py index b3ff43aca..8f58c3158 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -110,7 +110,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: config['ticker_interval'] = None # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config).exchange + exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange if args['print_one_column']: print('\n'.join(exchange.timeframes)) From 7cf7982565b2a21dd4f3fb864fbb9a269cf6c64f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 13 Oct 2019 13:12:20 +0300 Subject: [PATCH 02/50] 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 03/50] 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 04/50] 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 05/50] 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 06/50] 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 07/50] --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 08/50] 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 09/50] 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 10/50] 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 11/50] 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 12/50] 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 13/50] 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 14/50] 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 15/50] 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 16/50] 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 17/50] 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 18/50] 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 19/50] 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 20/50] 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 21/50] 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 22/50] 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 23/50] 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 24/50] 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 25/50] 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 26/50] 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 27/50] 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 28/50] 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 20dd3f2d671235cdcc66eeaf4ace98a6fbe00700 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Oct 2019 16:22:11 +0200 Subject: [PATCH 29/50] Clearly highlight potential problems with looking into the future --- docs/backtesting.md | 2 ++ docs/strategy-customization.md | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 837fcfa3b..34c5f1fbe 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -201,6 +201,8 @@ Since backtesting lacks some detailed information about what happens within a ca Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Also, keep in mind that past results don't guarantee future success. +In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions. + ### Further backtest-result analysis To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ca76071af..7e677d4ce 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -60,8 +60,7 @@ file as reference.** !!! Warning Using future data Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author needs to take care to avoid having the strategy utilize data from the future. - Samples for usage of future data are `dataframe.shift(-1)`, `dataframe.resample("1h")` (this uses the left border of the interval, so moves data from an hour to the start of the hour). - They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly in dry-runs. + Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document. ### Customize Indicators @@ -399,10 +398,10 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). -### Where is the default strategy? +### Where can i find a strategy template? -The default buy strategy is located in the file -[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py). +The strategy template is located in the file +[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py). ### Specify custom strategy location @@ -412,6 +411,19 @@ If you want to use a strategy from a different directory you can pass `--strateg freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ``` +### Common mistakes when developing strategies + +Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look into the future. +This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions. + +The following lists some common patterns which should be avoided to avoid frustration: + +- don't use `shift(-1)`. This uses data from the future, which is not available. +- don't use `.iloc[-1]` or any other absolute position in the dataframe, this will be different between dry-run and backtesting. +- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling().mean()` instead +- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead. + + ### Further strategy ideas To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk. From d6b6ded8bd51a19b8b87287e43b83d0bfd4f4836 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 20 Oct 2019 22:28:54 +0300 Subject: [PATCH 30/50] 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 31/50] 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 32/50] 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 33/50] 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 34/50] 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 35/50] 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 bedbd964fc12f9115c5942e1e6535770e144545b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Oct 2019 06:51:48 +0200 Subject: [PATCH 36/50] slightly rephrase strategy docs --- docs/strategy-customization.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7e677d4ce..ab7dcfc30 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -413,17 +413,16 @@ freqtrade --strategy AwesomeStrategy --strategy-path /some/directory ### Common mistakes when developing strategies -Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look into the future. +Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future. This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions. -The following lists some common patterns which should be avoided to avoid frustration: +The following lists some common patterns which should be avoided to prevent frustration: - don't use `shift(-1)`. This uses data from the future, which is not available. - don't use `.iloc[-1]` or any other absolute position in the dataframe, this will be different between dry-run and backtesting. - don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling().mean()` instead - don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead. - ### Further strategy ideas To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk. From b116cc75c4ceaa6435a773469d3ba2918fde2678 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Oct 2019 07:07:25 +0200 Subject: [PATCH 37/50] 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 c2566f2436066b0798727110826fe9063d9fb27b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 07:46:32 +0000 Subject: [PATCH 38/50] Bump numpy from 1.17.2 to 1.17.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.17.2 to 1.17.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.17.2...v1.17.3) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2767180ac..62ede475c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.17.2 +numpy==1.17.3 pandas==0.25.1 From e350bcc2eff707ad29a2a31b2fe05ee74e6e4c8f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 07:47:53 +0000 Subject: [PATCH 39/50] Bump ccxt from 1.18.1260 to 1.18.1306 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1260 to 1.18.1306. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.18.1260...1.18.1306) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index c2c5317dc..e84e7caea 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.18.1260 +ccxt==1.18.1306 SQLAlchemy==1.3.10 python-telegram-bot==12.1.1 arrow==0.15.2 From 657f1b6c451af1023e738d1b5bebf022e6c4ae89 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 07:49:35 +0000 Subject: [PATCH 40/50] Bump flake8-tidy-imports from 2.0.0 to 3.0.0 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 2.0.0 to 3.0.0. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/2.0.0...3.0.0) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6cf378c03..0578c03bd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==1.8.2 flake8==3.7.8 flake8-type-annotations==0.1.0 -flake8-tidy-imports==2.0.0 +flake8-tidy-imports==3.0.0 mypy==0.730 pytest==5.2.1 pytest-asyncio==0.10.0 From 364859394b7ecb1f10da19284ac0fce26cb5f164 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 07:50:16 +0000 Subject: [PATCH 41/50] Bump plotly from 4.1.1 to 4.2.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.1.1 to 4.2.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.1.1...v4.2.1) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 1f1df4ecc..235c71896 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.1.1 +plotly==4.2.1 From e5f06c201fa4489413a861db77807d6662c2d4b6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 08:29:57 +0000 Subject: [PATCH 42/50] Bump python-telegram-bot from 12.1.1 to 12.2.0 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.1.1 to 12.2.0. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.1.1...v12.2.0) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index e84e7caea..1e42d8a04 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -2,7 +2,7 @@ # mainly used for Raspberry pi installs ccxt==1.18.1306 SQLAlchemy==1.3.10 -python-telegram-bot==12.1.1 +python-telegram-bot==12.2.0 arrow==0.15.2 cachetools==3.1.1 requests==2.22.0 From f07b26f2453b7af82a3974331b2133d0de53c8e7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 08:33:48 +0000 Subject: [PATCH 43/50] Bump pandas from 0.25.1 to 0.25.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 0.25.1 to 0.25.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v0.25.1...v0.25.2) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62ede475c..8d9b4953f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.17.3 -pandas==0.25.1 +pandas==0.25.2 From 8872158a6a53e99a919bd70cf204d64b4c81e3a4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2019 10:29:50 +0000 Subject: [PATCH 44/50] Bump mypy from 0.730 to 0.740 Bumps [mypy](https://github.com/python/mypy) from 0.730 to 0.740. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.730...v0.740) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0578c03bd..f5cde59e8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==1.8.2 flake8==3.7.8 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.0.0 -mypy==0.730 +mypy==0.740 pytest==5.2.1 pytest-asyncio==0.10.0 pytest-cov==2.8.1 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 45/50] 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 a43d436f9863543fc9a010c256a8b0659f087713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 21 Oct 2019 19:43:44 +0200 Subject: [PATCH 46/50] Move decorators out of API Class --- freqtrade/rpc/api_server.py | 54 ++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 711202b27..95ee51eaf 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -2,7 +2,7 @@ import logging import threading from datetime import date, datetime from ipaddress import IPv4Address -from typing import Dict +from typing import Dict, Callable, Any from arrow import Arrow from flask import Flask, jsonify, request @@ -34,6 +34,34 @@ class ArrowJSONEncoder(JSONEncoder): return JSONEncoder.default(self, obj) +# Type should really be Callable[[ApiServer, Any], Any], but that will create a circular dependency +def require_login(func: Callable[[Any, Any], Any]): + + def func_wrapper(obj, *args, **kwargs): + + auth = request.authorization + if auth and obj.check_auth(auth.username, auth.password): + return func(obj, *args, **kwargs) + else: + return jsonify({"error": "Unauthorized"}), 401 + + return func_wrapper + + +# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency +def rpc_catch_errors(func: Callable[[Any], Any]): + + def func_wrapper(obj, *args, **kwargs): + + try: + return func(obj, *args, **kwargs) + except RPCException as e: + logger.exception("API Error calling %s: %s", func.__name__, e) + return obj.rest_error(f"Error querying {func.__name__}: {e}") + + return func_wrapper + + class ApiServer(RPC): """ This class runs api server and provides rpc.rpc functionality to it @@ -41,34 +69,10 @@ class ApiServer(RPC): This class starts a none blocking thread the api server runs within """ - def rpc_catch_errors(func): - - def func_wrapper(self, *args, **kwargs): - - try: - return func(self, *args, **kwargs) - except RPCException as e: - logger.exception("API Error calling %s: %s", func.__name__, e) - return self.rest_error(f"Error querying {func.__name__}: {e}") - - return func_wrapper - def check_auth(self, username, password): return (username == self._config['api_server'].get('username') and password == self._config['api_server'].get('password')) - def require_login(func): - - def func_wrapper(self, *args, **kwargs): - - auth = request.authorization - if auth and self.check_auth(auth.username, auth.password): - return func(self, *args, **kwargs) - else: - return jsonify({"error": "Unauthorized"}), 401 - - return func_wrapper - def __init__(self, freqtrade) -> None: """ Init the api server, and init the super class RPC From 73fa5bae968ecbf964e2360a4eb0a91c8d6db90c Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Tue, 22 Oct 2019 00:03:11 +0300 Subject: [PATCH 47/50] minor: Fix wording in a docstring --- freqtrade/rpc/api_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 95ee51eaf..67bbfdc78 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -66,7 +66,7 @@ class ApiServer(RPC): """ This class runs api server and provides rpc.rpc functionality to it - This class starts a none blocking thread the api server runs within + This class starts a non-blocking thread the api server runs within """ def check_auth(self, username, password): From 562e4e63de257d6260e9ec905261522fd7eaaea1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 22 Oct 2019 13:48:54 +0300 Subject: [PATCH 48/50] =?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 49/50] 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 From 336808ec543f4023d3241c22992d79a489fe2418 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Oct 2019 14:02:47 +0200 Subject: [PATCH 50/50] Correctly pass validate flag to fallback exchange too --- freqtrade/resolvers/exchange_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 8e55d1064..60f37b1c9 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -32,7 +32,7 @@ class ExchangeResolver(IResolver): logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") if not hasattr(self, "exchange"): - self.exchange = Exchange(config) + self.exchange = Exchange(config, validate=validate) def _load_exchange( self, exchange_name: str, kwargs: dict) -> Exchange: