diff --git a/docs/bot-usage.md b/docs/bot-usage.md index f44400e32..795f5f07d 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -12,17 +12,23 @@ This page explains the different parameters of the bot and how to run it. usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [--db-url PATH] [--sd-notify] - {backtesting,edge,hyperopt,create-userdir,list-exchanges} ... + {backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} + ... Free, open source crypto trading bot positional arguments: - {backtesting,edge,hyperopt,create-userdir,list-exchanges} + {backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. create-userdir Create user-data directory. list-exchanges Print available exchanges. + list-timeframes Print available ticker intervals (timeframes) for the + exchange. + download-data Download backtesting data. + plot-dataframe Plot candles with indicators. + plot-profit Generate plot showing profits. optional arguments: -h, --help show this help message and exit diff --git a/docs/utils.md b/docs/utils.md new file mode 100644 index 000000000..459960fac --- /dev/null +++ b/docs/utils.md @@ -0,0 +1,56 @@ +# Utility Subcommands + +Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section. + +## List Exchanges + +Use the `list-exchanges` subcommand to see the exchanges available for the bot. + +``` +usage: freqtrade list-exchanges [-h] [-1] [-a] + +optional arguments: + -h, --help show this help message and exit + -1, --one-column Print exchanges in one column. + -a, --all Print all exchanges known to the ccxt library. +``` + +* Example: see exchanges available for the bot: +``` +$ freqtrade list-exchanges +Exchanges available for Freqtrade: _1btcxe, acx, allcoin, bequant, bibox, binance, binanceje, binanceus, bitbank, bitfinex, bitfinex2, bitkk, bitlish, bitmart, bittrex, bitz, bleutrade, btcalpha, btcmarkets, btcturk, buda, cex, cobinhood, coinbaseprime, coinbasepro, coinex, cointiger, coss, crex24, digifinex, dsx, dx, ethfinex, fcoin, fcoinjp, gateio, gdax, gemini, hitbtc2, huobipro, huobiru, idex, kkex, kraken, kucoin, kucoin2, kuna, lbank, mandala, mercado, oceanex, okcoincny, okcoinusd, okex, okex3, poloniex, rightbtc, theocean, tidebit, upbit, zb +``` + +* Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): +``` +$ freqtrade list-exchanges -a +All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpro, bcex, bequant, bibox, bigone, binance, binanceje, binanceus, bit2c, bitbank, bitbay, bitfinex, bitfinex2, bitflyer, bitforex, bithumb, bitkk, bitlish, bitmart, bitmex, bitso, bitstamp, bitstamp1, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, cex, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, deribit, digifinex, dsx, dx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, gateio, gdax, gemini, hitbtc, hitbtc2, huobipro, huobiru, ice3x, idex, independentreserve, indodax, itbit, kkex, kraken, kucoin, kucoin2, kuna, lakebtc, latoken, lbank, liquid, livecoin, luno, lykke, mandala, mercado, mixcoins, negociecoins, nova, oceanex, okcoincny, okcoinusd, okex, okex3, paymium, poloniex, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, upbit, vaultoro, vbtc, virwox, xbtce, yobit, zaif, zb +``` + +## List Timeframes + +Use the `list-timeframes` subcommand to see the list of ticker intervals (timeframes) available for the exchange. + +``` +usage: freqtrade list-timeframes [-h] [--exchange EXCHANGE] [-1] + +optional arguments: + -h, --help show this help message and exit + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. + -1, --one-column Print exchanges in one column. + +``` + +* Example: see the timeframes for the 'binance' exchange, set in the configuration file: + +``` +$ freqtrade -c config_binance.json list-timeframes +... +Timeframes available for the exchange `binance`: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M +``` + +* Example: enumerate exchanges available for Freqtrade and print timeframes supported by each of them: +``` +$ for i in `freqtrade list-exchanges -1`; do freqtrade list-timeframes --exchange $i; done +``` diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index da9bef0b0..f860fd09d 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -31,6 +31,8 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] +ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] + ARGS_CREATE_USERDIR = ["user_data_dir"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] @@ -41,7 +43,7 @@ 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", "plot-dataframe", "plot-profit"] +NO_CONF_REQURIED = ["download-data", "list-timeframes", "plot-dataframe", "plot-profit"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"] @@ -103,7 +105,8 @@ class Arguments: :return: None """ from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge - from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges + from freqtrade.utils import (start_create_userdir, start_download_data, + start_list_exchanges, start_list_timeframes) subparsers = self.parser.add_subparsers(dest='subparser') @@ -136,6 +139,14 @@ class Arguments: list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) + # Add list-timeframes subcommand + list_timeframes_cmd = subparsers.add_parser( + 'list-timeframes', + help='Print available ticker intervals (timeframes) for the exchange.' + ) + list_timeframes_cmd.set_defaults(func=start_list_timeframes) + self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd) + # Add download-data subcommand download_data_cmd = subparsers.add_parser( 'download-data', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0c307e7ed..df7e5e2b4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -203,6 +203,9 @@ 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 @@ -214,10 +217,6 @@ class Exchange: self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) - if config.get('ticker_interval'): - # Check if timeframe is available - self.validate_timeframes(config['ticker_interval']) - def __del__(self): """ Destructor - clean up async stuff @@ -269,6 +268,10 @@ class Exchange: """exchange ccxt id""" return self._api.id + @property + def timeframes(self) -> List[str]: + return list((self._api.timeframes or {}).keys()) + @property def markets(self) -> Dict: """exchange ccxt markets""" @@ -361,7 +364,7 @@ class Exchange: return pair raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") - def validate_timeframes(self, timeframe: List[str]) -> None: + def validate_timeframes(self, timeframe: Optional[str]) -> None: """ Checks if ticker interval from config is a supported timeframe on the exchange """ @@ -374,10 +377,9 @@ class Exchange: f"for the exchange \"{self.name}\" and this exchange " f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") - timeframes = self._api.timeframes - if timeframe not in timeframes: + if timeframe and (timeframe not in self.timeframes): raise OperationalException( - f'Invalid ticker {timeframe}, this Exchange supports {timeframes}') + f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") def validate_ordertypes(self, order_types: Dict) -> None: """ diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 3a11ca4fc..b3ff43aca 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -99,3 +99,21 @@ def start_download_data(args: Dict[str, Any]) -> None: if pairs_not_available: logger.info(f"Pairs [{','.join(pairs_not_available)}] not available " f"on exchange {config['exchange']['name']}.") + + +def start_list_timeframes(args: Dict[str, Any]) -> None: + """ + Print ticker intervals (timeframes) available on Exchange + """ + config = setup_utils_configuration(args, RunMode.OTHER) + # Do not use ticker_interval set in the config + config['ticker_interval'] = None + + # Init exchange + exchange = ExchangeResolver(config['exchange']['name'], config).exchange + + if args['print_one_column']: + print('\n'.join(exchange.timeframes)) + else: + print(f"Timeframes available for the exchange `{config['exchange']['name']}`: " + f"{', '.join(exchange.timeframes)}") diff --git a/mkdocs.yml b/mkdocs.yml index f88ff5c18..2a50c97e4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,16 +14,17 @@ nav: - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md - - Edge positioning: edge.md + - Edge Positioning: edge.md + - Utility Subcommands: utils.md - FAQ: faq.md - Data Analysis: - Jupyter Notebooks: data-analysis.md - Strategy analysis: strategy_analysis_example.md - Plotting: plotting.md - SQL Cheatsheet: sql_cheatsheet.md - - Sandbox testing: sandbox-testing.md - - Deprecated features: deprecated.md - - Contributors guide: developer.md + - Sandbox Testing: sandbox-testing.md + - Deprecated Features: deprecated.md + - Contributors Guide: developer.md theme: name: material logo: 'images/logo.png' diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f8c556aeb..bf6025322 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -409,7 +409,8 @@ def test_validate_timeframes_failed(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'): + with pytest.raises(OperationalException, + match=r"Invalid ticker interval '3m'. This exchange supports.*"): Exchange(default_conf) diff --git a/tests/test_utils.py b/tests/test_utils.py index 9025e4906..55672c4c9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,7 +7,8 @@ import pytest 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_download_data, start_list_exchanges, + start_list_timeframes) from tests.conftest import get_args, log_has, patch_exchange @@ -73,6 +74,96 @@ def test_list_exchanges(capsys): assert re.search(r"^bitmex$", captured.out, re.MULTILINE) +def test_list_timeframes(mocker, capsys): + + api_mock = MagicMock() + api_mock.timeframes = {'1m': 'oneMin', + '5m': 'fiveMin', + '30m': 'thirtyMin', + '1h': 'hour', + '1d': 'day', + } + patch_exchange(mocker, api_mock=api_mock) + args = [ + "list-timeframes", + ] + pargs = get_args(args) + pargs['config'] = None + with pytest.raises(OperationalException, + match=r"This command requires a configured exchange.*"): + start_list_timeframes(pargs) + + # Test with --config config.json.example + args = [ + '--config', 'config.json.example', + "list-timeframes", + ] + start_list_timeframes(get_args(args)) + captured = capsys.readouterr() + assert re.match("Timeframes available for the exchange `bittrex`: " + "1m, 5m, 30m, 1h, 1d", + captured.out) + + # Test with --exchange bittrex + args = [ + "list-timeframes", + "--exchange", "bittrex", + ] + start_list_timeframes(get_args(args)) + captured = capsys.readouterr() + assert re.match("Timeframes available for the exchange `bittrex`: " + "1m, 5m, 30m, 1h, 1d", + captured.out) + + api_mock.timeframes = {'1m': '1m', + '5m': '5m', + '15m': '15m', + '30m': '30m', + '1h': '1h', + '6h': '6h', + '12h': '12h', + '1d': '1d', + '3d': '3d', + } + patch_exchange(mocker, api_mock=api_mock) + # Test with --exchange binance + args = [ + "list-timeframes", + "--exchange", "binance", + ] + start_list_timeframes(get_args(args)) + captured = capsys.readouterr() + assert re.match("Timeframes available for the exchange `binance`: " + "1m, 5m, 15m, 30m, 1h, 6h, 12h, 1d, 3d", + captured.out) + + # Test with --one-column + args = [ + '--config', 'config.json.example', + "list-timeframes", + "--one-column", + ] + start_list_timeframes(get_args(args)) + captured = capsys.readouterr() + assert re.search(r"^1m$", captured.out, re.MULTILINE) + assert re.search(r"^5m$", captured.out, re.MULTILINE) + assert re.search(r"^1h$", captured.out, re.MULTILINE) + assert re.search(r"^1d$", captured.out, re.MULTILINE) + + # Test with --exchange binance --one-column + args = [ + "list-timeframes", + "--exchange", "binance", + "--one-column", + ] + start_list_timeframes(get_args(args)) + captured = capsys.readouterr() + assert re.search(r"^1m$", captured.out, re.MULTILINE) + assert re.search(r"^5m$", captured.out, re.MULTILINE) + assert re.search(r"^1h$", captured.out, re.MULTILINE) + assert re.search(r"^1d$", captured.out, re.MULTILINE) + + def test_create_datadir_failed(caplog): args = [